软件开发中,构建是很重的一步,开发人员在这个过程上耗费的时间其实是一笔非常大的开销,所以许多团队都会进行一系列的定制,打造一套适合自己团队的构建工具,并且想方设法的让它“可持续发展”。

过去半年里,我们也进行了这方面的探索和尝试,目前经历了三个版本的迭代,已经应用到了几乎全部的前端业务上,大大小小几十个项目中。

出于许多原因,我们决定进行一次新的重构,在重构之前,想起了一些事情,决定记录下来。

通常为了项目的开发能够保持高效,团队一般会对技术基础设施做定期的维护更新,其中最常见的操作便是对已有项目运行环境和软件依赖进行例行的版本升级

Node 已经升级 v10 有一段时间了,版本也出到了10.3.0,npm的最近两次版本升级也带来了一些有意思的功能,暂且不表,后面再说。

升级 Node

升级Node已经不像前几年那么麻烦,如果你安装过nvm,那么只需要两条命令,node就升级完毕了。

没错,就这么简单。

更多关于Node v10的信息,可以查看这里

脚手架

下面将从经常遇到的问题历史包袱几次改版接下来的规划,来聊聊脚手架相关的事情。

升级常遇到的问题

相比较升级Node,升级脚手架就麻烦多了,因为有许多依赖的软件的升级是break change的,而且他们并不直接被你的项目依赖。

通常这个时候,我们有三个选择:

  1. 给该开源项目提交PR,写信给作者,等待合并,你编写的代码符合标准,以及你的功能理念符合主要维护者的价值观,那么功能会合并,但是很多时候,会出现维护者响应十分漫长,或者以各种理由拒绝合并,没有关系,还有其他的方案。
  2. 你fork一份该项目,进行修正,将依赖模块重命名或者添加私有scope,发布到私有仓库,如果公司的私有仓库建设完善,这个方案会是不错的选择,但是你需要定期关注原项目有何进展,考虑定期将有用的内容迁移到你的fork项目中,无形之间,造成了精力的分散,没关系,还有一个选择。
  3. 编写一个简单的补丁插件,对开源的代码进行补丁式的修改或者替换操作,不需要你提交的PR被合并你就可以享用你想用的功能,也不需要你建设或者维护私有仓库以及你fork的软件,如果你打补丁的文件被修改了,你的构建报错,这个时候,你更新一份patch就可以了,但是这个方案打补丁的软件必须晚于你的“补丁动作执行”时机,多数情况来说,是可以的,比如webpack-custom-pluginhexo-extend-plugin

来自两年前的历史包袱

公司内部项目最开始是基于webpack 1.0+定制实现的云端构建工具,该云端构建工具支持:mock模板,预览页面,按照规则生成构建文件三个主要功能。

随着公司基础设施的变化和团队融合后发布管理的变化,构建工具逐渐暴露出了一些问题:

  1. 原本项目少的时候,每个项目中都放一套构建配置,问题不大,但是随着业务发展,项目增长到几十个的时候,每个项目包含一套构建配置,后续升级维护都是问题,即使使用submodule的方案,也难逃依次到项目中执行升级操作的问题。
  2. 中心构建服务设计比较简单,但是依赖服务却不少,包含一个mongo实例,好像还有一个redis实例,如果本地想跑调试或者预览,必须把中心构建服务跑在本地,需要安装mongo等依赖,虽说有docker的帮助下,不是太大的问题,但是无形之中提高了使用门槛;并且中心机有一次硬件故障,恢复花费了相当长的时间,这个简单需求复杂依赖的问题必须解决。
  3. 项目中存放的构建脚本混乱不堪,依赖软件冗余,导致初始化安装很漫长,影响项目整体维护性和可迁移性。

这个时候,内部有几个刚性需求:

  1. 所有的项目进行前后端分离,构建工具必须能够进行有效支持,包含快速更新。
  2. 前端项目不能够再继续无节制膨胀,要按照进行合并收敛。
  3. 在人力资源有限的情况下,尽可能不去维护没必要维护的设施,比如构建机。

前端角度的构建改造

再三考虑,我们决定不在原有程序上进行打补丁式的迭代操作,考虑重新出发,做一套新的软件来进行持续的迭代发展,同时在项目中通过构建添加一些对前端、对用户有利的功能:

  1. 资源自动发布CDN,前端应用在用户端自动切换不同服务商的CDN。
  2. 前端资源使用标准模块化方案,支持热加载模块,比如调试打印模块,热替换模块进行调试。
  3. 同类型的项目按照业务线收纳到一个项目仓库里,通过目录结构来进行资源约束,支持跨项目页面打包出共有部分,以节约用户访问前端业务性能最佳。

理想很美好,现实很骨感:

  1. webpack本身不支持动态多入口的项目,如果把多个项目引入同一个仓库,那么每次添加一个项目,构建配置文件就得修改一次,不同的仓库,配置文件的差异肯定越来越大,后续难以维护是可预见的。
  2. webpack对多个entry构建出的内容分离的不是很好,使用文件添加hash的方式可以避免文件冲突,但是不能够很好的区分出不同项目的依赖,不能很好的支持构建出不同的项目,发布到不同的位置,或者对应的目录下,导致访问页面在二级目录,实际资源在一级目录的情况不可避免。
  3. webpack本身不支持多个cdn地址这种骚操作,默认的publicPath只支持写死一个地址。
  4. 多个项目构建如果放在一起,webpack预览构建的时间都是超级漫长的过程,然而在调试个别页面的时候,这个情况完全是可以避免掉的。
  5. 由于前后端分离,前端业务中如果存在一些交互开关,或者模块引用,基本上只能通过后端接口进行Restful API调用来获取了,开发阶段mock起来也很麻烦。
  6. 多个页面构建完毕,需要人工拼合预览地址,如果一个项目中有十几个以上的页面,并且还在持续不断的添加,这个过程也比较麻烦。

