vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.86k stars 548 forks source link

`<script setup>` #227

Closed yyx990803 closed 3 years ago

yyx990803 commented 3 years ago

This PR separates the <script setup> as proposed in #222 from the ref: sugar so it can be discussed and advanced on its own. It also has incorporated edits based on feedbacks in #222 that are specifically related to <script setup>.

<script setup>
// imported components are also directly usable in template
import Foo from './Foo.vue'
import { ref } from 'vue'

// write Composition API code just like in a normal setup()
// but no need to manually return everything
const count = ref(0)
const inc = () => { count.value++ }
</script>

<template>
  <Foo :count="count" @click="inc" />
</template>

Rendered

axetroy commented 3 years ago

I have some concerns that it will block tree-shake.

It is difficult to analyze whether a var has been used in the template

<template>
  <div></div>
</template>

<script>
// Can you know where the lodash is used?
import _ from "lodash"
</script>
mgdodge commented 3 years ago

Does auto-exposing imported vue components mean that PascalCase is now required in SFCs? Consider the following:

<script setup>
import MyDatePicker from './my-date-picker.vue';
</script>

<template>
    <!-- This is obvious -->
    <MyDatePicker />

    <!-- Will this still work? -->
    <my-date-picker />
</template>

Right now the vue style guide says this is "strongly recommended", but not required. The kebab-casing is something I've seen in plenty of code I've reviewed. As people refactor, this might be something worth calling out explicitly.

antfu commented 3 years ago

@mgdodge I believe they will work for both styles in template.

jods4 commented 3 years ago

Great that you have split the RFC! This part is much less controversial and might move forward faster. There's a lot to love in here.

First a general question: How much better do you think the dev experience + runtime perf of <script setup> will be over regular setup() w/ Composition API or even Options API? I have a feeling the answer is "a good amount" and it gets me thinking... That makes it really compelling and not as optional as it is presented. What do you think?

Top level bindings are exposed to template

I like the local component example but how does that work exactly? Does it work for local directives as well? Will there be no need to distinguish what's a component vs what's a directive? I assume this is gonna generate code like h(MyComponent), which is more optimized than the current string lookup, right? Will it work in <component :is=xyz> as well?

Closed by Default Closed by default for parent component + explicit exposure model is good. (random thought... could the explicit exposure be export then?).

I note that this closes the door to MVVM style testing practices, which encourages unit testing a component without actually executing any UI. Given that you generate bindings metadata anyway, it may be nice to have a function reserved for tests that could still return an object with instance data/functions that is bound to the view. Maybe behind a compilation flag if it needs to.

Another concern: when it comes to debugging, will we have easy access to instance data? Or will it be in a form that is really hard to get to?

export default can still be used inside <script setup> for declaring component options such as props.

That felt ok when you had the other export proposal. Now that there are none left, it feels out of place, esp. given that it will be lifted out of the block.

I don't have a great suggestion here but... maybe put it in a <script props> block, with simplified syntax? After all you put one-time initialization code in a separate block, <script setup> is supposed to be the setup() body.

To type setup arguments like props, slots and emit, simply declare them:

I ❤️ this, so much better than the current code. Can we come up with something lean to declare default values? It's a pretty common scenario.

How will it work for more advanced scenarios, e.g. if we want to tap into prop validation? I assume we will use the JS props syntax, will it still declare the props type automatically as it does today?

Note that the props type declaration value cannot be an imported type

Surely you mean this won't work:

import T from "types"
declare const props: T

But surely this would, right?

import T from "types
declare const props: { foo: T }

Usage alongside normal <script>

It's a bit weird to reference variables across scripts blocks but hey.

My understanding of the RFC is that components defined in <script setup> block are always the default export of the sfc file they're defined in, right?

How can I import the component that is defined in the <script setup> block of my own file? import X from "./same-file.vue" would work?

Therefore, <script setup> cannot be used with the src attribute.

I was really torn about that one and changed my mind several time, but I agree it's weird and tricky. There are some bits that would be tricky technically, and for users you couldn't really reference globals in other script blocks, or props and emits, and it'd be a bit weird.

The question: what happens when your component gets really big? is still relevant. You can extract code into external functions (in any way you want) and simply assign them to global variables in <script setup>. I think that's prob. the best solution.

I still feel differently about the ref part, but that's another RFC now.

