Skip to content
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
wants to merge 14 commits into from
132 changes: 132 additions & 0 deletions active-rfcs/0000-sfc-component-sugar.md
@@ -0,0 +1,132 @@
- Start Date: 2020-06-29
- Target Major Version: 2.x & 3.x
- Reference Issues: N/A
- Implementation PR: N/A

# Summary

Syntax sugar for reducing component import and registration boilerplate in Single File Components.

# Basic example

```html
<component src="./Foo.vue"/>
<component async src="./Bar.vue"/>
<component src="./Baz.vue" as="Qux" />

<template>
<Foo/>
<Bar/>
<Qux/>
</template>
```

# Motivation

Currently in SFCs you have to import a component and then pass it to the `export default { components: { ... } }` hash, which leads to a lot of redundancy: for a single component we are repeating its name 3 times: in the imported binding, the file name, and the components option.

The `<component/>` sugar requires the name of each component to be specified only once.

# Detailed design

## Normal Components

**Before**

```html
<template>
<Foo/>
</template>

<script>
import Foo from './Foo.vue'

export default {
components: {
Foo
}
}
</script>
```

**After**

```html
<component src="./Foo.vue"/>

<template>
<Foo/>
</template>
```

## Async Components

**Before**

```html
<template>
<Foo/>
</template>

<script>
import { defineAsyncComponent } from 'vue'

export default {
components: {
Foo: defineAsyncComponent(() => import('./Foo.vue'))
}
}
</script>
```

**After**

```html
<component async src="./Foo.vue" />

<template>
<Foo/>
</template>
```

## Component Renaming

By default, the component's locally registered name is inferred from its filename. But they can be renamed locally:

```html
<component src="./Foo.vue" as="Bar" />

<template>
<Bar/>
</template>
```

# Drawbacks

This would require updates in tools that parse SFC content for template analysis - e.g. Vetur & `@vuedx`.

However, since this information is going to be provided directly by `@vue/compiler-sfc` in the parsed SFC descriptor, it should remove some extra complexity from these tools as well.

# Alternatives

We considered implicitly registering imported components:

```html
<template>
<Foo/>
</template>

<script>
import Foo from './Foo.vue'
</script>
```

However, this approach has a few issues:

- `Foo` is unused in the script scope, making it annoying when using linter rules that check for unused variables.

- The only safe assumption about an import being a Vue component is the `.vue` extension, which makes it unable to support components authored in non-SFC formats (e.g. `import Foo from './Foo.ts'` can be a component but we can't really be sure).

# Adoption strategy

This is a fully backwards compatible new feature. However, we probably want to warn users against mixing the manual imports and `<component>` tags so that tools that rely on the extracted information can make safer assumptions.
252 changes: 252 additions & 0 deletions active-rfcs/0000-sfc-script-setup.md
@@ -0,0 +1,252 @@
- 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,
}
```

## 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 make implicitly injected variables like `$props` and `$emit` work with proper types, simply declare them:
yyx990803 marked this conversation as resolved.
Show resolved Hide resolved

```vue
<script setup 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'

type __$props__ = { msg: string }

export default defineComponent({
props: ({
msg: String
} as unknown) as undefined,
setup($props: __$props__) {
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.


## 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 }
```

## 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.