08Vue+TS实战

08Vue+TS实战

准备工作

新建一个基于 TS 的 Vue 项目:

image.png

在已存在项目中安装 TS

vue add @vue/typescript

请暂时忽略引发的几处 Error,它们不影响项目运行,我们将在后面处理它们。

TS 特点

TypeScript 是 JavaScript 的超集,它可编译为纯 JavaScript,是一种给 JavaScript 添加特性的语言扩展。TS 有如下特点:

  • 类型注解和编译时类型检查

  • 基于类的面向对象编程

  • 泛型

  • 接口

  • 装饰器

  • 类型声明

image.png

类型注解和编译时类型检查

使用类型注解约束变量类型,编译器可以做静态类型检查,使程序更加健壮。

基础类型

// ts-test.ts
let var1: string  // 类型注解
var1 = "林慕" // 正确
var1 = 4  // 错误

编译器类型推断可省略这个语法

let var2 = true

常见的原始类型:

  • string

  • number

  • boolean

  • undefined

  • null

  • symbol

类型数组

let arr: string[]
arr = ['林慕']  // 或 Array<string>

任意类型 any

let varAny: any
varAny = 'xx'
varAny = 3

任意类型也可用于数组

let arrAny: any[]
arrAny = [1,true,'free']
arrAny [1] = 100

函数中的类型约束

function greet(person: string): string{
  return 'hello,'+person
}

void 类型,常用于没有返回值的函数

function warn(): void{}

范例:HelloWorld.vue

<template>
 <div>
  <ul>
   <li v-for="feature in features" :key="feature">{{feature}}</li>
  </ul>
 </div>
</template>
<script lang='ts'>
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class Hello extends Vue {
 features: string[] = ["类型注解", "编译型语言"];
}
</script>

类型别名

使用类型别名自定义类型。

可以用下面这种方式定义对象类型:

const objType: {foo:string,bar:string}

使用 type 定义类型别名,使用更便捷,还能复用:

type Foobar = { foo: string, bar: string }
const aliasType: Foobar

范例:使用类型别名定义 Feature,types/index.ts

export type Feature = {
  id: number,
  name: string
}

使用自定义类型,HelloWorld.vue

<template>
 <div>
  <!--修改模板-->
  <li v-for="feature in features" :key="feature.id">{{feature.name}}</li>
 </div>
</template>
<script lang='ts'>
// 导入接口
import { Feature } from '@/types'

@Component
export default class Hello extends Vue {
  // 修改数据类型
  features: Feature[] = [{ id: 1, name: '类型注解' }]
}
</script>

联合类型

希望某个变量或参数的类型是多种类型其中之一。

let union: string | number
union = '1'
union = 1
// 以上都是可以的

交叉类型

想要定义某种由多种类型合并而成的类型使用交叉类型。

type First = {first: number}
type Second = {second: number}
// fas将同时拥有属性first和second
type fas = First & Second

范例:利用交叉类型给 Feature 添加一个 selected 属性。

// types/index.ts
type Select = {
  selected: boolean
}
export type FeatureSelect = Feature & Select

使用这个 FeatureSelect,HelloWorld.vue

features: FeatureSelect[] = [
  { id: 1, name: '类型注解', selected: false },
  { id: 2, name: '编译型语言', selected: true }
]
<li :class="{selected: feature.selected}">{{feature.name}}</li>

.selected {
  background-color: rgb(168, 212, 247)
}

函数

必填参:参数一旦声明,就要求传递,且类型需符合。

// 02-function.ts
function greeting(person: string): string {
  return 'hello,' + person
}
greeting('tom')

可选参数:参数名后面加上问号,变成可选参数。

function greeting(person: string, msg?: string):string {
  return 'hello,' + person
}

默认值:

function greeting(person: string, msg = ''): string{
  return 'hello,' + person
}

函数重载:以参数数量或类型区分多个同名函数。

// 重载1
function watch(cb: () => void): void
// 重载2
function watch(cb1: () =>void,cb2:(v1:any, v2:any) => void): void
// 实现
function watch(cb1: () => void, cb2?: (v1: any, v2: any) => void){
  if(cb1 && cb2){
    console.log('执行watch重载2')
  } else {
    console.log('执行watch重载1')
  }
}

范例:新增特性,Hello.vue

<div>
  <input type="text" placeholder="输入新特性" @keyup.enter="addFeature">
</div>
addFeature(e: KeyboardEvent) {
  // e.target 是 EventTarget 类型,需要断言为 HTMLInputElement
  const inp = e.target as HTMLInputElement
  const feature: FeatureSelect = {
    id: this.features.length + 1,
    name: inp.value,
    selected: false
  }
  this.features.push(feature)
  inp.value = ''
}

范例:生命周期钩子,Hello.vue

created() {
  this.features = [{ id:1, name: '类型注解' }]
}

class 的特性

TS 中的类和 ES6 中大体相同,这里重点关注 TS 带来的访问控制等特性。

// 03-class.ts
class Parent {
  private _foo = 'foo'  // 私有属性,不能在类的外部访问
  protected bar = 'bar'  // 保护属性,可以在子类中使用

  // 参数属性:构造函数参数加修饰符,能够定义为成员属性
  constructor(public tua = 'tua') {}

  // 方法也有修饰符
  private someMethod() {}

  // 存取器:属性方式访问,可添加额外逻辑,控制读写性
  get foo() {
    return this._foo
  }
  set foo(val) {
    this._foo = val
  }
}

范例:利用 getter 设置计算属性,Hello.vue

<template>
  <li>特性数量:{{count}}</li>
</template>
<script lang='ts'>
  export default class HelloWorld extends Vue {
    // 定义getter作为计算属性
    get count() {
      return this.features.length
    }
  }
</script>

接口

接口仅约束结构,不要求实现,使用更简单

// 04-interface
// Person接口定义了结构
interface Person {
  firstName: string;
  lastName: string;
}
// greeting函数通过Person接口约束参数解构
function greeting(person: Person) {
  return 'Hello,' + person.firstName + ' ' + person.lastName
}
greeting({firstName: 'Jane', lastName: 'User'}) // 正确
greeting({firstName: 'Jane'})  // 错误

范例:Feature 也可用接口形式约束,./types/index.ts

接口中只需定义结构,不需要初始化

export interface Feature {
  id: number;
  name: string;
}

Interface vs type aliases

泛型

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

不使用泛型

interface Result {
  ok: 0 | 1;
  data: Feature[];
}

使用泛型

interface Result<T> {
  ok: 0 | 1;
  data: T;
}

泛型方法

function getResult<T>(data: T): Result<T> {
  return {ok:1, data}
}

// 用尖括号方式指定T为string
getResult<string>('hello')
// 用类型推断指定T为number
getResult(1)

泛型优点

  • 函数和类可以支持多种类型,更加通用;

  • 不必编写多条重载,冗长联合类型,可读性好;

  • 灵活控制类型约束;

不仅通用且能灵活控制,泛型被广泛用于通用库的编写。

范例:用 axios 获取数据

配置模拟一个接口,vue.config.js

module.exports = {
 devServer: {
   before(app) {
     app.get('/api/list', (req, res) => {
       res.json([
         { id: 1, name: "类型注解", version: "2.0" },
         { id: 2, name: "编译型语言", version: "1.0" }
        ])
     })
   }
 }
}

使用接口,HelloWorld.vue

  async mounted () {
    const resp = await axios.get<FeatureSelect[]>('/api/list')
    this.features = resp.data
  }

声明文件

使用 TS 开发时如果要使用第三方 JS 库的同时还想利用 TS 诸如类型检查等特性就需要声明文件,类似 xx.d.ts。

同时,Vue 项目中还可以在 shims-vue.d.ts 中对已存在模块进行补充。

npm i @types/xxx

范例:利用模块补充 $axios 属性到 Vue 实例,从而在组件里面直接用。

// main.ts
import axios from 'axios'
Vue.prototype.$axios = axios
// shims-vue.d.ts
import Vue from 'vue'
import { AxiosInstance } from 'axios'

declare module 'vue/types/vue' {
  interface Vue {
    $axios: AxiosInstance;
  }
}

范例:给 router/index.js 编写声明文件,index.d.ts

