TypeScript + Vue 实践

TypeScript + Vue 实践

1. TS vs JS

    1. JS是弱类型语言,声明一个变量可以随意赋值不同的数据类型;
    2. JS是动态类型检查的,当产生类型检查的错误的时候,只有在执行的的时候才会显现出来,导致线上抛出异常甚至白屏。
    1. TS静态类型,可提升代码可读性,稳定性、可重构性;
    2. TS在编译时的类型检查,在编译时对代码错误进行检查并抛出错误。

2. TypeScript 快速指南

2.1 基础类型
  • boolean

    let isDone: boolean;
    isDone= false;
    
  • number

    let num: number = 2333;
    
  • string

    let str: string = '嘿嘿嘿';
    
  • null 和 undefined

    在非严格空检查模式下,null 和 undefined 是所有类型的子类型,可以作为任何类型变量的值; 在严格空检查模式(strictNullChecks)下,其是独立的类型。

    // 在非严格空检查模式下,以下三种情况不会报错 但在严格模式下都会报错
    let str: string = undefined;
    
    let obj: object = undefined;
    
    let num: number = 2;
    num = null;
    
  • void

    void 表示空类型,void 类型只能赋值为 null || undefined。也可以在函数中表示没有返回值。

    let v: void = null;
    
    let func = (): void => {
        alert('没有返回值');
    }
    
  • never

    never类型表示的是那些永不存在的值的类型。never 是任何类型的子类型,也可以赋值给任何类型,但没有任何类型是 never 的子类型;

    // 返回never的函数必须存在无法达到的终点
    function error(message: string): never {
        throw new Error(message);
    }
    
    // 推断的返回值类型为never
    // 虽然这个函数规定必须有 string 类型的返回值,但是由于 never 是任何类型的子类型,所以这里不会报错
    
    function fail(): string {
        return error('Something failed');
    }
    
  • any

    any 表示该值是任意类型,编辑时将跳过对他的类型检查。应在代码中尽量避免 any 类型的出现,因为这会失去 ts 的大部分作用 -- 类型检查。

  • object

    object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

  • 数组与元组

    数组

    有两种定义数组类型的方式,一种是直接在类型后面加上[], 表示元素为该类型的数组;

    第二种是使用数组泛型, 这种方式可以在不想在外边声明类型时候使用

    let arr: number[] = [];
    arr.push(1); 
    
    let list: Array<1 | 2> = [];
    list.push(3);  //  TS2345: Argument of type '3' is not assignable to parameter of type '1 | 2'.  Error: 类型“3”的参数不能赋给类型“1 | 2”的参数
    
    

    元组

    元组用来表示已知元素数量与类型的数组,在赋值时内部元素的类型必须一一对应,访问时也会得到正确类型

    let tuple: [string, number];
    tuple = [1, 'a'];  // Error: 不能将类型“[number, string]”分配给类型“[string, number]”
    tuple = ['a', 1];
    
  • 类型断言

    通过类型断言可以告诉编译器不进行特殊的数据检查和解构。TypeScript会假设你,程序员,已经进行了必须的检查。它没有运行时的影响,只是在编译阶段起作用

    // eg.1
    let someValue: any = "this is a string";
    
    let strLength: number = (<string>someValue).length;
    
    // eg.2
    let someValue: any = "this is a string";
    
    let strLength: number = (someValue as string).length;
    
    
2.2 类型推论

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型

// 变量x 会被推断为number类型,后续赋值非number类型会保 TS2322:Type '{0}' is not assignable to type '{1}'.不能将类型“{0}”分配给类型“{1}”
let x = 3;

不过有一种情况,当我们在定义的时候不赋值的情况,会被默认推论为any类型

let a;
a = 'a';	// ok
a = 7;	// ok

2.3 接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

例如:

interface Person {
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};

在上面的例子中,定义的变量比接口定义的属性多或者少都会编译报类型错误。当然了,有时候我们想定义一个对象上必须有某些属性或者其他可选属性,可以声明可选属性

interface Person {
    name: string;
    age?: number;	// 可选属性
}

let tom: Person = {
    name: 'Tom'
};
let jack: Person = {
    name: 'Tom',
    agr: 18
};


