一些 Vue 的常用处理
提示:你正阅读的文章的发布日期距今已经有 4 years了,其中的部分信息、个人观点或者措辞习惯等可能已经发生改变,因此仅供参考,请酌情阅读。
一些 Vue 的常用处理
目录
1. VueX 中对 Mutation、Action 的规范化调用
介绍
一直不是很能理解为什么 VueX 中的 Mutation、Action 的函数在定义以后却要用字符串这样一种形式来识别。我当然也想不出更好的模式啦。
例如:
mutations: {
mutation(state, pay) {
state.abc = pay;
}
}
store.commit('mutation', 123);
这就使这个过程完全丢失了 IDE 的支持。对于 VueX 中数据存放很多关系很复杂的情况来说,并不是很优雅,不过具体体现在维护而不是编写上。所以我更倾向于使用一种常量字符串的模式来定义函数名,并也在 commit 和 dispatch 函数参数中使用。对此,实际上官方给了一个很完整的示例了。
然而在这个示例中,到后期也会发展为这样的局面
export const MUTATION1 = 'MUTATION1';
export const MUTATION2 = 'MUTATION2';
// ...
export const MUTATIONn = 'MUTATIONn';
import { MUTATION1, MUTATION2, ...} from './mutationTypes'
如果将它们整合到同一个 Object 中,效果会怎么样呢?
当然这一切也只是从个人喜好出发的。正如官网所说:
Whether to use constants is largely a preference - it can be helpful in large projects with many developers, but it's totally optional if you don't like them.
实现
首先明确一点,常量字符串的内容就是 mutation 函数的名字。从这个角度出发,首先定义一个数组,里面包含了所有所需的 mutation。
const mutationTypes = ['m1', 'm2', ..., 'mN'] as const;
特别注意末尾的 as const,用于将这个变量的类型限定得尽可能小。也就是说 mutationTypes 总体上来说就是一个 string[],但实际上也可以看成一个由它所有的值所构成的一个联合类型数组。as const 将其类型明确规定为后者。
接下来将这个数组转换成一个联合类型,也就是 'm1' | 'm2' | ... | 'mN'。
type MutationTypes = (typeof mutationTypes)[number];
// 如果上一步没有 as const,这里得到的结果就是 string。
然后考虑将这个数组拓展成一个键和值相同的键值对,也就是 {m1: 'm1', ...}。这个键值对就是我们最终所要的结果。定义这个键值对的类型:
type Result = {
[K in MutationTypes]: string;
// 这里也可以把值类型定为 MutationTypes,但是没有必要。
};
接下来构建这个键值对:
let result: Partial<Result> = {};
types.forEach((k: MutationTypes) => {
result[k] = k;
});
Partial<Result> 是为了让其初始值可以为空对象 {}。最后导出为 Result 类型即可。
export default result as Result;
完整代码:
const mutationTypes = ['m1', 'm2', ..., 'mN'] as const;
type MutationTypes = typeof mutationTypes[number]
type Result = {
[K in MutationTypes]: string
}
let result: Partial<Result> = {};
types.forEach((k: MutationTypes) => {
result[k] = k;
})
export default result as Result;
使用:
import mt from './mutationTypes';
store.commit(mt.) // <-- 此时会出现 IDE 提示。
IDE 提示效果:

