根据业务背景,通天塔搭建系统需要独立部署并同时维护多个站点,并且在多个站点间需要处理各功能模块同异点的开发、更新、维护问题。依据业务实际情况,通天塔搭建系统因在面向国内主站等不同站点下对运营的深度诉求存在不同的定制及优化,考虑到历史代码、保持主站稳定性、减少项目代码复杂度等因素,对比在一个仓库内构建多套系统,团队决定基于主站版本剥离出一份精简抽象的通用版,使用通用版赋能国际化、商业化等集团业务。因此也引出了需要解决的2个问题,即多项目之间的功能模块复用和差异化配置。
前端领域处理通用模块一般有以下三种思路:
暴力:直接复制粘贴在多个项目中;
温柔:将通用代码封装成npm包,以依赖的形式注入在各个项目;
优雅:使用Bit等高阶模块管理工具。
在权衡之下,采用 Git Submodule(类 Bit 管理)+ Workspace + Npm包的方案,是不是听起来很麻烦,别急,下面我们来一一拆解。
Step 1 Git Submodule改造
在改造开始之前,我们的需求是:对原项目入侵程度最小、不需要很高的学习成本、使用成熟且稳定的API。因此初步决定在以下技术方案中进行选择:
1) single-SPA等微前端方案:
专注于混合技术栈的多个独立项目部署管理。但对模块甚至组件等细颗粒度支持力度不足。而在本次改造中,我们需要完成组件级别的复用;
2) Lerna:
Monorepo的集大成者,非常适合同时管理多个组件库、工具库;但需要学习lerna的语法,偏重多npm项目且互相引用较多的单一仓库项目。对于不需要发布多个npm只需要依赖管理的项目而言,lerna可以由workspace取代。
3) Webpack5的模块联邦:
依赖Webpack5,对老项目的改造工程量和不可控性太多,因此本次改造不做考虑。
4) Git Submodule:
Git Submodule是 Git早在2008年便提出的概念,它可以在一个Git仓库中,将其他Git仓库以子文件夹的形式进行存放,这一模式也被称为子模块开发模式。子模块里编写的代码,均可以像在原项目中书写一样进行使用;对于workspace的改造,对于代码开发其实是无感知的,并且有助于更好的管理子模块与整个项目的依赖。
综合实验开发的结果,团队决定采用改造成本更低,同时更适合不需要封装npm模块的Git Submoule作为架构升级方案。并在原项目的基础上,将可封装为“模块”的代码进行抽离,例如下图2-1中红框标注出来的内容:
将这些部分抽离到一个新的仓库后,我们就可以把这部分作为子模块“植入”到项目中:
添加子模块到项目根目录下
local-path] git submodule add [remote-url] [
其中[remote-url]需要替换为远程仓库的链接,[local-path]需要替换为本地存放该子模块的地址,一般建议将子模块直接存放在根目录下,便于查找与开发。需要注意的是,[local-path]也会作为子模块名称存放在Git配置中,如果你填写内容为src/client/submodule,那么其在Git注册表中的默认名称[submodule-name]也为此输入。
在完成以上命令后,就可以在目录中找到已注册的子模块,push到远端后也会界面上体现出来。如图2-2所示:
图2-2 注册的从库会以“名称@commit id”的形式标注
每当开发者修改完从库中的代码并推送到远端后,其他项目只需要执行一下git pull就可以完成代码更新了,下图2-3和2-4分别为使用npm包和git submodule的开发流程:
图2-3 使用npm包的开发流
改造完成后,其他组员只需要执行以下命令即可更新子模块的开发模式:
当项目中有.gitsubmodule文件时,执行此命令会初始化子模块到远程的链接
git submodule init
将指定commit id的子模块的远程代码拉取到本地
git submodule update --remote
同样需要注意的是,此时拉取的从库是不关联远程分支的,而是指定的Git版本号,需要手动checkout到所需的分支上。如图2-5所示:
如果想要删除从库,不能只单纯删除文件夹,需要执行以下步骤进行子模块的“卸载”:
在Git配置中卸载从库配置
git submodule deinit [submodule-name]
在Git缓存中删除从库cache
git rm --cached [submodule-name]
此时,目录中仅保留从库文件夹,需手动移除
rm -rf [submodule-name]
如果需要修改从库在本地文件夹的地址,则需要“删除”后重新“初始化”。更多的操作请参见Git官方文档——子模块。
在完成了上述操作后,Step 1就算完成了,加入了Git Submodule后,第一印象是:不适应。习惯于使用npm install和git push的我们,在切换开发模式后,经常会出现各种提交冲突,这也是子模块开发的缺点:
有一定上手成本;
项目执行git push前,需要保证本地子模块的变更均已提交(可以借助vscode或webstorm的插件解决);
对发版有一定挑战,如果没有接入CI/CD,需要保证发版同学本地仓库中的子模块为最新版本。
但是,经过社区开源的插件及内部自研插件的辅助下,以上问题都迎刃而解。Git Submodule的优势也一步步凸显出来:
迅速的代码移植,使得代码可以在多个项目中保持高复用性;
可依赖项目构建,摆脱打包配置的困扰。
Step 2 Workspace改造
在本次的商业化改造中,我们迁移了ui库以及module通用业务模块库,并将其作为子模块存放于项目中。以ui库为例,原先其是以npm包的形式安装在项目中的,同时其他子模块都有各自所需的依赖,我们因此选择使用yarn workspace的开发模式。改造也是十分的简单,只需在package.json中添加一个workspaces字段,并指定文件夹,如下命令,我们将submodule-a、submodule-b加入工作区:
{
"scripts": {...},
// 添加workspaces字段
"workspaces": ["submodule-a","submodule-b"],
"dependencies": {...}
}
然后我们卸载各个子模块以及根目录下的node_modules,并使用yarn install重新安装我们的依赖。那么,使用完workspace之后的好处有哪些呢?
依赖安装体积减小
我们会发现安装过后,相同版本的依赖被提升到了根目录下的node_modules,比如@babel、react等,而版本号不相同的依赖,比如eslint、@commitlint等,仍然会按照各自配置的版本号安装到各个项目的相对根路径下的node_modules中,如图2-6所示:
子模块可作为npm包的形式进行引入
在项目改造之前,ui库是以npm包的形式存在于项目中的,而在项目中,一般我们以:
import UI from '@jmfe/babel-ui';
这种形式来引入样式组件,在迁移为子模块之后,我们可以动态读取ui库的变化(如果对ui库的构建进行watch操作),而这一操作不再需要繁琐的npm link或者yarn link了。
注意:npm仅在7.x以上版本才支持workspace的模式,该版本仍未包含在Node.js LTS发行版中,目前Node.js的长期支持版为14.16.0,仅支持到npm 6.x版本,所以我们选择较为稳定的yarn。同时,因为少了npm install这样的操作,在项目中的版本号需要手动进行修改。
Step 3 npm包
在Step 2中,我们将原有的npm包--ui库作为工作区的一部分,这样避免了组件开发依赖link的问题。但我们仍保留了原有的引入格式与打包模式,因此在开发结束,ui库仍可以执行npm publish将构建好的版本以npm包的形式推送到远程。这样也可避免在其他未改造但已引入ui库的项目中,修改引入形式的操作。
除了针对代码复用的改造,为了实现站点差异化还需要在项目中维护多个站点的配置化,并将差异精确到按钮显隐、文字颜色这类级别。同时,我们也针对package.json的scripts进行了优化,帮助团队更好的管理与配置开发流。
1)站点配置
为每个站点维护一套config.js,在项目构建时,根据环境变量process.env来判断构建时需要读取的站点配置。同时,需要满足不同的引入格式:
静态引入:用于普通JS文件,以import直接使用配置项,例如webpack构建时所需要参数、server端启动时所需要的站点域名配置等。
动态引入:用于组件,由于技术栈为React,需要支持class和hooks两种语法。同时,我们将这些配置注入状态管理机中,可以在线动态修改配置项,以实现不同的展示。
2) 站点换肤
目前主流的换肤方案主要有以下几种:
由于原项目已经使用了预处理器,在改造过程中,我们可以使用webpack中的stylus-loader预设全局变量,然后在项目中根据变量动态修改主题色。在此基础上,针对ui库的打包,分别打包了预处理器编译后的版本dist(输出css文件),以及未处理的版本lib(输出stylus文件),并在此项目中引入lib包版本。
接下来修改webpack配置:
{
test: /\.styl?$/,
use: [
'style-loader',
{
loader: 'css-loader',
},
{
loader: 'stylus-loader',
options: {
stylusOptions: {
define: {
$site: process.env.SITE_ENV,
},
paths: [
// 使用 @import '~' 中 ~ 的查找路径
path.resolve(__dirname, '..', 'node_modules'),
path.resolve(__dirname, '..', 'src', 'client'),
],
import: [
// 变量注入:
// 由于lib版本babel-ui未显式引用内部变量文件
// 因此读取此处注入的变量文件
path.join(__dirname, '..', 'src', 'client', 'styles', 'variables.styl')
]}}}]}
因此在项目中的变量文件会最先进行加载,当引用*.styl文件时,遇到变量会优先在该文件中进行寻找替换:
图2-7 在webpack loader中修改样式解析配置
在后期的维护中,只需要修改项目中variables.styl一份文件即可。
在这次通天塔的通用版多站点架构升级过程中,团队尝试过新技术,例如Css in Js的样式隔离方案,也体验并实践了陌生但很稳定的Git Submodule方案。在该架构下,我们可以轻松地将一个已开发好的模块迁移到任何一个站点。以一个需要单人开发5天的需求为例,按照原有架构,我们迁移到某一个站点的成本约为3天,而使用通用版架构,由于自身的模块化开发规范与特性,其迁移成本约为1天。由此,我们可以得到如图3-1的时间对比:
因此,在可预见的范围内,通用版的架构将极大提升人效比,对于通天塔现有的5个站点模块复用而言,这能使我们缩减约50%的开发时间。在站点不断拓展的未来,提升的收益将更加明显。除此之外,新版架构能让我们在保证用户良好体验的同时,释放更多资源沉淀优质模块,并致力于更好的开发流程,以及项目更好的可维护性。
目前,基于通用版构建的通天塔系统已陆续完成开发并上线了部分站点的搭建服务。
这些只是架构改造刚刚迈出的第一步,在商业化通用版的开发过程中,还有站点、子模块可视化管理等等可以优化的方面。通天塔技术团队也会持续进行优化升级,以迎接更多更丰富的挑战。
编辑作者:通天塔大前端团队 曾淦
[1]Bit, https://bit.dev/
[2]Git submodule, https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%AD%90%E6%A8%A1%E5%9D%97