前言
想用 TS 建议升级成 Vue3。不建议 Vue2 使用 TS,体验并不好。
尤大在 Vue 3.2 发布的时候已经在微博给出了最佳实践的解决方案:
<script setup> + TS + Volar = 真香
Volar 是个 VS Code 的插件,其最大的作用就是解决了 template 的 TS 提示问题。
注意
使用它时,要先移除 Vetur,以避免造成冲突。
<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 script 语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 Typescript 声明 props 和发出事件。
- 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
详见官方文档 单文件组件
安装
安装
1
| npm install -g typescript
|
打开项目文件夹
编译
监听编译
监听编译某个文件
修改编译生成JS的位置和目标语法
打开tsconfig.json,修改outDir的值,并解除注释
1 2 3 4 5 6
| { "compilerOptions": { "target": "es5", "outDir": "./js/" } }
|
默认会转成ES6,这里建议转换为ES3或ES5来兼容大多数浏览器。
建议转换为es5,与 Vue 的浏览器支持保持一致。
语法
数据类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| let num: number = 1; let str: string = "hello"; let b: boolean = true; let n: null = null; let un: undefined = undefined; let f: any = 1;
console.log("typeof(str):"+typeof(str))
let arr1: Array<number> = [1, 2, 3]; let arr2: number[] = [1, 2, 3]; let arr3: string[] = ["aa", "bb", "cc"]; let arr4: Array<number | string> = [1, "aa", 3];
let tup: [string, string, number] = ['Dylan', 'male', 23]; console.log("typeof(tup):"+typeof(tup))
|
数组和元组的区别?
元组可以理解为一个固定长度,每一项元素类型都确定的数组。
我们看一个示例
1 2 3
| let tup: [string, string, number] = ['Dylan', 'male', 23]; tup.push("123"); tup[3] = 123;
|
示例2
1 2 3
| let tup: [string, string, number] = ['Dylan', 'male', 23]; tup.pop(); tup[2] = 456;
|
这个示例中我们可以发现元组的几个问题:
- 虽然长度固定,但是我们可以push元素,使之长度超过定义的长度,不会报错。但是根据下标取值的时候不能超过定义时的长度。
- push超出长度,转换的js是能够正常运行的,并且打印结果也是包含超出长度的元素,所以不建议通过push添加元素,建议通过下标设置。
- push的时候数据类型可以是定义的时候所包含的类型,不能是其它类型。
- 根据下标赋值时类型必须和定义的时候一样。
- pop删除元素后,我们依旧可以通过下标赋值。
类
接口 类 接口的实现 类的集成 与 方法重载
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 38 39 40 41 42 43 44 45 46
| interface Person { run():void; }
class Men implements Person { name: string; age: number;
constructor(name:string,age:number) { this.name = name; this.age = age; } run(): void { console.log("run"); }
talk(): void; talk(str:string): void; talk(num:number): void; talk(str:string,num:number): void; talk(par1?:any,par2?:any) { if (par1 == undefined && par2 == undefined) { console.log("method 1"); } else if (par2 == undefined) { if (typeof par1 == "string") { console.log("method 2"); } else if (typeof par1 == "number") { console.log("method 3"); } } else { console.log("method 4"); } } }
class SuperMen extends Men{
}
let men = new Men("小明", 18); men.talk(); men.talk("小明"); men.talk(18); men.talk("小明",18);
|
结果可以看到
1 2 3 4
| method 1 method 2 method 3 method 4
|
VUE2集成TS
新项目创建时可以直接选择ts,这里主要说旧项目升级的情况。
Vue的TS封装库
vue-class-component
vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化
vue-property-decorator
vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器
注意
这两个都要装,而不是只装vue-property-decorator。
安装依赖
1 2
| npm i vue-class-component vue-property-decorator --save npm i ts-loader typescript tslint tslint-loader tslint-config-standard -D
|
配置vue.config.js添加下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| module.exports = { configureWebpack: { resolve: { extensions: [".ts", ".tsx", ".js", ".json"] }, module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, enforce: 'pre', loader: 'tslint-loader' }, { test: /\.tsx?$/, loader: 'ts-loader', exclude: /node_modules/, options: { appendTsSuffixTo: [/\.vue$/], } } ] } }, }
|
新建tsconfig.json放在项目根目录
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 38 39 40 41
| { "compilerOptions": { "allowJs": true, "target": "esnext", "module": "esnext", "strict": true, "jsx": "preserve", "importHelpers": true, "moduleResolution": "node", "experimentalDecorators": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "baseUrl": ".", "types": [ "webpack-env" ], "paths": { "@/*": [ "src/*" ] }, "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ] }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx" ], "exclude": [ "node_modules" ] }
|
在src根目录下添加两个TS文件
新建shims-tsx.d.ts
1 2 3 4 5 6 7 8 9 10 11 12 13
| import Vue, { VNode } from 'vue'; declare global { namespace JSX { interface Element extends VNode {} interface ElementClass extends Vue {} interface IntrinsicElements { [elem: string]: any; } } }
|
新建shims-vue.d.ts
由于 TypeScript 默认并不支持 *.vue 后缀的文件,所以在 vue 项目中引入的时候需要创建一个shims-vue.d.ts 文件,放在项目项目对应使用目录下,例如 src/shims-vue.d.ts,用来支持*.vue 后缀的文件,没有这个文件,会发现 import 导入的所有.vue类型的文件都会报错。
1 2 3 4 5 6 7 8 9 10 11 12
| declare module '*.vue' { import Vue from 'vue'; export default Vue; }
declare module 'axios' { interface AxiosInstance { (config: AxiosRequestConfig): Promise<any> } }
|
添加tslint.json
1 2 3 4 5 6
| { "extends": "tslint-config-standard", "globals": { "require": true } }
|
main.js改成main.ts配置vue.config.js的入口为main.ts
1 2 3 4 5
| pages: { index: { entry: 'src/main.ts', } },
|
安装@typescript-eslint/parser
将eslint校验改成@typescript-eslint/parser
1
| npm install @typescript-eslint/parser --save
|
更改.eslintrc.js
1 2 3
| parserOptions: { parser: '@typescript-eslint/parser' },
|
代码格式
新旧写法对比
原写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script> export default { name: 'xx', components: {}, props: {}, data() { return {}; }, computed:{}, watch:{}, mounted() {}, methods: {} }; </script>
|
新写法(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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <template> <div class="home"> <HelloWorld :msg="message" /> {{ computedMsg }} </div> </template>
<script lang="ts"> import { Component, Prop, Vue, Watch, Emit } from "vue-property-decorator"; import HelloWorld from "@/components/HelloWorld.vue";
@Component({ components: { HelloWorld, }, }) export default class Home extends Vue { // props @Prop({ type: Number, default: 1, required: false, }) propA?: number;
@Prop() propB?: string;
// data public message = "你好 TS";
// computed public get computedMsg(): string { return "这里是计算属性" + this.message; }
// watch属性 @Watch("propA", { deep: true, }) public test(newValue: string, oldValue: string): void { console.log("propA值改变了" + newValue + " oldValue:" + oldValue); }
// $emit // 原来写法:this.$emit('mysend',this.message) // 现在直接写 this.bindSend() @Emit("mysend") bindSend(): string { return this.message; }
// mounted 生命周期钩子函数 mounted(): void { console.info("mounted"); this.mymethod(); }
// methods中的方法 public mymethod(): void { this.bindSend(); } } </script>
|
$emit
对比
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 38 39 40 41 42 43
| count = 0
@Emit("add-to-count") addToCount(n: number) { this.count += n; }
@Emit("reset") resetCount() { this.count = 0; }
@Emit('return-value') returnValue() { return 10; }
@Emit("on-input-change") onInputChange(num: number) { return num + 1; }
@Emit() promise() { return new Promise((resolve) => { setTimeout(() => { resolve(20); }, 0); }); }
|
从上面的示例可以看出
方法名就是$emit的第一个参数,除非设置了别名。
方法的返回值为$emit第二个参数。
方法的传参是$emit的第三个参数,如果方法没有返回值,则为第二个参数。
执行顺序为先执行方法体内的代码,再$emit。