关于组织代码重构的尝试

1,044 阅读9分钟

背景

楼主呢是年后入职了一个新公司,依稀记得当初的面试官跟我说公司的技术栈是React,结果入职后发现主要负责项目用的是jQuery(貌似这个项目当初是买来的,而且还是主要盈利项目之一)。该项目基于jQuery自己设计了一套MVC框架,但是这个框架却没有文档,不知道是不是在反复交接的过程中被交接没了。本身这个MVC框架的设计其实也还是可以的,但是它并不适合构建大型应用(也可能当初并没有想到项目规模会到现在的这个地步,外加这些年前端三巨头的迅猛发展,DOM操作显然已经不适合当前场景了)。同时公司内部并没有形成一种规范去约束代码,然后这个项目就变成了一个典型的巨石应用。

公司本身是以电商类业务为主,这个项目就必须区分不同的平台,大家也都知道,现在的电商平台多如牛毛,这就衍生出了许多不同的项目ABCDE等等。然后各个项目之间的代码又是互相拷贝,直接导致了项目中残存着大量的冗余代码,再加上没有采用eslint之类的工具用于规范代码格式,以及较少的代码注释等等问题,都在无形之中加大了这一整套项目的维护难度。因为其本身是商业项目,业务需求是不会停的,也就是说不可能停止业务迭代,给你几个月的时候让你重构所有的项目,这样造成的损失不是我等韭菜所能赔得起的。而重构的需求又放在这里,毕竟重构有助于提升工作效率,减少人力成本,也不会说出现把新人吓跑的这么一种情况。虽然是否重构不是我个人可以决定的,但是我可以通过与主管的交流来推进重构,主管那边似乎对于重构也没有什么较好的方案,不然估计也不会放着那么久都没动它。基于上述的一些现实问题,所以就需要一种模块化分割、持续性的重构方案。本着在其位谋其职的原则,我想到了两套方案:

  1. 通过自定义的webpack plugin来实现将新老项目聚合,此时新老项目在同一个文件夹下,在重构完成后,容易产生一些老项目的碎片。
  2. 通过启动第三个项目来实现新项目与老项目的资源文件的聚合,能较好的分离新老项目,不会产生额外的碎片。

其实一开始,我是想通过方案一来实现的,但是在实现过程中,一方面发现官网上关于plugin的内容很少,尤其是hooks的参数,一点详细说明都没有,只能被迫流下了没有技术的泪水。另一方面,发现如果将新老项目都写在一起,可能在重构过程中会有意无意地出现一些碎片代码或者碎片文件(特指老项目残留的一部分),这种情况在我看来其实是不完整的重构,因为留有了旧项目痕迹。所以在发现方案一并不是一种好的方案的时候,我选择了方案二,即现在完成的这个方案,暂时称它为加载中心好了。

git地址:点此跳转

加载中心

加载中心主要职责就是负责加载并聚合各个项目(或者资源),甚至不仅仅可以用来聚合新老项目,也可以聚合子项目。了解过微前端的同学这时候应该很容易的就想到这其实就是一种微前端的实现方式,只是我的方案更轻量,更适合用于项目重构这一场景(请允许我不要脸的这么觉得)。在具体实现上呢,也有参考了微前端框架qiankun的实现,感觉qiankun应该是设计的比较不错的一个微前端方案,来自云使用者的肯定。简单介绍过之后,接下来就要讲一些具体的使用方法。

文件目录

-- config 配置文件夹
-- dist 打包目录
-- loaders 自定义的loader,用于自动加载config下的内容
-- src 内部实现,不需要管这里面的东西
-- index.ejs
-- index.js
-- package.json
-- webpack.config.js

使用配置

使用方法很简单,只需在config文件夹下,写入我们需要加载的项目的配置文件即可,此文件应导出一个对象,具体配置属性见config文件夹下的样例文件old.js。注释已经写的满满当当了,照着写配置就好。如果想要加载其余新的项目或资源,在config文件夹下新增一个新的配置文件即可。如果指定了root,请确保index.ejs有对应的元素。

export default {
    // 项目名称,作为当前配置项的唯一标识符,可以不指定,因为实际上用不到此配置项,更多起到注释的作用
    name: 'old',
    // 必填,指定加载的类型,为资源或者项目,枚举值:project,resource
    type: 'project',
    // 非必填,指定一个根元素,当前项目的html元素会被加载到此元素下
    root: '#oldApp',
    // 必填,加载优先级,小数字代表高优先级
    priority: 1,
    // 当type === project时必填,因为要通过发起跨域请求去获取到目标项目的主页面内容,所以需要进行一层代理
    proxyUrl: './getOldHtml/index.html',
    // 非必填,当type === resource时有效,要加载的js文件
    scripts: [],
    // 非必填,当type === resource时有效,要加载的样式文件
    styles: [],
    // 非必填,指定后,符合正则匹配的资源将不会被加载
    exceptResourceRule: /^a.js$/,
    // 非必填,指定后会为所有的资源文件(不是一个完整链接的文件地址,即不包含domain的文件)添加的前缀地址,一般来说,即为代理地址
    prefixUrl: './getOldHtml',
    // 当前项目js文件加载前钩子
    beforeLoad() {
        // ...
    },
    // 当前项目所有js文件(不包括内联的js)加载后钩子
    afterLoad() {
        // ...
    },
    // 非必填,路由规则,只有在当前路由下,才会显示root元素
    // 如果不指定,则在任何情况下都会显示root元素
    // 如果指定,则在路由匹配的情况下,才会显示root元素,不匹配的情况下会为root元素设置display:none
    routesRule: /^printBatch$/,
    // 使用沙箱模式,此配置项会将当前项目配置下的,符合匹配条件的js文件运行在一个独立的window对象下
    // 一般的,不是很建议使用,如果用到了一些全局变量以外的东西,容易产生一些不可预见的错误
    // 正在考虑是否可以维护一组属性的集合,当访问window下的某些属性在这个集合中时,访问的是真实的window对象
    useSandbox: false,
    // 符合沙箱匹配的js文件,前置配置useSandbox: true
    sandboxRule: /1.js/,
}

EventBus

在当前window下挂载两个属性:window. $SyncEventBuswindow. $AsyncEventBus,这是两个事件总线系统,区分点在于触发事件的方式为同步或异步触发。提供以下方法:

on(eventname, fn, isMulityListers: boolean): 监听某事件,第三个参数表示此事件可拥有多个监听函数

emit(eventname, param): 触发某事件

off(eventname, ...fn): 取消对某些函数的监听

once(eventname, fn): 只会触发一次的事件,触发后会将事件名移除,不允许拥有多个监听函数

offAll(eventname): 取消当前事件名下的所有监听函数

clear(): 移除所有的事件监听

关于这个事件系统,我这边的设计是建议为一种一对一的关系,即一个事件名对应一个响应函数。这样设计的初衷在于减少一些隐形bug的发生。设想这么一种情况,程序猿A在某个组件里监听了事件,事件名为event,响应函数为fnA;程序猿B在另一个组件里也监听了event,响应函数为fnB。而实际上这只是一种偶发性的命名冲突,但是却很容易发生一些隐性的bug,因为fnA和fnB都被触发了,如果这是一种不能被直观地看出来的问题,那么排除问题可能会变得有些困难。通过这种一对一的方式,将从根本上排除这种情况的发生。但是如果我需要一种一对多的关系怎么办?没关系,在事件监听的时候,传入的第三个参数为true即为一种一对多的关系。

request

加载中心在获取到目标项目的页面时,使用的是fetch,如果觉得可能会有兼容性问题,可以使用传统的Ajax替代,修改src/request文件即可。

原理简介

通过发起http请求,来获取到目标的html文件,然后将html文件里的js和css资源通过import-html-entry提取出来,然后分别在新项目中引入。对于css,就是通过创建link标签依次插入文档中。对于js文件,由于需要沙箱处理,先通过http请求获取到目标文件的字符串内容,然后通过new Function()的形式创建并执行。当需要沙箱模式时,即通过一个创建一个自执行匿名函数的形式来改变window。

(function(window) {
    // 这里插入请求到的js内容
})(newWindow)

路由控制比较简单,因为公司目前用的hash路由,暂时只支持hash模式,对匹配到的才会将root元素显示出来,匹配不到时会设置为display: none

具体的代码实现,还是建议移步github查看:点此跳转,代码量其实不是很多,注释也写的蛮多的,应该比较好理解,hhh。

简单总结

具体的重构方向呢,是先重构出基础模块(指项目基础建设,用户授权一类的东西),然后从业务量相对小的模块出发,逐步扩展到大模块。因为重构需要比较久的时间,采用此方案也不会导致业务上的内容被落下,实现业务推进与重构同步。但是,我觉得真正需要的往往不是重构,往往是一种写好代码的意识、规范。不然,没准过个一两年又变成一个巨石应用了呢?

如果觉得有帮助的话,麻烦动动小手点个赞呗。如果对于重构,你有更好的方案的话,欢迎分享。