为什么要用 TypeScript
一开始是坚定地 Javascript 派,但是后来接触了其他同事用ts写的的项目之后发现 VSCode 对 TypeScript 的支持是相当不错(可真有你的微软),类型提示非常好用,给开发带来了比较大的效率提升。
为什么不用装饰器
- Class API 在 Vue 3 的 RFC 中被拒绝了
- 同事没装这个包
- 我个人也不是很喜欢装饰器
- 因为他使用了class的写法,于是如果你是从以前的项目进行迁移的话,非常麻烦。 虽然写起来真的很舒服,新项目可以尝试一下,不过现在的话没有到推荐的程度。
那么现在 Vue 自己也有了 vue-cli,也有了官方的 TypeScript 支持之后想挑战一下如果不使用装饰器,Vue现在能够怎么和 TypeScript 进行结合。
以前的写法
两年前曾经在 Vue 2.4 的时候就写过一篇关于Vue+TypeScript 的体验文章,因为导出的只是对象,里边的 this
没法预测类型,只能作为 ComponentOptions
进行导出。
我们需要在导出的时候先写对象上的属性内容,再把导出对象加上as类型,让ts知道我们这个对象的this上有什么东西,也就是我们需要重复书写类型与值,写了两遍属性,有点繁琐。
嘛当然至少是能用的,就是写的code太多了
2.5的变化
https://medium.com/the-vue-point/upcoming-typescript-changes-in-vue-2-5-e9bd7e2ecf08
具体的变化点可以看上边,原理没有提及,可以去看PR源码以及如果搜一下Google的话也有很多源码解析
原理是利用工具类 ThisType<T>
,在官网上是有详细例子进行解析的
简单拿 Vue 来做例子的话大概就是下面的样子
type DataDef<D> = D | (() => D);
type ComponentsOption<Data, Method> = {
data?: DataDef<Data>;
methods?: Method & ThisType<Data & Method>; // Type of 'this' in methods is Data & Method
}
function Vue<Data, Method>(desc: ComponentsOption<Data, Method>): Data & Method {
let data: Data;
if (typeof desc.data == 'function')
data = (desc.data as (() => Data))();
else if (desc.data)
data = desc.data;
else data = {} as Data;
let methods: object = desc.methods || {};
return { ...data, ...methods } as Data & Method;
}
let obj = Vue({
data() {
return {
x: 0,
y: 0
};
},
methods: {
moveBy(dx: number, dy: number) {
this.hello(); // Strongly typed this
this.x += dx; // Strongly typed this
this.y += dy; // Strongly typed this
},
hello() {
console.log('Hello');
}
}
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
obj.hello();
要注意的是,使用这个类型需要启用 noImplicitThis
flag
具体使用
项目的依赖安装等
新项目可以用 vue-cli 直接创建并添加typescript,非常简单,创建项目的时候选择就可以了。
旧项目的话如果同样是用 vue-cli 创建的项目的话,升级 vue-cli 后使用 vue add typescript
也是非常简单就能添加了。
如果不是的话,需要手动升级 Vue 到 2.5+ 以及升级相关组件比如 vuex 、vue-router 等,再对 webpack 进行相关的配置。 建议参照 vue-cli 生成的 webpack 配置。
在 Vue 中的写法
实例属性
无论在 ts 文件中使用 Vue.component
进行定义还是在单文件组件(*.vue
)中进行书写 TypeScript 都已经有了提示,但是在单文件组件中访问 $refs
等实例属性的话,需要在导出前使用 Vue.extend
export default {
methods: {
setMargin() {
(this.$refs.svg as SVGElement).style.margin = '-1px';// Property '$refs' does not exist on type '{ methods: { setMargin(): void; }; }'.
},
}
}
这是为什么呢?
你可能会在碰到这个问题之前碰到另一个在构建时 *.vue
文件无法被识别的问题
1 ERROR
• main.ts
[ts] Cannot find module './App'. (4 17)
又或者是并没有碰到这个问题,但你应该在项目的根目录下发现有一个叫做 shims-vue.d.ts
或者 shims-*.d.ts
这样的文件,这样的文件叫做 Declaration Files
。
打开这个文件后你应该能看到类似于
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
而 Vue 的默认导出有两个,一个是 interface Vue
,一个是 const Vue: VueConstructor
,在这里因为导出的是一个对象,因此适用于 VueConstructor
,如果使用 Vue.extend
进行导出,则符合 VueConstructor
类型,但如果不使用 Vue.extend
, 我并没有找到这部分的说明也没有追踪到相关源代码,而 Vetur 似乎仍然将他识别成了 VueConstructor
,但在编译时被识别成了 ComponentOptions
,因此没有办法通过编译。
Component 使用 as 类型断言(Type assertions)
Type assertions 是什么?
$refs
的使用
在上一个例子中你可能已经注意到
export default Vue.extend({
methods: {
setMargin() {
(this.$refs.svg as SVGElement).style.margin = '-1px';
},
}
})
使用 $refs
的时候我对属性进行了类型断言,这是因为 $refs
是在运行时附加上的属性并且随时产生变化,因此并不能提前得到具体类型,因此它被定义成 readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] };
于是不但可以转换成HTML元素,也可以根据需要转换成子组件的实例类型以用于调用子组件方法。
import Vue from "vue";
import Popup from "./Popup.vue";
export default Vue.extend({
name: "YetAnotherPopup",
methods: {
open() {
(this.$refs.popup as InstanceType<typeof Popup>).open();
},
close() {
return (this.$refs.popup as InstanceType<typeof Popup>).close();
},
},
components: {
Popup
}
});
这里用到的是 InstanceType
,因为导出的是 Vue
组件的构造器。
mixin
的使用
对于 mixin
,由于会有合并冲突,因此也并没有办法将方法与数据反映到被 mixin 的组件上。
同样的,当我们包括了mixin后,我们也可以使用类型断言
import { adIdMixin } from "@/mixins/adIdMixin";
export default Vue.extend({
mixins: [adIdMixin],
methods: {
click() {
(this as InstanceType<typeof adIdMixin>).sendTracking('purchase','click');
},
});
Vuex store
的类型提示
Vuex 也加入了 TypeScript 的类型定义。
构造 vuex
首先导出 state
export const state = {
loading: false,
valueA: null as string | null,
valueB: null as ({
numberValue: number;
stringValue: string;
})[] | null
};
其次构造 Action (Mutation 大同小异)
import { ActionTree, Commit } from "vuex";
import * as API from "@/utils/api";
import { state } from "./state";
// 如果你是 namespaced store ,可以在这里导入 rootState 对其他 store 进行修改
import { rootState } from "@/store/types";
export default {
// we didn't use rootState here
getInfo({ commit, state, _rootState }) {
state.loading = true;
return API.getInfo()
.then(res => commit('setState', res.data))
.finally(() => {
state.loading = false;
});
},
// 如果你是 namespaced store ,可以在这里导入 rootState 对其他 store 进行修改
// 如果不是,则再输入一次 `typeof state` ,因为此时 rootState == state
} as ActionTree<typeof state, typeof rootState>;
使用
typeof
与 类型断言让 TypeScript 自己推断类型,在初始化 state 时不用重复写两遍属性。
在 this.$store
使用 as
类型断言
在 Vue 中使用了 Vuex 后,this.$store
有了 State<any>
的定义,同样的因为这个属性是运行时动态添加变化,因此无法提前进行类型定义。
于是在代码中,我们可以使用 as
进行类型断言。
在这里,有三种方法。
方法一: 只对使用的 store 进行断言
import Vue from 'vue';
import { state as childState } from "@/store/childStore/state";
// import store from "@/store";
export default Vue.extend({
computed: {
isLoading() {
return (this.$store.childState as typeof childState).loading;
}
},
});
方法二: 对使用的 store 进行整个 RootState 转换
import Vue from 'vue';
import rootStore from "@/store";
export default Vue.extend({
computed: {
isLoading() {
return (this.$store as typeof rootStore).childState.loading;
// or `as Store<typeof rootState>`
// Store should be imported from vuex declartion
}
},
});
方法三: 直接导入store,不从 this.$store
上获取,无需书写任何类型断言
import Vue from 'vue';
import rootStore from "@/store";
export default Vue.extend({
methods: {
getLoading() {
return rootStore.childState.loading;
}
},
});
mapState
与 mapGetters
问题
很遗憾,mapState
与 mapGetters
由于只通过字符串来对实例添加属性,因此他们只能告诉TypeScript在这个实例上有这个属性,并不能添加属性类型,会被认为成any。
import Vue from "vue";
import { mapState } from "vuex";
export default Vue.extend({
computed: {
...mapState("childState", ["valueA"]),
},
methods: {
getNumber() {
this.valueA; // any
}
}
});
推荐只作为template映射进行使用
结语
这次探索了一下不用装饰器 ,Vue
和 TypeScript
到底可以结合到多少程度。 目前来看基本的类型推断都是够用,在项目中几乎不需要再使用装饰器以及 // @ts-ignore
。
TypeScript + VSCode 对开发效率的提升真的不是一点半点,自从尝试了 ts 后就对 ts 爱不释手了