Vue如何导入TypeScript

什么是Typescript?

TypeScript 是微软开发一款开源的编程语言,它是 JavaScript 的一个超集,本质上是为 JavaScript 增加了静态类型声明。任何的 JavaScript 代码都可以在其中使用,不会有任何问题。TypeScript 最终也会被编译成 JavaScript,使其在浏览器、Node 中等环境中使用。由于最终运行的仍然是 JavaScript,所以 TypeScript 并不依赖于环境的支持,也并不会带来兼容性问题。

同时,TypeScript 也是 JavaScript ES6 的超集。

Vue3.0 将使用 TS 重写,重写后的 Vue3.0 将更好的支持 TS。

GoogleAngular 2.0 也宣布采用 TypeScript 进行开发。

2019 年 TypeScript 将会更加普及,能够熟练掌握 TS,并使用 TS 开发过项目,将更加成为前端开发者的优势。

为什么要用TypeScript?

TypeScript 的类型注解是一种轻量级的为函数或变量添加约束的方式,它的优势在于静态类型检查。

概括来说主要包括以下几点:

  1. 静态类型检查

    即你编写的代码即使没有被执行到,一旦你编写代码时发生类型不匹配,语言在编译阶段即可发现。

  2. IDE 智能提示

    TypeScript 这一类语言之前,JavaScript 的智能提示基本完全依赖 IDE 提供的猜测,这种猜测可能并不正确,并且也缺乏更多的辅助信息。TypeScript 不仅自己写的类库有丰富的类型信息,也可以对其他纯 JS 项目进行类型标注 (DefinitelyTyped),便于使用者直接在 IDE 中浏览 API,效率大增。

  3. 代码重构

    有时候需要修改一些变量/属性/方法名,牵涉到属性和方法的时候,很多改动是跨文件的,不像普通变量可以简单定位 scope,属性方法名的重命名对于 JS 来说异常痛苦,一方面是修改本身就不方便,另一方面是改了还不确定该改的是不是改了,不该改的是不是也改了。

    TypeScript 的静态类型系统就可以较为完美的解决这个问题。

  4. 可读性

    类型就是最好的注释。对于阅读代码的人来讲,各种便利的类型一目了然,更容易明白作者的意图。

常见的变量类型定义:

// 布尔值
let isDone: boolean = false; // 相当于 js 的 let isDone = false;
// 变量定义之后不可以随便变更它的类型
isDone = true // 不报错
isDone = "我要变为字符串" // 报错

// 数字
let decLiteral: number = 6; // 相当于 js 的 let decLiteral = 6;

// 字符串
let name: string = "bob";  // 相当于 js 的 let name = "bob";

// 数组
 // 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:
let list: number[] = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];
// 第二种方式是使用数组泛型,Array<元素类型>:
let list: Array<number> = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];

// 在 TypeScript 中,我们使用接口(Interfaces)来定义 对象 的类型。
interface Person {
    name: string;
    age: number;
}
let tom: Person = {
    name: 'Tom',
    age: 25
};
// 以上 对象 的代码相当于 
let tom = {
    name: 'Tom',
    age: 25
};

// Any 可以随便变更类型 (当这个值可能来自于动态的内容,比如来自用户输入或第三方代码库)
let notSure: any = 4;
notSure = "我可以随便变更类型" // 不报错
notSure = false;  // 不报错

// Void 当一个函数没有返回值时,你通常会见到其返回值类型是 void
function warnUser(): void {
    console.log("This is my warning message");
}

// 方法的参数也要定义类型,不知道就定义为 any
function fetch(url: string, id : number, params: any): void {
    console.log("fetch");
}

不要使用如下类型NumberStringBooleanObject, 这些类型指的是非原始的装盒对象,它们几乎没在JavaScript代码里正确地使用过。应该使用类型numberstring,and boolean

使用非原始的object类型来代替Object

不确定的类型用Any,它是可以随便变更类型的

Vue 引入 TypeScript

Vue CLI 提供了内建的 TypeScript 工具支持。如果你是新创建项目,可以查看TypeScript 支持

现有的项目,首先需要安装一些必要/以后需要的插件:

# 安装vue的官方插件
npm i vue-class-component vue-property-decorator -S

#ts-loader typescript 必须安装,其他的相信你以后也会装上的
npm i ts-loader typescript tslint tslint-loader tslint-config-standard -D
  • vue-class-component:强化 Vue 组件,使用 TypeScript/装饰器 增强 Vue 组件。

  • vue-property-decorator:在 vue-class-component 上增强更多的结合 Vue 特性的装饰器

  • ts-loaderTypeScript Webpack 提供了 ts-loader,其实就是为了让webpack识别 .ts、 .tsx文件

  • tslint-loadertslint:我想你也会在.ts .tsx文件 约束代码格式(作用等同于eslint

  • tslint-config-standardtslint 配置 standard风格的约束

配置 webpack

  • 首先找到./build/webpack.base.conf.js

  • entry中的入口文件xx.js改成xx.ts,顺便把项目文件中的xx.js也改成xx.ts,里面内容保持不变。

  • 找到resolve.extensions 里面加上.ts 后缀 (是为了之后引入.ts的时候不写后缀)

  • 找到module.rules 添加webpack.ts的解析

    //webpack.base.conf.js
    module.exports = {
    	entry: {
    		'bxs-docs': './docs/docs.ts',
    		'bxs-demo': './docs/demo.ts'
    	},
    	...
    	resolve: {
    		//默认解析扩展路径,设置完成后再引入文件后可以节约后缀名
    		extensions: ['.js', '.vue', '.css', '.ts', '.md'],
    	},
    	module: {
    		rules: [
    			{
    				test: /\.ts$/,
    				exclude: /node_modules/,
    				enforce: 'pre',
    				loader: 'tslint-loader'
    			},
    			{
    				test: /\.tsx?$/,
    				loader: 'ts-loader',
    				exclude: /node_modules/,
    				options: {
    				appendTsSuffixTo: [/\.vue$/],
    				}
    			},
          		...
    		]
    	},
    	...
    }

ts-loader 会检索当前目录下的 tsconfig.json 文件,根据里面定义的规则来解析.ts文件(就跟.babelrc的作用一样)。tslint-loader 作用等同于 eslint-loader

添加 tsconfig.json

接下来在根路径下创建tsconfig.json文件。这里有一份参考配置,完整的配置请点击 tsconfig.json

{
  // 编译选项
  "compilerOptions": {
    // 输出目录
    "outDir": "./output",
    // 是否包含可以用于 debug 的 sourceMap
    "sourceMap": true,
    // 以严格模式解析
    "strict": true,
    // 采用的模块系统
    "module": "esnext",
    // 如何处理模块
    "moduleResolution": "node",
    // 编译输出目标 ES 版本
    "target": "es5",
    // 允许从没有设置默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
    // 将每个文件作为单独的模块
    "isolatedModules": false,
    // 启用装饰器
    "experimentalDecorators": true,
    // 启用设计类型元数据(用于反射)
    "emitDecoratorMetadata": true,
    // 在表达式和声明上有隐含的any类型时报错
    "noImplicitAny": false,
    // 不是函数的所有返回路径都有返回值时报错。
    "noImplicitReturns": true,
    // 从 tslib 导入外部帮助库: 比如__extends,__rest等
    "importHelpers": true,
    // 编译过程中打印文件名
    "listFiles": true,
    // 移除注释
    "removeComments": true,
    "suppressImplicitAnyIndexErrors": true,
    // 允许编译javascript文件
    "allowJs": true,
    // 解析非相对模块名的基准目录
    "baseUrl": "./",
    // 指定特殊模块的路径
    "paths": {},
    // 编译过程中需要引入的库文件的列表
    "lib": [
      "dom",
      "es2015",
      "es2015.promise"
    ]
  }
}

添加 tslint.json

在根路径下创建tslint.json文件。这里就很简单了,就是 引入 tsstandard 规范

{
  "extends": "tslint-config-standard",
  "globals": {
    "require": true
  }
}

让 ts 识别 .vue

由于 TypeScript 默认并不支持 *.vue 后缀的文件,所以在 vue 项目中引入的时候需要创建一个 vue-shim.d.ts 文件,放在项目对应使用目录下,例如 src/vue-shim.d.ts

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

//全局变量/方法声明
declare global {
  interface Window {
    SystemJS: any; // 如果不确定类型, 可定义为any
  }
}

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

而在代码中导入 *.vue 文件的时候,**需要写上 .vue 后缀。**原因还是因为 TypeScript 默认只识别 *.ts 文件,不识别 *.vue 文件:

import abcd from 'components/abcd.vue'

改造 .vue 文件

  • import .vue 的文件的时候,**要补全 .vue 的后缀,**否则会提示语法错误或找不到模块

  • script 标签上加上 lang="ts", 意思是让webpack将这段代码识别为typescript 而非javascript

  • vue-class-component:对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化。主要是配置以下属性:

    • components,注册子组件

    • filters,过滤器

    • directives,注册或获取全局指令。

    • ...

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

    • @Emit

    • @Inject

    • @Model

    • @Prop

    • @Provide

    • @Watch

Vue常见选项的转换

templatestyle模块不需要改,只需对script进行转换。

<template>
    <div></div>
</template>

<script lang="ts">
import Abcd from './Abcd.vue'
import { Vue, Component, Emit, Inject, Model, Prop, Provide, Ref, Watch, PropSync } from 'vue-property-decorator'
//或者
//import Vue from 'vue'
//import Component, { mixins } from 'vue-class-component'
//import { Emit, Inject, Model, Prop, Provide, Watch, PropSync, Ref } from 'vue-property-decorator'

// 一定要用Component修饰。详情查看下一章节
@Component({
    components: {
        Abcd
    }
})

//Demo相当于原来的name: 'Demo'
export default class Demo extends Vue {
    //data,直接写
    count: number = 0
    desc: string = '123'
    private tx: string = ''
	//用 public, private 等修飾詞來決定能不能從外部 (別的 class) 存取

	// @Prop(options: (PropOptions | Constructor[] | Constructor) = {})
    // @Prop装饰器接收一个参数,这个参数可以有三种写法:
    // Constructor,例如String,Number,Boolean等,指定 prop 的类型;
    // Constructor[],指定 prop 的可选类型;
    // PropOptions,可以使用以下选项:type,default,required,validator。
    @Prop()
    propA: number = 1

	// readonly是typeScriptr的属性,指定为只读
	// 感叹号是非null和非undefined的类型断言。如果不加,TypeScripts可能会报错"从未被初始化过"
    @Prop({
        type: [String, Number], //构造器
        default: 'default value', //默认值
        required: true, //必传
        validator() {} //检验器
    })
	readonly propA!: string | number
    //类型为Object,设置为any
    @Prop({
        default: () => {B
            return {}
        }
    })
    propB: any

	// @PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {})
    // @PropSync装饰器与@prop用法类似,二者的区别在于,@PropSync 装饰器接收两个参数:
    // propName: string 表示父组件传递过来的属性名;
    // options: Constructor | Constructor[] | PropOptions 与@Prop的第一个参数一致;
    // @PropSync 会生成一个新的计算属性。
    // @PropSync需要配合父组件的.sync修饰符使用
    @PropSync('name', { type: String })
    syncedName!: string

    // methods 直接在此定义
    private add() {
    	this.number++
    }
    
    // 生命周期直接定义为同名屬性
	created () {}
    mounted () {}

    // 计算属性
    get msg () {
        return 'computed ' + this.demo
    }

    // @Watch(path: string, options: WatchOptions = {})
    @Watch('child', {
        immediate: true,
        deep: true
    })
    onChildChanged (val: string, oldVal: string) {
        if (val !== oldVal) {
            console.log(val)
        }
    }
}
</script>

以上代码等价与:

<template>
    <div></div>
</template>
<script>
    
export default {
	name: "Demo",
    components: {
        Abcd
    },
	data () {
		return {
        	count: 0,
            desc: '123',
            private tx: ''
		}
	},
	props: {
		number: {
			type: Number,
            default: 1
		},
        propA: {
            type: [String, Number], //构造器
            default: 'default value', //默认值
            required: true, //必传
            validator() {} //检验器
        },
        propB: {
			type: Object,
            default: () => {B
            	return {}
            }
        },
		// @PropSync中定义的
        props: {
            name: {
                type: String
            }
        }
    },
	created () {},
    mounted () {},
	computed: {
        // @PropSync生成的计算属性
		syncedName: {
			get() {
                return this.name
            },
			set(value) {
				this.$emit('update:name', value)
			}
        },
		msg: {
			return 'computed ' + this.demo
		}
    },
	watch: {
        child: {
            immediate: true,
			deep: true,
			handler: 'onChildChanged'
        }
    },
	methods: {
		onChildChanged (val: string, oldVal: string) {
            if (val !== oldVal) {
                console.log(val)
            }
        }
    }
}
</script>

@Component

.vue文件中一定要用Component修饰,参数可不传。

@Component配置 vue-property-decorator不支持的属性,如: components,filters,directives等

<template>
  <span v-demo:foo.a="1+1">test</span>
</template>

<script lang="ts">
    import Abcd from './Abcd.vue'
    import Component from 'vue-class-component'
    @Component({
        components: {
            Abcd
        }, 
        filters: {},
        directives: {
            demo: {
				bind(el:any, binding:any, vnode:any, oldVnode: any) {
				}
            }
		}
    })
    export default class App extends Vue {}
</script>

Mixins

TypeScript中, 我们可以这么写:

// 定义要混合的类 mixins/index.ts
import {Vue, Component} from 'vue-property-decorator'

// 一定要用Component修饰
@Component
export default class myMixins extends Vue {
    mixinVal: string = 'Hello Mixin'
}

然后, 在其他组件中使用它:

<template>
	<div></div>
</template>

<script lang="ts">
    import Vue from 'vue'
    import Component, { mixins } from 'vue-class-component'
    import mixinDemo from './mixin'

    @Component()
    export default class App extends mixins(mixinDemo) {
        mounted () {
            window.console.log('mixinVal => ', this.mixinVal)
        }
    }
</script>

model

model是vue中的一个选项(用得比较少),它允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。

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

@Component
export default class YourComponent extends Vue {
  @Model('change', {
      type: Boolean
  }) readonly checked!: boolean
}

等价于:

export default {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: {
      type: Boolean
    }
  }
}

$emit

由@Emit修饰的函数,会在它们的原始参数之后,返回一个值。如果返回值是Promise,则在Promise执行后,再执行$emit

如果事件的名称没有通过事件参数提供,则使用函数名。在这种情况下,camelCase名称将转换为kebabo -case

如果@emit 对应的方法,还有别的参数,比如点击事件的 event,会在返回值之后。 也就是第三个参数。

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

@Component
export default class YourComponent extends Vue {
  count = 0

  @Emit()
  addToCount(n: number) {
    this.count += n
  }

  @Emit('reset')
  resetCount() {
    this.count = 0
  }

  @Emit()
  returnValue() {
    return 10
  }

  @Emit()
  onInputChange(e) {
    return e.target.value
  }

  @Emit()
  promise() {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(20)
      }, 0)
    })
  }
}

等价于:

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    addToCount(n) {
      this.count += n
      this.$emit('add-to-count', n)
    },
    resetCount() {
      this.count = 0
      this.$emit('reset')
    },
    returnValue() {
      this.$emit('return-value', 10)
    },
    onInputChange(e) {
      this.$emit('on-input-change', e.target.value, e)
    },
    promise() {
      const promise = new Promise(resolve => {
        setTimeout(() => {
          resolve(20)
        }, 0)
      })
      promise.then(value => {
        this.$emit('promise', value)
      })
    }
  }
}