还可以定义任意属性,不过一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集,例如:

interface Person {
    name: string;
    age?: number;
    [propName: string]: string;
}

let tom: Person = {
    name: 'Tom',
    age: 25,
    gender: 'male'
};
// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
//   Index signatures are incompatible.
//     Type 'string | number' is not assignable to type 'string'.
//       Type 'number' is not assignable to type 'string'.

上例中,任意属性的值允许是 string,但是可选属性 age 的值却是 numbernumber 不是 string 的子属性,所以报错了。

2.4 函数
// 给每个参数添加类型之后再为函数本身添加返回值类型,因为有typescript的类型推论,我们也可以省略声明函数的返回类型
function add(x: number, y: number): number {
    return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

可选参数

函数也可以定义可选参数,这一点和接口的可选参数语法一致,不过可选参数必须跟在必须参数后面,因为那样ts无法获取参数的对应关系。同时对于未传入的参数,ts和js一致,在访问时都会是undefined

function buildName(firstName: string, lastName?: string) {
    // ...
}

默认参数

在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型。

function buildName(firstName: string, lastName = "Smith") {
    // ...
}

剩余参数

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组。

2.5 类

在ts中的类和java相当类似, 和es6的class不同的是,成员变量需要先定义再使用,否则会保TS2339(类型“{1}”上不存在属性“{0}”。)这个高频错误码。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

说到类,那一定要讲类的继承,ts类的继承和es6一样 都是用extends关键词,同时如果需要在构造函数里访问 this的属性,我们 一定要先调用 super(),这一点也和es6一致。

成员变量的修饰

修饰符有public(公共)、private(私有)、protected(受保护)

成员变量默认的修饰都是public

  • public:没有访问限制
  • private:只能在当前类访问
  • protected:只能在当前类和当前类的字类被访问

只读属性:readonly

只读属性必须在声明时或构造函数里被初始化

静态属性:static

类的静态成员,这些属性存在于类本身上面而不是类的实例上,只能通过当前类来访问

2.6 声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

比如我们引入jquery.js这个库,在jq会把$符暴露在window上,在js里我们可以直接使用$,但是在ts里,编译器并不知道 $jQuery 是什么东西,这时候就需要我们添加声明语句来定义$,例如:

declare var jQuery: (selector: string) => any;

通常对于第三方库我们会把声明语句放在单独的文件(*.d.ts)里,声明文件也必须以.d.ts结尾,当然,jQuery 的声明文件不需要我们定义了,因为官方已经帮我们定义好了@types/jquery这个包,所以我们在开发第三方包的时候,最好也加上声明文件,方便你我他。

书写声明文件

declare var 声明全局变量

// let 和 var 一样 都是声明全局变量,不过const是声明全局变量,声明后不允许再去修改它的值
declare let jQuery: (selector: string) => any;

// 需要注意的是,声明语句中只能定义类型,切勿在声明语句中定义具体的实现
declare const jQuery = function(selector) {
    return document.querySelector(selector);
};
// ERROR: An implementation cannot be declared in ambient contexts.

declare function 声明全局方法

// src/jQuery.d.ts

declare function jQuery(selector: string): any;

// 函数的声明语句也支持函数重载
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;


// src/index.ts
jQuery('#foo');
jQuery(function() {
    alert('Dom Ready!');
});

declare class 声明全局类

// declare class 语句也只能用来定义类型,不能用来定义具体的实现
declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}

declare enum声明全局枚举类型

// 与其他全局变量的类型声明一致,declare enum 仅用来定义类型,而不是具体的值
declare enum Helper {
    State,
    Getters,
    Mutations,
    Actions
}

declare namespace声明(含有子属性的)全局对象

namespace 是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。

由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module 关键字表示内部模块。但由于后来 ES6 也使用了 module 关键字,ts 为了兼容 ES6,使用 namespace 替代了自己的 module,更名为命名空间。

随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace,而推荐使用 ES6 的模块化方案了。简单看一下namespace的声明:

// src/jQuery.d.ts

declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}

// src/index.ts

jQuery.ajax('/api/get_something');

interfacetype声明全局类型

除了全局变量之外,可能有一些类型我们也希望能暴露出来。在类型声明文件中,我们可以直接使用 interfacetype 来声明一个全局的接口或类型

