Closed Jinjiang closed 3 years ago
Some questions:
TS support
I believe @znck is working on making it possible for TS to recognize .vue
SFC files as actual TS files (https://github.com/znck/vue-developer-experience?).
This would currently make it possible to get the return type of the setup function to determine what properties a ref of a component has:
export type ComponentType<
T extends ComponentOptionsBase<any, object, any, any, any, any, any, any>
> = T extends ComponentOptionsBase<any, infer U, any, any, any, any, any, any> ? U : never;
for example.
I don't know if this is something that is deemed useful but I do think it is. I'm not sure how this would work with the expose
function, do you have any ideas?
Duplicate calls What happens if you call it twice within setup? Is that ok? If so, which takes precedence when naming conflicts occur?
Proxied
Are the results proxied in proxyRefs
, similar to setup state (https://github.com/vuejs/vue-next/pull/1682)? I.e. are 1st level refs automatically unreffed?
Component I/O
Currently, the input of a component can be declared using props
. I wouldn't have a problem with defining the output of a component similarly as well, as described in https://github.com/vuejs/rfcs/pull/135. You could then kind of think of the component object as: props
input, setup
some transformation function, expose
output.
expose
as part of the component object would also make it easier to type, you can extract the keys of the returned object from the setup
function here and thus create a nice type of the exposed output, solving the first part of my comment.
In Vue things should work both in Composition and Options API, so there should be a fallback to Options API (as suggested here: https://github.com/vuejs/rfcs/pull/135)
WIP branch for testing: https://github.com/vuejs/vue-next/commit/c6033fb32718cd0268342f3ea6cd6b8b557acc02
One of the problems introduced by this ʻexpose()API is whether the result returned by
returnwill be exposed and increase uncertainty, which depends on whether ʻexpose()
is called in setup()
.
此expose()
API引入的一个问题是,return
返回的结果是否会暴露增加了不确定性,这取决于是否在setup()
里调用了expose()
。
I considered another solution, which can keep the result of return
behavior unchanged:
Add two APIs context.proxy()
and context.render()
.
When you need to mount a variable in the rendering context, but do not want to expose it as an instance property, use context.proxy()
explicitly.
When you need to expose instance properties in the return statement and want to use the render function, please use context.render()
explicitly.
我考虑了另一种方案,它能让return
的行为结果保持不变:
增加两个APIcontext.proxy()
和context.render()
。
当需要在渲染上下文中挂载变量,但不希望作为实例属性暴露时,请显示地使用context.proxy()
。
当需要在return语句中暴露实例属性,又想要使用render函数时,请显示地使用context.render()
。
function setup(props, context) {
const count = ref(0)
const inc = () => count.value++
context.proxy({count}) // or: context.proxy('count', count)
context.render(() => JSX)
return {count, inc}
}
Or, you can also consider making three APIs, and at the same time clarify the declaration of instance members, and turn the return
statement into an optional convenient way of writing.
或者,还可以考虑做成3个API,同时将实例成员的声明也明确化,将return
语句变成一个只是可选的便捷写法。
context.public()
context.private()
context.render()
What happens when expose
is called multiple times? Do the exposed values get merged?
If multiple "exposes" get merged, I am immediately worried that it will become confusing what are "exposed"; because this means that third party compositions could also expose values?
Yep that was also my concern. This means that 3rd party compositions may override your own exposed values & the order of the calls matters: hard to debug.
Besides this, I think my TS support point still stands. I don't see how this will benefit TS type inference for components once this gets merged. Imagine TS support for Vue gets to the point where we can do this:
<template>
<my-component ref="myComponentRef"/>
</template>
<script lang="ts">
// SetupType<MyComponent> could actually infer the return type of `setup`
import MyComponent from "./MyComponent.vue"
const myComponentRef = ref<SetupType<MyComponent>>();
how would such a thing work with expose
?
What if we use a specific symbol in the returned object to denote a public component API?
import { componentApi } from 'vue';
// componentApi: symbol
function setup() {
return {
internalState: 42,
[componentApi]: {
publicMethod() { }
}
}
}
setup
, it's easy to infer the type of [componentApi]
.What about making expose
only accessible inside setup
(by passing it into the context
argument)? In this way third party composables cannot mess up with the public API.
import { ref } from 'vue'
export default {
setup(_, { expose }) {
const count = ref(0)
function increment() {
count.value++
}
expose({
increment
})
return { increment, count }
}
}
We need address how exposed properties or functions be added to instance type of the component.
I agree expose()
being a global function that can be called even in external functions can lead to unpredictability.
@jods4 's idea of using a symbol is interesting, but it won't work with <script setup>
which has no return statement.
@Justineo 's idea of exposing expose
via setup context works in <script setup>
, but may not have the same typing benefits. You also need to explicitly declare function expose()
.
how about this? https://github.com/cereschen/vue-setup-generator
It's important that the type is supported
@cereschen it's an interesting experiment (reminds me of https://crank.js.org/) but I don't think it is related to this specifically. This RFC is about exposing a different set of properties for template ref usage.
Regarding type inference: I'm thinking maybe the exposed type doesn't have to be somewhat provided via the component type itself - if we can support named type exports from SFCs.
For example:
<!-- Foo.vue -->
<script setup lang="ts">
import { ref, Ref, defineOptions } from 'vue'
const { expose } = defineOptions()
const count = ref(0)
// public API
export interface API {
count: number
}
expose<API>({ count })
</script>
In another file:
<!-- Bar.vue -->
<script lang="ts">
import { ref, watchEffect } from 'vue'
import Foo, { API as FooAPI } from './Foo.vue'
const foo = ref<FooAPI | undefined>()
watchEffect(() => {
console.log(foo.value && foo.value.count)
})
</script>
<template>
<Foo ref="foo"/>
</template>
This also already works for non-SFC components written in TS or TSX.
This should work with TS plugin too.
Now that export
is no longer used in <script setup>
to expose bindings to the template, was it discussed to use it for exposing public members of components?
<script setup>
import { ref } from 'vue'
export const count = ref(0)
export function increment() {
count.value++
}
</script>
The sfc compiler would aggregate all the exported binding using expose
inside setup()
.
With ref sugar, the export
can not be in the same line as the declaration, but in that case it is the same as with expose where the variable will need to be repeated:
ref: count = 0
export { count }
If this is an issue, a shortcut could be also provided:
export_ref: count = 0
For setup + render functions the ideal implementation would be something like this:
import { defineComponent, ref, render } from 'vue'
export default defineComponent({
setup (props) {
const publicState = ref('public')
const privateState = ref('private')
return {
publicState,
[render]: () => <div>{publicState.value}, {privateState.value}</div>
}
}
})
setup
can be used to infer the public API instead of explicitly defining an interfacerender
has access to the entire setup stateThe current expose()
implementation in 3.0.7 is kinda broken - it completely replaces the component's proxy, so things like this.$refs.foo.$el
don't work. This also breaks vue-test-utils: https://github.com/vuejs/vue-test-utils-next/issues/435
There's also a TODO to "infer public instance type based on exposed keys", which is possible with options.expose
but that doesn't work with render functions, and you can't do it with setupContext.expose
.
For setup + render functions the ideal implementation would be something like this:
import { defineComponent, ref, render } from 'vue' export default defineComponent({ setup (props) { const publicState = ref('public') const privateState = ref('private') return { publicState, [render]: () => <div>{publicState.value}, {privateState.value}</div> } } })
- The return value of
setup
can be used to infer the public API instead of explicitly defining an interfacerender
has access to the entire setup state
The expose
is not only used on the setup
with a render function. This implementation only focus on the render returned by the setup()
, personally I don't like this API because you are passing Symbols
The current
expose()
implementation in 3.0.7 is kinda broken - it completely replaces the component's proxy, so things likethis.$refs.foo.$el
don't work.
For this case I think you would rely on the setup template ref
instead of the vm.$refs
:
setup(){
const foo = ref()
expose({foo})
return ()=> h(div, {ref: foo})
}
To get the RawBindings
type the state has to be returned from setup, and the render function also needs to be declared in setup so it has access to the entire scope. Another alternative is to pass the render function to a callback instead:
// Render function
defineComponent({
setup(props, { render }) {
const foo = ref('public')
const bar = ref('private')
render(() => <div>{foo.value}, {bar.value}</div>)
return {
foo,
}
}
})
// Template
<div>{{ foo }}, {{ bar }}</div>
defineComponent({
expose: ['foo'], // Only foo is available externally
setup(props) {
const foo = ref('public')
const bar = ref('private')
return {
foo,
bar,
}
}
})
// External use
setup () {
const renderComponent = ref()
const templateComponent = ref()
onMounted(() => {
renderComponent.value.foo // 'public'
renderComponent.value.bar // undefined
templateComponent.value.foo // 'public'
templateComponent.value.bar // undefined
})
return () => (<>
<RenderComponent ref={renderComponent} />
<TemplateComponent ref={templateComponent} />
</>)
}
For this case I think you would rely on the setup template ref instead of the vm.$refs
It's about the child, if $refs.foo
is a component that uses expose
then the parent can't access its $el
.
The way I see it is: If you explicit expose something you want to prevent something else, because by default everything is accessed via the vm
, when you expose especially on options API, it would be fair to think everything else gets "hidden", you should be able to declare everything you want to expose.
It's about the child, if
$refs.foo
is a component that usesexpose
then the parent can't access its$el
.
Isn't this part of the motivation of the expose
API? On one hand it offers a way to expose API when returning render function in setup
, on the other hand it can help stop leaking internal implementations.
There's a drawback
of using expose
:
expose
regardless of the API you are using, it will hide the vm.$*
, which breaks a lot of more advanced usages, such as vue-test-utils
because it relies on vm.$el
.I think is expected a vue component to expose those vm.$*
internal apis, altho if you are exposing
explicitly the vm.$data
might not be good to also expose.
Altho expose
is intended for composition-api, I think it would be useful to also have it as an option on the options API
the use case:
defineComponent({
expose: ["process"],
data(){
return {
internalData: 1
}
},
methods: {
process() {
this.internalData++;
}
}
})
In this case I would expect the public instance only allow access to process
method and the $data
to be readonly
(altho the readonly is arguable)
The typing for it: https://github.com/vuejs/vue-next/pull/3399
@pikax it was suggested before the Composition API version: https://github.com/vuejs/rfcs/pull/135
Thank @CyberAP, I saw the implementation on the vue-next and thought it was this RFC, my bad.
I think this two RFC are closely related, because the internal behaviour will be the same, only the declaration API is different.
They can even work together for declaring the typescript typing:
// no expose typing
defineComponent({
setup(){
expose({test: 1})
}
})
// with expose typed
defineComponent({
expose: undefined as { test: number},
setup(){
expose({ test: 1})
}
})
// equivalent
defineComponent({
expose: ['test']
setup(){
return { test: 1 }
}
})
// or options
defineComponent({
expose: ['test'],
data(){
return {
test: 1
}
}
})
Can it support using with SFC State-driven CSS Variables (v-bind in
Rendered