ref

ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

@Ref(refKey?: string)refKey值可以为String,如果省略传输参数,那么会自动将属性名作为参数。

@Ref与@Emit的区别:@Emit在不传参数的情况下会转为 dash-case,而 @Ref不会转,为原属性名。

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

import AnotherComponent from '@/path/to/another-component.vue'

@Component
export default class YourComponent extends Vue {
  @Ref() readonly anotherComponent!: AnotherComponent
  @Ref('aButton') readonly button!: HTMLButtonElement
}

等价于

export default {
  computed() {
    anotherComponent: {
      cache: false,
      get() {
        return this.$refs.anotherComponent as AnotherComponent
      }
    },
    button: {
      cache: false,
      get() {
        return this.$refs.aButton as HTMLButtonElement
      }
    }
  }
}

另外还有些:@Inject@Provide,不常见,这里不例举了。。。

总结

vue+webpack导入typeScript,实践过程其实并不复杂:

  • 首先要对typescript有个初步了解,这个其实很简单,无非是对变量、函数、对象属性加了类型控制,可以查看前文中【常见的变量类型定义】,更详情的查看TypeScript 中文手册

  • 然后是将typescript安装到项目,新增一些配置文件,在webpack中将其引入;

  • 最后是vue中的一些选项:data、prop、created、methods、watch、computed等的写法转换。

参考链接

Vue 如何導入 TypeScript

Vue & TypeScript 初体验

vue + typescript 项目起手式

TypeScript 中文手册

最后更新于