Categories
程式開發

在vue2中使用ts


介紹

文本所介紹的內容是使用TypeScript 編寫Vue2.6.11 前端應用,具體demo 地址可訪問: vue-ts-demo“。

總結幾個月來在ts 環境中使用vue 的經驗,提煉一個最小可運行案例,該案例將包括:

搭建ts 項目,配置tsconfig.json單文件組件(template 組件)的使用tsx 組件的使用vue-router 的ts 方案vuex 的ts 方案api 類型定義的建議

項目搭建與配置

ts 環境下vue2 版本的項目可直接使用官方的腳手架vue-cli 進行搭建,根據項目組情況判斷是否需要使用tsx、css 預處理+css module、單元測試。

項目創建完成,默認生成一份tsconfig.json文件。 ts 配置項解釋可以參考TypeScript 官方教程“。

在package.json中默認安裝Vue類組件“,該庫通過裝飾器模式實現了vue 的ts 適配,也是官方推薦的使用ts 方式。不過更建議使用Vue屬性裝飾器“包,因為後者在前者基礎上進行了修改與擴充。vue-class-component擁有的功能vue-property-decorator都具備,並且功能更強大,也更易於使用。

對於使用Vuex 的項目,建議安裝Vuex模塊裝飾器“包,這是在ts 環境下中使用vuex 的一種解決方案。

由於vue 對jsx 的支持問題,如果想實現如同react 的組件props 的智能提示,需要安裝vue-tsx-支持“。

單文件組件(template 組件)的使用

組件實例

vue-class-component 允許我們通過使用類語法聲明vue 組件,需要使用@Component裝飾器。

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

@Component
export default class Index extends Vue {

}

//相当于

生命週期

生命週期鉤子的使用和原先使用的區別:在類語法中直接將生命週期生命為方法(方法名稱和生命週期名稱一致)。

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

@Component
export default class Index extends Vue {
created() {
console.log('created');
}
mounted() {
console.log('mounted');
}
}

//相当于

響應式數據data

類語法中可以直接定義為類的實例屬性作為組件的響應式數據。原始類型的數據不需要定義類型,ts 可以實現類型推斷,但是複雜的類型需要定義。

其中值得注意的一點是:當數據的值是undefined 或者只定義未賦初值,vue-class-component不會將該屬性修飾為響應式數據!這會導致異常。推薦方案是進行賦初值,或者擴展一個null 類型再賦值未null。

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

type User = {
name: string;
age: number;
};
@Component
export default class Index extends Vue {
message="hello world";
info: User = { name: 'test', age: 25 };
//如果数据的值是undefined或者未赋初值,则不会成为响应式数据。解决方案:追加类型定义null
count: number;
}

//相当于

計算屬性computed

類語法中的計算屬性的實現,是通過get 取值函數。

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

@Component
export default class Index extends Vue {
//computed定义
get introduction() {
return `姓名:${this.info.name}, 年龄:${this.info.age}`;
}
}

//相当于

數據監聽watch

類語法實現響應式的數據監聽,是由vue-property-decorator依賴提供@Watch裝飾器來完成

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 })
changeRouter(val: Route, oldVal: Route) {
console.log('$route watcher: ', val, oldVal);
}
}

//相当于

方法methods

在類語法實現原生vue 的方法的方式,即通過直接定義類方法成員。

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

@Component
export default class Index extends Vue {
hello(){
console.log('hello world');
}
}

//相当于

引入組件

和原生寫法一致,都需要先引入在註冊,區別在於類語法註冊在修飾器中。組件使用方式和vue 原生一致。

import { Vue, Component } from 'vue-property-decorator';
import Header from '../component/header/index.vue';

@Component({
components: {
Header,
},
})
export default class Index extends Vue {
}

//相当于

組件屬性props

類語法實現組件props 定義是通過裝飾器@Prop實現

import { Vue, Component, Prop } from 'vue-property-decorator';
import { User } from '@/types/one';

@Component
export default class Header extends Vue {
@Prop({ type: String, default: '标题' }) readonly title?: string;
//复杂类型type参数的值为Object,默认值需要以函数形式返回
@Prop({ type: Object, default: () => ({ name: '-', age: '-' }) }) readonly author!: User;
}