Tooling Compatiblity

I'm still wary about this, because even if you don't use this RFC, you probably still use Vetur. Not only should the compilation and refactorings work, but in my IDE a lot of things must happen as I type (lightbulbs, completions, errors, etc.) This means the solution needs to be robust on invalid/incomplete code and fast enough for interactive usage when I type.

Can you confirm that the following is a good understanding of your plans? With this RFC, the language service (both JS or TS) can see raw unmodified code from the script block when I'm typing. It is valid JS and everything is semantically as it should be. The only undeclared variables would be props and context, which a TS user would type with declare statements.

So what the IDE plugin needs to do in the <script setup> block is:

That would seem a reasonable complexity and it wouldn't negatively impact the coding experience.

yyx990803 commented 3 years ago

@axetroy

Re tree-shaking: we definitely can reverse prune the exposed bindings because we do know what exactly the template uses during the template compilation process. The only downside is maybe we'll have to process the script twice - but we can cache the previous AST and only redo code generation.

So yeah, short answer is this is fixable (just some extra implementation cost).

yyx990803 commented 3 years ago

@mgdodge

Does auto-exposing imported vue components mean that PascalCase is now required in SFCs?

It can keep supporting kebab-case.

yyx990803 commented 3 years ago

@jods4

How much better do you think the dev experience + runtime perf of ```

script setup ```js ```
Composable ```js import {h} from 'vue' export const useComposable = () => { return { MyComponent: {render: () => h('div', 'from composable')} } } ```
LinusBorg commented 3 years ago

It seems of unfair that script setup can return a component that comes from a composable and have it be registered, where regular setup can only return an unregistered component and needs to use <component :is=""/>.

I think You misunderstand that part of the RFC.

Only complements that you actually import from a .Vue file will be automatically available in the template.

yyx990803 commented 3 years ago

@3nuc interestingly this should already work with the latest implementation (since compileScript analyzes setup() return bindings as well). This is a bit sub-optimal since you are creating a fresh component definition on each parent component instantiation - if the component has props definitions, we won't be able to cache the props options normalization.

Unless there's particular reason for your component to be returned from the composition function, you should still do something like

import { Component, useComposable } from './composable'

Instead of returning it from the useComposable() call.

Also your <script setup> example is using old syntax. The syntax has changed in this RFC. (And P.S. single named exports is done as export { MyComponent })

yyx990803 commented 3 years ago

This RFC is now in final comments stage. An RFC in final comments stage means that:

The core team has reviewed the feedback and reached consensus about the general direction of the RFC and believe that this RFC is a worthwhile addition to the framework.

Final comments stage does not mean the RFC's design details are final - we may still tweak the details as we implement it and discover new technical insights or constraints. It may even be further adjusted based on user feedback after it lands in an alpha/beta release.

If no major objections with solid supporting arguments have been presented after a week, the RFC will be merged and become an active RFC.

jods4 commented 3 years ago

Should we split defineOptions into defineProps and defineContext? It would be consistent with the setup signature, which is setup(props, context), not setup(options). Also makes the most commonly used part, props, require less nested objects to be used:

const props = defineProps({ count: Number })
// vs
const { props } = defineOptions({ props: { count: Number } })

I also find the code a little nicer when you define many props + many emits to have the two separated.

~The RFC doesn't mention it, but if you're gonna transform the code and remove defineOptions call, you might remove defineComponent as well.~ EDIT: scrap that, defineComponent makes no sense in <script setup>. If you transform all SFC <script> blocks you may still remove it, but that's unrelated.

Before this ships, I would really like if you could design the API so that TS users have access to finer configuration details, in particular props default values. 🙏

Now that the solution is based off of a function call, that's not as tricky as it was before. I proposed those ideas further up in the thread:

// For reference, if we split defineOptions,
// then TS defineContext<E> takes the shape of emits as generic parameter
const { attrs, emits, slots } = defineContext<{ changed(value: number): void }>();

// Generic defineProps<P> and non-generic defineProps() don't have to work the same way.
// Here I pass the default values to `defineProps<P>` directly:
const props = defineProps<{ count: number }>({ count: 0 })

// Alternative idea, fluent configuration:
const props = defineProps<{ count: number }>().withDefaults({ count: 0 })
posva commented 3 years ago

@jods4 I think it makes more sense to keep the as PropType usage to type props that are more complex to make things consistent instead of having another syntax to learn. This would also allow to automatically infer things:

const props = defineProps({ count: Number, requiredCounter: { type: Number, required: true } })
const props = defineProps({ count: { type: Number, default: 0 } })
const props = defineProps({ movie: { type: Object as PropType<Movie>, required: true } })
jods4 commented 3 years ago

@posva The current TS syntax, including as PropType is horrible -- and hard to find out for beginners. Using a native TS type declaration for props is a major improvement and it's already in the RFC, here: https://github.com/vuejs/rfcs/blob/script-setup-2/active-rfcs/0000-script-setup.md#type-only-propsemit-declarations

My comment was about adding default values support to the RFC.

instead of having another syntax to learn

That's the point. as PropType is a new syntax to learn. Beginners using <script setup> won't have new syntax to learn as in TS passing a generic type is part of the basics everyone knows.

posva commented 3 years ago

Ah, I see what you mean now about supporting default values with an argument, I updated my comment to make it clearer. FYI PropType isn't new, it has been there a long time, people will be exposed to it by searching resources 😉 , it's factually what is easier to find out for beginners too

jods4 commented 3 years ago

people will be exposed to it by searching resources 😉 , it's factually what is easier to find out for beginners too

Factually? Here's real-world feedback from my team and I. It's just one anecdote, take it for what it's worth. We picked up Vue at version 3. How to type properties was a confusing mystery for most of the devs, including PropType and required: true.

Devs don't read all docs and don't remember everything. The best API is intuitive to use without looking up any docs. PropType is problematic in that regard because you need to know what to import and how to use it. There's no way it "appears" in a completion, intellisense or otherwise.

The only thing you should do is Google "how to type Vue props in Typescript". It might surprise you but it's an effort not everybody does. Some just go along with a cast from object or something (dare I say any).

In the same context, several devs thought all props were typed as optional properties in TS. As we use strict null typing, a lot of ! popped up in the code. Turns out it wasn't obvious to everyone that you should use a long-form declaration, add required: true and that the props type would now be non-nullable.

My opinion is that for TS users, this RFC is much more idiomatic, intuitive and the syntax is more concise. 👍

posva commented 3 years ago

You are taking my word out of context and putting it as if I wasn't in favor of the TS changes of this RFC. This is what I said:

factually what is easier to find out for beginners too

My opinion is that for TS users, this RFC is much more idiomatic, intuitive and the syntax is more concise. 👍

So to make it clear, yes, I agree!

The rest of your comment is basically your biased opinion presented as facts and you are clearly (based on previous interactions and the detail of your comments and analysis) a power user, way more advanced and experienced than most users out there, who do google things online pretty often.

jods4 commented 3 years ago

The rest of your comment is basically your biased opinion presented as facts

Come on, you make it sound as if I was trying to be misleading.

Most of that comment was facts, as I told you how it turned out, for real, in my team. Doesn't mean it's a general truth, as we're a small team and far from representative of every developper out there -- actually I said "It's just one anecdote, take it for what it's worth.". I thought feedback from real new Vue users might be hard to get, so I figured it was a valuable data point.

In the team devs have various levels of experience, from junior to more senior. When I said that some didn't figure it out, that casts, any and ! started popping up in our codebase: it is what happened. The worse bit is that some didn't figure it out although there were PropType usage already committed in other places in the same codebase.

Other factual data you can look up is questions on StackOverflow. This guy didn't seem to figure it out on his own: https://stackoverflow.com/questions/63796824/vue-js-typescript-how-to-set-prop-type-to-string-array And there are plenty more questions on SO about Vue props typing: https://www.google.com/search?biw=1447&bih=825&ei=C-67X7WiKIGdsAfN0pTYBg&q=how+to+type+Vue+prop+PropType+site%3Astackoverflow.com&oq=how+to+type+Vue+prop+PropType+site%3Astackoverflow.com&gs_lcp=CgZwc3ktYWIQA1De4AVY3uAFYPviBWgCcAB4AIABR4gBR5IBATGYAQCgAQGqAQdnd3Mtd2l6wAEB&sclient=psy-ab&ved=0ahUKEwj106WolZntAhWBDuwKHU0pBWsQ4dUDCA0&uact=5