本文不说明 webpack 的配置,简单介绍如何使用 TypeScript 来书写 Vue,阅读本文应同时对 TypeScript 和 Vue 有一定的了解。
如果你在为如何搭建一个支持 Vue in TS 的环境而苦恼,在这里我推荐使用官方脚手架 Vue CLI 3来生成一个项目;或是来试试我自己编写的脚手架,你可以直接拷贝这个例子。这两种方式都不需要你介入配置,可以直接开始代码的编写,日后再研究环境的配置。
编写本文时所使用的 Vue 版本为 2.x,TypeScript 版本为 3.x。
以下介绍的用 TS 编写 Vue 组件的方式与原来的方式不兼容,请不要对原来的项目直接进行改造。
使用 JS 编写如下简单组件:
<template>
<div>
<img :src="src" class="photo">
<p class="description">{{description}}</p>
</div>
</template>
<script>
export default {
data() {
return {
src: 'https://img2.vipcn.com/img2016/6/21/2016062150586477.jpg',
description: 'Saber-阿尔托莉雅·潘德拉贡'
}
}
}
</script>
使用 TS 之后:
<template>
<div>
<img :src="src" class="photo">
<p class="description">{{description}}</p>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class Post extends Vue {
src: string = 'https://img2.vipcn.com/img2016/6/21/2016062150586477.jpg'
description: string = 'Saber-阿尔托莉雅·潘德拉贡'
}
</script>
可以看出写法上的改变比较大,首先一定得把 script
标签的 lang
设置为 ts
或 tsx
;接着借助 vue-property-decorator
,以类声明的方式编写 Vue 组件。
下面简单介绍 vue-property-decorator
的使用,详细 API 可参看官方文档,同时也要参照vue-class-component。因为:
vue-property-decorator: This library fully depends on vue-class-component, so please read its README before using this library.
以下介绍包括,详情还请参照其中注释:
<!-- Post.vue -->
<template>
<div @click="handleClick">
<img :src="src" class="photo">
<p class="description">{{description}}</p>
<p>counter: {{Count}}</p>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
@Component
export default class Post extends Vue {
// data 属性
src: string = 'https://ae01.alicdn.com/kf/HTB1hZDNdEuF3KVjSZK9762VtXXaK.png'
count: number = 1
// computed 实现
get Count() {
return `点击了${this.count}次`;
}
// 指定 description 为 prop
@Prop({
default: 'default description',
/**
* `required`、`type` 为 vue 运行时的检查
*/
required: true,
type: String
})
description!: string
handleClick() {
// 点击事件,改变 data,更新界面
this.count += 1;
}
// mounted 生命周期钩子
mounted() {
console.log('mounted')
}
// watch
@Watch('count')
onCountChange(val: number, old: number) {
console.log(`new value: ${val}, old value: ${old}`)
}
}
</script>
在其他组件中使用 Post
组件:
<!-- App.vue -->
<template>
<div class="app">
<h1>app</h1>
<post description="Saber-阿尔托莉雅·潘德拉贡" />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
// 导入组件
import Post from '@/components/Post.vue';
@Component({
// 组件注册
components: {
Post
}
})
export default class App extends Vue {
}
</script>
本节需要注意一点,不要将点击事件、生命周期等方法写成箭头函数的形式,否则数据更新时界面不响应更新:
<!-- ... 其他内容 -->
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
@Component
export default class Post extends Vue {
handleClick = () => {
// 结果界面不响应更新
this.count += 1;
}
}
</script>
下面定义一个 store,由 store.ts
、./stores/homeStore.ts
两个文件构成,包含一个名为 homeStore
的 module,来看看具体实现。
/* store.ts */
import Vue from 'vue';
import Vuex from 'vuex';
// 从 homeStore 中导出类型和对象
import { IHomeState, homeStore } from './stores/homeStore';
Vue.use(Vuex);
// 定义 RootState
export interface RootState {
homeStore: IHomeState;
}
export default new Vuex.Store<RootState>({
modules: {
homeStore,
},
});
/* ./stores/homeStore.ts */
import { MutationTree, ActionTree } from 'vuex';
import { RootState } from '../store';
// 定义类型,描述 homeStore
export interface IHomeState {
title: string;
}
const state: IHomeState = {
title: 'home title',
};
export enum TYPES {
SET_TITLE = 'SET_TITLE',
}
const mutations: MutationTree<IHomeState> = {
[TYPES.SET_TITLE](s, newTitle: string) {
s.title = newTitle;
},
};
const actions: ActionTree<IHomeState, RootState> = {
setTitle({ commit }, ele) {
commit(TYPES.SET_TITLE, ele);
},
};
export const homeStore = {
namespaced: true,
state,
mutations,
actions,
};
代码实现上可以说是没有变化,就是加了不少类型用于描述、规范。下一步来看看具体怎么在组件中使用。
在此之前介绍一下 vuex-class,是专为 vuex 和 vue-class-component 制定的绑定工具,用于优化使用。
<!-- Home.vue -->
<template>
<div class="home">
<h1>this is home page</h1>
<p @click="changeTitle">
title from store: {{homeStore.title}}
<br>
click to change
</p>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { State, Action } from 'vuex-class';
import { IHomeState } from '@/stores/homeStore';
@Component
export default class Home extends Vue {
/**
* State 修饰后的属性时对应的 store 对象
* 此处为 homeStore
*
* this.homeStore.title 与 this.$store.homeStore.title 相同
*/
@State homeStore!: IHomeState;
/**
* Action 修饰后的属性是一个 function,即 store 中定义的 action
* 以下的使用等价:
*
* this.changeTitleAc('new title')
*
* this.$store.dispatch('new title')
*/
@Action('homeStore/setTitle') changeTitleAc!: Function;
count = 0;
changeTitle() {
this.changeTitleAc(`get a new title - ${this.count}`);
}
}
</script>
有的时候我们会把一些对象挂载在 Vue 原型上,这样能方便在组件中的使用,以 lodash 为例。
入口文件 index.ts:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import * as _ from 'lodash';
Vue.config.productionTip = false;
// 将 lodash 挂载到 Vue 原型
Vue.prototype._ = _;
new Vue({
router,
store,
render: h => h(App),
}).$mount('#root');
在 Home.vue 组件中使用:
<script lang="ts">
// ...
@Component
export default class Home extends Vue {
mounted() {
// 使用 lodash
console.log(this._.lowerCase('AABB'));
}
// ...
}
</script>
这时候不出 TS 的编译会抛给你一个错误,而这实际上是个类型描述问题,对于 TS 编译器来说它并不知道 _
的存在以及描述,这时候当我使用这个类型未知的内容时就会被禁止。
Property '_' does not exist on type 'Home'.
所以新建一个类型文件来告诉 TS 什么是 _
:
// src/@types/extend-vue.d.ts
// https://stackoverflow.com/questions/43142304/how-to-augment-the-vue-class-and-keep-typescript-definition-in-sync/43232151#43232151
// 1. Make sure to import 'vue' before declaring augmented types
import Vue from 'vue';
import * as _ from 'lodash';
// 2. Specify a file with the types you want to augment
// Vue has the constructor type in types/vue.d.ts
declare module 'vue/types/vue' {
// 3. Declare augmentation for Vue
interface Vue {
_: typeof _;
}
}
在学习过程中可以发现,TypeScript 和 Vue 2 的结合并不十分契合,可以看看尤大对此的说法——值得一看,Vue 作者尤雨溪的回答【TypeScript 不适合在 vue 业务开发中使用吗?】。
但是也不是说不能用,基于现状,这是我所知用 TS 编写 Vue 较好的方式了。或者,来期待 Vue 3 的发布。
最后,介绍一下我用 TS 写的 Vue 项目,项目进度很慢并随时可能 TJ。
参考链接 & 相关阅读: