TypeScript


资源


  1. TypeScript参考
  2. Vue+TypeScript
  3. 代码分支:web22-ts

知识点


  1. ts核心语法
  2. ts+vue
  3. 装饰器应用
  4. 装饰器原理
  5. vue-property-decorator源码解析

准备工作

新建一个基于ts的vue项目

image.png

在已存在项目中安装typescript

  1. vue add @vue/typescript

image.png

请暂时忽略引发的几处Error,它们不会影响项目运行,我们将在后面处理它们。

TS特点

  • 类型注解、类型检测
  • 接口
  • 泛型
  • 装饰器
  • 类型声明

类型注解和编译时类型检查

使用类型注解约束变量类型,编译器可以做静态类型检查,使程序更加健壮

类型基础
  1. // ts-test.ts
  2. let var1: string; // 类型注解
  3. var1 = "开课吧"; // 正确
  4. var1 = 4 ; // 错误
  5. // 编译器类型推断可省略这个语法
  6. let var2 = true;
  7. // 常⻅原始类型: string,number,boolean,undefined,null,symbol
  8. // 类型数组
  9. let arr: string[];
  10. arr = ['Tom']; // 或Array<string>
  11. // 任意类型any
  12. let varAny: any;
  13. varAny = 'xx';
  14. varAny = 3 ;
  15. // any类型也可用于数组
  16. let arrAny: any[];
  17. arrAny = [ 1 , true, "free"];
  18. arrAny[ 1 ] = 100 ;
  19. // 函数中的类型约束
  20. function greet(person: string): string {
  21. return 'hello, ' + person;
  22. }
  23. // void类型,常用于没有返回值的函数
  24. function warn(): void {}

范例,HelloWorld.vue

  1. <template>
  2. <div>
  3. <ul>
  4. <li v-for="feature in features" :key="feature">{{feature}}</li>
  5. </ul>
  6. </div>
  7. </template>
  8. <script lang='ts'>
  9. import { Component, Prop, Vue } from "vue-property-decorator";
  10. @Component
  11. export default class Hello extends Vue {
  12. features: string[] = ["类型注解", "编译型语言"];
  13. }
  14. </script>

类型别名

使用类型别名自定义类型

  1. // 可以用下面这样方式定义对象类型
  2. const objType: { foo: string, bar: string }
  3. // 使用type定义类型别名,使用更便捷,还能复用
  4. type Foobar = { foo: string, bar: string }
  5. const aliasType: Foobar

范例:使用类型别名定义Feature,types/index.ts

  1. export type Feature = {
  2. id: number,
  3. name: string
  4. }

使用自定义类型,HelloWorld.vue

  1. <template>
  2. <div>
  3. <!--修改模板-->
  4. <li v-for="feature in features" :key="feature.id">{{feature.name}}</li>
  5. </div>
  6. </template>
  7. <script lang='ts'>
  8. // 导入接口
  9. import { Feature } from "@/types";
  10. @Component
  11. export default class Hello extends Vue {
  12. // 修改数据结构
  13. features: Feature[] = [{ id: 1, name: "类型注解" }];
  14. }
  15. </script>

联合类型

希望某个变量或参数的类型是多种类型其中之一

  1. let union: string | number;
  2. union = '1'; // ok
  3. union = 1 ; // ok

交叉类型

想要定义某种由多种类型合并而成的类型使用交叉类型

  1. type First = {first: number};
  2. type Second = {second: number};
  3. // FirstAndSecond将同时拥有属性first和second
  4. type FirstAndSecond = First & Second;

范例:利用交叉类型给Feature添加一个selected属性

  1. // types/index.ts
  2. type Select = {
  3. selected: boolean
  4. }
  5. export type FeatureSelect = Feature & Select

使用这个FeatureSelect,HelloWorld.vue

  1. features: FeatureSelect[] = [
  2. { id: 1 , name: "类型注解", selected: false },
  3. { id: 2 , name: "编译型语言", selected: true }
  4. ];
  5. <li :class="{selected: feature.selected}">{{feature.name}}</li>
  6. .selected {
  7. background-color: rgb( 168 , 212 , 247 );
  8. }

