一、初步了解TypeScript
1.1、什么是TypeScript
- 它是以js为基础构建的语言
- 它是js的一个超集,换句话说就是对js的一个扩展(增强)
- 它可以在任何支持js的平台中执行,但TS不能被js解析器直接执行
- TS扩展了js,并添加了类型
- 浏览器中不能直接执行TS,需要经过编译后转成js后才行
1.2、TypeScript增加了什么?
- 增加了类型(这里说的类型是除js的数据类型外,增加了变量的类型等,就是在类型这块做了强化)
- 支持ES新特性
- 增加了一些ES中不具备的新特性(抽象类、工具、接口、装饰器等)
- 丰富的配置选项
- 强大的开发工具
下面做了它和javascript之间的区别对比:
TypeScript | Javascript |
---|---|
TS主要用于解决大型项目的代码复杂性 | 一种脚本语言,用于创建动态网页 |
可以在编译期间发现并纠正错误 | 作为一种解释性语言,只能在运行时发现错误 |
强类型,支持静态和动态类型 | 弱类型,没有静态类型选项 |
最终被编译成js代码,使浏览器可以理解 | 可以直接在浏览器中使用 |
支持模块、泛型和接口 | 不支持模块,泛型和接口 |
社区的支持仍在增长,而且还不是很大 | 大量的社区支持以及大量的文档和解决为题的支持 |
1.3、TypeScript工作流程
如你所见,在上图中有3个ts文件,这些ts文件都将被typescript编译器编译成3个不同的js文件,对于大多数公司项目开发过程中,我们还会对生成的js文件进行打包处理,然后再部署。
1.4、TypeScript下载
1、下载、安装Node.js (官网下载地址)
2、安装TS解析器(npm i -g typescript
)
3、在命令行中输入tsc,如果出现一堆代码就说明安装成功了
1.5、TS初体验
创建一个新的ts文件(hello.ts),具体内容如下:
function say(person:string){
return "hello," + person
}
console.log(say(TypeScript))
然后执行tsc hello.ts
命令,之后会在同级目录下生成一个编译后的hello.js文件:
function say(person) {
return "hello," + person;
}
console.log(say(TypeScript));
观察上述编译后的结果,可以发现person入参的类型信息在编译后被擦除了,TypeScript只会在编译阶段进行静态检查,而在运行时,编译生成的js与普通的js文件一样,并不会进行类型检查
二、TS的类型声明(静态类型)
2.1、给变量声明类型
let a : number; //声明a变量,类型为number
a=123 //√
a="aaaa" //报错 Type 'string' is not assignable to type 'number'.
let b:number = 123; //变量b声明和赋值同时进行时,ts会自动对变量进行类型检测,可以省略:number
=>可以简写为 let b =123
2.2、在函数中声明类型
//第一种情况:直接给函数形参声明数据类型,此时如果调用add()时传入的参数类型不符时会报错提示
function add(a:number,b:number){
return a+b
}
//第二种情况:给函数返回值声明类型
function add (a,b): number{ //在()后加:number声明了返回值必须是数字类型
return a+b
}
三、TS的基础类型
3.1、number
let num : //定义了b变量为数字类型
num = 123 //十进制ok
num = 0x7b // 十六进制ok
num= 0o174 // 八进制ok
num= 0b1101 //二进制ok
num= “ssss” //error,非数字类型报错
3.2、string
let str: string ;
str = "smith";
//也可以使用模板字符串的形式
let name:string = `Gene`;
let name1:string = `hello ${name}` //引用变量name
3.3、boolean
let b :boolean //定义了b变量为布尔类型
b = true //true //给b赋值为true是ok的
b = 123 //error //赋值非布尔类型都会报错
3.4、字面量
let a :10 //直接使用字面量来进行类型声明,有点类似于定义常量一样,a后续只能赋值为10
a=10; //√
a=11; //x
//这里还有一种或的用法“ | ”(联合类型)
let a: "male" | "female" //a可以赋值为male或者female
let a =male; //√
let a =female; //√
let a ="hello" //x
3.5、any(任意类型)
//any表示任意类型,一个变量设置为any后相当于对该变量关闭了TS的类型检测(不建议使用)
let a: any //显式any
a=12; //√
a="asd" //√
let b; //隐式any,声明变量时如果不指定类型,则TS解析器会自动判断变量的类型为any
b=12; //√
b="asd" //√
3.6、unknown(表示未知类型的值)
let a :unknown;
a=12; //√
a="haha"; //√
//当unknown类型的值赋值给另一个变量时(a还是上面的a)
let b:string
b=“hehe”
b=a; //此时编译器会报错,因为上面的a是unknown类型,b是字符串类型,不能这样赋值
那一定要赋值呢?可以用到一个叫**类型断言**的用法
b=a as string //这样写的意思就是a就是一个字符串类型,可以安心给b赋值
类型断言还有一种写法:b=<string> a
注意:当定义变量时,无法确认变量类型时,尽量都用unknown,不要用any,因为any可以给任意类型的值赋值,而且还不会报错,会破坏原先变量的类型,比如:let a :number; let b:any; b=“haha”;a=b;原先a是数字类型,b是any类型,但赋值为字符串了,然后把b字符串的值赋值给了数字类型的a,这样就破坏了a的数字类型了,为什么可以用unknown呢?因为同样这样操作,unknown会报错,会提示你不允许这么搞
3.7、void(空值)
//以函数为例
const func = (param:string):void=>{ //给函数返回值设置为void类型
console.log(111) //函数中没有返回,默认return undefined
}
//void类型可以赋值undefined和null,正是因为这个原因,所以上面的函数没有返回(返回undefined是ok的)
let a : void ;
a = undefined // ok
a = null //非严格模式下ok
3.8、never(永远不会返回结果)
function fn2(): never{ //这种直接抛出一个错误的函数就永远不会返回值,就可以设置为never,一般不用
throw new Error("报错了")
}
3.9、object(对象类型)
**对象结构**
let a: {} //用来指定a是一个对象
let a :{name:string} //表示a是一个对象,对象只能有一个属性就是name
let a: { name:string,[propName:string] :any } //表示a是一个对象,固定有一个name属性,其他属性只要是字符串类型的属性名都可以,属性值任意
[propName:string] :any //表示任意字符串类型属性
**函数结构**
语法:(形参:类型,形参:类型,...)=>返回值类型
let a :(a:number,b:number)=>number
3.10、array(数组)
两种方式1、类型[] ; 2、Array<类型>
例:
let a : string[]; //表示字符串数组
let a:number:[] //表示数字数组
let a :Array<number> //也表示数字数组
以上这些基本在js中都有的类型,下面我们要看的是ts中新增的类型
3.11、tuple(元组,固定长度和类型的数组)
//元组类型允许表示一个已知元素数量和类型的数组
let a :[string,number];
a=["a",123]; //√
a=[123,3422,123] //X,长度不一样,元素类型也不一样,第一个是字符串,第二个是数字
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
x[6] = true; // Error, 布尔不是(string | number)类型
3.12、enum(枚举)
//枚举它是对js标准数据类型的一种补充,它就相当于定义了一个有限可能的对象,比如,你要表示男或者女,只有两种可能,原先你按上面的方式定义如下:
let a:{name:string,gender:string} //这样表示name和gender的值都应该是字符串
当然你也可以这样:
enum Gender{
male:0;
female:1;
}
let a:{name:string,gender:Gender} //性别的类型就是定义的Gender枚举类型
a={name:"haha",gender:Gender.male}
默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如我们将Red的索引值改成从 1开始编号:
enum Color {
Red = 1,
Green,
Blue
}
let c: Color = Color.Green
console.log(c) //2,自动在前面的基础上+1
或者,全部都采用手动赋值:
enum Color {
Red = 1,
Green = 2,
Blue = 4
}
let c: Color = Color.Green;
console.log(c) //2
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
enum Color {
Red = 1,
Green,
Blue
}
let colorName: string = Color[2];
console.log(colorName); // 显示'Green'因为上面代码里它的值是2
补充1:这里补充下"&"的用法,它表示且,怎么用呢?
let a:{name:string} & {age:number} //表示对象a必须包含string类型name且包含数字类型age
补充2:类型的别名
有时候,自定义的类型规则很长,比如:
let a :1|2|3|4|5 //表示a可以是1、2、3、4、5中任意一个值
你可以抽取1|2|3|4|5为一个自定义type
type myType =1|2|3|4|5 ;
let a :myType; //a必须是myType类型,也就是上面的1|2|3|4|5类型
四、interface(接口)
接口其实就是定义了一个规范,对类进行了结构的限制,同时接口也可以当成类型声明来用,接口中的所有属性和方法都不能有实际的值,在接口中所有的方法都是抽象方法,某个类要像实现某个接口,需要用到implements,定义接口时需要用到interface a{…}
4.1、基本用法
对对象结构类型进行限制
// 老的方式:如果没有接口,你可能会这么限制类型
const getPerson = ({name,age}:{name:string,age:number})=>{ //限制属性name为string,age为number
return `${name} ${age}`
}
//接口实现方式
interface myInterface{
name:string,
age:number
}
const getPerson = ({name,age}:myInterface)=>{ //直接将定义的myInterface接口放到形参对象后面作为该形参对象的类型结构
return `${name} ${age}`
}
getPerson({name:"haha",age:12})
4.2、可选属性
如果部分属性不一定传,可以在定义接口时在属性名后加?
interface myInterface{
name ?:string, //这样就说明name属性可传可不传(可选)
age:number
}
4.3、额外的属性检查
定义的少,传入的多的情况,有三种方式绕开
//第一种方式处理:类型断言
interface myInterface{
name :string,
age:number
}
const getPerson = ({name,age})=>{
return `${name} ${age}`
}
getPerson({name:"ad",age:12,like:"篮球"} as myInterface ) //告诉程序我传入的对象就是myInterface类型结构,这样程序也不会报错了
//第二种方式处理:索引签名
interface myInterface{
name ?:string,
age:number
[propName:string]:any //这表明只要满足属性名类型为string,值为任意类型的任意属性都可以
}
const getPerson = ({name,age}:myInterface)=>{
return `${name} ${age}`
}
getPerson({name:"ad",age:12,like:"篮球"}) //这里调用函数时多传一个like参数也不会报错
//第三种方式处理:就是将要传入的对象赋值给另一个新的对象,然后函数中传入新赋值的那个对象,因为这个新的对象不会被额外参数检查,所以也不会报错
interface myInterface{
name ?:string,
age:number
}
const getPerson = ({name,age})=>{
return `${name} ${age}`
}
const newObj = {name:"123",age:33,like:"乒乓"} //赋值给newObj对象
fullname(newObj) //将newObj对象传入函数中
4.4、只读属性
属性名前用 readonly来指定只读属性:
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error! // p1的两个属性x和y都是只读属性,所以不能修改值
4.5、函数类型
接口除了描述带有属性的普通对象外,接口也可以描述函数类型
interface myFn{
(num1:number,num2:number):number //接受两个参数,都是number类型,:后面的number表示函数返回值类型时number
}
let add: myFn =(n1,n2)=>n1+n2 //ok
let add1: myFn =(n1,n2)=>`n1+n2` //error ,如果函数返回值是字符串类型就会报错
4.6、可索引类型
接口还可以描述那些有索引和值的类型,如对象和数组等
interface indexMap {
[index:number]:string //定义索引类型为数字,对应值类型为字符串
}
let myObj1:indexMap = {
1:"haha" //ok
}
let myObj2:indexMap = {
a:"haha" //error,索引类型定义为number
}
interface indexMap {
[index:string]:string //定义索引类型为string,值类型也为string
}
let myObj1:indexMap = {
a:"haha" //ok
}
let myObj2:indexMap = {
a:12 //error,值类型定义应为string
}
let myObj3:indexMap = {
2:"阿萨德" //ok,因为当数字作为索引时会被默认转换成string类型
}
4.7、接口继承
interface animal { //定义一个动物类型接口
weight:number
}
interface dog { //定义一个狗类型接口
color:string
}
let dog1 : dog={
color:"black" //ok此时dog1对象只需要拥有一个color属性就Ok了,不会报错
}
-----------------------------------------------
//我们改动下,让dog接口继承animal接口
interface dog extends animal { //dog接口继承animal接口
color:string
}
let dog2 : dog={ //此时dog2必须同时定义color和weight属性,这个weight属性是从animal接口中继承过来的
color:"black",
weight:123
}
4.8、混合类型
接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型
interface Counter {
(start: number): string; //函数类型,参数中有start,函数返回值类型为string
interval: number; //有个interval属性,类型为number
reset(): void; //有个属性叫reset,类型是函数,返回值为void
}
function getCounter(): Counter { //getCounter函数的返回值是接口Counter类型
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
五、类
5.1、属性修饰符
- public:公共的,默认就是这个,这个修饰符修饰的属性和方法在类定义的外面也可以访问到(可以创建类的实例访问)
- private:私有的,只能在类定义中访问,在类定义的外面通过类和实例都无法访问到,子类继承它,在子类中构造函数中通过super.也访问不到
- protected:受保护的,与private有点相似,但protected修饰的方法在子类继承中可以访问,但protected修饰的属性在子类中依旧无法访问,还有就是这个protected修饰符可以修饰constructor构造函数,一旦该类的构造函数被protected修饰,则该类就无法创建实例了,只能被子类继承
- readyOnly:只读的,实例无法修改属性,一般要和public写在一起,因为ts的类属性默认要有上面三个属性修饰符中的一个来修饰,所以一般只读属性的写法是:
public readOnly name:string
- static:静态的,静态属性和方法只能通过类自身去访问,实例是访问不到的,static可以和上面的修饰符一起配合写,效果就是两者都生效
5.2、参数属性
概念:这个参数属性的作用就是既可以修饰属性(公共的/私有的/受保护的)还可以帮你把这个属性放到实例上;
举个例子:正常我们定义一个共有可供实例对象使用的属性name需要在构造器中接受外部传进来的参数,然后内部通过this.name=传入的参数,给类的实例上挂载这个name属性,现在有了参数属性后可以简化这个写法,具体看下面例子:
//不加参数属性,也不用this.name去挂载属性,则实例s中就没有name属性
class S {
constructor( name:string) {}
}
const s = new S('haha');
console.log(s); // {} ,没有name属性
//加了参数属性,不加this.name挂载属性,实例中同样有name属性
class S {
constructor( public name:string) {}
}
const s = new S('haha');
console.log(s); // { name:"haha" }
5.3、存取器
概念:TS中的存取器和JS中的基本一样,就是一般内部有一个变量来控制存取器,将传入的值存入该变量,取的是这个变量的值,定义两个方法get和set,当要存值的时候,内部就会调用set方法,取值的时候会调用get方法
使用场景:一般存取器和private等修饰符一起使用,可用于属性的封装,有这么一个场景就是说,你定义的类,在创建实例后,实例对类内部的属性进行了随意修改(不符合常规逻辑,比如年纪小于0等类似),像这种情况如果你的项目中涉及money的话,就非常危险,那我们要如何阻止外界随意地修改类内部属性呢?就是将类内部的属性用private等修饰符修饰,然后通过存取器来当对外的“接口”
class Person {
private name:string //由于是私有属性,所以外部访问不了,所以要结合存取器进行存和取操作
constrcutor(name:string){
this.name=name
}
get myName(){
return this.name
}
set myName(value){
this.name=value
}
}
const p1=new Person(“哈哈”);
console.log(p1.name) //哈哈
p1.name="喜喜"
console.log(p1.name) //喜喜
5.4、抽象类(abstract)
使用场景:抽象类一般就是用来被其他类继承,而不能直接用它来创建实例,类名和内部属性、方法、存取器都用abstract关键字修饰
abstract class Person{ //抽象类,只用作继承
abstract say():void //抽象方法使用abstract开头,没有方法体,而且只能定义在抽象类中
get info():string
set info(value:string)
}
class Dog extends Person{
say() { //子类必须对父类的抽象方法进行重写,不然会报错
console.log("hehe")
}
}
const d1=new Dog()
d1.say() //hehe
注意:
1、如果抽象类中有抽象方法、属性,则子类在继承后必须重写抽象方法、和实现属性,否则会报错;
2、抽象方法和抽象存取器都不能包含实际的代码块,只需要指定方法名、方法参数、属性名、返回值类型就可以了,但是存值器不能指定返回类型,否则也会报错
5.5、构造函数
概念:构造函数会在new 类名,创建实例的时候自动执行;
class Person{
name:string //定义实例属性
age:number //定义实例属性
constructor(name:string,age:number){
//构造函数中的this就是创建实例时的那个实例对象,该函数在创建实例时就会被调用
this.name=name //给实例对象的name属性赋值
this.age=age //给实例对象的age属性赋值
}
}
const p1=new Person("小黑",20) // p1的类型就是Person
console.log(p1) //Person {name: "小黑", age: 20}
备注:这里有个小点:类其实你可以理解为一种特殊的类型,你用它创建出来的实例就是这个类型,上面倒数第2行的代码其实等同于
const p1:Person = new Person("小黑",20)
这行代码,只不过省略实例类型默认会给他赋创建它的类名类型
5.6、继承
class Person{ //Person类为父类
name:string
age:number
constructor(name:string,age:number){
this.name=name
this.age=age
}
}
class Dog extends Person{ //Dog类为字类,继承Person类,相当于拥有了name和age两个属性
say(){ //这是Dog类自己的方法
console.log("汪汪汪")
}
}
const d1=new Dog("小黑",20)
console.log(d1.name) //小黑
console.log(d1.age) //20
console.log(d1.say()) //汪汪汪
5.7、super(超类即父类)
主要用在两个地方,1、子类的方法中;2、子类的构造函数中,下面分别介绍这两处的用法
**--------------------用在子类方法中------------------**
class Person{
say(){
console.log("Person")
}
}
class Dog extends Person{
say(){
super.say() //这里的super代表父类或者父类实例都可以
}
}
const d1=new Dog("haha",18,"篮球")
console.log(d1.say()) //Person
**--------------------在子类构造函数中-------------------=**
class Person{ //父类
name:string
age:number
constructor(name:string,age:number){
this.name=name
this.age=age
}
}
class Dog extends Person{ //字类继承父类,并重写了constructor,就必须要写上super(),不然父类的构造函数就被子类重写覆盖了,不会执行了,所以子类也就拿不到name和age属性,除了要写super(),还要把所需的参数传过去(name,age)
like:string
constructor(name:string,age:number,like:string){
super(name,age,like) //这行代码相当于在调用父类的constructor构造函数
this.like=like
}
}
const d1=new Dog("haha",18,"篮球")
console.log(d1.name)
console.log(d1.age)
console.log(d1.like)
5.8、类实现接口
备注:就是类要去实现定义好的接口,通过接口来强制类必须包含某些内容
interface myInterface {
name:string
}
class A implements myInterface{ //类实现了上面定义的接口
public name :string //必须要有name属性,不然会报错,因为接口中有定义
}
注意:类实现接口后,接口检测的是实现该接口的类创建的实例是否满足接口中的要求,对于上面的例子我稍加改动,如果我在public后面再加上static,这就又会报错了,为什么呢?因为static修饰的属性只能通过类自身去访问,实例上并不会有有该属性,所以接口检测过程中发现实例上没有name属性就会报错
5.9、接口继承类
当接口继承类时,会继承这个类的成员,但是不包括去实现,也就是说只继承成员以及成员类型,接口会继承类的private和protected修饰的成员,当接口继承的类中包含这两个修饰符修饰的成员时,这个接口只可被这个类和他的子类实现,看的有点糊涂,直接看例子:
class A {
protected name:string //类A中有个protected修饰的成员name
constructor(name:string) {
this.name=name
}
}
interface B extends A {} // 接口B继承了类A,所以接口B中就会继承类A中的name属性
class C extends A implements B{ //类必须要先继承类A,然后才可以去实现接口B,不然会报错
name:string
constructor(name:string) {
super(name);
}
}
const c1 =new C("haha");
console.log(c1); //{name:”haha“}
六、函数
6.1、为函数定义类型
//有名函数
function add(x:number, y:number) :number{
return x + y;
}
// 匿名函数
let myAdd = function(x:number, y:number):number { return x + y; };
书写完整函数类型:
// (x: number, y: number) => number这一部分是不是有点熟悉(在interface中见过,修饰函数类型,参数x和y是number,函数返回值也是number)
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };
6.2、可选参数和默认参数
js中函数的参数是可选的,当你不传时,默认会是undefined,但typescript中不行
function func(a: string, b: string) {
return a + " " + b;
}
let result1 = func("Bob"); // error, 参数数量少了一个
let result2 = func("Bob", "Adams", "Sr."); // error,参数数量多了一个
let result3 = func("Bob", "Adams"); // ok
当然typescript中也可以实现让函数参数可选,那就必须在参数后面加“?”
function func(a: string, b?: string) {
if (a)
return a + " " + b;
else
return a;
}
let result1 = func("Bob"); // ok,返回Bob
let result2 = func("Bob", "Adams", "Sr."); // error, 参数数量多了
let result3 = func("Bob", "Adams"); // ok,a为Bob,b为Adams
注意:假设我们要指定参数a为可选参数,b为必填参数,那么参数a就必须放在b参数后面,换句话说ts的函数中的第一个参数必须是必填参数,不能是可选参数
typescript中也可以为函数添加默认参数值,而且会根据指定的默认值自动规定指定b为string类型,
function func(a: string, b=“haha”) { //相当于b:string="haha"
console.log(a+b)
}
let result1 = func("Bob"); // 返回 “Bobhaha”
6.3、剩余参数
有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments来访问所有传入的参数,在typescript中可以把所有参数收集到一个变量中
function func (a: string, ...other: string[]) { //多余参数都用other变量来接受,它是一个字符串类型的数组,等同于...["param1","param2"]这种写法
return a + " " + other.join(" ");
}
let b = func("hello", "1", "2", "3");
console.log(b) // hello 1 2 3
6.4、重载
重载是方法名字相同,而参数不同,返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
参数类型不同:
function disp(a:string):string;
function disp(a:number):number;
参数数量不同:
function disp(n1:number):void;
function disp(x:number,y:number):void;
参数类型顺序不同:
function disp(n1:number,s1:string):void;
function disp(s:string,n:number):void;
七、泛型
7.1、泛型初体验
//原先不用泛型来定义函数
function identity(arg: number): number {
return arg;
}
//或者使用any类型定义函数
function identity(arg: any): any {
return arg;
}
这样就会有局限性和问题,比如第一种情况这个函数只能接受number类型的参数,返回也是number类型,不能传别的类型,使用很局限,有时我们在调用函数时还不知道传入的参数是什么类型的,这时就不能使用第一个函数了,第二个函数用any修饰,就会导致信息丢失,因为假设当我们传入number类型时,返回的却是任何类型,而不一定是number类型,所以我们想要传入和返回类型一致时且不明确什么类型时,上述的两种做法就有些不妥,所以我们就引入了泛型(类型变量)。。。
function identity<T>(arg: T): T { //T就是类型变量,表示的是任意一种类型
return arg;
}
上述这样的写法就说明该函数可以允许传入任意类型的参数,返回值和传入的参数类型始终保持一致,这里我们就把这个identity函数称为泛型函数,因为它支持多种类型,不同于使用 any,它不会丢失信息,始终保持传入数值类型和返回数值类型一致
定义好了泛型函数后,我们要来看下怎么使用这个泛型函数,有两种方式:
//第一种是传入所有参数,知名T类型变量的具体类型
let output = identity<string>("myString"); //“myString”
第二种方法更普遍(推荐),利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型
let output = identity("myString"); //“myString”
我们其实还可以把泛型变量T当做类型的一部分使用,而不是整个类型,看个例子:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
上述例子可以这么理解:loggingIdentity还是一个泛型函数,因为引入了T类型变量,它接受一个参数arg,它的类型是一个内部元素是T类型的数组,函数返回值也是一个内部元素是T类型的数组,假设调用时传入的arg是[1,2,3],所以此时的T类型变量就代表着number类型,它只充当了数字数组的一部分(元素的类型),而不是代表着整个入参和返回值的类型
7.2、泛型接口
泛型除了可以创建泛型函数外,还可以创建泛型接口:
interface GenericIdentityFn { // 定义一个接口,指定为一个泛型函数类型,入参为T,返回值也为T
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity; // 指定myIdentity为GenericIdentityFn结构类型,然后将相同类型结构的泛型函数identity赋值给了myIdentity
一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数
interface GenericIdentityFn<T> { // 定义了一个泛型接口,结构类型为函数,函数入参为T类型,返回值为T类型
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity; // 这里在实际使用时将T类型指明为number
7.3、泛型类
泛型类使用( <>)括起泛型类型,跟在类名后面
class GenericNumber<T> { // 定义了一个泛型T类型的GenericNumber类
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
注意:类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
7.4、泛型约束
现在有这么个需求:我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求
interface Lengthwise { //创建一个包含length属性的接口,让泛型函数实现这个接口
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number类型不包含length属性
loggingIdentity({length: 10, value: 3}); // ok
7.5、在泛型约束中使用类型参数
你声明一个类型参数,且它被另一个类型参数所约束
function getProperty<T,K extends keyof T>(obj: T, key: K) { //这keyof的意思就是K类型是T类型所有属性key组成的数组中的一员
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(getProperty(x, "a")); // ok
console.log(getProperty(x, "b")); // ok
console.log(getProperty(x, "m")); // error
八、枚举
8.1、数字枚举
// 默认自增
enum Person {
name,
age,
love,
hobby
}
console.log(Person.name); // 0
console.log(Person.hobby); // 3
// 可人为自行修改
enum Person {
name = 4,
age = 3,
love = 2,
hobby = 1
}
console.log(Person.name); // 4
console.log(Person.hobby); // 1
8.2、数字枚举的反向映射
数字枚举成员还具有 反向映射 的特性,从枚举值到枚举名字,要注意的是 不会为字符串枚举成员生成反向映射。
enum Person {
name,
age
love,
hobby
}
console.log(Person[Person.love]); // love
8.3、字符串枚举
在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
enum Person {
name = 'NAME',
age = 'AGE',
love = 'LOVE',
hobby = 'HOBBY'
}
console.log(Person.name); // NAME
console.log(Person.hobby); // HOBBY
8.4、异构枚举
枚举可以混合字符串和数字成员,但基本不会这么用
enum Person {
name = 1,
age = 2,
love = 'LOVE',
hobby = 'HOBBY'
}
console.log(Person.name); // 1
console.log(Person.hobby); // HOBBY
8.5、常量枚举(const枚举)
常量枚举只能使用常量枚举表达式,不同于常规的枚举,它们在编译阶段会被删除
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
//编译后生成的js文件就是下面这行代码
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
九、类型推论
类型推论说白点就是在ts中,有时你写的语句没有声明类型,ts会自动帮你推断属于什么类型
//基本类型推论
let a = 123; //a就会自动被推断为number类型
a= "haha" //error,再次给a赋值为string类型就会报错
//联合类型推论
let arr = [1,"a"] //同样没给a声明类型,此时a会自动推论为<Array>[number | string]类型
arr = [12,23,true] // error,此时内部元素有boolean类型就会报错
//上下文类型推论(就是根据“=”一边来推论另一边)
window.onmousedown = function(event) {
console.log(event.a); //<- Error
};
//上述代码ts会根据=左边的鼠标点击函数类型来推断右侧函数表达式的类型,进一步推断event参数的类型为MouseEvent,因为MouseEvent中没有a的属性,所以会报错
//这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了
window.onmousedown = function(event: any) {
console.log(event.a); //ok
};
十、类型兼容性
TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性,来看下面例子
interface NameInterface {
name: string;
}
let x: NameInterface;
let y = { name: 'Alice', location: 'Seattle' };
let z ={ age:23 }
x = y; //ok
x = z //error ,因为z对象中没有包含name属性,不满足x的类型(接口定义的结构类型要求)
接下来我们看下函数的兼容性:
1、首先从函数参数的个数上来看
let x = (a: number) => 0; // x函数只有一个参数a
let y = (b: number, s: string) => 0; // y 函数有两个参数,一个number,一个string
y = x; // OK
x = y; // Error
// 如果上面例子不好理解的话,你再看下面这个例子你就会清楚了
let arr = [1,2,3,4]
arr.forEach((item,index,array)=>{console.log(item)}) // 这是完整的写法
arr.foreEach((item)=>{console.log(item)}) //只用到了item就只写item也是ok的(参数个数小于完整写法)
结论:当你要给一个函数赋值时,要赋值的函数参数个数必须小于等于被赋值的函数
2、函数返回值个数角度来看:
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error
3、函数返回值类型角度来看:
let x = ():string | number => 0 //x可以有两种返回值类型string和number
let y = ():string =>"a" //y只有一种返回值类型string
x=y //ok
y=x //error
4、函数参数的双向协变:
let funcA = (arg:number | string ):void=>{} // 函数A参数的类型可以是number,也可以是string
let funcB = (arg:number):void=>{} // 函数B参数的类型必须是number
funcA = funcB // OK
funcB = funcA // OK
5、枚举这块的兼容性:
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的
enum animalEnum {
dog,
cat
}
enum statusEnum {
on,
off
}
let a = animalEnum.dog
a = 4 //ok的,因为上面的赋值相当于是0,因为枚举和数字类型是兼容的
a = statusEnum.on //error,这里就会报错了,虽然statusEnum.on 的值也是0,但是他是属于不同的枚举的,所以不兼容
6、类这块的兼容性:
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
class Animal{
feet: number;
constructor(name: string, numFeet: number) {
this.feet = numFeet; //animal类的实例有个feet属性,是number类型
}
}
class Size {
feet: number;
constructor(meters: number) {
this.feet = meters //Size类的实例也有个feet属性,也是number类型
}
}
let a: Animal = new Animal("name1",4); //a是animal类的实例
let s: Size = new Size(8); //s是Size类的实例
a = s; //OK ,两者的属性类型相同都是number
s = a; //OK,两者的属性类型相同都是number
//但是如果我稍微改写下,就不一样了......
class Animal{
feet: number;
name:string;
constructor(name: string, numFeet: number) {
this.feet = numFeet;
this.name=name // 给Animal类的实例增加一个name属性,是string类型
}
}
...其他都不变
a = s; //error现在就会报错了,因为a实例要求有两个属性name和feet,但s只有一个feet
十一、高级类型
11.1、交叉类型
概念:交叉类型是将多个类型合并为一个类型(可以理解为“与”)。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性
let mergeFn = <T,U>(arg1:T,arg2:U):T&U =>{ //返回值就是T&U的交叉类型,因为既包含arg1又包含arg2的类型
let res={} as T&U; //类型断言为T&U的交叉类型
res = Object.assign(arg1,arg2) //res对象是对象arg1和arg2的合并
return res
}
let a = mergeFn({name:"haha"},{age:123}) // a就是T&U类型了,所以再给a赋别的类型就会报错
a =[123] //error
a={like:"123"} // error
a= {name:"123",age:34} //ok
11.2、联合类型
概念:联合类型就是 “ | ”操作符,例如:string | number,表明string和number两者之一都可以,好像“或”的逻辑
let func =(content: string | number):number=>{ //这个func函数的形参接受string或者number,返回值为number
if(typeof content ==="string"){return content.length}
else {return content.toString().length}
}
console.log(func(123)) //ok 3
console.log(func("sahd")) // ok 4
console.log(func(false)) //error
11.3、类型保护
概念:所谓的类型保护就是当某个变量的类型是联合类型时,但你想要对其可能的类型做区分,做各自不同的处理操作,比如变量a是string | number类型,你想要如果是string类型的话,就取它的长度,如果是number类型的话,就做加减运算,看下面这个例子
let res = [123,"asd"]
let getRandomValue = ()=>{ // 该函数是根据随机产生的number值来取res中元素的值
const number = Math.random()*10
if(number>5){
return res[0]
}else{
return res[1]
}
}
const a = getRandomValue() // 此时a变量是一个number | string类型的值
if(a.length){ // error,不能这么写,因为a有可能是number类型
console.log(a.length)
}else{
console.log(a * 10)
}
上述例子就可以用到类型保护,类型保护有多种方式:
//方式一:typeof,就是用typeof来判断变量的类型,只适用于number、string、boolean、symbol这四种类型用这种类型保护的方式,而且如果typeof用作类型保护的话,后面就必须跟===或者!==
改写:if(a.length) => if(typeof a === "stirng") // ok
//方式二: instanceof,通过这个标识符就直接判断当前实例是哪个类创建的了,也就确定了实例的类型
class c1 {
public name= "c1"
constructor() {
}
}
class c2 {
public age= "22"
constructor() {
}
}
let func =()=>{
return Math.random()<0.5 ? new c1() : new c2()
}
let a = func() // a是有两种可能的,要么就是c1的实例,有name属性,要么是c2的实例,有age属性
if(a instanceof c1){ // 这样写就是ok的,能确定下来a就是c1的实例,下面打印a.name也就不会报错
console.log(a.name)
}else{
console.log(a.age)
}
备注:这里有一点再讲下,就是在ts中如果只有两种类型可能的联合类型,如果在if语句中已经确认了其中一种,那么编译器就会自动识别确认else中的类型为另一种
11.4、null和undefined
概念:在ts中null和undefined也是不一样的,这和js是一样的,如果没有在ts的tsconfig配置文件中设置strictNullChecks:true,那么当你声明一个变量时,编译器会自动帮你将上undefined和null类型,使其称为一个联合类型,默认情况下,类型检查器认为 null与 undefined可以赋值给任何类型,看例子:
// 默认情况,假设此时strictNullChecks:false
let a = 123;
a= null // ok
a=undefined // ok
// 严格模式(strictNullChecks:true)
let a = 123;
a= null // error
a=undefined // error
11.5、类型别名
概念:所谓类型别名,其实就是用type来声明一个类型变量名,后面要接”=“号,这个接口定义时不太一样,要注意,后续定义别的变量时就可以使用这个类型别名了,这样定义的变量类型和这个类型别名的类型时一致的
type typeName =string //类型别名叫typeName,类型是string
let a: string = "123" //常规方法定义a变量为string类型
let b: typeName = "asd" //b变量用类型别名typeName,同样是string类型
//类型别名也可以和泛型结合使用
type typeInterface<T> ={a:T,b:T}
let A : typeInterface<string>
A = {a:"123",b:"123"} //ok
A = {a:123,b:123} // error,因为A是一个对象,里面有a和b属性都有string类型,所以number类型会报错
注意:
1、类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)
2、如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名
11.6、字符串字面量类型
概念:就是指定类型为一个具体的字符串的值
type a = "haha"
let b :a = "haha" // ok
let c :a ="123" // error,因为a的类型是”haha“的字符串字面量形式,所以c只能是”haha“
11.7、数字字面量类型
概念:就是指定类型为一个具体的数字的值
type a = 123
let b :a = 123// ok
let c :a =456 // error,因为a的类型是123的数字字面量形式,所以c只能是123
11.8、索引类型
概念:索引类型用到keyof标识符,后面一般接一个定义的接口,它会把接口中的属性名组成一个联合类型
interface Person {
name: string;
age: number;
}
let personProps: keyof Person; // 把Person接口中的name和age属性组成 'name' | 'age'的联合类型
personProps="name" // ok
personProps="age" //ok
personProps="haha " //error
11.9、映射类型
概念:所谓的映射类型就是将一个已知的类型每个属性都变为可选的(比如变为readOnly或者可选等),可以理解对已知类型中的每个属性进行一道转换
优势:假设原先有个interface,内部属性都是可读可写的,如下面例子中的info,如果你想要将里面的三个属性都变为readOnly的,你可以重新定义一个接口,然后照原样重写一版readOnly的接口,这对于属性值少的情况下也是可以的,但是如果该接口属性有几十个,你全部重写一边很麻烦,所以这就可以用到我们的映射类型了,效果直接看下面demo
interface info { // 定义一个接口,包含三个属性
name:string;
age:number;
sex:string
}
type readOnly<T> = { //类似于定义一个映射转换的函数,将传入的接口对象的每个属性前面都加上readOnly
readonly [P in keyof T] : T[P] // ts中的in,有点类似于js中的 for...in遍历
}
type newType =readOnly<info> // 此时newType类型就是一个拥有三个属性(name,age,sex),并且全部是readOnly的对象类型
let obj :newType={
name:"haha",
age:12,
sex:"男"
}
obj.age=133; //error ,属性为只读的,不允许修改
十二、模块
12.1、ES6模块回顾
在讲ts中的模块的时候,我们先回顾复习下原先ES6中的模块知识:
-
ES6中的模块导入导出是通过import 和export实现的,export导出有一些规则,我们来看下
//直接导出声明表达式 export const a = 12 //OK,导出一个变量a export function func(){ ... } //OK,导出一个函数a export class A {...} //OK,导出一个类 //先定义,最后统一对象形式导出 const a = 12; const func = function(){} export { a ,func } // OK //错误写法 const a = 123 export a // error export 123 // error
-
ES6中的模块中的导入语句要写在文件的顶层(这里的顶层不是指文件的最上面,而是指文件的最外层,不能出现在块级作用域中,因为ES6中的模块是静态编译,所以如果你把导入语句放在块级作用域中会报错的);
-
模块在导入导出的过程中可以给变量起别名,用as关键字,例如:
export { a: b,func }
,导出过程中把a变量更名为b,这样在引入的时候就要使用b而不是a了; -
引入的内容是只读的,不能对其值进行修改(除对象外,当然我们为了方便后续排查问题,我们不推荐修改)
-
如果原模块中的导出的值变化时,引入的地方也会动态发生改变
-
引入的js文件后缀.js是可以省略的
-
import引入语句有提升到文件最上面(有点js的var声明提升),就是即使你在使用导入的变量后再引入也是ok的
-
ES6中还有一种默认导出的方式(export default),它在一个模块中只能出现一次,它的用法:
//方式一:直接后面跟函数定义 export default function add(){ console.log(...) } // 方式二:也可以先定义,后导出变量名 function add (){ console.log(....) } export default add; //也可以直接导出值 export default "haha"
-
如果一个模块导出时同时存在export default (导出A) 和export(导出b和c) ,那么另一个文件在导入该模块时就要这么写了
import A,{ b, c } from "..."
-
如果要在js中实现按需加载的话,也就是想要在块级作用域中按需引入模块的话,可以使用import()方法
12.2、TS中的模块
基本上都是和ES6中的模块是一样的,有一些区别的点讲一下:
-
TS中的模块中有export = 和 import = require()这种导入导出的配合方式
//导出模块 const name = "123" export= name //导入模块 import name = require("./index")
-
TS中除了可以导出函数、变量、类之外,还允许导出接口和类型别名
// 导出模块,导出一个接口和一个类型别名
export interface typeA {
name:string
}
export type typeB = number
//导入文件,直接可以使用导入的接口和类型别名
import {typeA,typeB} from './index'
let c: typeA ={name:"123"}
let d :typeB= 123
十三、声明合并
13.1、同名接口的合并:
当定义的两个接口名相同时,ts会将其类型合并,也就是说要同时满足两个接口的类型:
interface typeA {
name:string
}
interface typeA {
age:number
}
let c : typeA={ // 此时给c变量赋值时就必须同事拥有nam和age属性,并且类型要与接口中的一致
name:"haha",
age:12
}
当两个定义的接口中函数成员重名时,ts会把它们当做函数重载的方式来处理:
interface typeA {
getRes(input:string):number
}
interface typeA {
getRes(input:number):string
}
let c : typeA={
getRes( text): any { //此时的getRes函数中的text入参的类型就是number和string的联合类型
if(typeof text ==="string"){
return text.length
}else{
return String(text)
}
}
}
console.log(c.getRes("123")) // 3
console.log(c.getRes(12341)) //12341
13.2、同名命名空间的合并:
两个同名命令空间中的export导出的部分会进行合并,没有export的那部分会被忽略(也就是不能合并)
// 上面两个命名空间的定义等同于第三个命名空间,就是直接合并了
namespace N {
export const name = "haha"
}
namespace N {
export const age = 123
}
||
namespace N {
export const name = "haha"
export const age = 123
}
13.3、命名空间和类的同名合并:
注意:同名的类必须要写在同名的命名空间的前面,然后两者合并后,会把命名空间导出的属性当做类的静态属性,注意是静态属性,只能通过类名来调用访问
class C{
constructor(){}
public say(){console.log(11)}
}
namespace C{
export const he = "ha"
}
const obj = new C(); //创建类的实例
console.log(obj.say()) //11
console.log(C.he) //"ha" ,只能通过类名来访问刚合并过来的命名空间中的静态属性
13.4、命名空间和函数的同名合并:
注意:同样的道理,同名函数必须写在命名空间的前面才会合并生效
function C(){
console.log(C.a) //此处输出的就是命名空间中导出的a变量
}
namespace C{
export const a = 123
}
console.log(C()) //123
13.5、命名空间和枚举的同名合并:
enum C{
name,
age,
sex
}
namespace C{
export const a = 123
}
console.log(C)
//输出结果:{
'0': 'name',
'1': 'age',
'2': 'sex',
name: 0,
age: 1,
sex: 2,
a: 123
}
十四、装饰器
注意 装饰器是一项实验性特性,在未来的版本中可能会发生改变
所以我们这里就暂时先不讨论了,具体有兴趣的同学可以去官网看:TypeScript官网
十五、Mixins(混入)
15.1、对象的混入
interface A {
a:string
}
interface B {
b:string
}
let Aa : A ={
a:"a"
}
let Bb : B ={
b:"b"
}
// @ts-ignore
let AB = Object.assign(Aa,Bb) //其实就是用到了Object.assign()方法,将两个对象进行混入,此时AB变量的类型就是A & B的交叉类型
console.log(AB); // {a: "a",b:"b"}
15.2、类的混入
class Aa {
public isA:boolean
public funcA(){}
}
class Bb {
public isB:boolean
public funcB(){}
}
class AB implements Aa,Bb {
public isA:boolean=false
public isB:boolean=false
public funcA:()=>void
public funcB:()=>void
constructor() {
}
}
function mixins(base:any,from:any[]){ // 定义一个方法,将类A和类B混入至类AB中
from.forEach(item=>{
Object.getOwnPropertyNames(item.prototype).forEach(key=>{
base.prototype[key]=item.prototype[key]
})
})
}
mixins(AB,[Aa,Bb])
const ab = new AB();
console.log(ab);
十六、编译选项
手动编译=>自动编译:
每个ts文件要正常使用都先需要编译(tsc a.ts),而且,每次修改内容后还需要重新编译,这相当于是每次都要手动编译,那有没有别的方式来改变现状,使其自动编译呢?有,可以在tsc a.ts -w
后面加上-w,这样这个a文件内容变化后就会自动编译,因为编译器会实时监听这个a文件的内容变化
单个文件编译=>多个文件编译:
上面提到的都是单个文件分别编译,那如果项目中文件很多,就会很不方便,那有没有多个文件(或者全部ts文件)一同编译呢?有,直接在项目文件夹下新建一个tsconfig.json配置文件,里面是一个json对象({}),然后就可以使用tsc 或者tsc -w
对整个项目的编译,这里提到的tsconfig.json文件就是来配置编译的选项的,有很多配置内容,接下来我们详细地来看下
tsconfig.json文件配置项:
-
include
- 定义希望被编译的文件目录
- 默认值:["**/*"] ,这里的 **表示任意文件夹,*表示任意文件
- 示例:{ include :["./src/**/*" ,"test/**/*"] }
- 上述配置表示src和test下面的所有文件都要编译 -
exclude
- 定义需要排除在外的目录
- 默认值:[ ‘node_modules’,“bower_components”,“jspm_packages”]
- 示例:{ exclude :[ "./src/hello/**/*" ] }
- 表示src/hello文件夹下的内容都不会被编译 -
extends
- 定义被继承的配置文件
- 示例:{ extends: "./config/base" }
- 表示当前编译配置文件会自动包含config目录下base.json中的所有配置信息 -
files
- 指定被编译文件的列表,只有编译文件少的时候才会用到这个配置项
- 示例:{ files:["a.ts","b.ts","c.ts"...]}
- 表示列表中的文件都会被编译 -
compilerOptions
该配置项是配置文件中最重要的也比较复杂的配置项- target:" es6 ", 用来指定编译后js文件的ES版本
- module:‘es6/commonjs…’ ,用来指定要使用的模块化规范
- outDir:‘./dist’ ,用来指定编译后文件所在的目录,原先是在每个ts文件同级处
- outFile:"./dist/app.js" , 将编译后的所有js文件合并成一个js文件,名叫app.js
- allowJs:false, 是否对js文件进行编译,默认false
- checkJs:false,检查js代码是否符合ts规范
- removeComments:false,是否移除注释
- noEmit:true,不生成编译文件(了解,使用场景不多)
- noEmitOnError:false,当有错误时不生成编译后的文件,默认是false
- alwaysStrict:false,表示编译后的js文件是否使用严格模式,默认false
- noImplicitAny:true,不允许隐式的any类型
- noImplicitThis:true,不允许不明确类型的this
- strictNullChecks:true,严格的检查空值
- strict:true,所有严格检查的总开关
十七、使用webpack打包ts
1、初始化项目:npm init -y
,在当前项目中生成一个package.json文件
2、安装下载webpack、webpack-cli、typescript、ts-loader
npm i webpack webpack-cli typescript ts-loader -D
3、新建一个webpack配置文件webpack.config.js
const path =require("path")
module.exports={
entry:'./src/index.ts',
output:{
path:path.resolve(__dirname,'dist'),
filename:"bundle.js"
},
module:{
rules:[
{
test:/\.ts$/,
use:"ts-loader",
exclude:/node-modules/
}
]
}
}
4、新建tsconfig.json配置文件
5、然后在package.json下的script中配置build:“webpack”,这样就可以用build命令来打包了
6、此时就可以执行npm run build