declare global扩展全局变量

对于一个 npm 包或者 UMD 库的声明文件,只有 export 导出的类型声明才能被导入。所以对于 npm 包或 UMD 库,如果导入此库之后会扩展全局变量,则需要使用另一种语法在声明文件中扩展全局变量的类型,那就是 declare global

使用 declare global 可以在 npm 包或者 UMD 库的声明文件中扩展全局变量的类型

// types/foo/index.d.ts

declare global {
    interface String {
        prependHello(): string;
    }
}
// 注意即使此声明文件不需要导出任何东西,仍然需要导出一个空对象,用来告诉编译器这是一个模块的声明文件,而不是一个全局变量的声明文件。
export {};


// src/index.ts

'bar'.prependHello();

declare module扩展模块

有时通过 import 导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法 declare module,它可以用来扩展原有模块的类型

// 如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块,再使用 declare module 扩展原有模块

// types/moment-plugin/index.d.ts

import * as moment from 'moment';

declare module 'moment' {
    export function foo(): moment.CalendarKey;
}

掌握以上内容我们基本上能用ts编写业务了,关于更多进阶用法可移步官方文档

3. 在vue项目中使用TS

3.1 tsconfig.json

是每个ts项目必备的配置文件,如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录。 tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。以下是项目目前配置:

{
    "include": ["src/**/*"],															// 编译包括目录
    "exclude": ["node_modules"],													// 编译排除目录
    "compilerOptions": {
        "allowSyntheticDefaultImports": true,				// 允许从没有设置默认导出的模块中默认导入
        "experimentalDecorators": true,							// 启用装饰器
        "allowJs": true,														// 允许编译 javascript 文件
        "noImplicitAny": false,										// 关闭表达式和声明上有隐含的any类型时报错
        "module": "esnext",								// 指定使用模块: 'commonjs', 'amd', 'system'...
        "target": "es5",														// 指定 ECMAScript 目标版本
        "isolatedModules": true,										// 将每个文件做为单独的模块
        "lib": ["dom", "es5", "es2015.promise"],		// 指定要包含在编译中的库文件
        "sourceMap": true,													// 生成相应的 '.map' 文件
        "baseUrl": ".",														// 用于解析非相对模块名称的基目录
        "paths": {																// 模块名到基于 baseUrl 的路径映射的列表
            "@/*": ["*", "src/*"]									// webpack 别名
        },																				// 包含类型声明的文件列表,第三方包,vue等
        "typeRoots": ["./src/types", "./node_modules/@types"]
    }
}


3.2 配置webpack rules 处理ts语法
// 先用ts-loader编译成js, 再用babel解析
{
    test: /\.tsx?$/,
    exclude: /node_modules/,
    use: [
        'babel-loader',
        {
            loader: 'ts-loader',
            options: {
                appendTsSuffixTo: [/\.vue$/]
            }
        }
    ]
},
 
extensions: ['.js', '.vue', '.json', '.ts'], // 引入ts文件时可省略.ts后缀
3.3 兼容处理
  1. 当搞定一切配置以后,首先第一个问题就是在ts里引入vue文件会报Cannot find module ...( ts 文件中找不到 .vue 文件),在 TypeScript 中,它仅识别 js/ts/jsx/tsx 文件,所以引入vue文件时需要加上后缀名,同时,我们需要显式告诉 TypeScript,vue 文件存在,并且指定导出 VueConstructor,这样就把.vue文件交给vue-loader处理。
declare module '*.vue' {
    import Vue from 'vue';
    export default Vue;
}
  1. 我们引入的第三方包、客户端暴露在全局window上的属性、绑定在vue原型上的全局组件等这些在原本的js语法都编译ok的语法,在ts中使用时都会报TS2339: Property * does not exist on type *,找不到该属性定义,所以需要我们手动去声明我们之前定义好的属性。

  2. 在第二点我们说了类型声明,那么我们在开发npm包的时候,对于api类的包,建议加上类型声明文件,这样在ts中引用时会有友好的提示以及静态类型检查。具体方法只需要在源包的package.json里加上typings,指向自己的类型声明文件即可:

     "typings": "./src/index.d.ts",
    