函数

必填参:参数一旦声明,就要求传递,且类型需符合

  1. // 02-function.ts
  2. function greeting(person: string): string {
  3. return "Hello, " + person;
  4. }
  5. greeting('tom')

可选参数:参数名后面加上问号,变成可选参数

  1. function greeting(person: string, msg?: string): string {
  2. return "Hello, " + person;
  3. }

默认值

  1. function greeting(person: string, msg = ''): string {
  2. return "Hello, " + person;
  3. }

*函数重载:以参数数量或类型区分多个同名函数

  1. // 重载 1
  2. function watch (cb1: () => void): void;
  3. // 重载 2
  4. function watch (cb1: () => void, cb2: (v1: any, v2: any) => void): void;
  5. // 实现
  6. function watch (cb1: () => void, cb2?: (v1: any, v2: any) => void) {
  7. if (cb1 && cb2) {
  8. console.log('执行watch重载2');
  9. } else {
  10. console.log('执行watch重载1');
  11. }
  12. }

范例:新增特性,Hello.vue

  1. <div>
  2. <input type="text" placeholder="输入新特性" @keyup.enter="addFeature">
  3. </div>
  1. addFeature(e: KeyboardEvent) {
  2. // e.target是EventTarget类型,需要断言为HTMLInputElement
  3. const inp = e.target as HTMLInputElement;
  4. const feature: FeatureSelect = {
  5. id: this.features.length + 1,
  6. name: inp.value,
  7. selected: false
  8. }
  9. this.features.push(feature);
  10. inp.value = "";
  11. }

范例:生命周期钩子,Hello.vue

  1. created() {
  2. this.features = [{ id: 1 , name: "类型注解" }];
  3. }

class的特性

ts中的类和es6中大体相同,这里重点关注ts带来的访问控制等特性

  1. // 03-class.ts
  2. class Parent {
  3. private _foo = "foo"; // 私有属性,不能在类的外部访问
  4. protected bar = "bar"; // 保护属性,可以在子类中访问
  5. // 参数属性:构造函数参数加修饰符,能够定义为成员属性
  6. constructor (public tua = "tua") { }
  7. // 方法也有修饰符
  8. private someMethod () { }
  9. // 存取器:属性方式访问,可添加额外逻辑,控制读写性
  10. get foo () {
  11. return this._foo;
  12. }
  13. set foo (val) {
  14. this._foo = val;
  15. }
  16. }

范例:利用getter设置计算属性,Hello.vue

  1. <template>
  2. <li>特性数量:{{count}}</li>
  3. </template>
  4. <script lang="ts">
  5. export default class HelloWorld extends Vue {
  6. // 定义getter作为计算属性
  7. get count () {
  8. return this.features.length;
  9. }
  10. }
  11. </script>

接口

接口仅约束结构,不要求实现,使用更简单

  1. // 04-interface
  2. // Person接口定义了解构
  3. interface Person {
  4. firstName: string;
  5. lastName: string;
  6. }
  7. // greeting函数通过Person接口约束参数解构
  8. function greeting (person: Person) {
  9. return 'Hello, ' + person.firstName + ' ' + person.lastName;
  10. }
  11. greeting({ firstName: 'Jane', lastName: 'User' }); // 正确
  12. greeting({ firstName: 'Jane' }); // 错误

范例:Feature也可用接口形式约束,./types/index.ts

  1. // 接口中只需定义结构,不需要初始化
  2. export interface Feature {
  3. id: number;
  4. name: string;
  5. }

Interface vs type aliases

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定
类型的一种特性。以此 增加代码通用性

  1. // 不用泛型
  2. // interface Result {
  3. // ok: 0 | 1;
  4. // data: Feature[];
  5. // }
  6. // 使用泛型
  7. interface Result<T> {
  8. ok: 0 | 1;
  9. data: T;
  10. }
  11. // 泛型方法
  12. function getResult<T> (data: T): Result<T> {
  13. return { ok: 1, data };
  14. }
  15. // 用尖括号方式指定T为string
  16. getResult < string > ('hello')
  17. // 用类型推断指定T为number
  18. getResult(1)

