• Vue 3.0前的 TypeScript 最佳入门实践


    前言

    我个人对更严格类型限制没有积极的看法,毕竟各类转类型的骚写法写习惯了。

    然鹅最近的一个项目中,是 TypeScriptVue,毛计喇,学之...…真香!

    1. 使用官方脚手架构建

    1. npm install -g @vue/cli

    2. # OR

    3. yarn global add @vue/cli

    新的 VueCLI工具允许开发者 使用 TypeScript 集成环境 创建新项目。

    只需运行 vue createmy-app

    然后,命令行会要求选择预设。使用箭头键选择 Manuallyselectfeatures

    接下来,只需确保选择了 TypeScript和 Babel选项,如下图:

    完成此操作后,它会询问你是否要使用 class-style component syntax

    然后配置其余设置,使其看起来如下图所示。

    Vue CLI工具现在将安装所有依赖项并设置项目。

    接下来就跑项目喇。

     

    总之,先跑起来再说。

    2. 项目目录解析

    通过 tree指令查看目录结构后可发现其结构和正常构建的大有不同。

    这里主要关注 shims-tsx.d.ts和 shims-vue.d.ts两个文件

    两句话概括:

    • shims-tsx.d.ts,允许你以 .tsx结尾的文件,在 Vue项目中编写 jsx代码

    • shims-vue.d.ts 主要用于 TypeScript 识别 .vue 文件, Ts默认并不支持导入 vue 文件,这个文件告诉 ts导入 .vue 文件都按 VueConstructor处理。

    此时我们打开亲切的 src/components/HelloWorld.vue,将会发现写法已大有不同

    1. <template>
    2. <div class="hello">
    3. <h1>{{ msg }}h1>
    4. div>
    5. template>
    6. <script lang="ts">
    7. import { Component, Prop, Vue } from 'vue-property-decorator';
    8. @Component
    9. export default class HelloWorld extends Vue {
    10. @Prop() private msg!: string;
    11. }
    12. script>
    13. <style scoped>style>

    至此,准备开启新的篇章 TypeScript极速入门 和 vue-property-decorator

    ## 3. TypeScript极速入门

    3.1 基本类型和扩展类型

    Typescript与 Javascript共享相同的基本类型,但有一些额外的类型。

    • 元组 Tuple

    • 枚举 enum

    • Any 与 Void

    1. 基本类型合集

    1. // 数字,二、八、十六进制都支持
    2. let decLiteral: number = 6;
    3. let hexLiteral: number = 0xf00d;
    4. // 字符串,单双引都行
    5. let name: string = "bob";
    6. let sentence: string = `Hello, my name is ${ name }.
    7. // 数组,第二种方式是使用数组泛型,Array<元素类型>:
    8. let list: number[] = [1, 2, 3];
    9. let list: Array = [1, 2, 3];
    10. let u: undefined = undefined;
    11. let n: null = null;

    2. 特殊类型

    1. 元组 Tuple

    想象 元组 作为有组织的数组,你需要以正确的顺序预定义数据类型。

    1. const messyArray = [' something', 2, true, undefined, null];
    2. const tuple: [number, string, string] = [24, "Indrek" , "Lasn"]

    如果不遵循 为元组 预设排序的索引规则,那么 Typescript会警告。

    ( tuple第一项应为 number类型)

    2. 枚举 enum*

    enum类型是对JavaScript标准数据类型的一个补充。像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

    1. // 默认情况从0开始为元素编号,也可手动为1开始
    2. enum Color {Red = 1, Green = 2, Blue = 4}
    3. let c: Color = Color.Green;
    4. let colorName: string = Color[2];
    5. console.log(colorName); // 输出'Green'因为上面代码里它的值是2

    另一个很好的例子是使用枚举来存储应用程序状态。

    3. Void

    在 Typescript中,你必须在函数中定义返回类型。像这样:

    若没有返回值,则会报错:

    我们可以将其返回值定义为 void:

    此时将无法 return

    4. Any

    Emmm...就是什么类型都行,当你无法确认在处理什么类型时可以用这个。

    但要慎重使用,用多了就失去使用Ts的意义。

    1. let person: any = "前端劝退师"
    2. person = 25
    3. person = true

    主要应用场景有:

    1. 接入第三方库

    2. Ts菜逼前期都用

    5. Never

    用很粗浅的话来描述就是:" Never是你永远得不到的爸爸。"

    具体的行为是:

    • thrownewError(message)

    • returnerror("Something failed")

    • while(true){}// 存在无法达到的终点

    3. 类型断言

    简略的定义是:可以用来手动指定一个值的类型。

    有两种写法,尖括号和 as:

    1. let someValue: any = "this is a string";
    2. let strLength: number = (someValue).length;
    3. let strLength: number = (someValue as string).length;

    使用例子有:

    TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:

    1. function getLength(something: string | number): number {
    2. return something.length;
    3. }
    4. // index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
    5. // Property 'length' does not exist on type 'number'.

    如果你访问长度将会报错,而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,此时需要断言才不会报错:

    1. function getLength(something: string | number): number {
    2. if ((something).length) {
    3. return (something).length;
    4. } else {
    5. return something.toString().length;
    6. }
    7. }

    安全导航操作符 ( ?. )和非空断言操作符(!.)

    安全导航操作符 ( ?. ) 和空属性路径: 为了解决导航时变量值为null时,页面运行时出错的问题。

    The null hero's name is {{nullHero?.name}}

    非空断言操作符:

    能确定变量值一定不为空时使用。

    与安全导航操作符不同的是,非空断言操作符不会防止出现 null 或 undefined。

    let s = e!.name; // 断言e是非空并访问name属性

    3.2 泛型: Generics

    软件工程的一个主要部分就是构建组件,构建的组件不仅需要具有明确的定义和统一的接口,同时也需要组件可复用。支持现有的数据类型和将来添加的数据类型的组件为大型软件系统的开发过程提供很好的灵活性。

    在 C#和 Java中,可以使用"泛型"来创建可复用的组件,并且组件可支持多种数据类型。这样便可以让用户根据自己的数据类型来使用组件。

    1. 泛型方法

    在TypeScript里,声明泛型方法有以下两种方式:

    1. function gen_func1(arg: T): T {
    2. return arg;
    3. }
    4. // 或者
    5. let gen_func2: (arg: T) => T = function (arg) {
    6. return arg;
    7. }

    调用方式也有两种:

    1. gen_func1('Hello world');
    2. gen_func2('Hello world');
    3. // 第二种调用方式可省略类型参数,因为编译器会根据传入参数来自动识别对应的类型。

    2. 泛型与 Any

    Ts 的特殊类型 Any 在具体使用时,可以代替任意类型,咋一看两者好像没啥区别,其实不然:

    1. // 方法一:带有any参数的方法
    2. function any_func(arg: any): any {
    3. console.log(arg.length);
    4. return arg;
    5. }
    6. // 方法二:Array泛型方法
    7. function array_func(arg: Array): Array {
    8. console.log(arg.length);
    9. return arg;
    10. }
    • 方法一,打印了 arg参数的 length属性。因为 any可以代替任意类型,所以该方法在传入参数不是数组或者带有 length属性对象时,会抛出异常。

    • 方法二,定义了参数类型是 Array的泛型类型,肯定会有 length属性,所以不会抛出异常。

    3. 泛型类型

    泛型接口:

    1. interface Generics_interface {
    2. (arg: T): T;
    3. }
    4. function func_demo(arg: T): T {
    5. return arg;
    6. }
    7. let func1: Generics_interface = func_demo;
    8. func1(123); // 正确类型的实际参数
    9. func1('123'); // 错误类型的实际参数

    3.3 自定义类型: Interface vs Typealias

    Interface,国内翻译成接口。

    Typealias,类型别名。

    以下内容来自:

    Typescript 中的 interface 和 type 到底有什么区别

    1. 相同点

    都可以用来描述一个对象或函数:

    1. interface User {
    2. name: string
    3. age: number
    4. }
    5. type User = {
    6. name: string
    7. age: number
    8. };
    9. interface SetUser {
    10. (name: string, age: number): void;
    11. }
    12. type SetUser = (name: string, age: number): void;

    都允许拓展(extends):

    interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface可以 extendstypetype 也可以 extendsinterface 。 虽然效果差不多,但是两者语法不同

    interface extends interface

    1. interface Name {
    2. name: string;
    3. }
    4. interface User extends Name {
    5. age: number;
    6. }

    type extends type

    1. type Name = {
    2. name: string;
    3. }
    4. type User = Name & { age: number };

    interface extends type

    1. type Name = {
    2. name: string;
    3. }
    4. interface User extends Name {
    5. age: number;
    6. }

    type extends interface

    1. interface Name {
    2. name: string;
    3. }
    4. type User = Name & {
    5. age: number;
    6. }

    2. 不同点

    type 可以而 interface 不行

    • type 可以声明基本类型别名,联合类型,元组等类型

    1. // 基本类型别名
    2. type Name = string
    3. // 联合类型
    4. interface Dog {
    5. wong();
    6. }
    7. interface Cat {
    8. miao();
    9. }
    10. type Pet = Dog | Cat
    11. // 具体定义数组每个位置的类型
    12. type PetList = [Dog, Pet]
    • type 语句中还可以使用 typeof获取实例的 类型进行赋值

    1. // 当你想获取一个变量的类型时,使用 typeof
    2. let div = document.createElement('div');
    3. type B = typeof div
    • 其他骚操作

    1. type StringOrNumber = string | number;
    2. type Text = string | { text: string };
    3. type NameLookup = DictionaryPerson>;
    4. type Callback = (data: T) => void;
    5. type Pair = [T, T];
    6. type Coordinates = Pair;
    7. type Tree = T | { left: Tree, right: Tree };

    interface可以而 type不行

    interface 能够声明合并

    1. interface User {
    2. name: string
    3. age: number
    4. }
    5. interface User {
    6. sex: string
    7. }
    8. /*
    9. User 接口为 {
    10. name: string
    11. age: number
    12. sex: string
    13. }
    14. */

    interface 有可选属性和只读属性

    • 可选属性

      接口里的属性不全都是必需的。有些是只在某些条件下存在,或者根本不存在。例如给函数传入的参数对象中只有部分属性赋值了。带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ?符号。如下所示

      1. interface Person {
      2. name: string;
      3. age?: number;
      4. gender?: number;
      5. }

    • 只读属性

      顾名思义就是这个属性是不可写的,对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 readonly来指定只读属性,如下所示:

      1. interface User {
      2. readonly loginName: string;
      3. password: string;
      4. }

    上面的例子说明,当完成User对象的初始化后loginName就不可以修改了。

    3.4 实现与继承: implementsvs extends

    extends很明显就是ES6里面的类继承,那么 implement又是做什么的呢?它和 extends有什么不同?

    implement,实现。与C#或Java里接口的基本作用一样, TypeScript也能够用它来明确的强制一个类去符合某种契约

    implement基本用法

    1. interface IDeveloper {
    2. name: string;
    3. age?: number;
    4. }
    5. // OK
    6. class dev implements IDeveloper {
    7. name = 'Alex';
    8. age = 20;
    9. }
    10. // OK
    11. class dev2 implements IDeveloper {
    12. name = 'Alex';
    13. }
    14. // Error
    15. class dev3 implements IDeveloper {
    16. name = 'Alex';
    17. age = '9';
    18. }

    而 extends是继承父类,两者其实可以混着用:

    class A extends B implements C,D,E

    搭配 interface和 type的用法有:

    3.5 声明文件与命名空间: declare 和 namespace

    前面我们讲到Vue项目中的 shims-tsx.d.ts和 shims-vue.d.ts,其初始内容是这样的:

    1. // shims-tsx.d.ts
    2. import Vue, { VNode } from 'vue';
    3. declare global {
    4. namespace JSX {
    5. // tslint:disable no-empty-interface
    6. interface Element extends VNode {}
    7. // tslint:disable no-empty-interface
    8. interface ElementClass extends Vue {}
    9. interface IntrinsicElements {
    10. [elem: string]: any;
    11. }
    12. }
    13. }
    14. // shims-vue.d.ts
    15. declare module '*.vue' {
    16. import Vue from 'vue';
    17. export default Vue;
    18. }

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

    这里列举出几个常用的:

    declare var 声明全局变量
    
    declare function 声明全局方法
    
    declare class 声明全局类
    
    declare enum 声明全局枚举类型
    
    declare global 扩展全局变量
    
    declare module 扩展模块

    namespace:“内部模块”现在称做“命名空间”

    moduleX{ 相当于现在推荐的写法 namespaceX{)

    跟其他 JS 库协同

    类似模块,同样也可以通过为其他 JS 库使用了命名空间的库创建 .d.ts 文件的声明文件,如为 D3 JS 库,可以创建这样的声明文件:

    1. declare namespace D3{
    2. export interface Selectors { ... }
    3. }
    4. declare var d3: D3.Base;

    所以上述两个文件:

    • shims-tsx.d.ts, 在全局变量 global中批量命名了数个内部模块。

    • shims-vue.d.ts,意思是告诉 TypeScript *.vue 后缀的文件可以交给 vue 模块来处理。

    3.6 访问修饰符: private、 public、 protected

    其实很好理解:

    1. 默认为 public
    2. 当成员被标记为 private时,它就不能在声明它的类的外部访问,比如:
    3. class Animal {
    4.   private name: string;
    5.   constructor(theName: string) {
    6.     this.name = theName;
    7.   }
    8. }
    9. let a = new Animal('Cat').name; //错误,‘name’是私有的

    protected和 private类似,但是, protected成员在派生类中可以访问

    1. class Animal {
    2.   protected name: string;
    3.   constructor(theName: string) {
    4.     this.name = theName;
    5.   }
    6. }
    7. class Rhino extends Animal {
    8. constructor() {
    9. super('Rhino');
    10. }
    11. getName() {
    12. console.log(this.name) //此处的name就是Animal类中的name
    13. }
    14. }

    4. Vue组件的 Ts写法

    从 vue2.5 之后,vue 对 ts 有更好的支持。根据官方文档,vue 结合 typescript ,有两种书写方式

    Vue.extend

    1. import Vue from 'vue'
    2. const Component = Vue.extend({
    3. // type inference enabled
    4. })

    vue-class-component

    1. import { Component, Vue, Prop } from 'vue-property-decorator'
    2. @Component
    3. export default class Test extends Vue {
    4. @Prop({ type: Object })
    5. private test: { value: string }
    6. }

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

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

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

    菜鸟才做选择,大佬都挑最好的。直接讲第二种吧:

    4.1 vue-class-component

    我们回到 src/components/HelloWorld.vue

    1. <script lang="ts">
    2. import { Component, Prop, Vue } from 'vue-property-decorator';
    3. @Component
    4. export default class HelloWorld extends Vue {
    5. @Prop() private msg!: string;
    6. }
    7. script>
    8. <style scoped>style>

    有写过 python的同学应该会发现似曾相识:

    • vue-property-decorator这个官方支持的库里,提供了函数 装饰器(修饰符)语法

    1. 函数修饰符 @

    “@”,与其说是修饰函数倒不如说是引用、调用它修饰的函数。

    或者用句大白话描述: @: "下面的被我包围了。"

    举个栗子,下面的一段代码,里面两个函数,没有被调用,也会有输出结果:

    1. test(f){
    2. console.log("before ...");
    3. f()
    4. console.log("after ...");
    5. }
    6. @test
    7. func(){
    8. console.log("func was called");
    9. }

    直接运行,输出结果:

    1. before ...

    2. func was called

    3. after ...

    上面代码可以看出来:

    • 只定义了两个函数: test和 func,没有调用它们。

    • 如果没有“@test”,运行应该是没有任何输出的。

    但是,解释器读到函数修饰符“@”的时候,后面步骤会是这样:

    1. 去调用 test函数, test函数的入口参数就是那个叫“ func”的函数;

    2. test函数被执行,入口参数的(也就是 func函数)会被调用(执行);

    换言之,修饰符带的那个函数的入口参数,就是下面的那个整个的函数。有点儿类似 JavaScript里面的 functiona(function(){...});

    2. vue-property-decorator和 vuex-class提供的装饰器

    vue-property-decorator的装饰器:

    • @Prop

    • @PropSync

    • @Provide

    • @Model

    • @Watch

    • @Inject

    • @Provide

    • @Emit

    • @Component (provided by vue-class-component)

    • Mixins (the helper function named mixins provided by vue-class-component)

    vuex-class的装饰器:

    • @State

    • @Getter

    • @Action

    • @Mutation

    我们拿原始Vue组件模版来看:

    1. import {componentA,componentB} from '@/components';
    2. export default {
    3. components: { componentA, componentB},
    4. props: {
    5. propA: { type: Number },
    6. propB: { default: 'default value' },
    7. propC: { type: [String, Boolean] },
    8. }
    9. // 组件数据
    10. data () {
    11. return {
    12. message: 'Hello'
    13. }
    14. },
    15. // 计算属性
    16. computed: {
    17. reversedMessage () {
    18. return this.message.split('').reverse().join('')
    19. }
    20. // Vuex数据
    21. step() {
    22. return this.$store.state.count
    23. }
    24. },
    25. methods: {
    26. changeMessage () {
    27. this.message = "Good bye"
    28. },
    29. getName() {
    30. let name = this.$store.getters['person/name']
    31. return name
    32. }
    33. },
    34. // 生命周期
    35. created () { },
    36. mounted () { },
    37. updated () { },
    38. destroyed () { }
    39. }

    以上模版替换成修饰符写法则是:

    1. import { Component, Vue, Prop } from 'vue-property-decorator';
    2. import { State, Getter } from 'vuex-class';
    3. import { count, name } from '@/person'
    4. import { componentA, componentB } from '@/components';
    5. @Component({
    6. components:{ componentA, componentB},
    7. })
    8. export default class HelloWorld extends Vue{
    9. @Prop(Number) readonly propA!: number | undefined
    10. @Prop({ default: 'default value' }) readonly propB!: string
    11. @Prop([String, Boolean]) readonly propC!: string | boolean | undefined
    12. // 原data
    13. message = 'Hello'
    14. // 计算属性
    15. private get reversedMessage (): string[] {
    16. return this.message.split('').reverse().join('')
    17. }
    18. // Vuex 数据
    19. @State((state: IRootState) => state . booking. currentStep) step!: number
    20. @Getter( 'person/name') name!: name
    21. // method
    22. public changeMessage (): void {
    23. this.message = 'Good bye'
    24. },
    25. public getName(): string {
    26. let storeName = name
    27. return storeName
    28. }
    29. // 生命周期
    30. private created ():void { },
    31. private mounted ():void { },
    32. private updated ():void { },
    33. private destroyed ():void { }
    34. }

    正如你所看到的,我们在生命周期 列表那都添加 privateXXXX方法,因为这不应该公开给其他组件。

    而不对 method做私有约束的原因是,可能会用到 @Emit来向父组件传递信息。

    4.2 添加全局工具

    引入全局模块,需要改 main.ts:

    1. import Vue from 'vue';
    2. import App from './App.vue';
    3. import router from './router';
    4. import store from './store';
    5. Vue.config.productionTip = false;
    6. new Vue({
    7. router,
    8. store,
    9. render: (h) => h(App),
    10. }).$mount('#app');

    npm iVueI18n

    1. import Vue from 'vue';
    2. import App from './App.vue';
    3. import router from './router';
    4. import store from './store';
    5. // 新模块
    6. import i18n from './i18n';
    7. Vue.config.productionTip = false;
    8. new Vue({
    9. router,
    10. store,
    11. i18n, // 新模块
    12. render: (h) => h(App),
    13. }).$mount('#app');

    但仅仅这样,还不够。你需要动 src/vue-shim.d.ts

    1. // 声明全局方法
    2. declare module 'vue/types/vue' {
    3. interface Vue {
    4. readonly $i18n: VueI18Next;
    5. $t: TranslationFunction;
    6. }
    7. }

    之后使用 this.$i18n()的话就不会报错了。

    4.3 Axios 使用与封装

    1. 新建文件 request.ts

    文件目录:

    -api
    
    - main.ts // 实际调用
    
    -utils
    
    - request.ts // 接口封装

    2. request.ts文件解析

    1. import * as axios from 'axios';
    2. import store from '@/store';
    3. // 这里可根据具体使用的UI组件库进行替换
    4. import { Toast } from 'vant';
    5. import { AxiosResponse, AxiosRequestConfig } from 'axios';
    6. /* baseURL 按实际项目来定义 */
    7. const baseURL = process.env.VUE_APP_URL;
    8. /* 创建axios实例 */
    9. const service = axios.default.create({
    10. baseURL,
    11. timeout: 0, // 请求超时时间
    12. maxContentLength: 4000,
    13. });
    14. service.interceptors.request.use((config: AxiosRequestConfig) => {
    15. return config;
    16. }, (error: any) => {
    17. Promise.reject(error);
    18. });
    19. service.interceptors.response.use(
    20. (response: AxiosResponse) => {
    21. if (response.status !== 200) {
    22. Toast.fail('请求错误!');
    23. } else {
    24. return response.data;
    25. }
    26. },
    27. (error: any) => {
    28. return Promise.reject(error);
    29. });
    30. export default service;

    为了方便,我们还需要定义一套固定的 axios 返回的格式,新建 ajax.ts

    1. export interface AjaxResponse {
    2. code: number;
    3. data: any;
    4. message: string;
    5. }
    6. 3. main.ts接口调用:
    7. // api/main.ts
    8. import request from '../utils/request';
    9. // get
    10. export function getSomeThings(params:any) {
    11. return request({
    12. url: '/api/getSomethings',
    13. });
    14. }
    15. // post
    16. export function postSomeThings(params:any) {
    17. return request({
    18. url: '/api/postSomethings',
    19. methods: 'post',
    20. data: params
    21. });
    22. }

    5. 编写一个组件

    为了减少时间,我们来替换掉 src/components/HelloWorld.vue,做一个博客帖子组件:

    1. <script lang="ts">
    2. import { Component, Prop, Vue } from 'vue-property-decorator';
    3. // 在这里对数据进行类型约束
    4. export interface Post {
    5. title: string;
    6. body: string;
    7. author: string;
    8. datePosted: Date;
    9. }
    10. @Component
    11. export default class HelloWorld extends Vue {
    12. @Prop() private post!: Post;
    13. get date() {
    14. return `${this.post.datePosted.getDate()}/${this.post.datePosted.getMonth()}/${this.post.datePosted.getFullYear()}`;
    15. }
    16. }
    17. script>
    18. <style scoped>
    19. h2 {
    20. text-decoration: underline;
    21. }
    22. p.meta {
    23. font-style: italic;
    24. }
    25. style>

    然后在 Home.vue中使用: