TypeScript入门(与VUE2集成)

前言

想用 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

打开项目文件夹

1
tsc -init

编译

1
tsc ts01.ts

监听编译

1
tsc -w

监听编译某个文件

1
tsc -w ts01.ts

修改编译生成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;

这个示例中我们可以发现元组的几个问题:

  1. 虽然长度固定,但是我们可以push元素,使之长度超过定义的长度,不会报错。但是根据下标取值的时候不能超过定义时的长度。
  2. push超出长度,转换的js是能够正常运行的,并且打印结果也是包含超出长度的元素,所以不建议通过push添加元素,建议通过下标设置。
  3. push的时候数据类型可以是定义的时候所包含的类型,不能是其它类型。
  4. 根据下标赋值时类型必须和定义的时候一样。
  5. 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
// shims-tsx.d.ts  src目录下
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
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
// shims-vue.d.ts   src目录下
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

//axios报错
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
// this.count += n; this.$emit('add-to-count', n)
@Emit("add-to-count")
addToCount(n: number) {
this.count += n;
}

// this.count = 0; this.$emit('reset');
@Emit("reset")
resetCount() {
this.count = 0;
}

// this.$emit('return-value', 10)
@Emit('return-value')
returnValue() {
return 10;
}

// 相当于 this.$emit('on-input-change', num+1, num)
@Emit("on-input-change")
onInputChange(num: number) {
return num + 1;
}

// 相当于
// const promise = new Promise((resolve) => {
// setTimeout(() => {
// resolve(20)
// }, 0)
// })
//
// promise.then((value) => {
// this.$emit('promise', value)
// })
@Emit()
promise() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(20);
}, 0);
});
}

从上面的示例可以看出

方法名就是$emit的第一个参数,除非设置了别名。

方法的返回值为$emit第二个参数。

方法的传参是$emit的第三个参数,如果方法没有返回值,则为第二个参数。

执行顺序为先执行方法体内的代码,再$emit