视频讲解:【7步搞定前端代码分层-哔哩哔哩】 b23.tv/w8SWewc
看一些资料,发现后端分层的思想很有意思。将代码拆分成多个层,定义好每个层做的事情,代码的职责划分清晰,代码的可维护性也有明显的提高。所以,后端甚至会将这些写到规范中。
以下是阿里的代码分层规范图示:
虽然前端项目架构没这么复杂,但是打开项目的文件夹看看,就能发现不少问题。
以 vue 项目为例,我们大部份的文件都放在了 src/views 文件夹里,与 views 同级的其他文件夹的代码量还不如一个业务代码的组件代码量多。而子文件夹,往往又只有 .vue 文件和 components 文件夹。很显然,这样的项目都是在 vue 中一把梭的。
那前端能不能借这个分层思想,把代码拆开呢?如之前的angularjs,把 MVC分层做好,然后大家跟着填相应的内容?最近,就尝试做一做这个代码分层。以下就分享一下实践的过程。
假设项目中有一个订单页面,目录结构如下:
- views
- orders
- index.vue
- components
- orders
index.vue 的代码如下:
<template>
<div class="search">
<!--省略搜索表单-->
<button click="getList">搜索</button>
</div>
<div class="list" v-for="item in list">
<!--省略列表内容-->
<button click="handleDelete">删除</button>
<button click="edit">编辑</button>
</div>
</template>
<script>
import api from '@/api';
export default {
data() {
return {
loading: false,
searchForm: {},
list: [],
}
},
methods: {
async getList() {
this.loading = true;
const result = await api.getList(this.searchForm);
this.list = result.data;
this.loading = false;
},
handleDelete(item) {
// 做一些操作,然后更新列表
this.getList();
},
edit(item) {
// 做一些操作,然后更新列表
this.getList();
},
}
}
</script>
从代码可以看到,这是一个简单的增删改查页面。
1. 统一命名
虽然页面就三个按钮,函数的命名方式却各式各样。所以,首先第一步就是先把这些都统一起来,在这里,我们统一将页面元素调用的方法,统一添加前缀为 handle。
<template>
<div class="search">
<!--省略搜索表单-->
<button click="getList">搜索</button>
</div>
<div class="list" v-for="item in list">
<!--省略列表内容-->
<button click="handleDelete">删除</button>
<button click="handleEdit">编辑</button>
</div>
</template>
<script>
import api from '@/api';
export default {
data() {...},
methods: {
async getList() {...},
handleDelete(item) {
// 做一些操作,然后更新列表
this.getList();
},
handleEdit(item) {
// 做一些操作,然后更新列表
this.getList();
},
}
}
</script>
可以看到,删除和编辑按钮的名称统一之后,看起来的确更舒服了。然而,细心的人会发现无法重命名的 getList,因为它即是按钮的点击事件执行的方法,又是其他事件间接调用的方法。一个方法,多种用途,它就不符合我们页面元素调用方法的规定了。
2. 添加中间方法
既然要遵守页面元素的方法使用统一命名,那就只能添加中间的方法 handleSearch ,再去调用 getList。
<template>
<div class="search">
<!--省略搜索表单-->
<button click="handleSearch">搜索</button>
</div>
<div class="list" v-for="item in list">...</div>
</template>
<script>
export default {
data() {...},
methods: {
async getList() {...},
handleSearch(){
this.getList();
},
...
}
}
</script>
看到这里的读者,也许会觉得多此一举。
然而,其实我们的代码在这里完成了分级。在严格的代码分级制度中,每一层仅允许调用其下一层的代码,不允许跨级别调用,也不允许一个复合函数同时调用多个不同级别的函数。
function 高级() {
// 不允许跨级调用低级的函数
中级1()
中级2()
}
function 中级1() {
// 不允许反向调用高级的函数
低级()
}
function 低级(){
// 不允许反向调用中高级的函数
doSomething()
}
我们借用这个概念,将按钮操作定为了高级别的方法,后端交互定为低级别的方法。所以我们在按钮操作中,不直接去与后端交互。
graph LR
点击按钮 -->
按钮操作 -->
与后端交互
分级有一个好处,就是我们阅读代码的时候,思想不需要在各种层级中来回跳,在复杂的业务开发中尤其有用。比如这里的搜索按钮,提交搜索表单时数据字典的转换、日期格式化处理,请求中的防抖处理,请求后的状态处理、数据提取,如果都写在 getList 中,除了代码臃肿之外,业务逻辑处理、前端事件处理、后端数据操作这些都耦合在一起。人的思维要在这几种操作中反复横跳的,是不连贯的,这就给阅读代码带来了极大的不便。这也是我们阅读没分级的代码时,常常头痛的原因。
代码分级已经完成,要做分层,就可以尝试把与后端交互的内容移出独立的文件中。
3. 传递数据,而不是修改全局数据
要做到文件独立,首先函数应该独立,不依赖外部数据。在 vue2 中,有 this. 前缀的数据,明显都是当前页面的全局数据。
<template>...</template>
<script>
export default {
data() {...},
methods: {
async getList(searchForm) {
this.loading = true;
const result = await api.getList(searchForm);
this.list = result.data;
this.loading = false;
},
handleSearch(){
// handleDelete、handleEdit修改同此
this.getList(this.searchForm);
},
...
}
}
</script>
此处,我们将 this.searchForm 改成从外部传入。每个调用 getList 的方法都把 this.searchForm 当作参数传递过去。
4. 深拷贝传入的对象数据
在 javascript 中,如果传入的数据是对象,实际上是地址引用,直接修改这个对象,会导致页面其他地方引用的这个对象,也会发生改变。这也是前端数据经常出错的一个原因,所以,最好对其做深拷贝之后再使用。
<template>...</template>
<script>
export default {
data() {...},
methods: {
async getList(searchForm) {
const searchInfo = JSON.parse(JSON.stringify(searchForm));
this.loading = true;
const result = await api.getList(searchInfo);
this.list = result.data;
this.loading = false;
},
...
}
}
</script>
这里用 JSON.parse(JSON.stringify(...)) 对 searchForm 进行深拷贝后,赋值给变量 searchInfo 做搜索。如果你有更喜欢的深拷贝方法,也可替代使用。
改造完成之后,还有两个 this 的引用。下面继续消除他们。
5. 将页面数据处理交回给更高级的函数处理
尝试再次将代码分级,使用一个中级别的函数来专门更新页面的数据变化。
<template>...</template>
<script>
export default {
data() {...},
methods: {
async getList(searchForm) {
const searchInfo = JSON.parse(JSON.stringify(searchForm));
const result = await api.getList(searchInfo);
return result && result.data;
},
async updatePage() {
this.loading = true;
this.list = await this.getList(this.searchForm);
this.loading = false;
},
handleSearch(item) {
this.updatePage();
},
handleDelete(item) {
...
this.updatePage();
},
handleEdit(item) {
...
this.updatePage();
}
}
}
</script>
此处新建了一个 updatePage 函数,处理了加载状态和页面列表数据,把 getList 相关的页面操作都承接过来了。getList 将请求后得到的数据处理后再返回给 updatePage。借助 async、await 我们很好地完成了这个步骤。
现在 getList 没有了 this 关键字,真正独立出来了。此时代码分级如下:
graph LR
点击按钮 --> 按钮操作 --> 页面处理 --> 与后端交互
代码的雏形已经形成。后面就可以考虑分层了。
6. 将请求处理独立成文件
我们将 getList 方法挪到文件中,此时的目录接口如下。
- views
- orders
- index.vue
- getList.js
- components
- orders
调试通过,重构初步完成。
7. 整合请求处理层
除了 getList 之外,我们开始按上面 1-6 处理其他方法、其他页面,于是我们与后端的交互越来越多,我们再考虑将其集中起来,放在跟 views 平级的一个目录中,如果暂时想不到其他命名,就将其作为 models 层好了(反正也没有明确的定义😜)。
- models
- orders
- index.js
- orders
- views
- orders
- index.vue
- components
- orders
至此,我们将项目抽象出了一层与后端数据交互的代码。
总结
本次实践,我们使用代码分级的思想,逐步把与后端交互的代码独立出来了,最后形成了一个 Models 层,从而实现了代码分层。
在现实的开发中,业务往往是更复杂的。利用分级、分层思想,确定好、规划好每一级、每一层要做的事情,往往让我们的思路以及代码都更加清晰,也让日后的维护变得更简单。
比如,后面我们就可以在 models 层中,在数据在与后端交互的时候,做一些处理,避免一些引起后端报错的问题。同时,也可以对返回给前端的内容做保底方案,同样可以避免前端出错导致页面中断。
// 重构后的 getList
import { pick } from "lodash-es";
export getList = async (searchForm) => {
const searchInfo = JSON.parse(JSON.stringify(searchForm));
// 仅提交后端需要的数据
const submitKey = ['orderId', 'productName', 'startTime', 'endTime',];
pick(searchInfo, submitKey);
const result = await api.getList(searchInfo);
if (result && result.data) {
return result.data
}
// 避免 undefind
return [];
}
这就是本次代码分层的实践,希望这次实践总结能给大家带来一些帮助,也希望大家能把代码分级、分层应用在在实际开发中,提升代码质量。
此文章都是以 vue2 为案例,感兴趣的也可以在 vue3 中实践一下,毕竟有 composition API / useHook,实践起来应该更简单。代码结构也许是这样的:
参考
- 优秀的代码都是如何分层的?
- 《改善代码质量的101个方法》