//相当于

事件觸發

ts 環境下vue 的事件觸發方式和js 環境下是一致的,區別只是事件回調定義的地方不同(ts 定義為類的實例方法,js 定義在methods 屬性中)。

ref 使用

在類語法中使用ref 需要藉助vue-property-decorator提供的@Ref裝飾器,使用方法如下:

//模板和原生vue保持一致

//相当于

mixins 使用

類語法使用mixins 需要繼承vue-property-decorator提供的Mixins 函數所生成的類。

Mixins 函數的參數是Vue 實例類,正確使用會用mixin 成員的的智能提示,使用方式如下:

// mixins.js
import Vue from 'vue';
import Component from 'vue-class-component';

// You can declare mixins as the same style as components.
@Component
export class Hello extends Vue {
/**
* mixin中的响应式数据
*/
mixinText="Hello mixins";

obj: { name: string } = { name: 'han' };
}

//index.vue

//相当于

插槽和scopedSlots

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

tsx 組件的使用

如果在項目中需要使用jsx,默認vue-cli 創建項目會提示是否支持jsx,但是由於vue 對jsx 的支持不完善,導致在使用不像react 那樣可以提示組件props 的類型定義,使用上非常難受。因此引入vue-tsx-support解決該問題。詳情請見:vue-tsx-support(github 文檔)

至於在vue 中如何使用jsx,推薦在Vue 中使用JSX 的正確姿勢“,該文詳細介紹了vue 實現jsx 的原理以及幾種props 的區別和使用。

tsx 組件的很多地方和template 組件使用方式一致,但是props 定義、scopedSlots 定義和使用,以及引入第三方組件之後的處理方式有差異。其他地方例如生命週期、data、computed、watch、methods、事件觸發、ref 使用都是一致的。

配置

下載完vue-tsx-support,我們需要配置tsconfig.json,設置內容如下:

"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "VueTsxSupport",
"...": "..."
},

之後,我們需要在項目入口處引入import ‘vue-tsx-support/enable-check’;。

現在tsx 組件的props 智能提示開始生效。

組件定義的方式

vue-tsx-support支持的tsx 組件定義方式可以使用類似與原生vue 的對象的寫法,或者類語法編寫。更推薦使用類語法編寫組件,這樣和模板寫法也更相近。

如果喜歡接近原生vue 的對像風格,可以參考:官方文檔“。

使用類語法編寫組件有兩種方式:

通過繼承vue-tsx-support提供的Component 類來編寫通過繼承Vue 類並且聲明_tsx成員

項目中一直在使用前者,但是最近總結經驗,發現後者更好些。主要是繼承Component 之後使用mixins 想要有智能提示的話,需要將定義掛載在Vue 上,不夠友好。因此推薦使用:通過繼承Vue 類並且聲明_tsx成員,下文都是針對該方案的說明。

組件實例

聲明tsx 組件,文件後綴必須為.tsx,這點和react 不同,react 在ts 文件中也是可以使用jsx 的,但是vue 不可以。如果一定要在.ts文件中,可以使用定義jsx 原始方式,具體可參照vue 官網:“。

在tsx 文件中,聲明組件的方式和template 組件是一致的。

import { Vue, Component, Prop } from 'vue-property-decorator';
import * as tsx from 'vue-tsx-support';

@Component
export default class Header extends Vue {
}

dataProps 定義

首先我們需要和template 組件一樣將所有的props 定義好。

然後根據情況,如果可以將所有data 數據、computed 方法、方法定義設置為私有,這樣可以使用vue-tsx-support提供的AutoProps別名,來聲明Props。如果有成員需要設置為public,可以使用tsx 提供的PickProps別名。

//AutoProps
import { Vue, Component, Prop } from 'vue-property-decorator';
import * as tsx from 'vue-tsx-support';
import { User } from '@/types/one';
import styles from './index.less';

@Component
export default class Header extends Vue {
_tsx!: tsx.DeclareProps>;

@Prop({ type: String, default: '标题' }) readonly title?: string;
@Prop({ type: Object, default: () => ({ name: '-', age: '-' }) }) readonly author!: User;

private goAboutMe() {
this.$router.push('/about');
}

render() {
return (

{this.title}


作者:
{this.author.name}


);
}
}