3.4 从一个vue组件讲起

从 vue2.5 之后,vue 开始支持 ts 写法。根据官方文档,vue 结合 typescript ,有两种书写方式:

**Vue.extend **

  import Vue from 'vue'

  const Component = Vue.extend({
  	// type inference enabled
  })

vue-class-component

import { Component, Vue, Prop } from 'vue-property-decorator'

@Component
export default class Test extends Vue {
  @Prop({ type: Object })
  private test: { value: string }
}

理想情况下,Vue.extend 的书写方式,是学习成本最低的。在现有写法的基础上,几乎 0 成本的迁移。

但是Vue.extend模式,需要与mixins 结合使用。在 mixin 中定义的方法,不会被 typescript 识别到

,这就意味着会出现丢失代码提示、类型检查、编译报错等问题。

这里我们使用后者vue-property-decoratorvuex-class提供的装饰器方法,能让我们更方便的使用ts的代码提示和类型检查。

vue-property-decoratorvuex-class提供的装饰器有:

vue-property-decorator的装饰器:

vuex-class的装饰器:

  • @State
  • @Getter
  • @Action
  • @Mutation

拿一份原始vue模板举例:

import {componentA,componentB} from '@/components';

export default {
	components: { componentA, componentB},
	props: {
    propA: { type: Number },
    propB: { default: 'default value' },
    propC: { type: [String, Boolean] },
  }
  // 组件数据
  data () {
    return {
      message: 'Hello'
    }
  },
  // 计算属性
  computed: {
    reversedMessage () {
      return this.message.split('').reverse().join('')
    }
    // Vuex数据
    step() {
    	return this.$store.state.count
    }
  },
  methods: {
    changeMessage () {
      this.message = "Good bye"
    },
    getName() {
    	let name = this.$store.getters['person/name']
    	return name
    }
  },
  // 生命周期
  created () { },
  mounted () { },
  updated () { },
}

替换成装饰器的写法为:

import { Component, Vue, Prop } from 'vue-property-decorator';
import { State, Getter } from 'vuex-class';
import { count, name } from '@/person'
import { componentA, componentB } from '@/components';

@Component({
		// 注册组件
    components:{ componentA, componentB},
})
export default class HelloWorld extends Vue{
	// 装饰props
	@Prop(Number) readonly propA!: number | undefined
  @Prop({ default: 'default value' }) readonly propB!: string
  @Prop([String, Boolean]) readonly propC!: string | boolean | undefined
  
  // 原data
  message = 'Hello'
  
  // 计算属性
	private get reversedMessage (): string[] {
  	return this.message.split('').reverse().join('')
  }
  // Vuex 数据
  @State((state: IRootState) => state.booking.currentStep) step!: number
	@Getter('person/name') name!: name
  
  // method
  public changeMessage (): void {
    this.message = 'Good bye'
  },
  public getName(): string {
    let storeName = name
    return storeName
  }
	// 生命周期
  private created ():void { },
  private mounted ():void { },
  private updated ():void { },
}

vue3.0的pre-alpha 版本出来以后,作者提供了@vue/composition-api核心包能让我们在vue2.x版本使用3.0的核心api

同样是上面的代码用vue3.0的语法写法如下:

import { componentA, componentB } from '@/components';
import {
    createComponent,
    onUpdated,
    onMounted,
    computed,
    Ref,
    ref,
} from '@vue/composition-api';
import { useStore, useGetters } from '@/hooks/vuex';

export default createComponent({
    components: { componentA, componentB },
    props: {
        propA: { type: Number },
        propB: { default: 'default value' },
        propC: { type: [String, Boolean] }
    },
    setup(props, context) {
        // 组件数据
        const message: Ref<string> = ref('Hello');
        // vuex 数据
        const $store = useStore();
        // 计算属性
        const reversedMessage = computed(() => {
            return message.value.split('').reverse().join('');
        });
        const step = computed(() => {
            return $store.state.count;
        });

        function changeMessage() {
            message.value = 'Good bye';
        }
        function getName() {
            let { name } = useGetters('person', ['name']);
            return name;
        }

        // 生命周期
        onMounted(() => {});
        onUpdated(() => {});
    }
});
# vue  typescript 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×