魔方是转转内部的一个可视化搭建平台,用于快速搭建一个活动页面。本次主要分享下在做环境隔离时遇到的一些问题以及解决办法。
魔方基础依赖介绍
- A提供了本地运行组件的能力以及组件需要的所有第三方依赖
- B提供了配置区的一些常用表单项,如跳转配置、展示终端配置等
- C提供了预览区的一些常用能力,如跳转、埋点上报等
- A依赖B和C,B和C又依赖A
一个魔方组件,通常只需要依赖A即可,因为在安装A的时候会自动将B & C的内容打包生成到A中。
"dependencies": {
"A": "^1.0.0"
}
为什么要做环境隔离
之前,我们在编译某个基础依赖(例如 B)时:
- 会发布一个正式包(B@1.0.1)
- 并将测试服务器上专门存放公共依赖的文件下的node_modules删除,然后执行npm install重新安装依赖
- 在安装中会使用我们最新发布的B@1.0.1。(此处实际是在一个临时文件夹中操作,然后安装后再复制出来的,可以减少测试环境的不可用时间)
举一个常见的场景来说明下这种模式的问题——小明在开发B,小红在开发C,两人都在测试环境进行了编译,导致发出去了两个正式包B@1.0.1 & C@1.0.1 。那么此时,如果小明开发测试完了,想要上个线,那么在服务器上执行到npm i的 时候,就会把小红还未测试完成的包C@1.0.1给安装到线上环境去。(其实在测试服务器上两个人的代码也是混合在一起的,不过毕竟是测试环境,影响较小)
从这个场景分析,可以发现有两个主要的问题:
- 测试环境发布正式包,导致线上无法区分
- npm install正常只会安装正式包,不会安装beta包
设计思路
针对上述的两个问题,对应的解决办法就是:
- 测试环境发beta包,线上发正式包
- 使用npm install package@version(-beta) 替换npm install
第一点就不说了,大致就是先npm view packageName versions获取包的所有版本,然后根据环境去获取最新的正式包版本或者是最新的beta版本,然后修改版本号再发包。
第二点,在更新依赖的时候,通过指定版本号的形式去安装我们最新发布的包。只不过在线上环境中,安装的是正式包,测试环境中安装的beta包。
看起来一切是那么的美好,但现实并不总是一帆风顺......
问题复现&解决
初始化一个文件temp,并执行npm i先将所有依赖装一遍。
此时temp/node_modules下的情况为(此处只举例A和B,C与B情况相同,不再重复)
A: "A@1.0.0" A/node_modules下:无其他依赖
B: "B@1.0.0" B/node_modules下:无其他依赖
接下来,执行 npm i B@1.0.0-beta.1去单独更新B。
执行结果:
A: "A@1.0.0" A/node_modules下:B@1.0.0
B: "B@1.0.0-beta.1" B/node_modules下:无其他依赖
根据npm包安装的机制,默认情况下是不会使用beta包的,A依赖的B: "^1.0.0"需要使用稳定的版本,所以beta版本被放在最外层,而将之前的B@1.0.0放在了A/node_modules下。
魔方的基础依赖在使用前会在 A下执行一个externals命令(postinstall:npm run externals)将B的内容打成dist放在A目录下。但是node_modules依赖的查找顺序是先从当前文件目录下查找的,所以生成dist文件时使用的将会是A/node_modules/下的B@1.0.0,而不是最外层的B@1.0.0-beta.1
所以,我需要手动删除A/node_modules/B@1.0.0,再去执行externals命令。
那接下来我们再试试在此基础上更新A,npm i A@1.0.0-beta.1。
结果就是出现了更多冗余的依赖。。。
A: "A@1.0.0-beta.1" A/node_modules下:B@1.0.0、A@1.0.0
B: "B@1.0.0-beta.1" B/node_modules下:B@1.0.0、A@1.0.0
原因跟之前一样,我们安装的beta版本的A不符合B所依赖的A: "^1.0.0",就导致B下的node_modules中又多了一个A@1.0.0,然后这个A@1.0.0的又依赖一个稳定版本的B,所以在同级目录下还会再多一个B@1.0.0
同样的,我们仍需要先手动的去删除这些冗余的、不符合我们要求的依赖。
综上,为了确保我们项目中使用的都是我们刚发布的beta包,我们需要在每一次更新依赖时都执行一下这两条命令去清除冗余的依赖,然后再去执行打包命令。
rm -rf ./node_modules/A/node_modules/
rm -rf ./node_modules/B/node_modules/
rm -rf ./node_modules/C/node_modules/
cd node_modules/A
npm run externals
然而,到这一步还没完事,我将代码部署到测试服务器上后,经常出现依赖没有安装完成或安装完没有生成dist文件的情况,总是执行到一半就“中断”了。但是我在本地测试的时候却不会出现这种问题。
经过一步一步的排查,最终将问题定位到了这一行代码
await shelljs.shellExec()
查看shelljs.shellExec方法:
exports.shellExec = function (command, options = {}) {
return new Promise((resolve, reject) => {
Object.assign(options, {timeout: 300000});
shell.exec(command, options, () => {});
});
};
经常中断,难道是过了超时时间?我试着将超时时间从5min改到10min,部署至测试服务器,再次更新依赖,一切正常了......(不得不吐槽这个测试服务器的性能甚至不如我的Mac)
再一看编译时间,耗时>10min,!真棒。(取反😡)
项目名 | 编译耗时 |
A | 10:08 |
B | 10:38 |
优化
如此低效率的更新显然不能让人满意,而且由于魔方自身的原因,当编译基础依赖时,其他人是不能再部署其他魔方服务的,这就会阻塞其他人的流程,对开发人员的体验是十分差的。
「首先就是先分析问题找出原因:」
- 很容易想到的,当安装beta版本的依赖时,总是会额外产生很多冗余的稳定版本的依赖。
- 安装依赖耗时么?耗时,但是至于这么耗时么?不至于。耗时中的大头另有其因,其实就是A中的externals命令,打包,这个是比较耗费时间的通常需要1-2分钟。
所以在一定程度上是问题1导致了问题2——安装了冗余的依赖,其中冗余的A会自动执行externals 命令导致耗时过久。
「所以我们首先需要解决的就是避免安装冗余的依赖:」
A需要依赖B,是因为需要将B的内容打包生成到A下使用。
B需要依赖A只是因为本地开发时方便调试。
如果去掉B的依赖项,那就可以在安装B@beta时避免额外安装A。但是,本地开发B的场景还是很多的,因此需要想个办法尽可能的减少由此带来的对开发体验的影响。
于是我在脚本中加入了一个自动检查并安装依赖的命令depcheck。
// dev之前先检查A:
// list可以列出当前工程下的A情况以及版本
// 如果没有会返回一个假值并走到npm i命令去安装A
"predev": "npm list A || npm i --no-save A"
这样,在安装B@beta的时候就不会额外安装A也不会额外执行externals命令了。
那么在安装A@beta的时候呢?还是会额外安装冗余的B然后自动执行externals,然后删除冗余的B,再手动执行一次externals。
「接下来需要针对A再次进行优化:」
由于A是强依赖B的所以不能去掉依赖项,冗余的B肯定是避免不了的,不过这又有什么关系呢,安装再删除一共也影响不了几秒钟。
但重点是A中有这样一个脚本命令:postinstall:npm run externals该命令是为了在开发时安装依赖等场景可以自动执行externals以减少操作次数&降低学习成本。
这就会导致第一次执行externals的时候实际使用的是冗余的稳定版本B,而非我们需要的最外层的B@beta,所以还需要删掉冗余依赖然后额外执行一次externals,这才是最耗时的部分。
“要是在npm i的时候可以不执行postinstall就好了”,带着这个期许,我找到了一个好用的参数——--ignore-scripts(忽略依赖中的脚本命令,不去执行任何脚本)
npm i A@beta --ignore-scripts
这样一来,在安装A的时候,也不会额外执行externals命令了!
「优化前后编译耗时对比:」
项目名 | 优化前编译耗时 | 优化后编译耗时 |
A | 10:08 | 04:42 |
B | 10:38 | 04:17 |
总结
以上,就是在对魔方基础依赖环境隔离改造的思路和问题的解决:
- 通过发布beta版本的包来区分测试环境与线上环境
- 但是带来了编译速度严重下降的问题
- 通过去掉B中的依赖项来避免安装冗余的依赖
- 针对A,在npm i的使用加入--ignore-scripts命令来避免额外执行打包命令externals