于是,吭哧吭哧的搞了一周多,第一个版本把这些事情基本都解决了:

  1. 从业务代码中抽离了构建工具,项目变的轻盈和灵活。
  2. 在原本webpack的基础上封装了一层,对开发者提供几个常规接口:预览、构建、部署、测试;对webpack提供动态获取入口,以及构建后处理产物的能力。
  3. 基于umd模块的封装,实现了一个loader,支持动态加载不同地址的资源,包括脚本、样式、图片等。
  4. 构建支持使用项目名称过滤,减少同时构建预览的计算量。
  5. 页面模板支持注入配置,方便开发过程的调试。
  6. 页面构建完毕会生成报告,预览构建完毕会打开一个管理页面,让你一目了然有哪些页面可以交互。

这个阶段很类似很早以前的淘宝,一条命令业务代码正式上线,剩下的就是更新页面引用资源或者在某个时机把页面彻底替换掉,比如在内网执行一条curl命令。

遵守后端规则的发布改造

好景不长,使用了一段时间,出现了一个新问题:团队合并,发布方式按照后端的模式,走全量发布。

对于业务来说,可扩展性提升以及产物可维护性提升,是十分好的事情。

但是对已有前端构建发布模式来说,存在一定的挑战,全量发布会清除掉当前构建不存在的文件内容,之前的增量内容要进行妥善保存,并逐步迁移,增加了一大堆额外工作量,既要改构建工具,又要改业务代码。

尤其针对一些运营一次性需求的页面来说,全量发布的造成了很大的麻烦;对于线上同一个业务来说,全量发布也带来了额外的发布风险。

在一段时间后,通过使用新的运营配置化方案和同业务线代码按照业务模式再次拆分,这个问题算是解决了。

再次改造

然而问题解决没有多久,又出现了两个新问题:

  • 团队再次合并,废弃了前端自己维护的Nginx服务,仅保留回源机功能,前端前后端分离的模板直接同机部署到后端服务器上。
  • 要求前端先进行CI/CD试水,前端发布流程再次动荡。

废弃Nginx服务,影响最大的并不是前端团队在实际业务中可触达的技术领域,而是把前端业务资源版本管理彻底推翻了,配套的模块加载,模块调试,动态concat全部废弃:

  • 后端业务及其是不太接受编译三方模块到后端业务nginx
  • 废弃前端nginx服务之后,线上模块化调试暂时只能通过proxy软件劫持进行

原来类似的文件版本管理模式,有兴趣可以围观。

对接CI/CD是件好事,也是趋势,相比较本地发布,有面板和审计,模块化的设计,扩展性也不错;相比较自己维护中心构建机器,也省了维护的人力成本。

但是对于刚刚“被迫重构”过一次的前端构建和发布流程来说,还没完全运行稳定就再次重构,简直就是在进行技术债务累积。

  • 原本的CDN灾备自动切换,前端模块系统方案因为时间原因,直接下掉,降级为hard code模式,和webpack window library mode。
  • 使用上的功能bugfix和两次调整调整揉在一起,代码散发出了bad smell
  • 因为公司网络问题,脚手架的依赖和项目依赖被一起打包在了docker image中,更新起来有一定的成本,导致最近新做的前端模块化方案出现脚手架的问题,更新起来一直比较麻烦。

当前和未来

半年过去了,脚手架已经修修补补迭代了三个版本,基于webpack 3.0 + babel 6.0的代码基上还残留了两个bug没有解决。

使用文章开头提到的Node v10进行项目初始化,可以看到项目已经很老了:

面对接下来小程序同构开发的需求、大概率引入的weex,是时候进行一波为了开发效率、开发体验而做的重构了。

目前的规划有这些:

  1. 将 webpack 和 bebel 进行完全升级,配套的 preset 支持从项目导入一部分。
  2. 将 多入口 构建模式改变为 动态获取单入口进行循环构建。
  3. 将三方依赖进行固化,重写一些依赖陈旧软件效率不高的软件包。
  4. 支持快速构建新的构建工具镜像,让业务使用CI构建的时候,仅使用构建工具镜像即可,取代之前约定使用的大单体镜像。

为什么呢?

  1. 越来越多的插件开始支持新版本的 webpack 和 babel,停留在老版本能享受的只有祖传bug,当然,如果业务模式没有变化,那么不升级亦可,支持挂载外部配置,并进行配置合并,可以在最少配置项的情况下对项目进行定制化操作。
  2. 多入口构建是个小众需求,webpack支持短期支持的可能行不大,不如使用传统的单入口模式进行构建,通过改进单任务构建速度来提升构建效率,构建结果的维护上需要消耗的精力还更少些,至于重复构建代码产生的效率降低的问题,使用分布式构建应该能够解决一部分。
  3. 项目能够快速顺利初始化也是开发可维护性的衡量指标之一,一个项目半年之后初始化不畅,或者只能在特定版本下进行初始化,从历史经验来看,这个项目的可持续性是打问号的,开发人员的开发效率一定不会特别高,更别谈开发体验了。
  4. 现有模式打一个镜像太漫长了,严重影响开发效率。

– To be continued