源宝导读:ERP平台的前端底层使用了Vue作为组件的基础架构,而使用了TypeScript语言进行组件的封装与开发。本文将简要介绍平台在使用TypeScript和Vue框架进行老功能重构时的经验总结。
一、背景
下面主要探讨是以下三个方面:
- 目前项目中使用到的vue+ts的哪些特性,还有哪些特性值得去使用,不会涉及到太多的ts语法知识;
- 老项目的迁移为ts,有哪些点需要改造;
- 各抒己见,探讨下各位都有哪些心得和见解。
二、为什么要用typescript
TypeScript简单介绍:
- 是 JavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。
- TypeScript 是 JavaScript 的超集,这意味着他支持所有的 JavaScript 语法。并在此之上对 JavaScript 添加了一些扩展,如 class / interface / module 等。这样会大大提升代码的可阅读性。
总结优势:
- 静态类型检查: 类型校验,能够避免许多低级代码错误;
- IDE 智能提示: 使用一个方法时,能清楚知道方法定义的参数和类型和返回值类型;使用一个对象时,只需要.就可以知道有哪些属性以及属性的类型;
- 代码重构: 经过不停的需求迭代,代码重构避免不了,在重构时,如果前期有清晰和规范的接口定义、类定义等,对于重构帮助很大;
- 规范性和可读性: 类似于强类型语言,有了合理的类型定义、接口定义等,对于代码实现的规范性和可读性都有很大提高,不然搜索整个项目这个方法在哪里调用、怎么定义等。
个人认为最有价值点:写代码前,会先构思功能需求的整体代码架构。
三、安装和起步
一般我们会面临两个情况:
- 新项目创建;
- 觉得ts不错,想将老项目切换为vue+ts。
3.1、新项目起步
- 安装vue-cli3.0;
- vue create vue-ts-hello-world;
- 选择Manually select features,勾选typescript。其他配置根据项目情况勾选。
3.2、老项目切换为vue+ts
- 安装ts依赖(或使用yarn);
- yarn add vue-class-component vue-property-decorator;
- yarn add ts-loader typescript tslint tslint-loader tslint-config-standard —dev。
- 配置 webpack,添加ts-loader和tslint-loader;
- 添加 tsconfig.json;
// 这是平台目前用的tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strictNullChecks": false,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"jest"
],
"paths": {
"@/*": [
"src/*"
],
// 别名追加
"components/*": [
"src/components/*"
],
},
"lib": [ // 编译过程中需要引入的库文件的列表
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules",
"ui-tests"
]
}
备注: ts 也可支持 es6 / es7,在 tsconfig.json中,添加对es6 / es7的支持。
"lib": [
"dom",
"es5",
"es6",
"es7",
"es2015.promise"
]
- 添加 tslint.json 或者 prettierrc(可以视情况而定)。
// 目前平台使用的是.prettierrc.js
module.exports = {
"$schema": "http://json.schemastore.org/prettierrc",
"singleQuote": true,
"endOfLine": "auto",
"semi": false
}
- 让 ts 识别 .vue。
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
- 而在代码中导入 .vue 文件的时候,需要写上 .vue 后缀。原因还是因为 TypeScript 默认只识别 .ts 文件,不识别 *.vue 文件。
- 添加vue-shim.d.ts,让vue文件给vue模块来编译。
- 改造 .vue文件,将vue中script切换为
<script lang="ts">;
- 改造.js文件,修改为ts语法,定义类型等。
四、vue+ts常用的装饰器
这里主要用到了vue-property-decorator,这个是在vue-class-component基础上做了一层增强,新增了一些装饰器,使用更加便捷。这里只分享一些常用的,对于老项目改写vue文件很有用:
4.1、@Component
标识该vue文件是一个组件,并且可以引入其他组件。
非ts版本:
import MyComponent from '@/components/MyComponent'
export default {
components: {
MyComponent
}
}
ts版本:
import { Vue, Component } from 'vue-property-decorator'
import MyComponent from '@/components/MyComponent'
@Component({
components: {
MyComponent
}
})
export default class YourComponent extends Vue {
}
备注:这里不管有没有引入其他组件,都必须要使用@Component,目的是为了注册这个组件。否则在其他组件各种莫名其妙的问题。比如:路由找不到组件,而且不会报错。
4.2、@Prop
非ts版本:
export default {
props: {
propA: {
type: Number
},
propB: {
default: 'default value'
},
propC: {
type: [String, Boolean]
},
propD: {
type: Object,
default: () => {},
validator(val: object) {
return val.prop = '1'
}
}
}
}
ts版本:
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Prop(Number)
readonly propA: number | undefined
@Prop({ default: 'default value' })
readonly propB!: string
@Prop([String, Boolean])
readonly propC: string | boolean | undefined
// 也可以一起
@Prop({type: Object, default: () => {},
validator(val: object) {
return val.prop = '1'
}
})
readonly propD!: object // 只是举例,一般会定义一个interface
}
4.3、@Watch
非ts版本:
export default {
watch: {
child: {
handler: 'onChildChanged',
immediate: false,
deep: false
},
person: [
{
handler: 'onPersonChanged1',
immediate: true,
deep: true
},
{
handler: 'onPersonChanged2',
immediate: false,
deep: false
}
]
},
methods: {
onChildChanged(val, oldVal) {},
onPersonChanged1(val, oldVal) {},
onPersonChanged2(val, oldVal) {}
}
}
ts版本:
import { Vue, Component, Watch } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Watch('child')
onChildChanged(val: string, oldVal: string) {}
@Watch('person', { immediate: true, deep: true })
onPersonChanged1(val: Person, oldVal: Person) {}
@Watch('person')
onPersonChanged2(val: Person, oldVal: Person) {}
}
4.4、@Provide和@Inject
场景:一般用于父级嵌套比较深的子孙vue组件,但是数据不是很方便传到深层级vue组件中,利用树型结构组件。
非ts版本:
// 父组件
provide () {
return {
OptionGroup: this
}
}
// 子孙组件
inject: ['OptionGroup']
ts版本:
父组件:
@Provide()
getObj () {
return this
}
子孙组件:
@Inject() getObj!: any
get obj() {
return this.getObj()
}
Privide的弊端:
- 依赖注入它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难;
- 同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。
建议:
一般不推荐过度使用。
- provide 和 inject的绑定并不是可响应的,这是刻意为之的。但是,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的;
- 如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像
Vuex
这样真正的状态管理方案了。
4.5、@Ref
非ts版本:
export default {
computed() {
anotherComponent () {
return this.$refs.anotherComponent
},
button () {
return this.$refs.aButton
}
}
}
ts版本:
import { Vue, Component, Ref } from 'vue-property-decorator'
import AnotherComponent from '@/Components/another-component.vue'
@Component
export default class YourComponent extends Vue {
@Ref() readonly anotherComponent!: AnotherComponent
@Ref('aButton') readonly button!: HTMLButtonElement
// 我们目前是这样使用的
$refs!: {
popover: any
search: HcProjectSelectSearch
tree: HcProjectTree
}
}
4.6、@Emit
用的很少,参数和时机不是很好控制。
非ts版本:
export default {
methods: {
handleClick(e) {
this.$emit('click', e)
},
loadData() {
const promise = new Promise(resolve => {
setTimeout(() => {
resolve(20)
}, 0)
})
promise.then(value => {
this.$emit('load', value)
})
}
}
}
ts版本:
import { Vue, Component, Emit } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Emit('click')
handleClick(e) {
// todo
}
@Emit()
promise() {
return new Promise(resolve => {
setTimeout(() => {
resolve(20)
}, 0)
})
}
}
五、mixin改写
定义mixin:
export const cusMixin = {
mounted() {
this.$refs = {}
// $0 instanceof HTMLElement
// this.$refs = {}
console.log('mixin mounted')
},
beforeUpdate() {
this.$refs = {}
// console.log('global mounted')
},
updated() {
this.$refs = {}
// console.log('global mounted')
}
}
引入mixin:
import { Vue, Component } from 'vue-property-decorator'
import cusMixin from '@/mixin'
@Component({
components: {},
mixins: [cusMixin]
})
export default class YourComponent extends Vue {}
// 或者尝试使用
import { Component, Mixins, Vue } from 'vue-property-decorator';
import { MyOtherMixin } from './MyOtherMixin';
@Component
export class MyMixin extends Vue {
private created() {
console.log('what?');
}
}
@Component
// 继承多个mixin,使用数组 [MyMixin, MyOtherMixin]
export default class App extends Mixins(MyMixin) {
private test = "test";
private laowang = 'laowang';
created() {
console.log(this.test)
console.log(this.Kitchen)
console.log(this.Tv)
}
}
六、vue识别全局的方法和变量
- vue-shim.d.ts文件中,增加如下代码:
import Vue from 'vue'
import VueRouter, { Route } from 'vue-router'
import { Store } from 'vuex'
// 声明全局方法
declare module 'vue/types/vue' {
interface Vue {
// 内部变量
$router: VueRouter;
$route: Route;
$store: Store<any>;
// element-ui等组件
$Message: any
$Modal: any
// 自定义挂载到Vue.prototype上的变量
$api: any
$mock: any
$configs: any
}
}
七、vuex的改写
关于store的改造,配置和结构和原来一样,具体编码设计没有特定套路,根据项目具体设计改写为ts的语法。
主要是关于ts在vue如何使用,目前主流的方案是vue-class-component + vuex-class,一般常用的mapGetters和mapActions改写:
yarn add vuex-class
非ts版本:
import { mapGetters, mapActions } from 'vuex'
export default Vue.extend({
computed: {
...mapGetters({
'name',
'age'
})
},
methods: {
...mapActions([
'setNameAction'
])
}
})
ts版本:
import { Vue, Component } from 'vue-property-decorator'
import { Getter, Action } from 'vuex-class'
import { Test } from '@/store'
export default class YourComponent extends Vue {
@Getter('name') name: string
@Getter('age') age: number
@Action('setNameAction') setNameAction: Function
get innerName (): string {
return this.name
}
get innerAge (): number {
return this.age
}
setName (name: string) {
this.setNameAction(products)
}
}
备注:tsconfig.json需要调整下:
{
"compilerOptions": {
// 启用 vue-class-component 及 vuex-class 需要开启此选项
"experimentalDecorators": true,
// 启用 vuex-class 需要开启此选项
"strictFunctionTypes": false
}
}
八、vue render jsx语法改写
改写的原理还是和上面类似,都是借助目前流行的两个库,除了使用vue-property-decorator以外,还需要借助vue-tsx-support,vue-tsx-support是在Vue外面包装了一层,将prop、event等以泛型的方式加了一层ts接口定义传了进去,目的是为了防止ts的类检查报错。
- 步骤:
- 引入
yarn add vue-tsx-support --dev;
- 导入ts声明,在main,ts中
import "vue-tsx-support/enable-check";
- 在
vue.config.js
中extensions
添加.tsx。
- 使用:
import { Component, Prop } from "vue-property-decorator";
import * as tsx from "vue-tsx-support";
interface YourComponentsProps {
name?: string
age?: number
}
@Component
export default class YourComponents extends tsx.Component<YourComponentsProps> {
@Prop() public name!: string;
@Prop() public age!: number;
protected render() {
return (
<div>
<h1>姓名:{this.name}</h1>
<h1>年龄:{this.age}</h1>
</div>
);
}
}
这里jsx改写为tsx大致简单了解下,如果大家有兴趣,以后可以一起学习探讨下。
九、思考
- 关于老项目ts的改造,如何才能平滑过渡,不影响现有的功能。
- 在vue中ts的实践,数据、视图、控制器分层设计的问题。
------ END ------
作者简介
罗同学: 研发工程师,目前负责ERP建模平台的设计与开发工作。