携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
概述
公司有个工具型的项目,由最开始 PC 网页版,一共延伸出 9 个端(后续还需要再拓展两个手机移动端),感觉现阶段的项目框架设计不够“优雅”,甚至有点臃肿。这两天见缝插针,自己调研了一下优化方案,尝试优化优化,但可能因为原始项目已经比较旧(很多依赖库都是很早期的版本),各种依赖、运行构建细节都报一堆莫名其妙的错误,暂时没有解决,只能战略性挂起。下文记录了这次优化尝试的细节,作为这次优化的归档,后续再找机会深入深入。
写着写着发现文章稍微有点长,如果想直接了解 pnpm 和其 workspaces 的基础使用方法,可以直接看下文 “Demo 分析” 一节;如果想了解整个事情的由来、构想方案、工具调研的话,还是要结合行文三个小结按顺序食用 O(∩_∩)O哈哈~
原始需求
现状
上文提到,这个工具型项目一共延伸出 9 个端(如果算上每个端都有 4 种环境的话,是不是应该算 36 套才对呢?(#^.^#)
),目前的项目设计如下:
PS:为了让行文更好表达和理解,以下会罗列出一些必要的变量和截图,但同时为了保护公司项目隐私,以下变量、截图都改了名字,跟公司项目不同,如后续同事希望通过这篇文章去了解项目的话可以把改了的变量名字代号入座,转换转换。
- 使用
MY_GUI
全局变量标识当前是哪个端web
:PC 网页版student-pc
:学生 PC Electron 客户端student-pad
:学生平板客户端live-student-pc
:学生 PC Electron 客户端 - 直播版live-student-pad
:学生平板客户端 - 直播版teacher-pc
:老师 PC Electron 客户端player
:H5 播放展示版exam
:考试系统版lesson
:录播课版
- 使用
MXC_ENV
全局变量标识当前是哪个环境dev
:本地开发环境test
:线上测试环境uat
:线上预发布环境prod
:线上生产环境
- 对于
models
(我们用的是 dva 管理数据流)、services
、components
,都是分两种情况,一种是各端共用,一种是端内定制,如下图:services
、components
都是这样的做法,就不一一展示了。这三种文件夹里面都是根据目录结构划分公共的、端私有的代码
可能一眼看上去会有两种感觉:
- 内容好像很多的样子
- 虽然有很多端,而且后续也会再扩展,但最大多数代码都是多端共用的(可能占 80%以上),端内定制的相对少( 20% 左右)
- 做过外包的朋友可能会有种似曾相识的感觉。打个比方,外包公司一般都会有一套“模板”电商代码,然后基于一套“模板”,根据不同客户的不同需求,会延伸出客户A版本、客户B版本、客户C版本。。。这个场景就跟我现在这个项目很类似
- 怎么?有问题?看上去挺整齐划一的
- 在目前这个版本之前其实还有一个更早的版本,我接手的时候比现在看到的更乱:
- 公用的没有抽离,每个端都 copy 一套代码,然后在 copy 的基础上添加各端的定制内容,导致出现一大堆看起来差不多,但转角就遇到一个
if-else
的惊悚代码~ - 没有统一的环境变量,靠 url 上传参数来识别当前是属于哪个端(所有代码杂糅在一起,遇事就
if-else
) - 接手后把所有内容都全面梳理一遍才有目前看到的效果
- 公用的没有抽离,每个端都 copy 一套代码,然后在 copy 的基础上添加各端的定制内容,导致出现一大堆看起来差不多,但转角就遇到一个
- 虽然现在看起来整齐很多,但仅局限于小团队的整齐,一旦把时间拉长,后续这个项目加入更多人,感觉很容易会失控
- 现在的整齐是基于“强约束”的整齐,在提前设计好的规范下安排好哪些内容应该是公共的、哪些内容是端内定制的
- 但,一旦拉长时间维度,后续接手的人舍弃这种强约束的话,很容易就会回到当前的情况,各种代码 copy,各种
if-else
满天飞
- 在目前这个版本之前其实还有一个更早的版本,我接手的时候比现在看到的更乱:
优化方案构想模型
上面第一张图是目前的现状,如果俯瞰整个项目的话,会先看到的是一个整体,然后在里面再分出各个端的内容,最后通过 webpack 打包多页面的方法,把各端自己的内容串起来,打包出各端的内容。粗略一点理解的话,可以理解成上面 9 各端,最后会打包出 9 个 html 入口文件(现实操作不是这样弄,但可以象征性这样理解)。
第二张图是构想模型,借助微前端、npm 库的思路,俯瞰项目的话,最先看到的不是一个整体,而是单独的各个端(可以理解成各个端就是一个项目),然后在项目中按需引入公共库里面的内容(神似我们平时加载各种第三方工具库,每个库都解决一定领域的问题),然后再各个端自己打包出自己的内容出来。
为什么要这样翻转?
- 前面提到,目前这个版本的“整齐”是基于“强约束”才有的效果,想象一下,如果基于第一张图,拉长时间维度,加入更多的人进来开发,突破强约束后,后面新人俯瞰整个项目的时候可能就是一个乱糟糟的“屎山”。
- 如果是基于第二张图,“强约束”的要求更多是公共库,各端不太需要有太大的“强约束”,只要我们确保公共库处于“强约束”状态,一个端就算再乱,也不会影响到其它端。新人接手的时候,可能只需要关注某几个端,或者只关注一个端,虽然慢慢上手,再去看公共库,这样对整体而言可能就不会有一上来就觉得很乱的感觉。(个人构想,接受反驳和讨论,碰撞更好的思路)
工具调研
npm 包
基于上述的构想模型,最容易想到的就是把公共部分打包成一个 npm 包,放在私有 npm 仓库中,各端项目中 npm install
然后直接使用。
理论上来说这个方法可以实现上述构想模型,但实际使用的话会有以下问题:
- 公共库的代码有可能经常需要修改,一旦改了,可能 9 各端都要同时更新(后续开发时,可能自己都会默默来上一句:“我真的会栓Q~”)
- 9 各端就要建 10 个项目(公共库 + 9 个端),想想都可怕
微前端——EMP
微前端方向感觉 YY 的 EMP 相对比较合适,对于阿里的 qiankun 虽然感觉也有一点点符合原始需求场景和上述构想模型,但总感觉“味道”不对~
关于 EMP 的设计以及应用,可以参考 YY 团队的这几篇文章:
跟我们上述构想模型非常贴合,而且也能解决上述“npm 包”中提及到的问题
但也存在一个没法逾越的鸿沟——杀鸡用牛刀:
- 为了实现构想模型,引入一个更大的库,上手成本指数上升
- 可能只用到 EMP 10% 的内容,但为此引入了一整个生态(还没深入研究 EMP,纯属直观感受)
lerna
lerna 是一个管理包含多个软件包(package)的工具,用于更方便实现 Monorepo ( monorepo
和 multirepo
的含义可以参考这篇:Monorepo简介与建设实践)
这么一看,Monorepo 的思考跟上述构想模型也非常贴合
再深入研究一下,发现 lerna 中实现 Monorepo 的部分好像就是 Bootstrap,而据江湖传闻,早期 lerna 中实现 Monorepo 的部分不太好用(不知道目前 lerna 最新的 V5 版本是不是也这样,有了解的朋友可以补充补充,Thanks♪(・ω・)ノ),大多数大佬用的话基本都会把 Bootstrap 换成 yarn 的 workspaces,把整个 Monorepo 托管给 yarn 实现,而且他们还很搭
详细关于 lerna 以及搭配 yarn 的 workspaces可以见:
- Monorepo最佳实践之Yarn Workspaces
- 【译】配置 Monorepo 的几种工具 lerna、npm、yarn 及其性能对比
- PS:比较推荐这篇译文,详细解释了 lerna、npm、yarn 以及 workspaces 的关系和前世今生
综合来看,如果想要实现上述构想模型,实现 Monorepo 基本上就能符合,lerna 除了实现 Monorepo 之外,还有很多很强大的功能,比如:发布多个 npm 包、版本管理之类的,但我们的需求场景并不需要这些内容,所有基本把范围锁定在 yarn 的 workspaces
pnpm的workspaces
搜着搜着,发现 pnpm 也有 workspaces,而且据说 pnpm 更牛,既然如此,果断上车~
上车入坑指南可见以下两位大佬的文章:
- 都2022年了,pnpm快到碗里来!
- One For All:基于pnpm + lerna + typescript的最佳项目实践 - 理论篇
- [译]用 PNPM Workspaces 替换 Lerna + Yarn
好,锁定就开干!
Demo 分析
详细源码可见:demo-pnpm-workspaces
从头搭建也很简单:
- 按官方步骤安装好 pnpm
- 新建好文件夹后,直接在根目录执行
pnpm init
初始化项目 - 创建 pnpm-workspace.yaml、.npmrc
- pnpm-workspace.yaml 是开启 workspaces 的配置文件
- .npmrc 用来配置信息,
engine-strict=true
结合根目录的 package.json 中的engines
字段,可以指定运行的 node 版和 pnpm 版本
- 创建好 packages 文件夹中的内容,并进入各自的目录执行
pnpm init
进行初始化components
目录模拟公共组件models
目录模拟公共 modelsweb
目录模拟 web 端项目
- 在跟目录下执行
pnpm add @pn/components @pn/models -r --filter @pn/web
把 components 和 models 引入到 web 中,这样在 web 里面就可以像 import 第三份库那样直接使用- 可能有朋友会问,packages 目录下为什么每个包的包名都要加一个
@pn/
前缀。实际上这个只是为了区分其它“真正的”第三方库,纯粹是为了打开node_modules
目录时方便查找(反正也不用真发布到 npm 仓库上) pnpm add @pn/components @pn/models -r --filter @pn/web
这个命令有几个细节:add
命令可以参考:pnpm add-r
参数可以参考:pnpm -r, --recursive--filter
参数可以参考:过滤- 另外,如果直接进入到
packages/web
目录下,可以直接执行pnpm add @pn/components @pn/models
不用带-r
和--filter
参数,效果也是一样的
- 补充好 start 的命令即可以了,详细可以看 package.json 和 web-package.json 两份 package.json 文件的
scripts
字段的start
命令
另外,如果想把项目放在 apps 目录下,把公共库放在 packages 目录下,可以直接在 pnpm-workspace.yaml
增加一行 - 'apps/*'
然后在根目录执行:
pnpm add @pn/components @pn/models -r --filter @pn/exam
pnpm run start:exam
复制代码
也可以实现类似的效果,这样可能看起来会更整洁
总结
虽然最终没能在真正的项目里用上 pnpm 和 workspaces,但也积累了一点经验,后续再见缝插针安排时间看能不能突破实践中遇到的问题。
软件开发没有银弹,各端、各领域都在“不满”中逐步探索下一阶段的“更优解”!