[TOC]

1. 概述

  • Vue3使用Typescript进行了重写,优化了渲染机制,重写实现了数据响应式原理,同时也支持了组合式API的写法,还引入了诸多新特性和带来更好的性能。
  • TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。

本文首先介绍了Vue3和Typescipt 常用的一些基本语法,然后用上手demo来实践,更好的体验Vue3 + Typescipt进行开发。

2. Vue3 新特性

2.1 createApp

const app = Vue.createApp({})

返回一个提供应用上下文的应用实例。应用实例挂载的整个组件树共享同一个上下文。可以在 createApp 之后链式调用其它方法。

相比Vue2 new Vue()创建Vue实例,从同一个Vue构造函数创建的每个根实例共享相同的全局配置:

  • 在测试阶段,全局配置很容易意外地污染其他测试用例
  • 全局配置使得在同一页面上的多个“app”之间共享同一个 Vue 副本非常困难,但全局配置不同

2.2 组合式 API

用一张图看变化:
Vue3 + Typescipt 开发初探 - 图1
为什么不用之前的组件的option
data、computed、methods、watch 组织逻辑在大多数情况下都有效。然而,当我们的组件变得更大时,逻辑关注点的列表也会增长。这可能会导致组件难以阅读和理解,尤其是对于那些一开始就没有编写这些组件的人来说。而通过setup可以将该部分抽离成函数,让其他开发者就不用关心该部分逻辑了.

setup() {
  // ref 是一个函数,它接受一个参数,返回的就是一个神奇的 响应式对象 。
  我们初始化的这个 0 作为参数包裹到这个对象中去,在未来可以检测到改变并作出对应的相应。
  const count = ref(0)
  const double = computed(() => {
    return count.value * 2
  })
  const increase = () => {
    count.value++
  }
  return {
    count,
    increase,
    double
  }
}
  • setup作为一个组件选项,在创建组件之前执行并作为组合式 API 的入口点
  • 下面为setup的类型声明: ```typescript interface Data {

}

interface SetupContext { attrs: Data slots: Slots emit: (event: string, …args: unknown[]) => void }

function setup(props: Data, context: SetupContext): Data

> 从这里可以看出类型声明和 TS 类型的好处,可以看查看参数的类型。



<a name="vksQP"></a>
## 2.3 响应式
要为 JavaScript 对象创建响应式状态,可以使用 `reactive `方法:
```typescript
import { reactive } from 'vue'

// 响应式状态
const state = reactive({
  count: 0
})

当为一些基本类型时,可以使用ref

import { ref } from 'vue'

const count = ref(0)

2.4 生命周期

选项式 API setup
beforeCreate
created
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

2.5 computed 与 watch

  • computed ```typescript const count = ref(1) const plusOne = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 } })

plusOne.value = 1 console.log(count.value) //


- **watchEffect**

在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。
```typescript
const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)
  • watch
    const count = ref(0)
    watch(count, (count, prevCount) => {
    /* ... */
    })
    

    2.6 v-model

    默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和update:modelValue 作为事件。
    可以通过向 v-model 传递参数来修改默认名称:
    <my-component v-model:title="bookTitle"></my-component>
    
    子组件就可以通过$emit('update:title', $event.target.value)进行值的传递。
    类似于Vue2中的.sync写法

    3. TypeScript入门

TypeScript是JavaScript的超集,对于开发人员还是比较好上手的,主要是要习惯Type优先的写法,本章总体介绍一下TypeScript:

Typescript 官网地址: https://www.typescriptlang.org/zh/

3.1 安装

安装 Typescript:

npm install -g typescript

使用 tsc 全局命令:

// 查看 tsc 版本
tsc -v
// 编译 ts 文件
tsc fileName.ts

使用ts-node:

//安装
npm install -g ts-node


//使用ts-node即可
ts-node fileName.ts

3.2 基本数据类型

  • Boolean
  • Number
  • String
  • Null
  • Undefined

  • boolean/number/string类型,没啥好说的,最常用的三个类型,定义方式为: ```typescript let isDone: boolean = false;

let num: number = 6;

let name: string = “bob”;


- **undefined/null**
   - 默认情况下`null`和`undefined`是所有类型的子类型。 就是说你可以把 `null`和`undefined`赋值给`number`类型的变量。
   - 当编译时指定了`--strictNullChecks`选项,`null`和`undefined`只能赋值给`void`和它们各自

- **any **: 相当于不进行类型检查,尽量不要用
```typescript
let notSure: any = 4
notSure = 'maybe it is a string'

