TS 入门和Vue实践

TS 入门和Vue实践

一、TS 快速上手

从 JavaScript 程序员的角度总结思考,快速上手理解 TypeScript。

1. 关于TS

TypeScript 是 JavaScript 的一个超集,可以编译成纯 JavaScript。TypeScript 在 JavaScript 的基础上添加了可选的静态类型基于类的面向对象编程

TypeScript 提供最新的和不断发展的 JavaScript 特性,下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:

1.png
TypeScript 工作流程
2.jpg

注意:TypeScript 编译的时候即使报错了,还是会生成编译结果,我们仍然可以使用这个编译之后的文件。

2. 基础

2.1 基本语法
:<TypeAnnotation>

TypeScript的基本类型语法是在变量之后使用冒号进行类型标识,这种语法也揭示了TypeScript的类型声明实际上是可选的。

(1) 原始值类型
let bool: boolean = false;
let num: number = 10;
let str: string = 'sip';

// var bool = false;
// var num = 10;
// var str = 'sip';
(2) 特殊类型
  • Any

    任意值(Any)用来表示允许赋值为任意类型。 在任意值上访问任何属性 / 调用任何方法都是允许

    let notSure: any = 'any';
    notSure = 1;
    

未声明类型的变量

变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型: 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:

let something;
// 等同于 let something: any;
  • Void

    void 类型像是与 any 类型相反,它表示没有任何类型 ;在 TypeScript 中,可以用 void 表示没有任何返回值的函数:

    function alertName(): void {
        alert('My name is Tom');
    }
    

声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefinednull

let unusable: void = undefined;
  • Null 和 Undefined

    let u: undefined = undefined;
    let n: null = null;
    

void 的区别是,undefinednull 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:

// 这样不会报错
let num: number = undefined;
// 这样也不会报错
let u: undefined;
let num: number = u;

void 类型的变量不能赋值给 number 类型的变量:

let u: void;
let num: number = u;

// Type 'void' is not assignable to type 'number'.
2.2 类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。

以下代码虽然没有指定类型,但是会在编译的时候报错:

let num = 'seven';
num = 7;

// error TS2322: Type '7' is not assignable to type 'string'.

事实上,它等价于:

let num: string = 'seven';
num = 7;

// error TS2322: Type '7' is not assignable to type 'string'.

TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查

let num;
num = 'seven';
num = 7;
2.3 联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

let strNum: string | number;
strNum = 'seven';
strNum = 7;

let strNum2: string | number;
strNum2 = true;

// error TS2322: Type 'true' is not assignable to type 'string | number'
2.4 对象的类型——接口

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

  • 例子

    // 接口 Person (接口一般首字母大写)
    interface Person {
        name: string;
        age: number;
    }
    
    let man: Person = {
        name: 'Tom',
        age: 25
    };
    

定义的变量比接口多/少了一些属性都是不允许的,赋值的时候,变量的形状必须和接口的形状保持一致

  • (1) 可选属性

    可选属性的含义是该属性可以不存在,但不允许添加未定义的属性

    interface Person {
        name: string;
        age?: number;
    }
    
    let man: Person = {
        name: 'Tom'
    };
    
    let man2: Person = {
        name: 'Tom',
        age: 25
    };
    
    let man3: Person = {
        name: 'Tom',
        tel: '1370000000'
    };
    // error TS2322: Type '{ name: string; tel: string; }' is not assignable to type 'Person'. 
    // Object literal may only specify known properties, and 'tel' does not exist in type 'Person'.
    
  • (2) 任意属性

    interface Person {
        name: string;
        age?: number;
        [propName: string]: any;
    }
    
    let man: Person = {
        name: 'Tom',
        gender: 'male'
    };
    

    使用 [propName: string] 定义了任意属性取 string 类型的值。

    1. 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

    2. 一个接口中只能定义一个任意属性。如果接口中有多个类型的属性,则可以在任意属性中使用联合类型:

  • (3) 只读属性

    interface Person {
        readonly id: number;
        name: string;
        age?: number;
        [propName: string]: any;
    }
    
    let man: Person = {
        id: 99999,
        name: 'Tom',
        gender: 'male'
    };
    
    man.id = 10000;
    // error TS2540: Cannot assign to 'id' because it is a read-only property.
    
2.5 数组的类型
  • 类型 + 方括号

    数组的项中不允许出现其他的类型:

    let fibonacci: number[] = [1, '1', 2, 3, 5];
    
    // Type 'string' is not assignable to type 'number'.
    
  • 数组泛型

    let fibonacci: Array<number> = [1, 1, 2, 3, 5];
    
  • 用接口表示数组

    interface NumberArray {
        [index: number]: number;
    }
    let fibonacci: NumberArray = [1, 1, 2, 3, 5];
    
  • 类数组

    类数组(Array-like Object)不是数组类型,比如 arguments

    function sum() {
        let args: {
            [index: number]: any;
            length: number;
            callee: Function;
        } = arguments;
    }
    
  • any 在数组中的应用

    let list: any[] = ['1', 1, { key: 'value' }];
    
2.6 函数的类型
  • 函数声明

    function sum(x: number, y: number): number {
        return x + y;
    }
    // 注意,输入多余的(或者少于要求的)参数,是不被允许的:
    sum(1, 2, 3);
    // error TS2554: Expected 2 arguments, but got 3.
    sum(1);
    // error TS2554: Expected 2 arguments, but got 1.
    
  • 函数表达式

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

    上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。

如果需要我们手动给 mySum 添加类型,则应该是这样:

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

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

  • 用接口定义函数的形状

    interface SearchFunc {
        (source: string, subString: string): boolean;
    }
    
    let mySearch: SearchFunc;
    mySearch = function(source: string, subString: string) {
        return source.search(subString) !== -1;
    }
    

    采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

  • 可选参数

    前面提到,输入多余的(或者少于要求的)参数,是不允许的。那么如何定义可选的参数呢?

    与接口中的可选属性类似,我们用 ? 表示可选的参数:

    function buildName(firstName: string, lastName?: string) {
        if (lastName) {
            return firstName + ' ' + lastName;
        } else {
            return firstName;
        }
    }
    let tomcat = buildName('Tom', 'Cat');
    let tom = buildName('Tom');
    

需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必需参数了