2. Refs 的调用
介绍
一般在使用 this.$refs 的时候会面临没有任何 TypeScript 提示的问题,当对其进行复杂操作,如访问子组件的方法或数据以及操作 DOM 时,TypeScript 完全无法理解你在干什么。对此,实际上可以将其作为一个自定义类型的计算属性或者函数返回值来解决。这在需要对子组件进行频繁访问的情境下很有意义。
实现
实现起来很简单,在这里只是提供一个思路。
<template>
<div ref="myElement">
<component ref="myComponent" />
</div>
</template>
import Component from './Component.vue';
export default Vue.extend({
components: {
Component
},
computed: {
myElement(): HTMLDivElement {
return this.$refs.myElement as any;
},
myComponent(): InstanceType<typeof Component> {
return this.$refs.myComponent as any;
}
}
});
这样的话直接访问 this.myElement 和 this.myComponent 就可以了,或者也可以将 computed 的内容换到 methods 里,使用 this.myElement().xxx 进行访问。但这里还会存在一些瑕疵,毕竟这一部分的类型操作不是由 TS 而是由你自行决定的。
问题:计算属性无法获得值
因为 refs 本身并不是响应式的,这里放到 computed 里属于是强行加上响应式。实际上只有在组件被渲染以后,this.$refs 才能获取到值,否则就是 undefined。这导致直接访问相应计算属性总会得到一个 undefined 的结果,可能造成崩溃。解决办法是识别组件 mount,在此之前不返回 refs 的值。
myComponent(): InstanceType<typeof Component> | undefined {
if (!isMounted) return;
return this.$refs.myComponent as any;
}
InstanceType<typeof Component>是很好用的一个类型推断,它可以用来直接识别出实例化以后的类型。将该类型作为返回值类型,可以让 IDE 中直接提示子组件所具有的方法和值等,十分方便。如果直接使用typeof Component,得到的是未实例化的原始 class 类型。
然后在 mounted 钩子里加上 this.isMounted = true 即可。参考 StackOverflow - Using Refs in a Computed Property。
另外还有一种特殊情况是这个组件并不会马上被使用,这就需要根据组件自身 mount 的情况来定了。解决思路是在组件内部的 mounted 钩子中触发事件,例如 this.$emit('mounted'),然后父组件监听该事件改变自身相关变量,再加上类似于上面的 if (!isMounted) return; 结构。
如果嫌麻烦,可以使用函数的方法,使用体验基本没差。
问题:组件本身可能就是 undefined
通常情况下这是不会的,但是如果存在相关的可能,就不能不考虑。此时应当将返回值定为相应类型与 undefined 的联合类型。
3. 定义全局函数
介绍
不仅仅是全局函数,也可以是类实例或者简单的值。唯一的目的是调用方便。这参考了一些注入的全局变量,例如 this.$route、this.$store。开发早期如果不需要 VueX 而需要采用 event bus 的方案的话,也可以使用这种办法来定义全局量,从而可以在任何地方方便访问。
实现
要写入全局变量,只需要在 main.ts 中定义 Vue 的 prototype。
import Vue from 'vue';
import Hello from 'hello';
Vue.prototype.$hello = new Hello();
Vue.prototype.$bus = new Vue(); // event bus 实际上就是一个空的 Vue 实例
但是这样还不够,TS 并不知道这是什么意思。要解决这一点,需要在项目编译路径(一般就是 src 里的任意一个位置)编写一个 .d.ts,内容如下:
import Vue from 'vue';
import Hello from 'hello';
let h = new Hello();
let v = new Vue();
declare module 'vue/types/vue' {
interface Vue {
$hello: typeof h;
$bus: typeof v;
}
}
上面两者的类型写法等价于用 InstanceType<typeof class> 的写法。这样就可以在实际编写中随便用了。
4. 融合样式代码
介绍
写 Less 的时候用到的一些变量如果想要起到全局的作用,仅仅在 main.ts 里 import 并不够,还需要在每个 CSS 代码中加入。使用这个插件可以让所有样式代码互通有无。这个插件由官方文档推荐,既可以通过 Vue CLI 使用,也可以自行配置到 Webpack 中。
实现
对于 Vue CLI,安装相应的插件并配置即可。
vue add style-resources-loader
参考配置(vue.config.js):
const path = require('path');
module.exports = {
pluginOptions: {
'style-resources-loader': {
preProcessor: 'less',
patterns: [path.resolve(__dirname, './src/styles/*.less')] // 这里填要融合的样式文件。
}
}
};
对于 Webpack 请查看仓库:yenshih/style-resources-loader。
5. 自制状态管理
介绍
通常情况下想到全局的状态管理,最原始的方式是 eventbus,然而只能实现事件的通讯传递;数据在传递过程中必须有一层操作。若想用正规且有保障的方式,可以用 pinia 或者 VueX,然而都有杀鸡用牛刀之势。
实现
一个最简单的状态管理,其实可以直接用 object 键值对来实现。但是为了对其的修改可以引起视图的更新,需要让其变得 reactive。这可以通过 Vue 自带的函数 reactive 实现。
import { reactive } from 'vue';
const states = {
state1: '123',
state2: '123',
state3: '12'
};
declare module 'vue/types/vue' {
interface Vue {
$states: typeof states;
}
}
Vue.prototype.$states = reactive(states);
此时在组件里可以通过调用 this.$states.state3 = '123' 引起视图的更新。
6. v-model 的使用
在编写 checkbox、dialog 等组件的时候,经常需要在父组件内控制子组件的状态。例如
<template>
<dialog v-model="dialogState" />
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
dialogState: false
};
}
});
</script>
这一点在子组件内如何实现呢?官方提供了 v-model 这一方案,实际上是语法糖。
<template>
<div>
<input v-model="data" />
<input :value="data" @input="data = $event.target.value" />
</div>
</template>
以上两行代码应当是等价的。具体的逻辑是
- 父组件向子组件传入
data(对应 props 中的value) - 子组件使用此
data - 有需要时,子组件使用
$emit触发input事件提出更新,父组件更新相应的值,导致data传入子组件 props 的值发生变化 - 子组件使用新的
data值
这一整个逻辑都可以使用 v-model='data' 做到。例如对话框
父组件:
<template>
<div>
<dlg v-model="dialog">
<div class="btn" @click="dialog = false">关闭</div>
</dlg>
</div>
</template>
子组件:
<template>
<div class="dialog">
<slot />
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
dialog: Boolean
},
mounted() {
window.addEventListener('keydown', e => {
if (e.key === 'Escape') {
this.$emit('input', false);
}
});
}
});
</script>