草庐IT

魔方基础依赖环境隔离实践

杨双星 2024-02-07 原文

魔方是转转内部的一个可视化搭建平台,用于快速搭建一个活动页面。本次主要分享下在做环境隔离时遇到的一些问题以及解决办法。

魔方基础依赖介绍

  • 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给安装到线上环境去。(其实在测试服务器上两个人的代码也是混合在一起的,不过毕竟是测试环境,影响较小)

从这个场景分析,可以发现有两个主要的问题:

  1. 测试环境发布正式包,导致线上无法区分
  2. npm install正常只会安装正式包,不会安装beta包

设计思路

针对上述的两个问题,对应的解决办法就是:

  1. 测试环境发beta包,线上发正式包
  2. 使用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

优化

如此低效率的更新显然不能让人满意,而且由于魔方自身的原因,当编译基础依赖时,其他人是不能再部署其他魔方服务的,这就会阻塞其他人的流程,对开发人员的体验是十分差的。

「首先就是先分析问题找出原因:」

  1. 很容易想到的,当安装beta版本的依赖时,总是会额外产生很多冗余的稳定版本的依赖。
  2. 安装依赖耗时么?耗时,但是至于这么耗时么?不至于。耗时中的大头另有其因,其实就是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

总结

以上,就是在对魔方基础依赖环境隔离改造的思路和问题的解决:

  1. 通过发布beta版本的包来区分测试环境与线上环境
  2. 但是带来了编译速度严重下降的问题
  3. 通过去掉B中的依赖项来避免安装冗余的依赖
  4. 针对A,在npm i的使用加入--ignore-scripts命令来避免额外执行打包命令externals

有关魔方基础依赖环境隔离实践的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby-on-rails - 在 ruby​​ .gemspec 文件中,如何指定依赖项的多个版本? - 2

    我正在尝试修改当前依赖于定义为activeresource的gem:s.add_dependency"activeresource","~>3.0"为了让gem与Rails4一起工作,我需要扩展依赖关系以与activeresource的版本3或4一起工作。我不想简单地添加以下内容,因为它可能会在以后引起问题:s.add_dependency"activeresource",">=3.0"有没有办法指定可接受版本的列表?~>3.0还是~>4.0? 最佳答案 根据thedocumentation,如果你想要3到4之间的所有版本,你可以这

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. ruby-on-rails - 在 Rails 开发环境中为 .ogv 文件设置 Mime 类型 - 2

    我正在玩HTML5视频并且在ERB中有以下片段:mp4视频从在我的开发环境中运行的服务器很好地流式传输到chrome。然而firefox显示带有海报图像的视频播放器,但带有一个大X。问题似乎是mongrel不确定ogv扩展的mime类型,并且只返回text/plain,如curl所示:$curl-Ihttp://0.0.0.0:3000/pr6.ogvHTTP/1.1200OKConnection:closeDate:Mon,19Apr201012:33:50GMTLast-Modified:Sun,18Apr201012:46:07GMTContent-Type:text/plain

  5. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  6. Vscode+Cmake配置并运行opencv环境(Windows和Ubuntu大同小异) - 2

    之前在培训新生的时候,windows环境下配置opencv环境一直教的都是网上主流的vsstudio配置属性表,但是这个似乎对新生来说难度略高(虽然个人觉得完全是他们自己的问题),加之暑假之后对cmake实在是爱不释手,且这样配置确实十分简单(其实都不需要配置),故斗胆妄言vscode下配置CV之法。其实极为简单,图比较多所以很长。如果你看此文还配不好,你应该思考一下是不是自己的问题。闲话少说,直接开始。0.CMkae简介有的人到大二了都不知道cmake是什么,我不说是谁。CMake是一个开源免费并且跨平台的构建工具,可以用简单的语句来描述所有平台的编译过程。它能够根据当前所在平台输出对应的m

  7. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  8. 软件测试基础 - 2

    Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功

  9. ES基础入门 - 2

    ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear

  10. ruby-on-rails - ruby gem如何在rails环境下工作 - 2

    我试图在rails中了解rubygems是如何变得可以自动使用的,而不是在使用required的文件中gem? 最佳答案 这是通过bundler/setup完成的:http://bundler.io/v1.3/bundler_setup.html.它在您的config/boot.rb文件中是必需的。简而言之,它首先将环境变量设置为指向您的Gemfile:ENV['BUNDLE_GEMFILE']||=File.expand_path('../../Gemfile',__FILE__)然后它通过要求bundler/setup将所有ge

随机推荐