文章目录
  1. 1. 第一部分:XX项目代码鉴赏
    1. 1.1. 比较差的部分
      1. 1.1.1. 发票/附件上传(同一组件实现多次)
      2. 1.1.2. 频繁使用$parent属性交互
      3. 1.1.3. 金额计算直接使用原生js+运算符
      4. 1.1.4. 项目结构组织混乱
    2. 1.2. 比较好的部分
      1. 1.2.1. 表格配置项的抽离
    3. 1.3. 其他
  2. 2. 第二部分:组合式API
    1. 2.1. 痛点
      1. 2.1.1. 例1: 最基础的数据请求
      2. 2.1.2. 例2: 一个复杂结构的组件
        1. 2.1.2.1. 旧代码:
        2. 2.1.2.2. 改造:
        3. 2.1.2.3. 对比:
      3. 2.1.3. 例3: 对第一部分XX项目的改造
    2. 2.2. 状态组件
      1. 2.2.1. 路由返回拦截
      2. 2.2.2. v-model封装

这篇文章是2020年末我在公司内部做分享时写的讲稿,由于已经离职,文章中涉及到的一些内网链接和图片已经失效,今天把它尽可能的整理还原一下,分享出来。

第一部分:XX项目代码鉴赏

这是一个由多人接手、开发人员水平良莠不齐、缺乏代码管理规范的中大型VUE2项目,我们可以从这个项目中总结经验,讨论如何避免这些差写法造成的诸多问题、并发掘学习好的写法。

比较差的部分

发票/附件上传(同一组件实现多次)

同样的功能,代码也都几乎是一样的(存在一些细微差别),写了6份:

每次改动都要6个一起改,测起来也累

发票/附件上传

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

频繁使用$parent属性交互

改这样的代码非常头疼,看着也很不优雅

无法理解的代码
应当尽量避免这种爷孙组件(a组件->b组件->c组件->d组件)的方法调用,传事件出去让“当事人”处理,如果层级太多处理起来麻烦,可以实现一个Bus来专门处理事件:

1
2
3
4
// bus.js
import Vue from 'vue'
const Bus = new Vue()
export default Bus
1
2
3
// 组件d
import Bus from '@/libs/bus'
Bus.$emit('applicantAction', type)
1
2
3
4
5
// 组件a
import Bus from '@/libs/bus'
Bus.$on('applicantAction', type => {
// do something...
})

金额计算直接使用原生js+运算符

众所周知,0.1 + 0.2 !== 0.3,这样写会出现非常多的金额错误

很多相似的代码

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

项目结构组织混乱

项目中有频繁使用 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 };
},
};

xy的值都是响应式的,如果把它们放在模板中,页面会随着他们的更新而变化

痛点

以下示例列出了 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>

虽然是一块完整的逻辑,但数据、状态、方法分散在各处,且 loadingerror等状态的可复用性为零

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>

逻辑更集中了,且loadingerror等状态都是响应式的

例2: 一个复杂结构的组件

旧代码:

Vue CLI UI file explorer(vue cli ui的文件浏览器组件)
这是 Vue 官方团队的大佬写的,相信是比较有说服力的一个案例了。

这个组件有以下的几个功能:

  1. 跟踪当前文件夹状态并显示其内容
  2. 处理文件夹导航(打开,关闭,刷新…)
  3. 处理新文件夹的创建
  4. 切换显示收藏夹
  5. 切换显示隐藏文件夹
  6. 处理当前工作目录更改

它的结构如图(不同的色块代表着不同的功能点,图源:vue官网):
options代码结构

你作为一个新接手的开发人员,能够在茫茫的 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'
// ...other import
</script>

最后把页面的业务进行拆分,大概可以分为三个模块,它们在各自的函数中只需要关注输入和输出:

三个函数

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

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封装示例

文章目录
  1. 1. 第一部分:XX项目代码鉴赏
    1. 1.1. 比较差的部分
      1. 1.1.1. 发票/附件上传(同一组件实现多次)
      2. 1.1.2. 频繁使用$parent属性交互
      3. 1.1.3. 金额计算直接使用原生js+运算符
      4. 1.1.4. 项目结构组织混乱
    2. 1.2. 比较好的部分
      1. 1.2.1. 表格配置项的抽离
    3. 1.3. 其他
  2. 2. 第二部分:组合式API
    1. 2.1. 痛点
      1. 2.1.1. 例1: 最基础的数据请求
      2. 2.1.2. 例2: 一个复杂结构的组件
        1. 2.1.2.1. 旧代码:
        2. 2.1.2.2. 改造:
        3. 2.1.2.3. 对比:
      3. 2.1.3. 例3: 对第一部分XX项目的改造
    2. 2.2. 状态组件
      1. 2.2.1. 路由返回拦截
      2. 2.2.2. v-model封装