//PickProps
import { Vue, Component, Prop } from 'vue-property-decorator';
import * as tsx from 'vue-tsx-support';
import { User } from '@/types/one';
import styles from './index.less';

@Component
export default class Header extends Vue {
_tsx!: tsx.DeclareProps>;

@Prop({ type: String, default: '标题' }) readonly title?: string;
@Prop({ type: Object, default: () => ({ name: '-', age: '-' }) }) readonly author!: User;

goAboutMe() {
this.$router.push('/about');
}

render() {
return (

{this.title}


作者:
{this.author.name}

);
}
}

eventProps 定義

_tsx成員的類型可以定義為交叉類型,將事件類型定義混入到_tsx中就可以了

import * as tsx from 'vue-tsx-support';
import { Vue, Component, Prop } from 'vue-property-decorator';
export default class Header extends Vue {
_tsx!: tsx.DeclareProps> & tsx.DeclareOnEvents;
render(){
return


}
}

scopedSlotsProps 定義

vue 中的scopedSlots 相當於react 中的renderProp。

tsx 組件中定義如下:

import * as tsx from 'vue-tsx-support';
import { Vue, Component, Prop } from 'vue-property-decorator';
export default class Header extends Vue {
//这样就声明了两个scopedSlot,默认的scopedSlot参数类型为空,header参数类型为string
$scopedSlots!: tsx.InnerScopedSlots;
render(){
return


}
}

mixins 使用

mixins 使用和template 組件保持一致

第三方組件props 推斷

由於vue 實現的jsx 沒有參數類型提示,因此引入第三方組件也是沒有props 提示。所有我們需要使用vue-tsx-support來進行jsx 支持。

這裡我創建一份propsCovert.ts文件,使用vue-tsx-support提供的ofType 方法來對第三方組件的props 進行定義推導。

遞歸第三方組件的dataProps,並將其類型推導出。 eventProps 定義為索引類型,參數類型定義為any。 scopedSlotsProps 同樣定義為索引類型,參數類型定義為any。

之後每次使用第三方組件,只要用antdPropsConvert 方法包裝下即可在使用時得到props 的智能提示。

如果是單頁應用,也可以創建一份組件清單文件,在該文件中轉換所有的組件並導出,這樣就省的一次次轉換。

//propsConvert.ts

import { ofType } from 'vue-tsx-support';

type PowerPartial = {
// 如果是 object,则递归类型
[U in keyof T]?: T[U] extends Function ? Function : T[U] extends object ? PowerPartial : T[U];
};
type Omit = Pick;
type OmitVue = PowerPartial;

interface AnyEvent {
[key: string]: any;
}
interface AnyScopedSlots {
[key: string]: any;
}

function antdPropsConvert(componentType: new (...args: any[]) => T) {
return ofType().convert(componentType);
}
export { antdPropsConvert };

// sider.tsx
import { Vue, Component } from 'vue-property-decorator';
import * as tsx from 'vue-tsx-support';
import { Button as AButton } from 'ant-design-vue';
import styles from './index.less';
import { antdPropsConvert } from '@/utils/propsConvert';

const Button = antdPropsConvert(AButton);

@Component
export default class Sider extends Vue {
_tsx!: tsx.DeclareOnEvents;

$scopedSlots!: tsx.InnerScopedSlots;

render() {
return (


{this.$scopedSlots.default && this.$scopedSlots.default()}


);
}
}

事件修飾符

如何在tsx 組件中使用事件修飾符,推薦官方教程,修飾符

遺留問題

在單文件組件模式中,文件跳轉正常(ctrl+鼠標點擊可以跳轉到定義),但是暫未實現路徑的智能提示。

在.tsx | .ts文件引入.vue,路徑智能提示正常,但是會發生無法跳轉到vue 文件的情況。

vue-router 的ts 方案

vue-router官方已經支持ts,在我們使用vue-cli創建了ts 項目之後就可以使用。

但是如果我們需要在組件中定義路由鉤子函數,需要先在全局進行註冊

// class-component-hooks.js
import Component from 'vue-class-component';