// 在任意值上访问任何属性都是允许的:
notSure.myName
// 也允许调用任何方法:
notSure.getName()
  • void
    • 声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull
    • 当一个函数没有返回值时,你通常会见到其返回值类型是 void ```typescript let unusable: void = undefined;

function warnUser(): void { console.log(“This is my warning message”); }


- **联合类型 **
```typescript
let numberOrString: number | string
  • 类型别名 ```typescript type a = string

type ab = string || number


<a name="jLHcv"></a>
## 3.3 数组和元祖

- **Array**
```typescript
/** 子元素是数字类型的数组 */
let arr1: number[] = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let arr2: string[] = ['x', 'y', 'z'];


//使用 Array 泛型

/** 子元素是数字类型的数组 */
let arr3: Array<number> = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let arr4: Array<string> = ['x', 'y', 'z'];

推荐使用 [] 这种形式来定义数组。一方面可以避免与 JSX 的语法冲突,另一方面可以减少不少代码量。

  • Tuple

元组类型允许表示一个已知元素数量类型的数组

// Declare a tuple type
let x: [string, number];

x = ['hello', 10]; // OK

x = [10, 'hello']; // Error

3.4 接口

在声明一个对象函数或者时,先定义接口,确保其数据结构的一致性。

  • 接口描述一个对象: ```typescript // 我们定义了一个接口 Person interface Person { name: string; age: number; }

// 接着定义了一个变量 tom,它的类型是 Person。这样, //我们就约束了 tom 的形式必须和接口 Person 一致。 let tom: Person ={ name: ‘tom’, age: 20 }

//也可以用可选属性: interface Person { name: string; age?: number; } let tom: Person = { name: ‘tom’ }

//还有只读属性, 那么可以用 readonly 定义只读属性 interface Person { readonly id: number; name: string; age?: number; }


- 接口描述一个类:
```typescript
interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}
  • 接口也可以描述函数类型: ```typescript interface SearchFunc { (source: string, subString: string): boolean; }

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


<a name="xHLQv"></a>
## 3.5 类型断言

- 有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息
- 通过_类型断言_这种方式可以告诉编译器,“相信我,我知道自己在干什么”
- 一般使用as语法,还可以使用`<类型>变量`
```typescript
let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

3.6 Class

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name
  }
  run() {
    return `${this.name} is running`
  }
}
const snake = new Animal('sss')

// 继承的特性
class Dog extends Animal {
  bark() {
    return `${this.name} is barking`
  }
}

const hhh = new Dog('hhh')
console.log(hhh.run())
console.log(hhh.bark())

类的成员可以用访问修饰符修饰:

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

3.7 泛型

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

//使返回值的类型与传入参数的类型是相同的
function identity<T>(arg: T): T {
    return arg;
}

//可以指定T
let output = identity<string>("myString");  // type of output will be 'string'

//也可以让编译器进行类型推论
let output = identity("myString");  // type of output will be 'string'
  • 泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法

interface IWithLength {
  length: number;
}

function echoWithLength<T extends IWithLength>(arg: T): T {
  console.log(arg.length)
  return arg
}

4. 开发上手

4.1 初始化

使用vue-cli,进行初始化

vue create vue3-demo

选择Vue 3.xTypesScipt初始化工程
image.png

初始化后目录结构:

