vue2使用typescript | composition API + tsc or sfc

vue2升级到typescript的方案有很多种。
vue2比较令人诟病的地方还是对ts的支持,对ts支持不好是vue2不适合大型项目的一个重要原因。
其根本原因是Vue依赖单个this上下文来公开属性,并且vue中的this比在普通的javascript更具魔力(如methods对象下的单个method中的this并不指向methods,而是指向vue实例)。换句话说,尤大大在设计Option API时并没有考虑对ts引用的支持)。(由于Vue2.x版本有设计断层,导致很多类型是通过declare方式推导出,而不是基于class的API,这也是为什么Vue3.0用typescript重写的原因之一。)
方案:

传统方案:vue-property-decorator | vue class component | vue-facing-decorator

vue2对ts的支持主要是通过vue class component,vue-property-decorator这里主要依赖装饰器。
vue-property-decorator的用法:
https://github.com/kaorun343/vue-property-decorator
目前该库已经不积极维护了,如果继续使用类组件的装饰器的写法,作者推荐使用vue-facing-decorator
vue-facing-decorator: https://github.com/facing-dev/vue-facing-decorator

vue-property-decorator方案缺点

  • vue class component与js的vue组件差异太大,另外需要引入额外的库,学习成本大幅度增高。
  • 依赖于装饰器语法。而目前装饰器目前还处于stage2阶段(可查看tc39 decorators),在实现细节上还存在许多不确定性,这使其成为一个相当危险的基础。
  • 复杂性增高。采用Vue Class Component且需要使用额外的库,相比于简单的js vue组件,显然复杂化。

tsx组合方案:Vue Components + TypeScript