泛型优点:

  • 函数和类可以支持多种类型,更加通用
  • 不必编写多条重载,冗⻓联合类型,可读性好
  • 灵活控制类型约束

不仅通用且能灵活控制,泛型被广泛用于通用库的编写。

范例:用axios获取数据
安装axios:npm i axios -S

配置一个模拟接口,vue.config.js

  1. module.exports = {
  2. devServer: {
  3. before (app) {
  4. app.get('/api/list', (req, res) => {
  5. res.json([
  6. { id: 1, name: "类型注解", version: "2.0" },
  7. { id: 2, name: "编译型语言", version: "1.0" }
  8. ])
  9. })
  10. }
  11. }
  12. }

使用接口,HelloWorld.vue

  1. async mounted() {
  2. console.log("HelloWorld");
  3. const resp = await axios.get < FeatureSelect[] > ('/api/list')
  4. this.features = resp.data
  5. }

声明文件

使用ts开发时如果要使用 第三方js库 的同时还想利用ts诸如类型检查等特性就需要声明文件,类似xx.d.ts

同时,vue项目中还可以在shims-vue.d.ts中对已存在 模块进行补充
npm i @types/xxx

范例:利用模块补充$axios属性到Vue实例,从而在组件里面直接用

  1. // main.ts
  2. import axios from 'axios'
  3. Vue.prototype.$axios = axios;
  1. // shims-vue.d.ts
  2. import Vue from "vue";
  3. import { AxiosInstance } from "axios";
  4. declare module "vue/types/vue" {
  5. interface Vue {
  6. $axios: AxiosInstance;
  7. }
  8. }

范例:给krouter/index.js编写声明文件,index.d.ts

  1. import VueRouter from "vue-router";
  2. declare const router: VueRouter
  3. export default router

装饰器

装饰器用于扩展类或者它的属性和方法。@xxx就是装饰器的写法

属性声明:@Prop

除了在@Component中声明,还可以采用@Prop的方式声明组件属性

  1. export default class HelloWorld extends Vue {
  2. // Props()参数是为vue提供属性选项
  3. // !称为明确赋值断言,它是提供给ts的
  4. @Prop({type: String, required: true})
  5. private msg!: string;
  6. }

事件处理:@Emit

新增特性时派发事件通知,Hello.vue

  1. // 通知父类新增事件,若未指定事件名则函数名作为事件名(羊肉串形式)
  2. @Emit()
  3. private addFeature(event: any) {// 若没有返回值形参将作为事件参数
  4. const feature = { name: event.target.value, id: this.features.length + 1 };
  5. this.features.push(feature);
  6. event.target.value = "";
  7. return feature;// 若有返回值则返回值作为事件参数
  8. }

变更监测:@Watch
  1. @Watch('msg')
  2. onMsgChange(val:string, oldVal:any){
  3. console.log(val, oldVal);
  4. }

状态管理推荐使用:vuex-module-decorators

vuex-module-decorators通过装饰器提供模块化声明vuex模块的方法,可以有效利用ts的类型系统。

安装

  1. npm i vuex-module-decorators -D

根模块清空,修改store/index.ts

  1. export default new Vuex.Store({})

定义counter模块,创建store/counter

  1. import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-decorators'
  2. import store from './index'
  3. // 动态注册模块
  4. @Module({ dynamic: true, store: store, name: 'counter', namespaced: true })
  5. class CounterModule extends VuexModule {
  6. count = 1
  7. @Mutation
  8. add () {
  9. // 通过this直接访问count
  10. this.count++
  11. }
  12. // 定义getters
  13. get doubleCount () {
  14. return this.count * 2;
  15. }
  16. @Action
  17. asyncAdd () {
  18. setTimeout(() => {
  19. // 通过this直接访问add
  20. this.add()
  21. }, 1000);
  22. }
  23. }
  24. // 导出模块应该是getModule的结果
  25. export default getModule(CounterModule)

