-
Notifications
You must be signed in to change notification settings - Fork 545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SFC Improvements #182
Closed
+442
−0
Closed
SFC Improvements #182
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
96c72ff
sfc improvements
yyx990803 390e0a2
improve component sugar example
yyx990803 22bfdc8
fix example consistency
yyx990803 d805c9b
fix example
yyx990803 2d94fbf
fix typo
yyx990803 391a10c
make it clear let bindings are not part of the RFC
yyx990803 923348c
use export default for options
yyx990803 b1c7267
update `<script setup>` details + require setup signature via attribu…
yyx990803 3a2b0e7
update `<style vars>` proposal
yyx990803 3e0e5c8
update TS section regarding setup argument usage
yyx990803 45365a9
edit(sfc): add details on exposing components, transform API and bind…
yyx990803 6adbdbd
remove component sugar rfc
yyx990803 c0de170
update transform API example
yyx990803 e19fcb9
typo (#207)
MBearo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
- Start Date: 2020-06-29 | ||
- Target Major Version: 2.x & 3.x | ||
- Reference Issues: N/A | ||
- Implementation PR: N/A | ||
|
||
# Summary | ||
|
||
Introduce a compile step for `<script setup>` to improve the authoring experience when using the Composition API inside Single File Components. | ||
|
||
# Basic example | ||
|
||
```html | ||
<template> | ||
<button @click="inc">{{ count }}</button> | ||
</template> | ||
|
||
<script setup> | ||
import { ref } from 'vue' | ||
|
||
export const count = ref(0) | ||
export const inc = () => count.value++ | ||
</script> | ||
``` | ||
|
||
# Motivation | ||
|
||
When authoring components using the Composition API, very often `setup` is the only option that's being used. This results in some unnecessary boilerplate: | ||
|
||
```js | ||
import { ref } from 'vue' | ||
|
||
export default { | ||
setup() { | ||
const count = ref(0) | ||
const inc = () => count.value++ | ||
|
||
return { | ||
count, | ||
inc, | ||
} | ||
}, | ||
} | ||
``` | ||
|
||
In addition, one of the most often complained about aspect of the Composition API is the necessity to repeat all the bindings that need to be exposed to the render context using a return object. | ||
|
||
This RFC introduces a compiler-powered alternative for the usage of `<script>` inside SFCs that greatly reduces the amount of boilerplate: | ||
|
||
```diff | ||
import { ref } from 'vue' | ||
|
||
-export default { | ||
- setup() { | ||
- const count = ref(0) | ||
+export const count = ref(0) | ||
- const inc = () => count.value++ | ||
+export const inc = () => count.value++ | ||
|
||
- return { | ||
- count, | ||
- inc | ||
- } | ||
- } | ||
-} | ||
``` | ||
|
||
# Detailed design | ||
|
||
When a `<script>` tag in an SFC has the `setup` attribute, it is compiled so that the code runs in the context of the `setup()` function of the component. All ES module exports are considered values to be exposed to the render context and included in the `setup()` return object. | ||
|
||
## Using `setup()` arguments | ||
|
||
Setup arguments can be specified as the value of the `setup` attribute: | ||
|
||
```vue | ||
<script setup="props, { emit }"> | ||
import { watchEffect } from 'vue' | ||
|
||
watchEffect(() => console.log(props.msg)) | ||
emit('foo') | ||
</script> | ||
``` | ||
|
||
will be compiled into: | ||
|
||
```js | ||
import { watchEffect } from 'vue' | ||
|
||
// setup is exported as a named export so it can be imported and tested | ||
export function setup(props, { emit }) { | ||
watchEffect(() => console.log(props.msg)) | ||
emit('foo') | ||
return {} | ||
} | ||
|
||
export default { | ||
setup, | ||
} | ||
``` | ||
|
||
## Exposing Components | ||
|
||
Exports from `<script setup>` are also available to the template when rendering components. For example: | ||
|
||
```vue | ||
<script setup> | ||
export { default as Foo } from './Foo.vue' | ||
export { default as Bar } from './Bar.vue' | ||
export const ok = Math.random() | ||
</script> | ||
|
||
<template> | ||
<Foo/> | ||
<Bar/> | ||
<component :is="ok ? Foo : Bar"/> | ||
</template> | ||
``` | ||
|
||
## Declaring props or additional options | ||
|
||
One problem with `<script setup>` is that it removes the ability to declare other component options, for example `props`. We can solve this by treating the default export as additional options (this also aligns with normal `<script>`): | ||
|
||
```vue | ||
<script setup="props"> | ||
import { computed } from 'vue' | ||
|
||
export default { | ||
props: { | ||
msg: String, | ||
}, | ||
inheritAttrs: false, | ||
} | ||
|
||
export const computedMsg = computed(() => props.msg + '!!!') | ||
</script> | ||
``` | ||
|
||
This will compile to: | ||
|
||
```js | ||
import { computed } from 'vue' | ||
|
||
const __default__ = { | ||
props: { | ||
msg: String, | ||
}, | ||
inheritAttrs: false, | ||
} | ||
|
||
export function setup(props) { | ||
const computedMsg = computed(() => props.msg + '!!!') | ||
|
||
return { | ||
computedMsg, | ||
} | ||
} | ||
|
||
__default__.setup = setup | ||
export default __default__ | ||
``` | ||
|
||
Since `export default` is hoisted outside of `setup()`, it cannot reference variables declared inside. For example, if the default export object references `computedMsg`, it will result in a compile-time error. | ||
|
||
## With TypeScript | ||
|
||
`<script setup>` should just work with TypeScript in most cases. To type setup arguments like `props` and `emit`, simply declare them: | ||
|
||
```vue | ||
<script setup="props" lang="ts"> | ||
import { computed } from 'vue' | ||
|
||
// declare props using TypeScript syntax | ||
// this will be auto compiled into runtime equivalent! | ||
declare const props: { | ||
msg: string | ||
} | ||
|
||
export const computedMsg = computed(() => props.msg + '!!!') | ||
</script> | ||
``` | ||
|
||
The above will compile to: | ||
|
||
```vue | ||
<script lang="ts"> | ||
import { computed, defineComponent } from 'vue' | ||
|
||
export default defineComponent({ | ||
props: ({ | ||
msg: String | ||
} as unknown) as undefined, | ||
setup(props: { | ||
msg: string | ||
}) { | ||
const computedMsg = computed(() => props.msg + '!!!') | ||
|
||
return { | ||
computedMsg, | ||
} | ||
} | ||
}) | ||
</script> | ||
``` | ||
|
||
- Runtime props declaration is automatically generated from TS typing to remove the need of double declaration and still ensure correct runtime behavior. | ||
|
||
- In dev mode, the compiler will try to infer corresponding runtime validation from the types. For example here `msg: String` is inferred from the `msg: string` type. | ||
|
||
- In prod mode, the compiler will generate the array format declaration to reduce bundle size (the props here will be compiled into `['msg']`) | ||
|
||
- The generated props declaration is force casted into `undefined` to ensure the user provided type is used in the emitted code. | ||
|
||
- The emitted code is still TypeScript with valid typing, which can be further processed by other tools. | ||
|
||
Note that the `props` type declaration value cannot be an imported type, because the SFC compiler does not process external files to extract the prop names. | ||
|
||
## Usage alongside normal `<script>` | ||
|
||
There are some cases where the code must be executed in the module scope, for example: | ||
|
||
- Declaring named exports that can be imported from the SFC file (`import { named } from './Foo.vue'`) | ||
|
||
- Global side effects that should only execute once. | ||
|
||
In such cases, a normal `<script>` block can be used alongside `<script setup>`: | ||
|
||
```vue | ||
<script> | ||
performGlobalSideEffect() | ||
|
||
// this can be imported as `import { named } from './*.vue'` | ||
export const named = 1 | ||
</script> | ||
|
||
<script setup> | ||
import { ref } from 'vue' | ||
|
||
export const count = ref(0) | ||
</script> | ||
``` | ||
|
||
the above will compile to: | ||
|
||
```js | ||
import { ref } from 'vue' | ||
|
||
performGlobalSideEffect() | ||
|
||
export const named = 1 | ||
|
||
export function setup() { | ||
const count = ref(0) | ||
return { | ||
count | ||
} | ||
} | ||
|
||
export default { setup } | ||
``` | ||
|
||
## Transform API | ||
|
||
The `@vue/compiler-sfc` package exposes the `compileScript` method for processing `<script setup>`: | ||
|
||
```js | ||
import { parse, compileScript } from '@vue/compiler-sfc' | ||
|
||
const descriptor = parse(`...`) | ||
|
||
if (descriptor.script || descriptor.scriptSetup) { | ||
const result = compileScript(descriptor) // returns SFCScriptBlock | ||
console.log(result.code) | ||
console.log(result.bindings) // see next section | ||
} | ||
``` | ||
|
||
The compilation requires the entire descriptor to be provided, and the resulting code will include sources from both `<script setup>` and normal `<script>` (if present). It is the higher level tools' (e.g. `vite` or `vue-loader`) responsibility to properly assemble the compiled output. | ||
|
||
## Template Binding Optimization | ||
|
||
The `SFCScriptBlock` returned by `compiledScript` also exposes a `bindings` object, which is the exported binding metadata gathered during the compilation. For example, given the following `<script setup>`: | ||
|
||
```vue | ||
<script setup="props"> | ||
export const foo = 1 | ||
|
||
export default { | ||
props: ['bar'] | ||
} | ||
</script> | ||
``` | ||
|
||
The `bindings` object will be: | ||
|
||
```js | ||
{ | ||
foo: 'setup', | ||
bar: 'props' | ||
} | ||
``` | ||
|
||
This object can then be passed to the template compiler: | ||
|
||
```js | ||
import { compile } from '@vue/compiler-dom' | ||
|
||
compile(template, { | ||
bindingMetadata: bindings | ||
}) | ||
``` | ||
|
||
With the binding metadata available, the template compiler can generate code that directly access template variables from the corresponding source, without having to go through the render context proxy: | ||
|
||
```html | ||
<div>{{ foo + bar }}</div> | ||
``` | ||
|
||
```js | ||
// code generated without bindingMetadata | ||
// here _ctx is a Proxy object that dynamically dispatches property access | ||
function render(_ctx) { | ||
return createVNode('div', null, _ctx.foo + _ctx.bar) | ||
} | ||
|
||
// code generated with bindingMetadata | ||
// bypasses the render context proxy | ||
function render(_ctx, _cache, $setup, $props, $data) { | ||
return createVNode('div', null, $setup.foo + $props.bar) | ||
} | ||
``` | ||
|
||
## Usage Restrictions | ||
|
||
Due to the difference in module execution semantics, code inside `<script setup>` relies on the context of an SFC. When moved into external `.js` or `.ts` files, it may lead to confusions for both developers and tools. Therefore, **`<script setup>`** cannot be used with the `src` attribute. | ||
|
||
# Drawbacks | ||
|
||
This is yet another way of authoring components, and it requires understanding the Composition API first. | ||
|
||
# Adoption strategy | ||
|
||
This is a fully backwards compatible new feature. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yyx990803 I looked up the
BindingMetadata
typedef in compiler-core. Following types are defined:'data' | 'props' | 'setup' | 'options'
I think an additional value
A: 'import'
, forexport { A } from './a'
could lead to interesting optimizations.The compiler would then be free to reference the import directly, without reactivity and bypassing the setup state for references to
A
.That's particularly interesting for local components (or directives).
This RFC also looks for a syntax to import local components. With the ability to recognize imports (constants) and reference them directly, it doesn't need one!
If the template does
<MyButton />
and the script setup contains anexport { MyButton } from './my-button'
identified as such, the compiler could produce the equivalent ofh(MyButton, ...)
.Without this knowledge, the local component would suffer two drawbacks:
MyButton
on the setup object.MyButton
is a variable that could change, so the component is dynamic and precludes the static optimizations.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we are not doing this is that the template and script parts of an SFC are typically executed as separate modules to
1. allow each having its own loader pipelines (webpack specific)
2. allow template to be individually hot-reloaded (thus preserving component state).
Regarding the drawbacks:
Component access only goes through the setup object, which is not a full reactive object (it's a proxy that only checks for ref unwrapping, so the cost is fairly cheap).
<MyButton/>
directly compiles toh($setup.MyButton)
, so there is no dynamic assumptions here.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm missing a piece here.
Setup properties can mutate, right? Do you know/assume
$setup.MyButton
is a constant rather than a reactive value that will change?If you assume
MyButton
can mutate, then isn't the code equivalent to<component :is="MyButton">
, which precludes some optimizations compared to a static<MyButton>
?https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%3Ccomponent%20%3Ais%3D%5C%22xy%5C%22%20%2F%3E%5Cr%5Cn%3Cmy-xy%20%2F%3E%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22prefixIdentifiers%22%3Afalse%2C%22optimizeImports%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup%22%2C%22foo%22%3A%22setup%22%2C%22bar%22%3A%22props%22%7D%2C%22optimizeBindings%22%3Afalse%7D%7D
Or does the syntax
<MyButton>
imply that it must be static, even though it comes fromsetup
?In this case, what happens if that assumption is violated by user, do you emit a warning in DEV?