序言
Vue2 的项目写了几年,基本都是使用 js 的方式去书写。我发现随着项目的壮大这其中会有很多问题影响着项目的后续开发和维护,
比如:
文件的跳转
文件(组件间)的关系难以一次性的理清,尤其是新人进入的项目,如果通过当前的代码在一个庞大的项目里去翻找特定的文件,想必是很痛苦的(查找引用关系,还有概率遇到重名的文件需要自行辨别),但由于现在的 sfc 文件和 VSCode 的 vetur 插件并不是很完美做到文件跳转。如果此时能做到一键跳转,岂不是减少不必要的查找时间,又能理清楚文件(组件)间的关系,减少许多心智成本。
类型提示
虽然现在 Vetur 和 Volar 这两款 VSCode 插件已经能做到简单的 props 和 event 的提示(这需要一定的前提,比如:jsconfig 或 tsconfig 做好路径别名的配置),但是遇到 attributes 和 event 的透传还是有心无力的。提示依然会有不准确的情况。(即:any)
最后,我发现使用 TS 的方式去编写项目可以完美的避免以上的问题。到现在 2023 年,vue2、3 都已经完全支持 TS,类型提示妥妥的没问题,然后 VSCode 又天生支持 TS 文件识别,文件跳转更不是问题。
Vue 项目使用 TS 开发的一些历程
早期 Vue2 并不是用 TS 做类型检查的,而是用的 Flow (Facebook 出的类型检查工具,现在已经完败给 TS 了)。当时想在 Vue2 用 TS 还是挺麻烦的,因为没有官方的声明文件,需要自行在项目里加上。
现在 Vue2 已经完全支持 TS ,尤其是 2.7 支持了 composition API,可以完美支持 TS 了
Vue2 的 Vue.extend()
劣势:由于 object-based 的设计,在运行时需要 merge options,然后再进行 mixins 合并,以及原型链的操作,导致类型提示不稳定。其表现在 this 的指向不确定,需要通过自行手动写相应的声明文件去保证能有正常的类型提示,非常繁琐。
截止到现在 2023 年, Vetur 和 Volar 都已经可以辅助生成类型声明。已把上述的问题解决。
因为 Volar 自身有一个 language server 去进行代码读取并最后生成类型声明,供用户使用。其实就是预先执行分析结果。
vue-class-component
这是一个 Vue2 到 Vue3 前的过渡方案。由于上述的 this 指针不稳定,且 VSCode 插件的代码提示功能还未完成,那么 class component 是一种不错的项目 TS 化的方案。将所有 vue options 通通 flat 拍平 然后用 TSX 的方式去编写项目,就可以获得完全的 TS 体验。
相关技术库
- vue-class-component // 这是官方提供的 class component 实现的工具库
- vue-property-decorator // 这是第三方基于 vue-class-component 封装的装饰器库,从操作性上比官方要轻松,且更符合逻辑直觉。
一些用法:(这里将直接使用 vue-property-decorator )
使用 SFC(.vue) 开发组件
<!-- 不完全展示,但已囊该大部分开发场景 -->
<template>
<div>
<div>Home 组件是 vue-class-component</div>
<HelloWorld></HelloWorld>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator";
import HelloWorld from "@/components/HelloWorld.vue";
// 注册组件
@Component({
components: {
HelloWorld
}
})
export default class HomeView extends Vue {
// 该私有变量已经具有响应式。再也不用写一个 data 函数了
// 注意:该写法还处于实验性阶段,需要 babel 辅助。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes#%E5%AD%97%E6%AE%B5%E5%A3%B0%E6%98%8E
msg = "hello world";
// 使用 装饰器 解放双手!
@Watch("msg")
onMsgChanged(newVal: any, oldVal: any) {
console.log(newVal, oldVal);
}
// 这是继承 Vue 类的成员声明周期方法。
mounted() {
console.log("mounted");
}
}
</script>
<style scoped></style>
以上这种写法,比起 options API 他的层级更少,配合装饰器的使用,代码的直观性更好,this 指针更符合直觉。
TSX
要是喜欢用 TSX 的老铁,直接把 script 部分 copy 到 tsx 即可!
这里会衍生一个新的问题,就是 tsx 的样式没有自动化隔离,全都以全局的方式展示,这会导致一系列的样式污染。
那么 TSX 文件下如何加载隔离样式?
css module 方案。
通过引入一个 style 变量的方式,赋值于动态 class 属性。使其样式能够被隔离。
具体操作:需要分以下几种构建方式去使用。
- webpack
只需要调整一下 vue-loader 的配置即可。
https://vue-loader.vuejs.org/zh/guide/css-modules.html#css-modules
- vue-cli
关键的 vue.config.js 的配置
https://cli.vuejs.org/zh/config/#css-requiremoduleextension 该配置默认开启,即:你必须写 .module 作为辨识
https://cli.vuejs.org/zh/guide/css.html#css-modules
默认已经配置好,只需要把相关的 css 文件(css,scss,less,stylus)后缀前加一个 .module 即可
比如:xxx.module.scss
import { Component, Vue } from "vue-property-decorator";
// 这里有可能会有 编辑器报错 。
import style from "./CompA.module.scss";
@Component
export default class CompA extends Vue {
msg: string = "TSX Components";
render() {
return <div class={style.msg}> {this.msg} </div>;
}
mounted() {
// 这里会打印一个 style 的 json 对象。
// {"msg": "o0HKb7FvIIwldriac7I2"}
console.log(style);
}
}
如果编辑器报错了,你还需要手动在 shims-vue.d.ts 这个文件中添加声明。
declare module "*.scss" {
const classes: { readonly [key: string]: string };
export default classes;
}
这里使用默认的配置会有一些小问题,这里的类名辨识度较低,默认就一个 hash 值,在浏览器上还很不好找到这个类名
具体配置:
// vue.config.js
module.exports = {
css: {
loaderOptions: {
css: {
modules: {
// 详细配置在这里:https://github.com/webpack/loader-utils#interpolatename
localIdentName: "[name]-[hash]"
}
}
}
}
};
总结:真的不如不如 SFC 的样式编写。(哎)
css 类名提示 TBD,没搞成功
- vite(TBD)
composition API
Vue3 与 TS 高度契合的搭配方式。在 volar 的加持下,搭配 composable 函数,轻松实现逻辑整合统一化。不像 options API ,一旦文件体积大了。模型层、逻辑层 就会被散成一片,每次管理一个几千行代码的 SFC 文件时,我们会为此苦恼,总是来回的切换 模型层、逻辑层 这两大块代码,composition API 的出现,能让我们更好的关注关联性高的代码。
https://vuejs.org/guide/extras/composition-api-faq.html 官方解释文档
TBD: 我真的不知道该怎么写好?我对 composition API 的实践太少了
这里 TS 的出现,我认为更多是配合 composable 函数,前提是这个函数封装得不错 (比如:VueUse 这个库),重复动作的封装和类型的提示,在 setup script 下的使用,是一种全新的编程体验。
其实,这就是 hooks ,函数式编程。
在我看来,hooks 更多是让用户忘记掉 this 指针(相较于 class-component ),能更好的把身心投放到直观的编程当中(逻辑,状态等),而不是去猜 this 指的是什么?
- 通过 defineComponent() 创建 vue 实例去编写 tsx 代码。
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const msg = ref("TSX Component");
return () => {
return <div>{msg.value}</div>;
};
}
});
- 还有 vue2 和 vue3 的混合体。(vue2 项目过渡到 vue3 的一种重构写法)
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const msg = ref("TSX Component");
return {
msg
};
},
render() {
return <div>{this.msg}</div>;
}
});
统统写在一个 setup 函数里,视图、数据模型、逻辑能更好的组织在一起,这是我对这种代码的看法。灵活度最高!
class-component 和 hooks 对比
维度 | class-component | hooks |
---|---|---|
书写体验 | 面向对象,适合绝大部分人,它很直观易懂 | 组合式函数,可以把灵活度拉到最高,刨除令人疑惑的 this |
状态管理与复用 | 需要在成员属性上先确定,但到了后期会变得很庞大,维护成本提升。状态复用通过 mixins ,后期维护是一个噩梦 | 手动声明式的引入需要的状态,并可以更好的去组织逻辑 |
但貌似,React 和 Vue 团队都更倾向于转 hooks ,这是为什么呢?
经过了解,class component 并不能很好的做状态复用,我们通过 mixins 的方式去复用 状态、方法 ,关于 mixins 的弊端如下:
- 命名冲突:如果多个 mixins 混合进同一个组件中,可能会出现命名冲突的问题,因为不同的 mixins 可能会定义相同的属性或方法。
- 神秘依赖:使用 mixins 可能会导致组件的依赖关系变得不明确,因为你可能不知道 mixins 中使用了哪些其他组件或库。
- 全局污染:mixins 中的属性和方法会被添加到组件中,这可能会导致全局污染的问题,因为它们可能会与其他组件或全局命名空间中的变量或方法发生冲突。
- 调试困难:使用 mixins 可能会使调试变得更加困难,因为你不知道组件的属性和方法是来自哪里,特别是当 mixins 中的属性和方法与组件内部的属性和方法发生冲突时。
综上所述,mixins 是一种弊大于利的解决方案,然后就是推进 hooks 的发展了。
总结
SFC 还是 TSX 好?
我觉得这还是得看你的编程喜好。我觉得 tsx 属于最灵活极端的一类。而 sfc 是符合中庸之道的,在灵活和用户体验性上做到两者权衡。
20230507 新增:
vue3 setup() 不返回响应式数据,将无法使用 vue-dev-tool 调试。这个很影响使用体验,vue3 使用 tsx 将毫无优势可言。
tsx 可以通过 Parent.component 的方式去引用组件
参考文章
https://blog.csdn.net/u011590754/article/details/120333953 > https://samarthnehe.medium.com/react-hooks-vs-class-components-c344b59f3bc
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!