使用,App.vue

  1. <p @click="add">{{$store.state.counter.count}}</p>
  2. <p @click="asyncAdd">{{count}}</p>
  1. import CounterModule from '@/store/counter'
  2. @Component
  3. export default class App extends Vue {
  4. get count () {
  5. return CounterModule.count
  6. }
  7. add () {
  8. CounterModule.add()
  9. }
  10. asyncAdd () {
  11. CounterModule.asyncAdd()
  12. }
  13. }

装饰器原理

装饰器是 工厂函数 ,它能访问和修改装饰目标。

类装饰器,07-decorator.ts
  1. //类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
  2. function log (target: Function) {
  3. // target是构造函数
  4. target.prototype.log = function () {
  5. console.log(this.bar);
  6. }
  7. }
  8. @log
  9. class Foo {
  10. bar = 'bar'
  11. }
  12. const foo = new Foo();
  13. // @ts-ignore
  14. foo.log();

方法装饰器
  1. function rec (target: any, name: string, descriptor: any) {
  2. // 这里通过修改descriptor.value扩展了bar方法
  3. const baz = descriptor.value;
  4. descriptor.value = function (val: string) {
  5. console.log('run method', name);
  6. baz.call(this, val);
  7. }
  8. }
  9. class Foo {
  10. @rec
  11. setBar (val: string) {
  12. this.bar = val
  13. }
  14. }
  15. foo.setBar('lalala')

属性装饰器
  1. // 属性装饰器
  2. function mua (target, name) {
  3. target[name] = 'mua~~~'
  4. }
  5. class Foo {
  6. @mua ns!: string;
  7. }
  8. console.log(foo.ns);

稍微改造一下使其可以接收参数

  1. function mua (param: string) {
  2. return function (target, name) {
  3. target[name] = param
  4. }
  5. }

实战一下:实现Component装饰器

  1. <template>
  2. <div>{{msg}}</div>
  3. </template>
  4. <script lang='ts'>
  5. import { Vue } from "vue-property-decorator";
  6. function Component (options: any) {
  7. return function (target: any) {
  8. return Vue.extend(options);
  9. };
  10. }
  11. @Component({
  12. props: {
  13. msg: {
  14. type: String,
  15. default: ""
  16. }
  17. }
  18. })
  19. export default class Decor extends Vue { }
  20. </script>

显然options中的选项都可以从目标组件定义中找到:

  1. function Component (target: any): any {
  2. const option: any = {};
  3. const proto = target.prototype;
  4. const keys = Object.getOwnPropertyNames(proto);
  5. keys.forEach((key) => {
  6. if (key !== "constructor") {
  7. const descriptor = Object.getOwnPropertyDescriptor(proto, key);
  8. if (descriptor) {
  9. if (typeof descriptor.value === "function") {
  10. if (["created", "mounted"].indexOf(key) !== - 1) {
  11. option[key] = proto[key];
  12. } else {
  13. const methods = (!option.methods || (option.methods = {})) as any;
  14. methods[key] = descriptor.value;
  15. }
  16. } else if (descriptor.get || descriptor.set) {
  17. const computed = (!option.computed || (option.computed = {})) as any;
  18. computed[key] = {
  19. get: descriptor.get,
  20. set: descriptor.set,
  21. };
  22. }
  23. }
  24. }
  25. });
  26. option.data = function () {
  27. const vm = new target();
  28. const data: any = {};
  29. Object.keys(vm)
  30. .filter((key) => !key.startsWith("_") && !key.startsWith("$"))
  31. .forEach((key) => {
  32. data[key] = vm[key];
  33. });
  34. return data;
  35. };
  36. return Vue.extend(option);
  37. }

作业


写一个装饰器

思考题


  1. 把手头的小项目改造为ts编写
  2. 探究vue-property-decorator中各装饰器实现原理,能造个轮子更佳