// Register the router hooks with their names
Component.registerHooks(['beforeRouteEnter', 'beforeRouteLeave', 'beforeRouteUpdate']);

然後需要給Vue 類型擴展定義

import Vue from 'vue';
import { Route, NavigationGuardNext } from 'vue-router';
declare module 'vue/types/vue' {
// Augment component instance type
interface Vue {
beforeRouteEnter?(to: Route, from: Route, next: NavigationGuardNext): void;

beforeRouteLeave?(to: Route, from: Route, next: NavigationGuardNext): void;

beforeRouteUpdate?(to: Route, from: Route, next: NavigationGuardNext): void;
}
}

使用前,在項目的入口文件引入註冊文件即可。

import '@/utils/class-component-hooks';
import Vue from 'vue';
import 'vue-tsx-support/enable-check';
import App from './App';
import router from './router';
import store from '@/modules';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');

然後在組件中定義路由鉤子,即可獲得準確的提示。

vuex 的ts 方案

為了在ts 環境中使用vuex,vue 社區推出了Vuex模塊裝飾器",其工作方式和vue-property-decorator一致,都是通過裝飾器來實現。

模塊創建

vuex-module-decorators中常使用的成員:VuexModule, Module, Mutation, Action, getModule。

創建步驟:

定義Module 實例之前,我們需要先定義state 的接口,這是為了之後vuex-module-decorators進行類型檢測。自定義Module 類型,繼承VuexModule 類型,並實現state 的接口使用@Module裝飾器裝飾自定義module,如果是動態Module(意味著引入的時候自動注入到vuex 中),需要傳參dynamic, store, name給Module 函數定義action 和mutation 我們都需要使用對應的裝飾器@Action、@Mutation導出自定義Module,將自定義Module 作為函數參數傳遞給getModule 函數,該module 中所有的state,action,mutation 都綁定在導出對像上

完整示例:

import { VuexModule, Module, Mutation, Action, getModule } from 'vuex-module-decorators';
import store from './index';

type TodoItem = {
id: string;
content: string;
isDone: boolean;
};
type TodoListState = {
todos: TodoItem[];
};
const todos: TodoItem[] = [
{
id: '0',
content: 'todo-item1',
isDone: false,
},
{
id: '1',
content: 'todo-item2',
isDone: true,
},
{
id: '2',
content: 'todo-item3',
isDone: false,
},
];
@Module({ dynamic: true, store, name: 'todoListModule' })
class TodoListModule extends VuexModule implements TodoListState {
todos: TodoItem[] = [];

//获取当前的todoList
@Action
async getAllTodoItems() {
const data = await new Promise((resolve) => {
setTimeout(resolve, 1000, todos);
});
this._saveTodos(data);
}

@Mutation
private _saveTodos(data: TodoItem[]) {
this.todos = data;
}
}
export default getModule(TodoListModule);

store 創建和使用

創建store 實例,由於項目是使用動態導入module,因此很簡潔。

如果需要在入口文件定義好全部module,可以參照官方教程“。

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// Declare empty store first, dynamically register all modules later.
const Store = new Vuex.Store({});
export default Store;

vuex 使用和原生vue 一致,都是引入store 的入口文件,然後將其傳入Vue 實例中

在組件中使用vuex(動態導入Module)

使用步驟:

需要導入對應的module 文件導入state,因為state 成員通過計算屬性使用,因此在ts 中需要通過get 函數導入調用action 或者mutation 方法,直接調用對應的Module 即可

import { Component, Vue } from 'vue-property-decorator';
import TodoListModule from '@/modules/todoList';

@Component
export default class Index extends Vue {
get todos() {
return TodoListModule.todos;
}

created() {
TodoListModule.getAllTodoItems().then(() => {
console.log('todos', this.todos);
});
}
}

api 類型定義的建議

在項目中,定義api 接口的類型是個麻煩事,尤其是接口很多的情況下。如果手動定義,成本會很大,也影響效率。當接口修改(這是常常發生的),我們將不得不進行同步的修正。

因此我建議使用阿里團隊出品的pont庫,該庫有效的解決了api 接口定義的麻煩問題。

詳情請見官網: