vue3 使用有感

本文最后更新于:2022年9月19日 早上

假若没有看见光明,我本可以忍受黑暗。

场景

自上家公司从去年 5 月份开始成功推广 react 之后,很长一段时间吾辈一直在使用它,而今年,离职之后新的公司再次使用 vue3,因而见证了两个 team 踏入了同一条河流。不过 vue 作者说 vue3 使用 ts 重写,对其支持很好,吾辈姑且安心了一点,但经过近一个月的实践,吾辈还是发现了种种问题。

从 vue 迁移到 react 的原因参考: 2020 吾辈在公司推动的前端技术演进

基本感受

  • vue3 的生态不稳定,而且仍然比较小
  • vue3 和 ts 的结合仍然不算好
  • webstorm/vscode 对 vue3+ts 的支持比较糟糕

vue3 的生态

vue 官方承认自身社区比 react 更小,但他们很明智的没有说出来,这究竟代表了什么。

  • vue+ts 结合的体验很糟糕,下面会详述
  • vue 的大版本升级虽然没有 react 那么快,但每次升级都是完全破坏性的,整个社区都要重新构建
  • ant-design-vue 2 使用 vue3 重构,但使用体验仍然比不上官方的 ant-design(例如 Table 的能力还远远比不上 ant-design)
  • antv 系列官方基本优先或仅支持 react,例如只有 react 版本的 G2Plot/Graphin
  • 结合 storybook 有点恶心,需要在字符串中写模板
  • vue-router 开发环境和生产环境的行为不一致,开发环境支持 import(),生产环境则必须是 () => import()
  • 许多周边 npm 包都是 next/beta/rc 版本
  • 异步组件比较奇葩,用了一种 map 的方式实现

vue+ts 结合

  • 模板是个失败的设计,很多问题都是由模板衍生而来
  • props 和 ts 结合仍有问题
  • 模板中只能编写 js,但 vue-tsc 会使用 tsc 检查,这会导致一些奇怪的问题很难解决
  • 使用 tsx 也会存在一些问题,但可能是现有条件下最好的解决方案(antdv 使用该方式),参考:极致的开发体验!Vite + Vue 3 + tsx 完整教程

vue 模板到底有什么问题?

相比于 vue 模板,jsx(或者说 react)的设计非常巧妙,它不需要 props/attrs/emits/slot/指令 这一系列 vue 的功能,而仅仅只需要 props,而它能够使得 dts 就能够完全支持 react 组件的接口定义,进而使编辑器能够通过 ts 做提示、导航和重构。而 vue 一时看起来方便开发者的设计,例如 props 支持定义类型、必填校验及 getter/setter,未在 props 声明的属性会被放到 attrs 并自动绑定到组件根元素上,前者在和 ts 结合时导致要重复定义 props 的类型,后者导致在支持 fragments 时反而需要手动指定绑定 attrs 的元素。

下面是一个 antdv List 组件的使用示例,如果不看文档,没人知道 slot 到底可以挂的是羊头,还是狗肉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<a-list item-layout="horizontal" :data-source="data">
<a-list-item slot="renderItem" slot-scope="item, index">
<a-list-item-meta
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
>
<a slot="title" href="https://www.antdv.com/">{{ item.title }}</a>
<a-avatar
slot="avatar"
src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
/>
</a-list-item-meta>
</a-list-item>
</a-list>
</template>

vscode vetur 和 jetbrians(web-types) 为了解决开发体验的问题选择了两条不同的道路,但都存在一些问题,参考:web-types.json 目前仍然有问题,包括某些 slot/item 仍然是错误的,可以想见,既然连 antdv 这么流行的 vue ui 组件库都无法保证没有问题,更别提整个 vue 生态中的其他 ui 组件库了。

props 和 ts 结合还有什么问题?

props 在 vue3 的 ts 有所增强,至少能够使用 PropType 复杂类型了,例如下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { defineComponent, PropType } from 'vue'
interface User {
name: string
age: number
}

export default defineComponent({
name: 'List',
props: {
// 使用 PropType 定义复杂类型
list: Array as PropType<User[]>,
},
setup(props) {
// 这儿的 props 类型是正确的
console.log(props.list)
return {}
},
})

但它仍然存在一些问题

无法复用现有的类型。例如当我们已经有一个后端返回的数据结构类型了,而我们希望组件仅需要其中的部分字段,而这无法使用 ts 的类型操作完成。究其原因,还是因为 vue 的 props 定义仍然是值而非类型。
无法使用 ts 的可选属性。vue props 仍保留定义 required/default/validator 的功能,所以并不能使用 ts 的可选属性。下面的示例代码将上面限制表现的的淋漓尽致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// react
interface User {
name: string
age?: number // 利用了 ts 的可选属性
address: string
}
// 使用 ts 的类型操作,只取 name/age 字段
const User: React.FC<Pick<User, 'name' | 'age'>> = (props) => {
return (
<div>
{props.name} {props.age}
</div>
)
}
// vue
defineComponent({
name: 'User',
props: {
// 重新定义,和 ts 格格不入
name: {
type: String,
required: true,
},
age: Number,
},
})

无法使用泛型。最典型的莫过于 List 组件,我们希望传入的 data 数据决定 slot 中参数的类型,目前这是不可能的。下面是 react 中的泛型组件示例(本质上是泛型函数,对 state=>ui 的抽象是真的彻底)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义泛型函数
function List<T>(props: {
list: T[]
renderItem(item: T, i: number): ReactNode
}) {
return <ul>{props.list.map(props.renderItem)}</ul>
}

interface User {
name: string
age?: number
}