如果写过react,这个方案风格会更舒服。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Vue, { VueConstructor, CreateElement, VNode } from 'vue';
interface InputInstance extends Vue {
composing: boolean;
}
export default (Vue as VueConstructor<InputInstance>).extend({
name: 'demo-input',
props: {},
data() {},
// TODO 更Vue其它组件一样
render(h: CreateElement): VNode {
// TODO 逻辑
const classes = []
const wrapperAttrs = {...this.$attrs};
const wrapperEvents = {...this.$listeners};
// JSX 语法
return (
<div class={classes} {...{ attrs: wrapperAttrs, on: wrapperEvents }}>
<input/>
</div>
);
}
}

Vuex Store的痛

在ts里面使用vuex非常的蛋疼。
vuex ts版相关的vuex-classvuex-module-decorators两个库应该是目前用的最多的如果是老旧项目,个人推荐先使用vuex-class过度。

tsx+composion API

当迁移到 Vue 3 时,只需简单的将 @vue/composition-api 替换成 vue 即可。你现有的代码几乎无需进行额外的改动。可开启 .ts、.tsx 支持。
问题1:是否有必要使用类vue-property-decorator?
没这个必要,这个写法如果后续迁移vue3还得改,还存在一定学习成本,这里本意只是引入使用ts,顺便学习一下vue3的组合式API写法。

动机与目的

  • 更好的逻辑复用与代码组织
    1. 随着功能的增长,复杂组件的代码变得越来越难以阅读和理解。
    2. 目前缺少一种简洁且低成本的机制来提取和重用多个组件之间的逻辑。
  • 更好的类型推导 Vue 当前的 API 在集成 TypeScript 时遇到了不小的麻烦,其主要原因是 Vue 依靠一个简单的 this 上下文来暴露 property,我们现在使用 this 的方式是比较微妙的。(比如 methods 选项下的函数的 this 是指向组件实例的,而不是这个 methods 对象)。
  • mixins 带来的问题
    1. 渲染上下文中暴露的 property 来源不清晰。例如在阅读一个运用了多个 mixin 的模板时,很难看出某个 property 是从哪一个 mixin 中注入的。
    2. 命名空间冲突。Mixin 之间的 property 和方法可能有冲突,同时高阶组件也可能和预期的 prop 有命名冲突。
    3. 性能方面,高阶组件和无渲染组件需要额外的有状态的组件实例,从而使得性能有所损耗。

与现有的 API 配合

组合式 API 完全可以和现有的基于选项的 API 配合使用。

  • 组合式 API 会在 2.x 的选项 (data、computed 和 methods) 之前解析,并且不能提前访问这些选项中定义的 property。
  • setup() 函数返回的 property 将会被暴露给 this。它们在 2.x 的选项中可以访问到。

[最终选择]:vue2.7升级+typescript支持

vue2.7是vue2最新的次级版本,提供了内置的组合式API支持。

什么是组合式 API?

组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:

  • 响应式 API:例如 ref() 和 reactive(),使我们可以直接创建响应式状态、计算属性和侦听器。
  • 生命周期钩子:例如 onMounted() 和 onUnmounted(),使我们可以在组件各个生命周期阶段添加逻辑。
  • 依赖注入:例如 provide() 和 inject(),使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。

组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 更改状态、触发更新的函数
function increment() {
count.value++
}

// 生命周期钩子
onMounted(() => {
console.log(`计数器初始值为 ${count.value}。`)
})
</script>

<template>
<button @clic

虽然这套 API 的风格是基于函数的组合,但组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程通常强调数据不可变。

为什么要有组合式 API?

更好的逻辑复用

组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷

更灵活的代码组织

许多用户喜欢选项式 API 的原因是它在默认情况下就能够让人写出有组织的代码:大部分代码都自然地被放进了对应的选项里。然而,选项式 API 在单个组件的逻辑复杂到一定程度时,会面临一些无法忽视的限制。这些限制主要体现在需要处理多个逻辑关注点的组件中,这是我们在许多 Vue 2 的实际案例中所观察到的。
处理相同逻辑关注点的代码被强制拆分在了不同的选项中,位于文件的不同部分。在一个几百行的大组件中,要读懂代码中的一个逻辑关注点,需要在文件中反复上下滚动,这并不理想。另外,如果我们想要将一个逻辑关注点抽取重构到一个可复用的工具函数中,需要从文件的多个不同部分找到所需的正确片段。
image-20231205180831963

更好的类型推导

近几年来,越来越多的开发者开始使用 TypeScript 书写更健壮可靠的代码,TypeScript 还提供了非常好的 IDE 开发支持。然而选项式 API 是在 2013 年被设计出来的,那时并没有把类型推导考虑进去,因此我们不得不做了一些复杂到夸张的类型体操才实现了对选项式 API 的类型推导。但尽管做了这么多的努力,选项式 API 的类型推导在处理 mixins 和依赖注入类型时依然不甚理想。
相比之下,组合式 API 主要利用基本的变量和函数,它们本身就是类型友好的。用组合式 API 重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。大多数时候,用 TypeScript 书写的组合式 API 代码和用 JavaScript 写都差不太多!这也让许多纯 JavaScript 用户也能从 IDE 中享受到部分类型推导功能。

更小的生产包体积

搭配 script setup使用组合式 API 比等价情况下的选项式 API 更高效,对代码压缩也更友好。这是由于 script setup> 形式书写的组件模板被编译为了一个内联函数,和 Script setup> 中的代码位于同一作用域。不像选项式 API 需要依赖 this 上下文对象访问属性,被编译的模板可以直接访问 script setup> 中定义的变量,无需从实例中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。

升级vue2.7

安装、升级依赖:
image.png
深度选择器改写::v-deep、/deep/为:deep()

vue2.7 vs vue3 区别

image.png

vue 2.7使用

image.png
image.png
image.png

引入typescript

升级所需要事项

  1. 检查你的vue-eslint 是否为 9+版本,否则在启动服务的时候会报错,接下来下载 typescript ts-loaderyarn add typescript ts-loader -D , 如果在安装过程中出现问题,切换到淘宝源进行下载,

(注意:一直tsconfig.json一直使用不了,导致找不到ts的module,最后发现是ts-loader的版本过高的原因。)

  1. 下载成功后使用 npx tsc –init 接下来你的项目中会生成 tsconfig.json文件如下所示
  2. 此文件为 typescript的配置文件,如果你无法使用此命令行,并且报错的情况,建议配置一下npx,或者尝试使用 tsc –init来进行初始化操作。
  3. tsconfig.json的配置如下 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
27
28
29
30
31
ruby
复制代码{
// red squiggle line appears here
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": "src",
// "types": ["webpack-env"],
"paths": {
"@/*": ["./*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": ["node_modules"],
"compileOnSave": false
}
  1. 紧接着进行配置你的 vue.config.json 配置 configureWebpack,我的是函数形式,如果你是对象形式可以自行修改
1
2
3
4
5
6
7
8
9
10
javascript
复制代码config.module.rules.push({
test: /.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/.vue$/], // ts-loader
configFile: path.resolve(__dirname, './tsconfig.json')
}
});
  1. .eslintrc.js 配置

  1. 在src文件夹下创建shims-vue.d.ts 文件,配置为
1
2
3
4
5
typescript
复制代码declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

安装并配置:
image.png

composion API用法

组合式 API (Composition API)

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 script setup>](https://cn.vuejs.org/api/sfc-script-setup.html) 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。
script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 JavaScript 函数——它自然可以访问与它一起声明的所有内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
count.value++
}

// 生命周期钩子
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
<button @click="increment">Count is: {{ count }}</button>
</template>

注意vue2.7和vue3的差异,其他语法composition api是通用的。
可以去官网看语法。

vue官方推荐使用sfc的方式构建应用

css的模块化引入,之前在sfc的方式,是通过给style标签加上scoped属性,实现样式的模块化。
而在jsx中,是无法使用scoped的用法,另外直接导入xxx.scss的方法会使得样式挂载在全局中,解决方案可以使用css module。
image.png

