在vue2中使用ts
介绍
文本所介绍的内容是使用 TypeScript 编写 Vue2.6.11 前端应用,具体 demo 地址可访问: 。
总结几个月来在 ts 环境 中使用 vue 的经验,提炼一个最小可运行案例,该案例将包括:
项目搭建与配置
ts 环境下 vue2 版本的项目可直接使用官方的脚手架 vue-cli 进行搭建,根据项目组情况判断是否需要使用 tsx、css 预处理+css module、单元测试。
在中默认安装,该库通过装饰器模式实现了 vue 的 ts 适配,也是官方推荐的使用 ts 方式。不过更建议使用包,因为后者在前者基础上进行了修改与扩充。拥有的功能都具备,并且功能更强大,也更易于使用。
对于使用 Vuex 的项目,建议安装包,这是在 ts 环境下中使用 vuex 的一种解决方案。
由于 vue 对 jsx 的支持问题,如果想实现如同 react 的组件 props 的智能提示,需要安装。
单文件组件(template 组件)的使用
组件实例
vue-class-component 允许我们通过使用类语法声明 vue 组件,需要使用装饰器。
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 或者只定义未赋初值,不会将该属性修饰为响应式数据!这会导致异常。推荐方案是进行赋初值,或者扩展一个 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
类语法实现响应式的数据监听,是由依赖提供装饰器来完成
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 定义是通过装饰器实现
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保持一致
//相当于
mixins 使用
类语法使用 mixins 需要继承提供的 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
//相当于
slots 和 scopedSlots
slots 和 scopedSlots 的使用方式和原生 vue 保持一致。
tsx 组件的使用
如果在项目中需要使用 jsx,默认 vue-cli 创建项目会提示是否支持 jsx,但是由于 vue 对 jsx 的支持不完善,导致在使用不像 react 那样可以提示组件 props 的类型定义,使用上非常难受。因此引入解决该问题。详情请见:
至于 在 vue 中如何使用 jsx,推荐,该文详细介绍了 vue 实现 jsx 的原理以及几种 props 的区别和使用。
tsx 组件的很多地方和 template 组件使用方式一致,但是 props 定义、scopedSlots 定义和使用,以及引入第三方组件之后的处理方式有差异。其他地方例如生命周期、data、computed、watch、methods、事件触发、ref 使用都是一致的。
配置
下载完,我们需要配置,设置内容如下:
"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "VueTsxSupport",
"...": "..."
},
之后,我们需要在项目入口处引入。
现在 tsx 组件的 props 智能提示开始生效。
组件定义的方式
支持的 tsx 组件定义方式可以使用类似与原生 vue 的对象的写法,或者类语法编写。更推荐使用类语法编写组件,这样和模板写法也更相近。
使用类语法编写组件有两种方式:
项目中一直在使用前者,但是最近总结经验,发现后者更好些。主要是继承 Component 之后使用 mixins 想要有智能提示的话,需要将定义挂载在 Vue 上,不够友好。因此推荐使用:通过继承 Vue 类并且声明成员,下文都是针对该方案的说明。
组件实例
声明 tsx 组件,文件后缀必须为,这点和 react 不同,react 在 ts 文件中也是可以使用 jsx 的,但是 vue 不可以。如果一定要在文件中,可以使用定义 jsx 原始方式,具体可参照。
在 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 方法、方法定义设置为私有,这样可以使用提供的,来声明 Props。如果有成员需要设置为 public,可以使用 tsx 提供的。
//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 定义
成员的类型可以定义为交叉类型,将事件类型定义混入到中就可以了
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<{ onClick: string }>;
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<{ default?: void,header?:string }>;
render(){
return
}
}
mixins 使用
mixins 使用和 template 组件保持一致
第三方组件 props 推断
由于 vue 实现的 jsx 没有参数类型提示,因此引入第三方组件也是没有 props 提示。所有我们需要使用来进行 jsx 支持。
这里我创建一份文件,使用提供的 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, AnyEvent, AnyScopedSlots>().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<{ onClick: string }>;
$scopedSlots!: tsx.InnerScopedSlots<{ default?: void }>;
render() {
return (
{this.$scopedSlots.default && this.$scopedSlots.default()}
);
}
}
事件修饰符
遗留问题
在单文件组件模式中,文件跳转正常(ctrl+鼠标点击可以跳转到定义),但是暂未实现路径的智能提示。
在文件引入,路径智能提示正常,但是会发生无法跳转到 vue 文件的情况。
vue-router 的 ts 方案
官方已经支持 ts,在我们使用创建了 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 社区推出了,其工作方式和一致,都是通过装饰器来实现。
模块创建
中常使用的成员:。
创建步骤:
完整示例:
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,因此很简洁。
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)
使用步骤:
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 接口的类型是个麻烦事,尤其是接口很多的情况下。如果手动定义,成本会很大,也影响效率。当接口修改(这是常常发生的),我们将不得不进行同步的修正。
因此我建议使用阿里团队出品的库,该库有效的解决了 api 接口定义的麻烦问题。