import VueRouter from 'vue-router'
declare const router: VueRouter
export default router

装饰器

装饰器用于扩展类或者它的属性和方法,@xxx 就是装饰器的写法。

属性声明:@Prop

除了在 @Component 中声明,还可以采用 @Prop 的方式声明组件属性。

export default class HelloWorld extends Vue {
  // Props() 参数是为 Vue 提供属性选项,加括号说明 prop 是一个装饰器工厂,返回的才是装饰器,参数一般是配置对象
  // !称为明确赋值断言,它是提供给 TS 的
  @Prop({type: String, required: true})
  private msg!: string  // 这行约束是写给 TS 编译器的
}

事件处理:@Emit

新增特性时派发事件通知,Hello.vue

// 通知父类新增事件,若未指定事件名则函数名作为事件名(羊肉串形式)
@Emit()
private addFeature(event: any){  // 若没有返回值形参将作为事件参数
  const feature = { name: event.target.value, id: this.features.length + 1}
  this.features.push(feature)
  event.target.value = ''
  return feature  // 若有返回值则返回值作为事件参数
}

变更监测:@Watch

@Watch('msg')
onMsgChange(val:string, oldVal:any){
  console.log(val,oldVal)
}

状态管理推荐使用:vuex-module-decorators

vuex-module-decorators 通过装饰器提供模块化声明 Vuex 模块的方法,可以有效利用 TS 的类型系统。

安装

npm i vuex-modulw-decorators -D

根模块清空,修改 store/index.ts

export default new Vuex.Store({})

定义 counter 模块,创建 store/counter

import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-
decorators'
import store from './index'
// 动态注册模块
@Module({ dynamic: true, store: store, name: 'counter', namespaced: true })
class CounterModule extends VuexModule {
  count = 1
  @Mutation
  add () {
    // 通过this直接访问count
    this.count++
  }
  // 定义getters
  get doubleCount () {
    return this.count * 2;
  }
  @Action
  asyncAdd () {
    setTimeout(() => {
      // 通过this直接访问add
      this.add()
    }, 1000);
  }
}
// 导出模块应该是getModule的结果
export default getModule(CounterModule)

使用,App.vue

<p @click="add">{{$store.state.counter.count}}</p>
<p @click="asyncAdd">{{count}}</p>
import CounterModule from '@/store/counter'
@Component
export default class App extends Vue { 
 get count() {
  return CounterModule.count
}
 add() {
  CounterModule.add()
}
 asyncAdd() {
  CounterModule.asyncAdd()
}
}

装饰器原理

装饰器是加工厂函数,他能访问和修改装饰目标。

类装饰器:类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

function log(target: Function){
  // target 是构造函数
  console.log(target === Foo) // true
  target.prototype.log = function() {
    console.log(this.bar)
  }
}

@log
class Foo{
  bar = 'bar'
}

const foo = new Foo()
// @ts-ignore
foo.log()

方法装饰器

function rec (target: any, name: String, descriptor: any) {
  // 这里通过修改descriptor.value扩展了bar方法
  const baz = descriptor.value
  descriptor.value=function(val: string){
    console.log('run method',name)
    baz.call(this,val)
  }
}

class Foo{
  @rec
  setBar(val:string){
    this.bar = val
  }
}

foo.setBar('lalala')

属性装饰器

function mua(target,name){
  target[name]='mua~'
}

class Foo{
  @mua ns!:string
}

console.log(foo.ns)

稍微改造一下使其可以接收参数:

function mua(params:string){
  return function (target, name){
    target[name] = param
  }
}

实战一下:


<template>
  <div>{{ msg }}</div>
</template>
<script lang='ts'>
import { Vue } from "vue-property-decorator";
function Component(options: any) {
  return function(target: any) {
    return Vue.extend(options);
  };
}
@Component({
  props: {
    msg: {
      type: String,
      default: ""
    }
  }
})
export default class Decor extends Vue {}
</script>

显然 options 中的选项都可以从 Decor 定义中找到。

参考资料

  1. TypeScript:https://www.typescriptlang.org/

  2. TypeScript 支持 Vue:https://cn.vuejs.org/v2/guide/typescript.html

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