─public
└─src
    ├─assets
    ├─components
    ├─App.vue
    ├─main.ts
    ├─shims-vue.ts
    └─tsconfig.json
  • shims-vue.d.ts ```typescript / eslint-disable / declare module ‘*.vue’ { import type { DefineComponent } from ‘vue’ const component: DefineComponent<{}, {}, any> export default component }
> shims-vue.d.ts是为了typescript做的适配定义文件,因为.vue文件不是一个常规的文件类型,TypeScript是不能理解vue文件是干嘛的,加这一段是是告诉 TypeScript,vue文件是这种类型的

- **tsconfig.json**
> [https://www.typescriptlang.org/docs/handbook/compiler-options.html](https://www.typescriptlang.org/docs/handbook/compiler-options.html)

typescript 的编译配置文件,里面含有很多的编译选项:

```typescript
{
  "compilerOptions": {
    "noImplicitAny": false, // 不允许隐式的any类型
    "target": "es5", // 目标语言的版本
    "module": "esnext",// 生成代码的模板标准
    "strict": true, // 开启所有严格的类型检查
    "jsx": "preserve", // JSX相关
    "importHelpers": true, // 通过tslib引入helper函数
    "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导
    "skipLibCheck": true,
    "esModuleInterop": true, // 允许export=导出,由import from 导入
    "allowSyntheticDefaultImports": true,
    "sourceMap": true, // 生成目标文件的sourceMap文件
    "baseUrl": ".", // 解析非相对模块的基地址
    "types": [ // 加载的声明文件包
      "webpack-env"
    ],
    "paths": { // 路径映射
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [ // 指定编译需要编译的文件或目录。
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  "exclude": [ // 指定编译器需要排除的文件或文件夹
    "node_modules"
  ]
}
  • App.vue ```vue

`export default defineComponent({ .. })`相比`export default { .. }` 能更好的获得提示和类型推导
<a name="rRPab"></a>
## 
<a name="ohSqW"></a>
## 4.2 实现一个Rate组件
使用 Vue3 和 TS实现一个简单的评分组件,如图为el-rate的想过<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/338495/1622427017381-04c7f6aa-8b99-40dc-aa81-21c815c69b93.png#clientId=ue21096bd-0527-4&from=paste&height=155&id=u11891152&margin=%5Bobject%20Object%5D&name=image.png&originHeight=155&originWidth=671&originalType=binary&size=4115&status=done&style=stroke&taskId=u8aba1fe3-cd18-4ce0-838e-c86b485d92a&width=671)
```vue
<el-rate v-model="value1"></el-rate>

css样式直接引入element已有的样式,这里以实现简单功能为主。

编写Rate.vue文件:


<template>
  <div class="el-rate">
    <span
      v-for="(item, key) in max"
      :key="key"
      class="el-rate__item"
      style="cursor: pointer"
      @mousemove="setCurrentValue(item)"
      @mouseleave="resetCurrentValue"
      @click="selectValue(item)"
    >
      <i
        class="el-rate__icon"
        :class="'el-icon-star-' + (currentValue >= item ? 'on' : 'off')"
        style="color: #f7ba2a"
      ></i>
    </span>
  </div>
</template>

<script>
import { ref } from "vue";

export default {
  name: "Rate",

  props: {
    modelValue: {
      type: Number,
      default: 0,
    },
  },

  //传入props和context
  setup(props, context) {

    //定义为响应式
    let currentValue = ref(props.modelValue);

    //设置
    const setCurrentValue = (val) => {
      currentValue.value = val;
    };

    //重置
    const resetCurrentValue = () => {
      currentValue.value = props.modelValue;
    };


    const selectValue = (val) => {
      // 向父级传值,//modelValue为默认的,可查看vue3 v-model的变化
      context.emit("update:modelValue", val);
    };

    //模板中用的需要进行retuan
    return {
      currentValue,
      setCurrentValue,
      resetCurrentValue,
      selectValue,
    };
  },
};
</script>

App.vue中引入使用,进行测试:

<template>
  <div id="app">
    <h1>RateValue: {{data.rateValue}}</h1>
    <rate v-model="data.rateValue"></rate>
  </div>
</template>

<script>
import {reactive} from 'vue'
import Rate from '@/components/Rate.vue'
import Calendar from '@/components/Calendar'


export default {
  name: 'App',
  components: {
    Rate
  },
  setup() {
    let data = reactive({
      rateValue: 3
    })
    return {
      data
    }
  }
}
</script>

实现了简单的评分组件:
image.png

4.3 PropType类型验证

假如一个组件接收的prop参数为对象类型,可以使用TS的interface定义接口进行类型验证,也能带来友好的编辑器提示。

实现一个简单card展示组件:

<template>
  <ul>
    <li v-for="column in list" :key="column.id">
      <img :src="column.img" >
      <div>{{column.title}}</div>
      <div>{{column.description}}</div>
    </li>
  </ul>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
export interface CardProps {
  id: number;
  title: string;
  img: string;
  description: string;
}
export default defineComponent({
  name: 'Card',
  props: {
    list: {
      //
      type: Object as PropType<CardProps>,
      required: true
    }
  }
})
</script>

Vue 对定义了 type 的 prop 执行运行时验证。要将这些类型提供给 TypeScript,我们需要使用 PropType 强制转换构造函数

5. 兼容性问题

Vue3是不支持IE11 ,Vue2 的2.7版本将会支持 Vue3 的写法, 以兼容IE11
image.png

同时 Vue3在今年的Q2将成为默认版本,npm 和文档都会指向 V3 版本

6.总结

本文简单的介绍了Vue3+TS 的上手实战,关于Vue3的新特性还有很多没有介绍,可以查看官网的文档进行学习和实践。

TypeScript也在慢慢普及,可以给开发者带来更好的体验(类型提示,智能补全等),但是在习惯上还需要一段时间去学习和应试。