tsx vs sfc

优势

  1. 除了“标签语法”外,都是 JS 原生的语法,学习成本较小
  2. 同时意味着更灵活,可控性强,留给开发者全部的发挥空间
  3. 与 TS 完美结合
  4. 可以随着 ES、TS 的迭代升级自然进化,能力与开发体验都会不断提升
  5. 相较于 template 的 props、attrs、emits、slots 等概念,JSX 只有 props 一个概念,心智负担较小

劣势

  1. 部分人对于在 JS 里混入 HTML 语法表示很抗拒,感觉违背了逻辑与表现分离的原则
  2. 灵活意味着代码质量与程序员的水平强相关,也就是不好保证代码质量的下限
  3. 相应的,一些优化点需要程序员自己控制,比如 React 中的 useCallback、useMemo,相信能彻底弄明白的人不是多数
  4. 没有 template(SFC) 提供的 style 处理能力,如 scoped、module、状态驱动等

其他研发者的意见:
JSX vs. Vue Template(SFC) 如果使用 JSX,就要放弃一些 Vue Template 带来的特性,比如一些自带的性能优化、状态驱动的动态 CSS 等。需要像 React 一样手动去做这些处理,这也就意味着有可能会降低整体工程质量的下限,当然好处就是更加顺滑的 TS 体验。对于这些点的取舍,还要同学们根据实际情况自己判断。毕竟软件开发本质上,是一项寻求平衡的妥协的艺术。同样实现简单的父子组件通信:
1 tsx需要对emits和slots特殊处理,例如@click 在 TSX 中要变为 onClick,自定义 emit 也要由 @child-click 变为 onChildClick。
2 functional Component(即只有 props、emits、slots 传入的组件),可以完美的使用 TSX;
3 没有 props 和 emits 传入的组件(如本文的 Parent 组件),使用 TSX 还算可以接受,但是实际上组件内部没有太多的利用上 TS;
4 普通组件,尤其是带了 props 和 emits 的组件,用 TSX 形式实在是有点强人所难

因此,为了兼顾vue2的大部分同事的使用习惯和vue的特点,还是选择使用sfc比较方便。
(或者先使用sfc的方式,熟悉composion api的用法。在后续考虑tsx,tsx比较接近react的写法)

setup 中使用 vuex、vue-router

由于项目版本 vuex、vue-router 均为 v3,组合式 API 中,我们需要使用一些新的函数来代替访问 this等方法,如:this.$store、this.$router、this.$route。
解决方案:也用到了 getCurrentInstance,通过它封装一些方法使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { getCurrentInstance } from 'vue'

export function useStore() {
const { proxy } = getCurrentInstance()
const store = proxy.$store
return store
}
export function useRoute() {
const { proxy } = getCurrentInstance()
const route = proxy.$route
return route
}
export function useRouter() {
const { proxy } = getCurrentInstance()
const router = proxy.$router
return router
}

还有一些其他写法可以参考此 issue:github.com/vuejs/vue-r…
然而组合式 API 下 vuex 的mapState, mapGetters, mapActions 和 mapMutations 辅助函数依然是无法使用的。可以尝试vuex-composition-helpers这个库,帮助轻松使用 Vuex 和 Composition API。
vue-router方案二
使用 unplugin-vue-define-options
unplugin-vue-define-options
安装
npm i unplugin-vue-define-options -D

使用
vue.config.js中添加插件

1
2
3
4
5
module.export = {
configureWebpack(config) {
config.plugins.push(require("unplugin-vue-define-options/webpack")())
}
}
1
2
3
4
5
6
7
8
9
<script>
import { defineComponent } from "vue";

defineOptions({
beforeRouteEnter(to, from, next) {
// do somethings...
}
});
</script>

补充:关于 getCurrentInstance

注意:getCurrentInstance 作为访问内部组件实例的方法,官方是不鼓励在应用程序代码中使用的.

1
2
3
4
5
6
7
8
9
10
import { getCurrentInstance } from 'vue'

const MyComponent = {
setup() {
const internalInstance = getCurrentInstance()

internalInstance.appContext.config.globalProperties // access to globalProperties
}
}

并且 getCurrentInstance 只在 setup 或生命周期钩子中工作:

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
const MyComponent = {
setup() {
const internalInstance = getCurrentInstance() // works

const id = useComponentId() // works

const handleClick = () => {
getCurrentInstance() // doesn't work
useComponentId() // doesn't work

internalInstance // works
}

onMounted(() => {
getCurrentInstance() // works
})

return () =>
h(
'button',
{
onClick: handleClick
},
`uid: ${id}`
)
}
}

// also works if called on a composable
function useComponentId() {
return getCurrentInstance().uid
}