前端代码复用一直是一个很重要的话题,也是一个很难的话题。在前端开发中,我们经常会遇到很多重复的代码,比如说,我们经常会在不同的页面中使用相同的组件,或者是相同的功能。这个时候,我们就需要考虑如何将这些重复的代码进行复用。在这篇文章中,我将会和大家分享一些前端代码复用的精髓。
1. 组件复用
我们在 GitHub 上可以找到很多优秀的前端组件库,比如说 Ant Design、Element UI、Vant 等等。这些组件库都提供了很多优秀的组件,不难发现,这些个组件并不是一朝一夕一次性推出来的,一定都是经过了很多版本迭代,很多人的使用和测试的,而且是非常通用的,比如,按钮,选择框,数据库,表单,dialog等等。这些组件,我们可以直接拿来使用。这些组件库的优势在于,它们提供了很多现成的组件,我们可以直接拿来使用,而且这些组件库都是经过了很多人的使用和测试的,所以它们的质量是非常有保障的。这样的方式其实就是一种组件复用的方式。
那么,回归到我们自己的项目中,我们应该如何进行组件复用呢?其实,我们可以将一些通用的组件进行封装,然后在需要的地方进行引用。比如说,我们可以将一些通用的表单组件进行封装,然后在需要的地方进行引用。这样的方式可以大大提高我们的开发效率,而且也可以减少我们的代码量。
举一个例子,比如说我们有一个通用的联系人组件,可能很多个页面都会用到这个组件,这个时候我们就可以将这个组件进行封装,然后在需要的地方进行引用。这样的方式可以大大提高我们的开发效率,而且也可以减少我们的代码量,而且也可以减少我们的维护成本。一个可能的代码示例如下:
jsx
import React from 'react';
import { Input, Button } from 'antd';
//通用的联系人函数式组件
//渲染一个联系人面板,包含姓名、电话、身份证号等信息,敏感信息自动打码
function Contact(props) {
const { name, phone, idCard } = props;
//敏感信息打码
const maskIdCard = idCard => {
return idCard.replace(/(\d{4})\d+(\d{4})$/, '$1****$2');
};
return (
<div className="contact">
<div className="name">姓名:{name}</div>
<div className="phone">电话:{phone}</div>
<div className="idCard">身份证号:{maskIdCard(idCard)}</div>
{if (props.children) {
<div className="divider"></div>
<div className="extra">{props.children}</div>
}}
</div>
);
}
export default Contact;
2. 逻辑复用
实际上,在前端领域中,很多业务因为其复杂的交互,在如PC和移动端的一些操作会存在较大的差异,因此在前端组件上做复用可能会比较不现实,即便强行做了组件复用,也会导致组件的复杂度增加,维护成本增加。
哪怕是目前流行的前端框架,也无法完全解决这个问题。有人会说 比如 taro 或者 uni-app不就解决了一套代码解决了多端问题吗?但是实际上,这些框架也是通过一套代码生成多端代码,而不是真正的逻辑复用。
真正到了要写pc端页面和移动端页面的时候,我们就会发现,很多界面组件是无法复用的。比如说,我们在移动端页面中可能会有一些滑动操作,而在 PC 端页面中可能会有一些点击操作,另外pc端的本身可用空间比较多,一屏显示的内容比较多,而移动端的本身可用空间比较少,一屏显示的内容比较少,在布局上也会有很大的差异。
就比如,uni-app开发App和小程序,实际上也很难做到逻辑复用,因为App和小程序的交互方式是不一样的,App是原生的交互方式,两者在底层能力上都会有一些不同,因此我接触过的多数团队,都是App和小程序分别些一个页面,举一个例子,一个页面的目录结构可能是这样的,他会有严格的区分 app 还是 miniprogram,还是h5的
shell
├── src
│ ├── pages
│ │ ├── HomePage
│ │ │ ├── app.vue // 为App编写的代码
│ │ │ ├── miniprogram.vue // 为小程序编写的代码
│ │ │ ├── h5.vue // 为H5编写的代码
│ │ │ ├── index.vue // 通用的代码
│ ├── models
│ ├── services
│ ├── utils
├── package.json
├── tsconfig.json
├── README.md
其中,index.vue 代码示例如下:
vue
<template>
<div>
<component :is="currentComponent" />
</div>
</template>
<script>
import app from './app.vue';
import miniprogram from './miniprogram.vue';
import h5 from './h5.jsx';
export default {
data() {
return {
currentComponent: null
};
},
created() {
// 根据当前的运行环境选择组件
if (__PLATFORM__ === 'app') {
this.currentComponent = app;
} else if (__PLATFORM__ === 'miniprogram') {
this.currentComponent = miniprogram;
} else if (__PLATFORM__ === 'h5') {
this.currentComponent = h5;
}
}
};
</script>
因此,我们看到在一些复杂的业务逻辑场景,尤其是交互差异形势巨大的时候,一套代码解决多端问题是不现实的,往往,实现上应该是多套代码,准确来说是一种编码语言,多端分开实现。因此我们需要把精力放在逻辑上做复用上。
虽然在前端界面上,做到前端交互代码复用可能实施难度比较大,甚至在一些场景上不大现实,但是在逻辑复用上,我们还是可以做到的。比如说,我们可以将一些通用的逻辑进行封装,然后在需要的地方进行引用。比如说,我们可以将一些通用的请求逻辑进行封装,然后在需要的地方进行引用。这样的方式可以大大提高我们的开发效率,而且也可以减少我们的代码量。
下面,我们来说说业务逻辑复用的姿势,其实在前端研发领域,我们或多或少接触过一些设计模式,比如 MVC,MVVM,MVP,MVI 等等。这些设计模式都是为了解决一些通用的问题,比如说,MVC 是为了解决数据和视图的分离问题,MVVM 是为了解决数据和视图的双向绑定问题,MVP 是为了解决视图和业务逻辑的分离问题,MVI 是为了解决视图和状态的分离问题。
下面分别介绍一下这几种设计模式:
- MVC:MVC 是 Model-View-Controller 的缩写,它是一种软件架构模式,它将软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。MVC 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。MVC 模式的核心是模型、视图、控制器三个部分之间的交互。
其架构图使用mermaid语法描述如下:
- MVVM:MVVM 是 Model-View-ViewModel 的缩写,它是一种软件架构模式,它是 MVC 模式的一种变种。MVVM 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。MVVM 模式的核心是模型、视图、视图模型三个部分之间的交互。
其架构图使用mermaid语法描述如下:
- MVP:MVP 是 Model-View-Presenter 的缩写,它是一种软件架构模式,它是 MVC 模式的一种变种。MVP 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。MVP 模式的核心是模型、视图、控制器三个部分之间的交互。
其架构图使用mermaid语法描述如下:
- MVI:MVI 是 Model-View-Intent 的缩写,它是一种软件架构模式,它是 MVC 模式的一种变种。MVI 模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。MVI 模式的核心是模型、视图、意图三个部分之间的交互。
其架构图使用mermaid语法描述如下:
今天要说的这个前端业务逻辑复用,其实可以参考或者直接使用上述一些模式,比如MVP,我们专注于打造通用的M层和P层,然后在不同的页面中引用这些通用的M层和P层,这样就可以实现逻辑复用。这样的方式可以大大提高我们的开发效率,而且也可以减少我们的代码量。
那么,具体点,我们怎么去实施呢?假设我们现在有三个端:
- 小程序
- H5
- PC
我们如何打造这样的通用的M层和P层呢?这就比较考验我们对业务的抽象能力了,我们需要将业务逻辑进行抽象,然后将这些抽象的业务逻辑进行封装,然后在不同的页面中引用这些抽象的业务逻辑。我们也许需要糅合一些设计模式,比如说,我们可以使用观察者模式,将一些通用的业务逻辑进行封装,然后在不同的页面中引用这些通用的业务逻辑。我们也需要遵循一些设计原则,我觉得可能最重要的是单一职责原则,我们需要将业务逻辑进行抽象,然后将这些抽象的业务逻辑进行封装,然后在不同的页面中引用这些抽象的业务逻辑。另外就是开闭原则,我们需要将业务逻辑进行抽象,然后将这些抽象的业务逻辑进行封装,然后在不同的页面中引用这些抽象的业务逻辑。
ok,扯了这么多,有点口干舌燥了,准备开始动手写代码了,下面我们来看一个具体的例子,比如说,我们现在有一个企业用户管理流。
这个里面可能设计到的业务逻辑有:
- 企业认证
- 个人用户实名认证
- 法人授权
- 员工管理
- 权限审批
- 企业信息管理
- 联系人管理
- 印章管理
等等,就不写下去了。接下来我们就上述的业务逻辑进行抽象,然后将这些抽象的业务逻辑进行封装,然后在不同的页面中引用这些抽象的业务逻辑。我们的目标是做出这个M 层,注意这个层几乎完全是使用typescript实现的,不依赖任何前端框架,这样子即便你小程序使用 uni-app开发使用的是vue,h5使用的是react,pc使用的是angular,也可以使用这个M层。
shell
├── src
│ ├── models
│ │ ├── EnterpriseUserManager.ts // 你的M层类
│ │ ├── index.ts // 导出所有的models
│ ├── services
│ │ ├── authService.ts // 包含企业认证、个人用户实名认证等方法的服务
│ │ ├── userService.ts // 包含员工管理、权限审批等方法的服务
│ │ ├── enterpriseService.ts // 包含企业信息管理、联系人管理等方法的服务
│ │ ├── sealService.ts // 包含印章管理的方法的服务
│ │ ├── index.ts // 导出所有的services
│ ├── utils
│ │ ├── api.ts // 包含API调用的工具函数
│ │ ├── index.ts // 导出所有的utils
│ ├── app.ts // 主应用文件
│ ├── index.ts // 应用入口文件
├── package.json // npm包管理文件
├── tsconfig.json // TypeScript配置文件
├── README.md // 项目说明文件
typescript
class EnterpriseUserManager {
// 这些属性可能需要根据你的业务需求进行调整
private enterpriseId: string;
private userId: string;
constructor(enterpriseId: string, userId: string) {
this.enterpriseId = enterpriseId;
this.userId = userId;
}
// 企业认证
async enterpriseCertification(certificationInfo: any): Promise<any> {
// 这里应该包含实际的认证逻辑
}
// 个人用户实名认证
async individualCertification(certificationInfo: any): Promise<any> {
// 这里应该包含实际的认证逻辑
}
// 法人授权
async legalPersonAuthorization(authorizationInfo: any): Promise<any> {
// 这里应该包含实际的授权逻辑
}
// 员工管理
async manageEmployee(employeeInfo: any): Promise<any> {
// 这里应该包含实际的员工管理逻辑
}
// 权限审批
async permissionApproval(approvalInfo: any): Promise<any> {
// 这里应该包含实际的审批逻辑
}
// 企业信息管理
async manageEnterpriseInfo(info: any): Promise<any> {
// 这里应该包含实际的企业信息管理逻辑
}
// 联系人管理
async manageContact(contactInfo: any): Promise<any> {
// 这里应该包含实际的联系人管理逻辑
}
// 印章管理
async manageSeal(sealInfo: any): Promise<any> {
// 这里应该包含实际的印章管理逻辑
}
}
export default EnterpriseUserManager;
然后,我在我的业务页面中引用这个M层,比如说,我在我的企业认证流中的页面引入这个M层,他的小程序vue,和h5 react端的代码可能是这样的:
小程序端
vue
<template>
<view>
<!-- 你的页面组件 -->
</view>
</template>
<script>
import { EnterpriseUserManager } from '@/models';
export default {
data() {
return {
enterpriseUserManager: null
};
},
created() {
this.enterpriseUserManager = new EnterpriseUserManager('enterpriseId', 'userId');
// 使用enterpriseUserManager进行企业认证
this.enterpriseUserManager.enterpriseCertification(certificationInfo)
.then(response => {
// 处理认证成功
})
.catch(error => {
// 处理认证失败
});
}
};
</script>
H5端
jsx
import React, { useEffect } from 'react';
import { EnterpriseUserManager } from '@/models';
function EnterpriseCertificationPage() {
useEffect(() => {
const enterpriseUserManager = new EnterpriseUserManager('enterpriseId', 'userId');
// 使用enterpriseUserManager进行企业认证
enterpriseUserManager.enterpriseCertification(certificationInfo)
.then(response => {
// 处理认证成功
})
.catch(error => {
// 处理认证失败
});
}, []);
return (
// 你的页面组件
);
}
export default EnterpriseCertificationPage;
前端页面关注的是视图的渲染,而不是业务逻辑的处理,这样的方式可以大大提高我们的开发效率,维护行提高也是不言而喻的。
代码自动生成
我们在实践代码复用的时候,发现一个问题,那就是代码规范问题,具体按照什么样的模式来写代码,才能方便后续的这个业务逻辑能够被复用到多个端,我们可能需要一个标准的模板,定义出一套复用的框架,然后业务逻辑的开发者只需要按照这个模板来写代码,然后就可以实现业务逻辑的复用。
那么,具体的实操,我们该如何做呢?我们可以使用 Yeoman和VS Code Extension Generator 这两个工具来实现代码自动生成。Yeoman 是一个用于生成项目模板的工具,VS Code Extension Generator 是一个用于生成 VS Code 插件的工具。这者配合起来做这个事情,简直太合适不过,想一想,右键对着文件夹,点击生成代码,然后就生成了一套标准的业务逻辑代码框架,然后研发小伙伴只需要按照这个框架来写代码,就可以实现业务逻辑的复用了。
下面是一个 Yeoman 的模板示例:
shell
├── generators
│ ├── app
│ │ ├── index.js
│ │ ├── templates
│ │ │ ├── index.js
│ │ │ ├── index.test.js
│ │ │ ├── miniProgram.js
│ │ │ ├── h5.js
│ │ │ ├── pc.js
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── .gitignore
├── package.json
一个好的可复用的业务逻辑模块,是需要配置自动化测试的,这样可以保证业务逻辑的稳定性,也可以保证业务逻辑的可维护性。index.js里面在设计是,需要遵循设计模式的一些原则,尽量面向接口编程,而不是面向实现编程,这样可以保证业务逻辑的可扩展性,也可以保证业务逻辑的可维护性。
这里,每个每块的外部依赖可能,不太一样,需要处理的业务逻辑也不太一样,如何设计这个模板才比较优雅呢?这里肯定是需要一些业务逻辑的抽象,然后将这些抽象的业务逻辑进行封装,进行模块间的连接,注意,一定是抽象,具体的实现应该交给业务逻辑的开发者来实现,如果存在端的差异,这块不抽象肯定是不行的。
这个是个 templates/index.js 的模板示例,我们这里给出一个MVP层里面 M 层的模板示例:
javascript
// 抽象策略类
class AbstractStrategy {
validateData(data) {
// 通用的数据验证逻辑
}
handleError(error) {
// 通用的错误处理逻辑
}
log(message) {
// 通用的日志记录逻辑
}
// 有明显差一点可以写一个抽象,具体在不同平台端 中实现
businessLogic(data) {
throw new Error('This method must be overridden');
}
}
class MiniProgramStrategy extends AbstractStrategy {
businessLogic(data) {
// 小程序的业务逻辑
// 可以使用 this.validateData, this.handleError, this.log
}
}
class H5Strategy extends AbstractStrategy {
businessLogic() {
// H5的业务逻辑
}
}
class PCStrategy extends AbstractStrategy {
businessLogic() {
// PC的业务逻辑
}
}
// 工厂类
class StrategyFactory {
static getStrategy(platform) {
switch (platform) {
case 'miniProgram':
return new MiniProgramStrategy();
case 'h5':
return new H5Strategy();
case 'pc':
return new PCStrategy();
default:
throw new Error(`No strategy found for platform ${platform}`);
}
}
}
// 使用策略
const platform = 'miniProgram'; // 这个值可能来自用户的输入或者配置文件
const strategy = StrategyFactory.getStrategy(platform);
strategy.businessLogic();
下面是一个 VS Code Extension Generator 的模板示例:
shell
├── src
│ ├── extension.ts
│ ├── test
│ │ ├── extension.test.ts
├── package.json
其中 Yeoman
里面templates里面的文件就是生成的代码的模板,extension.ts
生成代码实际上就是node fs来基于模板生成代码。
总结
感觉,这是最近关于前端代码复用性的一些思考,前端代码复用是一个很重要的话题,是一个不能回避的问题,也是一个很难的问题。但是虽难,总会找到一些突破口,比如,我们可以把整体进行拆分,发现逻辑层是可以做比较大规模的复用的,然后我们可以使用一些设计模式,比如MVP,来实现逻辑复用,然后我们可以使用 Yeoman 和 VS Code Extension Generator 这两个工具来实现代码自动生成,这样就可以配合我们更好的实现前端代码复用了。
关注我的公众号 老码沉思录,每天推送前端技术干货,带你深度解读前端技术,让你成为技术高手。