背景
由于之前的数据库防火墙产品与数据库审计产品使用的是同一套代码,随着两个产品功能的差异越来越大,代码的冗余度和偶合度越来越高,为了便于后期维护以及添加新功能,所以基于原来的项目代码,进行了代码结构拆分。
> 注意:本次拆分只拆分了可以拆分的部分,有的模块例如:规则、关于我们,是没有进行拆分的,一是有的模块很简单,没必要拆分;二是有的模块原先写得代码偶合太严重,无法拆分,如果要拆分,需要花费大量精力去梳理代码,同时还要后端配合拆分。
目的
将此次代码拆分方案记录下来,便于后来的开发人员快速熟悉项目结构。
拆分前
流程设计
项目拆分前,区分数审和防火墙功能的流程如上图所示。
访问系统时,先加载入口文件 main.js,然后加载登录页 login.vue,加载登录页的同时获取产品模式并保存到 session.storage.platformType 中,接着用户登录系统进入具体页面,该页面同时包含了数审和防火墙的功能,根据 session.storage.platformType 保存的值来判断,具体显示哪个功能。
目录结构设计
项目拆分前的目录结构如上图所示。
该目录结构是初始化一个 Vue 项目时的基本目录,根据目录结构,基本上看不出该项目包含了两个不同的产品,也不知道不同产品模式下会加载哪一部分文件,结构不清晰。
存在的问题
通过分析,可以发现当前的系统流程和目录结构是存在很多问题的,大概总结下:
- 加载登录页的时候才获取产品模式,如果登录页加载完成,而产品模式还未获取或者获取不到,那么登录页显示的产品信息有可能是错误的;
- 每次进入一个具体页面,如果同时包含了数审和防火墙的功能,都要重新判断一次,当前的功能是数审的还是防火墙的;
- 目录结构不清晰,不清楚哪些模块是公共模块,哪些是数审独有的模块,哪些是防火墙独有的模块;
- 可维护性和可扩展性差。数审的代码和防火墙的代码混在一个文件内,改代码的时候需要重头看一遍才知道哪块代码属于数审,哪块代码属于防火墙。如果想要添加一个功能,还得继续加逻辑判断,代码越来越臃肿;
- 业务逻辑混乱,与后端通信使用了同一个接口。
拆分后
流程设计
项目拆分后,区分数审和防火墙功能的流程如上图所示。
访问系统时,先加载入口文件 main.js,该文件中写了路由拦截相关的逻辑,在路由拦截时,如果没有产品模式,则要先获取产品模式,否则会被拦截,进不去系统。获取产品模式后,根据当前产品模式,会注册对应的登录页、路由配置、接口请求等。当注册完毕后,每次访问具体的页面,都应该是独立的模块了。
目录结构设计
项目拆分后的目录结构如上两个图所示。
该目录结构经过拆分,已经可以清晰地看到不同产品之间分离出来的文件了,从入口文件开始,经过路由拦截后,会加载指定的登录页,然后把对应产品需要的文件合并到公共文件中。比如:http 请求、路由配置等。最终结果,程序只会把需要的文件加载进去。
解决的问题
经过重新设计,解决了当前项目存在的一些问题:
- 在加载登录页之前,通过路由拦截,必须先获取产品模式,才能进入系统,登录页是在获取到产品模式之后才加载的,不会出现产品信息显示错误的问题;
- 只有在第一次进入系统或刷新页面的时候,才会重新获取产品模式,合并产品模式对应的文件,合并的文件是拆分后的文件,不需要在文件内再次判断该有数审的功能还是防火墙的功能。
- 目录结构清晰,防火墙相关的功能模块文件都放在 views-fw 文件夹内。
- 提高了项目的可维护性和可扩展性,降低了代码的偶合度。数审的代码和防火墙的代码基本已经拆分开,如果想要添加一个防火墙的功能,只需要在防火墙相关的文件夹内新增对应功能模块的文件即可。
- 业务逻辑清晰,与后端通信使用的是不同的接口,公共模块使用的接口按原来的不变,数审独有的接口在 url 前面增加了 audit 前缀,防火墙独有的接口在 url 前面增加了 firewall 前缀。
关键代码
/**
* @Name: main.js
* @Author: cqfeng
* @Description: 项目入口 js 文件
* @MainFunction: 引入和注册一些全局资源
**/
//...省略的代码...
// 路由拦截,使用全局导航守卫beforeEach
router.beforeEach(async (to, from, next) => {
// 如果没有产品模式,先获取产品模式
if (!store.state.productMode.productMode) {
await store.dispatch('productMode/SET_PRODUCT_MODE');
}
//...省略的代码...
/**
* @Name: product-mode.js
* @Author: cqfeng
* @Description: 产品模式相关逻辑处理文件
* @MainFunction: 保存当前产品模式,根据不同产品模式配置 产品登录页、http 请求 等资源
**/
import Vue from 'vue';
import portService from '@/assets/js/service/http/http'; // axios请求
import router from '@/router/index'; // 默认路由配置,动态路由配置
import httpAAS from '@/assets/js/service/http/http-aas'; // 数审独有的 http 请求
import httpFW from '@/assets/js/service/http/http-fw'; // 防火墙独有的 http 请求
import globalConstant from '@/assets/js/const/global-constant'; // 项目全局常量
export default {
namespaced: true,
state: {
productMode: '', // 产品模式,进入系统或刷新页面时获取
},
mutations: {
// 修改产品模式
changeProductMode: function (state, value) {
state.productMode = value;
},
},
actions: {
async SET_PRODUCT_MODE({ commit, state }) {
let res = await portService.getProductMode();
if (res.data.code === 0) {
commit('changeProductMode', res.data.data.productMode);
}
// 如果是数审产品
if (state.productMode === globalConstant.COMMON.AAS) {
// 设置产品登录页
router.addRoutes([
{
path: '/login',
name: 'login',
component: resolve => {
require(['@/views/login.vue'], resolve);
},
}
]);
// 合并 http 请求,挂载到 Vue 原型上
Vue.prototype.portService = Object.assign(portService, httpAAS);
}
// 如果是防火墙产品
else if (state.productMode === globalConstant.COMMON.DBSG) {
// 设置产品登录页
router.addRoutes([
{
path: '/login',
name: 'login',
component: resolve => {
require(['@/views/views-fw/login.vue'], resolve);
},
}
]);
// 合并 http 请求,挂载到 Vue 原型上
Vue.prototype.portService = Object.assign(portService, httpFW);
}
}
}
};
总结
经过拆分,数审和防火墙的代码目录已经算是基本分开了,这个过程花费的力气也很大,也引发了我的一些思考,一套代码多个项目的这种方案是否算好的方案,还有没有其他更好的方案。如果项目一开始,就按照一套代码多个项目的设计来开发,会不会后期的维护成本会低一些?这些问题我也没有答案,希望后来者能够继承历史经验,更好地开发出优秀的项目。
其他实现方式
起初设计拆分方案的时候,有两种方案,一种是通过获取产品模式动态改变当前产品功能,一种是在打包时通过打包参数打包指定产品包。最终的方案是选择第一种。