这篇文章是2020年末我在公司内部做分享时写的讲稿,由于已经离职,文章中涉及到的一些内网链接和图片已经失效,今天把它尽可能的整理还原一下,分享出来。
第一部分:XX项目代码鉴赏
这是一个由多人接手、开发人员水平良莠不齐、缺乏代码管理规范的中大型VUE2项目,我们可以从这个项目中总结经验,讨论如何避免这些差写法造成的诸多问题、并发掘学习好的写法。
比较差的部分
发票/附件上传(同一组件实现多次)
同样的功能,代码也都几乎是一样的(存在一些细微差别),写了6份:
每次改动都要6个一起改,测起来也累

可以看出这是团队缺乏沟通造成的问题,当我们需要使用一个别人已经实现,但是不能完全满足自己需求的公用组件时,及时和同事沟通,而不是自己再单独COPY一份修改。当这样的行为越来越多,往后合并或维护起来会非常的累。
频繁使用$parent属性交互
改这样的代码非常头疼,看着也很不优雅

应当尽量避免这种爷孙组件(a组件->b组件->c组件->d组件)的方法调用,传事件出去让“当事人”处理,如果层级太多处理起来麻烦,可以实现一个Bus来专门处理事件:
1 2 3 4
| import Vue from 'vue' const Bus = new Vue() export default Bus
|
1 2 3
| import Bus from '@/libs/bus' Bus.$emit('applicantAction', type)
|
1 2 3 4 5
| import Bus from '@/libs/bus' Bus.$on('applicantAction', type => { })
|
金额计算直接使用原生js+运算符
众所周知,0.1 + 0.2 !== 0.3,这样写会出现非常多的金额错误

好在并不是所有人都忽略了这个问题,有些地方对结果做了一些简单的处理,也有些使用内置的Math对象计算,但大多数都是没处理的。
为了统一处理方式,可以建立数字计算处理规范,或使用一个第三方的库:
math.js(500k的体积过于臃肿,不推荐)
number-precision

项目结构组织混乱
项目中有频繁使用 vuex(处理状态),mixin(主要用于代码分流),class(主要用于逻辑复用),这些文件散落在各处并被随意引用




处理建议:尽量在src或模块目录下建立单独的文件夹来存放,层级太深很不方便。配置项应该写在单独的config文件或文件夹中
比较好的部分
表格配置项的抽离
表格的配置项写在data里又臭又长,一些模块会把这些配置项抽出来,写在单独的js文件中
这些js文件除了输出表格配置文件,还输出一个sendThis方法,用来获取当前组件实例。代码链接

