7步实现前端代码分层

2,642 阅读7分钟

视频讲解:【7步搞定前端代码分层-哔哩哔哩】 b23.tv/w8SWewc

看一些资料,发现后端分层的思想很有意思。将代码拆分成多个层,定义好每个层做的事情,代码的职责划分清晰,代码的可维护性也有明显的提高。所以,后端甚至会将这些写到规范中。

以下是阿里的代码分层规范图示:

b1e788106427e1e0184974a4daf88ae0b695f9a4.png

虽然前端项目架构没这么复杂,但是打开项目的文件夹看看,就能发现不少问题。

以 vue 项目为例,我们大部份的文件都放在了 src/views 文件夹里,与 views 同级的其他文件夹的代码量还不如一个业务代码的组件代码量多。而子文件夹,往往又只有 .vue 文件和 components 文件夹。很显然,这样的项目都是在 vue 中一把梭的。

那前端能不能借这个分层思想,把代码拆开呢?如之前的angularjs,把 MVC分层做好,然后大家跟着填相应的内容?最近,就尝试做一做这个代码分层。以下就分享一下实践的过程。

假设项目中有一个订单页面,目录结构如下:

  • views
    • orders
      • index.vue
      • components

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。借助 asyncawait 我们很好地完成了这个步骤。

现在 getList 没有了 this 关键字,真正独立出来了。此时代码分级如下:

graph LR
点击按钮 --> 按钮操作 --> 页面处理 --> 与后端交互

代码的雏形已经形成。后面就可以考虑分层了。

6. 将请求处理独立成文件

我们将 getList 方法挪到文件中,此时的目录接口如下。

  • views
    • orders
      • index.vue
      • getList.js
      • components

调试通过,重构初步完成。

7. 整合请求处理层

除了 getList 之外,我们开始按上面 1-6 处理其他方法、其他页面,于是我们与后端的交互越来越多,我们再考虑将其集中起来,放在跟 views 平级的一个目录中,如果暂时想不到其他命名,就将其作为 models 层好了(反正也没有明确的定义😜)。

  • models
    • orders
      • index.js
  • views
    • orders
      • index.vue
      • components

至此,我们将项目抽象出了一层与后端数据交互的代码。

总结

本次实践,我们使用代码分级的思想,逐步把与后端交互的代码独立出来了,最后形成了一个 Models 层,从而实现了代码分层。

架构.png

在现实的开发中,业务往往是更复杂的。利用分级、分层思想,确定好、规划好每一级、每一层要做的事情,往往让我们的思路以及代码都更加清晰,也让日后的维护变得更简单。

比如,后面我们就可以在 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,实践起来应该更简单。代码结构也许是这样的:

前端代码分层.drawio.png

参考