export const Hello: React.FC = () => {
return (
<List
list={[{ name: '琉璃' }] as User[]}
renderItem={(item, i) => <li key={i}>{item.name}</li>}
/>
)
}

模板中只能编写 js,但 vue-tsc 会使用 tsc 检查,这会导致一些奇怪的问题很难解决

模板里不能使用 ts,但 vue-tsc 却会检查 ts,然后就 GG 了

1628152139838

如上图,就因为模板中的函数参数没有指定类型导致 vue-tsc 报告错误了,但模板中也确实不能定义参数类型,所以目前只能将之放到 script 中并在 setup 函数返回(是不是感觉挺蠢的?)

以及模板中出现下面这个错误,也很奇葩(估计是粘合层 vue-tsc 没有修改 tsc 的类型检查器)

1
2
src/App.vue:25:14 - error TS2322: Type '{ for: string; }' is not assignable to type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'.
Property 'for' does not exist on type 'DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>'. Did you mean 'htmlFor'?

需要更换 vue-tsc 为 vls,配置 vite 插件 vite-plugin-checker

更新依赖

1
2
yarn add -D vls vite-plugin-checker
yarn remove vue-tsc

配置 vite.config.ts

1
2
3
4
5
6
7
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import checker from 'vite-plugin-checker'

export default defineConfig({
plugins: [vue(), checker({ typescript: true, vls: true })],
})

如果遇到下面这个错误

1
2
3
src/main.ts:2:17 - error TS2307: Cannot find module './App.vue' or its corresponding type declarations.

2 import App from './App.vue'

请在 vite-env.d.ts 或 env.d.ts 中声明 vue 文件的类型定义

1
2
3
4
5
6
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

webstorm/vscode 对 vue3+ts 的支持比较糟糕

具体表现在提示、导航和重构。吾辈在想,是不是 vue 的开发者都习惯了这种问题,代码导航靠 C-F 一个个查找,重构时 CS-F 一个个手动替换(群友吐槽说:“不然怎样,就 vue 那个框架设计,完全不考虑静态分析好吧……估计反馈给 yyx,得到的回答是:这么爱静态分析,滚去用 angular”)

  • 在 monorepo 中 vue-ts 提示很慢
  • monorepo 中同时包含 react 子模块时,无法使用 vue tsx,webstorm 会默认为是 react tsx
  • 重构不支持自动重命名 ref/setup 返回值/模板引用

一些感受到的优点

上面吐槽了那么多,vue 及其社区也并非没有可取之处

  • vue3 的 hooks api 对心智负担确实更小,不太容易出现 react hooks 两个烦人的问题
    • 强制要求依赖,但很容易错误(各种依赖引发的问题)
    • 状态声明的方式独树一帜,与一般 js 隔离了。例如许多基于闭包实现的高阶函数都必须重写,但在 vue hooks 中则不用(好吧其实也需要,但至少能生效)
  • vite/vuepress 很好用,比 snowpack/docusaurus 更好用(即便吾辈使用的是 react 技术栈,但仍然使用它们作为构建和文档工具)

其他

使用异步组件

参考:vue3 官方文档

  1. 全局注册所有异步组件(能够以编程的方式注册的方法)

    1
    2
    3
    4
    5
    6
    const components: Record<string, Lazy<Component>> = {
    hello: () => import('hello'),
    }
    ;[...Object.entries(components)].forEach(([k, v]) => {
    app.component(k, defineAsyncComponent(v))
    })
  2. 通过 component 组件渲染

    1
    <component is="hello" />

可以看到,基本上就是通过 map 的方式维护,要在局部引用之前还必须先在全局注册一下昭告天下 xd


更新

使用 defineAsyncComponent 包裹之后就像普通组件一样使用就好了

1
2
3
const AsyncHelloWorld = defineAsyncComponent(() => import('./HelloWorld.vue'))

<AsyncHelloWorld />

使用 provide/inject 模拟 react context

为什么要模拟 react context?
因为 vue3 仍然基于字符串实现的 provide/inject,而字符串可以任意写且不包含类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { inject, provide } from 'vue'

// context 实例本身就已经包含了 key 与值的类型了
interface Context<T> {
key: Symbol
init?: T
}

/**
* 创建一个上下文,用于在当前组件的所有子孙组件共享状态
* 基于 provide/inject 实现,但支持强类型,而且使用 Symbol,永不重复而且无需考虑命名问题
* @param init
* @param key
*/
export function createContext<T>(init?: T, key?: string): Context<T> {
return {
key: Symbol(key),
init,
}
}

/**
* 为所有子孙组件注入状态
* @param context
* @param value
*/
export function useProvide<T>(context: Context<T>, value: T) {
provide(context.key, value)
}

/**
* 使用祖先节点注入的状态,可能为 null/undefined
* @param context
*/
export function useInject<T>(context: Context<T>): T | undefined {
return inject(context.key, context.init)
}

使用

1
2
3
4
const stringContext = createContext()
useProvide(stringContext, 'hello')
const value = useInject(stringContext)
console.log(value)

关于这点,官方提出了自己的解决方案,参考: https://v3.cn.vuejs.org/api/composition-api.html#provide-inject

1
2
3
const key: InjectionKey<string> = Symbol()
provide(key, 'foo') // 若提供非字符串值将出错
const foo = inject(key) // foo 的类型: string | undefined

总结

上面只是吾辈的一些吐槽,但考虑到成本问题,在公司推广 react 短期来看仍然是不现实的(vue3、ts 甚至 esnext 都有人未能基本掌握)。


vue3 使用有感
https://blog.rxliuli.com/p/930763abc6ae4cfbbb45bc17a9947596/
作者
rxliuli
发布于
2021年8月5日
许可协议