function buildName(firstName?: string, lastName: string) {
    if (firstName) {
        return firstName + ' ' + lastName;
    } else {
        return lastName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');

// error TS1016: A required parameter cannot follow an optional parameter.
  • 参数默认值

    TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受「可选参数必须接在必需参数后面」的限制

    function buildName(firstName: string = 'Tom', lastName: string) {
        return firstName + ' ' + lastName;
    }
    let tomcat = buildName('Tom', 'Cat');
    let cat = buildName(undefined, 'Cat');
    
  • 重载

    重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

2.7 声明文件

声明文件必需以 .d.ts 为后缀。

一般来说,ts 会解析项目中所有的 *.ts 文件,当然也包含以 .d.ts 结尾的文件。

  • declare var 声明全局变量 (declare let 和 declare const)
  • declare function 声明全局方法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有子属性的)全局对象
  • interface 和 type 声明全局类型
  • export 导出变量
  • export namespace 导出(含有子属性的)对象
  • export default ES6 默认导出
  • export = commonjs 导出模块
  • export as namespace UMD 库声明全局变量
  • declare global 扩展全局变量
  • declare module 扩展模块
  • /// <reference /> 三斜线指令

3. 补充

3.1 元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let strNumberList: [string, number];

strNumberList = ['hello', 10]; // OK
strNumberList = [10, 'hello']; // webstorm会报红, 编译会出错
3.2 枚举

enum类型是对JavaScript标准数据类型的一个补充。

enum Color {Red, Green, Blue}       // 默认从0开始编号
let color: Color = Color.Green;

// 编译后
var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
    Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
var color = Color.Green;


// enum Color {Red = 1, Green, Blue}  指定从1开始编号
// enum Color {Red = 1, Green = 2, Blue = 4}    手动赋值
3.3 泛型

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

二、TS 在 Vue2.x 的实践

1. 构建

通过官方脚手架构建安装 (Vue CLI 3)
// 1. 如果没有安装 Vue CLI 就先安装

npm install --global @vue/cli

// 2. 创建一个新工程,并选择 "Manually select features (手动选择特性)" 选项

vue create project-name

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

接下来,只需确保选择了 TypeScript 和 Babel 选项,然后配置其余设置,设置完成 vue cli 就会开始安装依赖并设置项目 。

[图片上传失败...(image-7844bb-1608444165739)]

2. 目录解析

安装完成打开项目,目录结构如下:

|-- project-name
    |-- .browserslistrc     # browserslistrc 配置文件 (用于支持 Autoprefixer)
    |-- .gitignore
    |-- .eslintrc.js        # eslint 相关配置
    |-- babel.config.js     # babel-loader 配置
    |-- package-lock.json
    |-- package.json        # package.json 依赖
    |-- postcss.config.js   # postcss 配置
    |-- README.md
    |-- tsconfig.json       # typescript 配置
    |-- vue.config.js       # vue-cli 配置
    |-- public              # 静态资源 (会被直接复制)
    |   |-- favicon.ico     # favicon图标
    |   |-- index.html      # html模板
    |-- src
    |   |-- App.vue         # 入口页面
    |   |-- main.ts         # 入口文件 加载组件 初始化等
    |   |-- shims-tsx.d.ts
    |   |-- shims-vue.d.ts
    |   |-- assets          # 主题 字体等静态资源 (由 webpack 处理加载)
    |   |-- components      # 全局组件
    |   |-- router          # 路由
    |   |-- store           # 全局 vuex store
    |   |-- styles          # 全局样式
    |   |-- views           # 所有页面

ts构建的项目目录与之前用js构建的区别不大,区别主要是之前 js 后缀的现在改为了ts后缀,还多了tsconfig.jsonshims-tsx.d.tsshims-vue.d.ts这几个文件:

  • tsconfig.json: typescript配置文件,主要用于指定待编译的文件和定义编译选项
  • shims-tsx.d.ts: 允许.tsx 结尾的文件,在 Vue 项目中编写 jsx 代码
  • shims-vue.d.ts: 主要用于 TypeScript 识别.vue 文件,Ts 默认并不支持导入 vue 文件
tsconfig.json`推荐配置
// tsconfig.json
{
  "compilerOptions": {
    // 与 Vue 的浏览器支持保持一致
    "target": "es5",
    // 这可以对 `this` 上的数据 property 进行更严格的推断
    "strict": true,
    // 如果使用 webpack 2+ 或 rollup,可以利用 tree-shake:
    "module": "es2015",
    "moduleResolution": "node"
  }
}

注意: 需要引入 strict: true (或者至少 noImplicitThis: true,这是 strict 模式的一部分) 以利用组件方法中 this 的类型检查,否则它会始终被看作 any 类型。

3. 使用

3.1 在 vue 中使用 typescript 常用的几个库
  • vue-class-component:

    是一个 Class Decorator,该库通过装饰器模式实现了 vue 的 ts 适配,也是官方推荐的使用 ts 方式

  • vue-property-decorator:

    是在 vue-class-component 基础上进行了修改与扩充

  • vuex-module-decorators:

    是在 typeScript 环境下中使用 vuex 的一种解决方案

3.2 上手

要让 TypeScript 正确推断 Vue 组件选项中的类型,我们需要使用 Vue.componentVue.extend 定义组件。

3.2.1 vue-class-component

vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
  props: {
    propMessage: String
  }
})
export default class App extends Vue {
  // initial data
  msg = 123

  // use prop values for initial data
  helloMsg = 'Hello, ' + this.propMessage

  // lifecycle hook
  mounted () {
    this.greet()
  }

  // computed
  get computedMsg () {
    return 'computed ' + this.msg
  }

  // method
  greet () {
    alert('greeting: ' + this.msg)
  }
}
</script>
3.2.2 vue-property-decorator

vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:

  • @Emit
  • @Inject
  • @Model
  • @Prop
  • @Provide
  • @Watch
  • @Component (从 vue-class-component 继承)
(1) 组件声明
  • 引入组件:和原生写法一致,都需要先引入再注册
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';

@Component({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {
  
}
</script>
(2) 响应式data 和 Prop声明
  • data

    类语法中可以直接定义为类的实例属性作为组件的响应式数据

  • prop

    类语法实现组件 props 定义是通过装饰器@Prop实现

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  private name: string;
    @Prop() private msg!: string;
}
  • !: 表示一定存在,?: 表示可能不存在。这两种在语法上叫赋值断言
  • @Prop(options: (PropOptions | Constructor[] | Constructor) = {})
    • PropOptions,可以使用以下选项:type,default,required,validator
    • Constructor[],指定 prop 的可选类型
    • Constructor,例如 String,Number,Boolean 等,指定 prop 的类型
(3) 生命周期函数

生命周期钩子的使用和原先使用的区别:在类语法中直接将生命周期生命为方法(方法名称和生命周期名称一致)。

public created(): void {   
   console.log('created'); 
}  
public mounted(): void {   
   console.log('mounted') 
}
(4) Watch 监听属性

类语法实现响应式的数据监听,是由 vue-property-decorator 依赖提供 @Watch 装饰器来完成。

  • @Watch(path: string, options: WatchOptions = {})

  • 其中,options 包含两个属性

    • immediate?:boolean 侦听开始之后是否立即调用该回调函数
    • deep?:boolean 被侦听的对象的属性被改变时,是否调用该回调函数
import { Vue, Component } from 'vue-property-decorator';

@Component
export default class Index extends Vue {
  
  //watch定义,其中Wacth装饰器第一个参数:响应式数据字符串(也可以定义为'a.b');
  //第二个参数options成员[immediate,deep]分别对应的是原生的用法
  @Watch('$route', { immediate: true })
  private changeRouter(val: Route, oldVal: Route) {
    console.log('$route watcher: ', val, oldVal);
  }
}
(5) computed 计算属性

类语法中的计算属性的实现,是通过 get 取值函数。

public get getName() {
  return 'computed ' + this.name;
}

getName 是计算后的值,name 是被监听的值

(6) method

在类语法实现原生 vue 的方法的方式,即通过直接定义类方法成员。

public clickFunc(): void {
  console.log(this.name)
  console.log(this.msg)
}
(7) 事件触发

ts 环境下 vue 的事件触发方式和 js 环境下是一致的,区别只是事件回调定义的地方不同(ts 定义为类的实例方法,js 定义在 methods 属性中)。

(8) ref

类语法中使用 ref 需要借助vue-property-decorator提供的@Ref装饰器

(9) mixins

类语法使用 mixins 需要继承vue-property-decorator提供的 Mixins 函数所生成的类。

Mixins 函数的参数是 Vue 实例类

(10) slots 和 scopedSlots

slots 和 scopedSlots 的使用方式和原生 vue 保持一致。

三、tsconfig.json

概述

如果一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是TypeScript项目的根目录。

tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。 一个项目可以通过以下方式之一来编译:

使用tsconfig.json
  • 不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。
  • 不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。

当命令行上指定了输入文件时,tsconfig.json文件会被忽略。

示例
{
    // 编译选项
  "compilerOptions": {
    // 编译输出目标 ES 版本
    "target": "esnext",
    // 采用的模块系统
    "module": "es2015",
    // 以严格模式解析
    "strict": true,
    "jsx": "preserve",
    // 如何处理模块
    "moduleResolution": "node",
    // 启用装饰器
    "experimentalDecorators": true,
    // 允许从没有设置默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
    // 定义一个变量就必须给它一个初始值
    "strictPropertyInitialization" : false,
    // 允许编译javascript文件
    "allowJs": true,
    // 是否包含可以用于 debug 的 sourceMap
    "sourceMap": true,
    // 忽略 this 的类型检查, Raise error on this expressions with an implied any type.
    "noImplicitThis": false,
    // 解析非相对模块名的基准目录 
    "baseUrl": ".",
    // 给错误和消息设置样式,使用颜色和上下文。
    "pretty": true,
    // 设置引入的定义文件
    "types": ["webpack-env", "mocha", "chai"],
    // 指定特殊模块的路径
    "paths": {
      "@/*": ["src/*"]
    },
    // 编译过程中需要引入的库文件的列表
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  // ts 管理的文件
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  // ts 排除的文件
  "exclude": ["node_modules"]
}

"compilerOptions"可以被忽略,这时编译器会使用默认值。在这里查看完整的编译器选项列表。

"files"指定一个包含相对或绝对文件路径的列表。

"include""exclude"属性指定一个文件glob匹配模式列表。 支持的glob通配符有:

  • * 匹配0或多个字符(不包括目录分隔符)
  • ? 匹配一个任意字符(不包括目录分隔符)
  • **/ 递归匹配任意子目录
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容