除此之外,像这样的表格页面(拥有搜索、排序、分页),还有没有更好的处理办法?我们讲完下一个话题将会以这种页面来举例。
其他
推荐阅读:javascript代码整洁之道
第二部分:组合式API
先看官方示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { ref, onMounted, onUnmounted } from "vue";
export function useMousePosition() { const x = ref(0); const y = ref(0);
function update(e) { x.value = e.pageX; y.value = e.pageY; }
onMounted(() => { window.addEventListener("mousemove", update); });
onUnmounted(() => { window.removeEventListener("mousemove", update); });
return { x, y }; }
|
使用:
1 2 3 4 5 6 7 8 9
| import { useMousePosition } from "./mouse";
export default { setup() { const { x, y } = useMousePosition(); // other logic... return { x, y }; }, };
|
x 和 y的值都是响应式的,如果把它们放在模板中,页面会随着他们的更新而变化
痛点
以下示例列出了 options API 在使用时的一些不便之处,以及 composition API 带来的改变。
例1: 最基础的数据请求
Options的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <template> <div v-if="error">failed to load</div> <div v-else-if="loading">loading...</div> <div v-else>hello {{fullName}}!</div> </template>
<script> import { createComponent, computed } from 'vue'
export default { data() { // 集中式的data定义 如果有其他逻辑相关的数据就很容易混乱 return { data: { firstName: '', lastName: '' }, loading: false, error: false, }, }, async created() { try { // 管理loading this.loading = true // 取数据 const data = await this.$axios('/api/user') this.data = data } catch (e) { // 管理error this.error = true } finally { // 管理loading this.loading = false } }, computed() { // 没人知道这个fullName和哪一部分的异步请求有关 和哪一部分的data有关 除非仔细阅读 // 在组件大了以后更是如此 fullName() { return this.data.firstName + this.data.lastName } } } </script>
|
虽然是一块完整的逻辑,但数据、状态、方法分散在各处,且 loading、error等状态的可复用性为零
Composition API 的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div v-if="error">failed to load</div> <div v-else-if="loading">loading...</div> <div v-else>hello {{fullName}}!</div> </template>
<script> import { createComponent, computed } from 'vue' import useSWR from 'vue-swr' // 用 Composition 封装的一个状态组件
export default createComponent({ setup() { // useSWR帮你管理好了取数、缓存、甚至标签页聚焦重新请求、甚至Suspense... const { data, loading, error } = useSWR('/api/user', fetcher) // 轻松的定义计算属性 const fullName = computed(() => data.firstName + data.lastName) return { data, fullName, loading, error } } }) </script>
|
逻辑更集中了,且loading、error等状态都是响应式的
例2: 一个复杂结构的组件
旧代码:
Vue CLI UI file explorer(vue cli ui的文件浏览器组件)
这是 Vue 官方团队的大佬写的,相信是比较有说服力的一个案例了。
这个组件有以下的几个功能:
- 跟踪当前文件夹状态并显示其内容
- 处理文件夹导航(打开,关闭,刷新…)
- 处理新文件夹的创建
- 切换显示收藏夹
- 切换显示隐藏文件夹
- 处理当前工作目录更改
它的结构如图(不同的色块代表着不同的功能点,图源:vue官网):

你作为一个新接手的开发人员,能够在茫茫的 method、data、computed 等选项中一目了然的发现这个变量是属于哪个功能吗?比如「创建新文件夹」功能使用了两个数据属性,一个计算属性和一个方法,其中该方法在距数据属性「一百行以上」的位置定义。
改造:
根据官方示例的写法,可以把相同的逻辑快抽离出来,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| // “新建文件夹”功能 function useCreateFolder(openFolder) { // originally data properties const showNewFolder = ref(false); const newFolderName = ref("");
// originally computed property const newFolderValid = computed(() => isValidMultiName(newFolderName.value));
// originally a method async function createFolder() { if (!newFolderValid.value) return; const result = await mutate({ mutation: FOLDER_CREATE, variables: { name: newFolderName.value, }, }); openFolder(result.data.folderCreate.path); newFolderName.value = ""; showNewFolder.value = false; }
return { showNewFolder, newFolderName, newFolderValid, createFolder, }; }
|
对比:

逻辑看起来一目了然,很快能定位到功能点对应的代码
例3: 对第一部分XX项目的改造
基于例2的思路,我们还可以对XX项目中最经常用到的经典数据表页面进行改造。
首先,因为这是个VUE2的旧项目,需要添加@vue/composition-api并在main.js中声明
1 2
| import VueCompositionAPI from '@vue/composition-api' Vue.use(VueCompositionAPI)
|
在业务页面中,就可以这样引入并使用了:
1 2 3 4
| <script> import { ref, reactive, onMounted, onUnmounted } from '@vue/composition-api'
</script>
|
最后把页面的业务进行拆分,大概可以分为三个模块,它们在各自的函数中只需要关注输入和输出:

而最后在一个setup里,会把这些零件组装起来,提供给模板必要的方法、状态和数据

页面结构一下就清晰了很多。
讨论:基于这个思路,JSX/TSX是否更适合这个组织方式,应该怎么实现?可以动手尝试一下,或阅读vant组件库源码
状态组件
基于组合式API,还能把各个业务模块中相同的状态控制行为抽离,实现以往不能做到的纯状态控制组件。
路由返回拦截
如果想实现多层弹窗出现时,用户返回操作需要按顺序把弹窗先一层层关掉,那我们一般都在路由级组件中通过钩子拦截路由行为,如果涉及到的弹窗比较多,可能还需要做很多判断,大概类似于这样:
1 2 3 4 5 6 7 8 9 10
| import { onBeforeRouteLeave } from 'vue-router' export default { beforeRouteLeave(to, from, next) { if (/** 判断状态 */)) { // 清除打开的各种弹出层... } else { next() } }, }
|
beforeRouteLeave 只在路由级的组件中有效,复用性为零
Composition API
1 2 3 4 5 6 7 8 9 10 11 12
| import { onBeforeRouteLeave } from 'vue-router' export default { setup () { onBeforeRouteLeave((to, from) => { const answer = window.confirm( 'Do you really want to leave? you have unsaved changes!' ) // return false 停留在当前页面 if (!answer) return false }) } }
|
可以在任何组件的setup中使用,可以进行进一步的封装
进阶封装示例(支持处理多层弹窗):

使用示例如下图:

v-model封装
原始方法定义一个可在子组件修改的 title 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| export default { model: { prop: 'title', event: 'change' }, props: { // 使用 `title` 作为 model 的 prop title: { type: String, default: 'Default title' } }, data () { return { displayTitle: this.title } }, watch: { displayTitle (val) { this.$emit('input', val) } }, methods: { changeTitle () { this.displayTitle = 'xxx' } } }
|
而在组合式API中,也可以把这个和业务本身无关的重复行为进行封装:
useVModel封装示例