经验 | 支付宝前端构建工具的发展和未来的选择

2022-06-29 17:21:59 浏览数 (1)

对 spm 历史不感兴趣的同学可以直接从 ant tool 段落读起

下文说说我理解的支付宝前端构建工具发展史,从 spm 到 ant tool,再到未来我们可能会走的路。

spm1 spm2

在谈及 spm1 spm2 时,我们不得不回过头去看当时的历史背景,时间大概是 2012 年左右,当时前端模块化非常火热,伴随模块化的浪潮,模块加载器就不约而同成成为不得不做的命题。所以那会儿出现了 seajs 等一系列的模块加载器。所以起初 spm 的定位是 sea.js 配套的打包工具。但是新的问题又来了,模块化进程其实非常快,但是这些模块要何去何从呢,由于当时 npm 并不接受浏览器的包发布在其上,所以 spm 源服务器就应运而生了,现在骂声很多,但是那个时候源服务器的产生是有其历史价值的。

所以那会儿 spm 演变为一个前端组件包管理器,和 npm 托管 node 包一样,它实际管理着各类 module 的生命周期。所以那会儿它不包含实际的构建功能,而具体的构建功能,当时是写了扩展交由 gulp,或者衍生的 spm-build 等处理。

从这演变可以得到的结论是,spm 那会儿更看重的是遵从 CMD 规范的模块生态圈。但是比较可惜的是,没过多久 npm 开始接受了浏览器的包,并且 CommonJS 规范也越来越得到公众的认可,npm 的活跃度、 CommonJS 的广泛度、和 seajs 之间的复杂度一度让 spm 淹没在吐槽声中。这时 spm 3 应运而生。

spm3

spm3 应该是 一个 all in one 的大跨步,它涵盖了浏览器模块生命周期,包括初始化、本地化调试、文档、发布、单元测试、构建、源服务等功能。spm 3 解决了很多以往 seajs 项目中的构建问题

但是在我看来那会儿最重要的事情是 编码书写规范从 CMD 规范全面转向 CommonJS。可以窥探出的是 CMD 真在逐步退出历史舞台。拥抱社区的进程真在一步一步的推进。

但是那会儿 spm 和 seajs 还是存在着屡不清的关系。

工具在业务内的复杂度开始初步显现,spm 2 和 spm 3 共存一度让大家头疼,我认为这也是工具收敛最初的来源。

spm3.4 - spm3.6

在这个系列的版本进程中,最重要的事情是撇开了 seajs 这个历史包袱,把 sea.js 的功能合并进入 spm-sea,构建工具开始全面拥抱社区的解决方案。

但 all in one 的配置方式,也把维护人员带入到了另一个深渊,因为配置会存在互斥性,同时配置达到一定量级和复杂度后,要想要新增一个配置,或者某个配置会引发群体效应时,我们都不敢动了,即使有严格的用例。

随着业务项目的复杂度的提升,性能这个词开始被广大的开发者所注重,从最原先几秒的构建时间上升到了分钟,甚至几十分钟。这些点都是我们那会儿无法预计的。

但也就那时 15 年初,webpack 开始进入大家的视线,一时间所有开发人员都对 webpack 宠宠欲动,但 webpack 高度的学习成本,函数式的配置方式,也让大家望而却步,但不乏开发同学对其尝鲜。

所以那会儿出现了 spm2 spm3 spm3.4 spm3.6 spm-sea 。

spm3.6.x ~

基于 webpack 的大火,和其优异的生态圈,spm 3.6.x 的 build 核心变更为了 webpack,但依旧提供配置式的方式来介入具体的构建过程。我也在这个时间段开始进入 spm 的维护。

受益于 webpack 天然生态,我们在各方面得到了一劳永逸的效果。然后好景并没有很长,由于 react 生态圈的兴起,大量优异的模块在社区涌动,而众所周知, spm 其实绑定了 spmjs.io 这个模块生态圈,所以开源生态和闭源生态之间的矛盾越发的开始变得激烈。

无奈之下,spmjs.io 源服务开始能同步社区的模块到其生态圈,在这个过程中,虽然放缓了矛盾,但是源服务器因此而频频出现故障也让我们苦恼不已。进而源稳定性越来越成为其中的一个话题。

所以后续才有了放弃 spm 源进驻 npm 源的一系列事情。

spm 源进驻 npm 源这是一个看似简单的命题,就是把 spm 上所有的包全部在 npm 上发布一遍。 然后我们却花了大量的精力,1. 首先我们必须把所有包所有版本全部需要发布一次;2. 包需要做内外网的隔离;3. 包存在同名情况;4. 需要重写原有包的部分内容,但之后如何同步给包所对应的仓库,因为有些模块并不存在实际仓库 等等。

当然所有问题都会被解决,最终我们顺利迁移 2000 模块,上万个版本,spmjs.io 源服务器也如期下线。

在以上进程中,作为开发者,我最大的感触是,想要去维护好自己的一个生态圈是一件多么难的事情,特别是在通用领域上,比如构建、调试、源等和社区保持好良好关系的重要性。当然每个时期都会有一定的局限性,所以大家都是在跌跌撞撞中得到成长。


在经历了,spm 一系列的变更后,构建工具已经完全是放射性了,如何在构建层的收敛成为了我们不得不面对的问题。

另外在如上所说,spm 配置式的方式,达到一定量级和复杂度后,要想要新增一个配置,或者修改某个配置经常会引发群体效应,有时根本没法改,工具对于维护人员的束缚日趋明显,而随着业务类型的增加,比如 H5 开发的井喷,导致开发人员的个性化需求猛增,变革变得更加急迫。

在这个过程中我们经过了很长时间的讨论,围绕的点可以归结为工具 **中心化和去中心化 **。

什么是中心化:

中心化的思路本质上是 all in one, 即我们基本上需要去覆盖开发人员的整个工作流,从项目初始化,开发,构建,调试和联调,以及发布,可能还会衍生测试,proxy,文档等其他服务。中心化的思路好处是,用户体验度高,入口具有唯一性。但缺点也很明显,all in one 就是大,另外由于内置了什么可能的方案,用户个性化需求基本不能满足,同时达到一定程度后,工具会变得没法维护。

什么是去中心化:

去中心化思路本质上是工具模块化开发,即我们去落实用户在整个工作流中可能会需要的解决方案,在用户在特定业务场景中需要某个功能时,加载和选择对应的模块即可。这种方式的好处是,让各个解决方案成为了单点模块,用户在最终使用时可以选择性使用,缺点是成本相对较高。为此我们通过脚手架来解决相关问题。

基于这个场景下 ant tool 的历史使命出现了,ant tool 想要达到一个非常灵活的状态,同时把所有的业务场景通过某种扩展配置的方式收敛到一种形态的工具上。

ant tool

ant tool 只是一个 代号,在 ant tool 体系下有很多下沉至开源社区的职责单一的模块,而这些模块具备了:构建、调试等所有功能。

所以 ant tool 对于开发者的视图是一个一个的散点,而散点是我们给出的解决方案,诸如

  • atool-build: 是对 webpack 的进一步封装,它会为你默认生成一套配置文件并调用 webpack 进行构建;
  • dora: 一个开发服务器,通过插件的方式集合各种调试方案,比如 webpack、livereload、browsersync、数据 mock、本地代理、weinre、jsonapi 等等;
  • atool-test: 前端测试集成方案;
  • atool-doc: 前端文档方案;
  • moggles: 无线端离线包集成解决方案。

与此同时我们也尝试给前端工具下了一个定义:前端开发工具是一个把 规范化输入内容 转化为 规范化输出内容 的转化器。

在这个定义下,我们细化了各类业务场景做为规范化的输入,但是如何规范化呢,答案是 细分业务类型脚手架。

脚手架很好的解决了工具有点到面的过程,相对降低了工具的门槛。

所以要用一句话来概括 ant tool 可以归结为 ** 一套更加 面向社区 的、更加 轻薄灵活 的,并以 脚手架做为输出口径 的解决方案 **。

但随着脚手架方案的普及,弊端也随之而来了。

总结来说使用脚手架的问题是:

  1. 作为开发人员,没有意识升级脚手架,导致脚手架的内容并没有跟上工具迭代的步子
  2. 脚手架的内容会随着业务的变更而变化,但是脚手架在某一刻被初始化后就成型了,前期的项目很难跟进到最新脚手架带来的福利。其次,有些脚手架的变更会影响整体的代码组织形式,这更难让一个成熟的已有业务升级上来。最难的是,很多开发者并没有意识,去升级 deps 或者构建类的调试类的配置,对于普通开发者而言完全是一个黑盒。
  3. 由业务方维护的脚手架变更频繁,今天可能是 A 明天可能就是 B 了,用户很难弄清楚,到底应该用的什么。

灵活的 webpack.config.js 带来的配置灾难

因为 atool-build 只是针对共性业务,实际业务场景下可能并不能满足,所以为了灵活性,我们在 atool-build 中引入了 webpack.config.js,这个配置的作用在于给用户一个时机覆盖 atool-build 内的配置。

这是一个看似合情合理的需求,但为何我现在把它描述为一种灾难呢?

atool-build 的定位是通用性的,所以到具体业务场景 100% 会出现不够用的情况进而使用扩展,目前基本所有的业务类型脚手架全部需要基于 atool-build 的配置,在做一层业务化,这个情况同样也发生在 doc site chair-atool fengdie 等基于 atool-build 做二次封装的。小规模使用情况下问题都不会显露,当大规模使用后,问题就越来越显现了。这种思路下,atool-build 将很难新增 feature 或者 修改内置参数。原因是在用户配置中很可能已经对其进行了修改,而再当有内置配置发生更新时,很多业务配置中的相关判断将会失效。从而影响整体用户配置的生效,从而影响构建结果的正确性。

举例来说之前 atool-build 在 0.10.x 版本时尝试对 .icon.svg 的 svg 应用 svg-sprite-loader, 同时对原有的 .svg 配置的 test 变更为 .svg 非 .icon.svg 。就这样的改动,业务线非常多的项目就出问题了,原因在于,用户端对 .svg 文件的 test 进行了强行的依赖。

atool-build 给到的启示是,一旦开了灵活度极高的 webpack.config.js 都很可能让构建的主体变得难以升级,但个性化需求永远存在,如何解开这个难题是关键的关键。另外大量脚手架都需要在 atool-build 的基础上自定义配置,这种集中式的看似通用性的通用配置,是否真的合适与实际多变的业务场景。

转而我们再来看看其他的解决思路。

create-react-app

create-react-app 是 fb 家创建 react 应用的工具。使用它可以很方便的创建一个 fb 推荐的 react 应用,其内部会包含构建所需的所有配置内容。那它是是如何看待,要在内置配置上做修改这个需求呢,fb 的答案很明确,这就是我们的最佳实践,你要改,那么使用 eject 功能吧。 eject 本质是把 cra 中内置的 webpack 配置一口气全部给到用户,同时也很抱歉,你的项目已经脱离于 cra。

反问我们能那么做吗? 我们面对的不单单只是 pc react 应用场景,还有 mobile 等其他使用业务场景。

但是 cra 的优点也非常凸显,它能把其中的体验做到极致。

roadhog

由于 create-react-app 的默认配置不能满足需求,而他又不提供定制的功能,于是云谦同学基于 cra 现有代码的基础上实现了一个可配置内置配置的版本 roadhog,roadhog 针对 dva 做过很多优化,所以这也是云谦把 roadhog 定位服务于 dva 应用的很重要一部分原因。另外所谓的可配置就是对已有的内置的 loader 或者 plugin 传递一些参数,或者功能的开启或者关闭等。这某种程度上解决了既要 create-react-app 的优雅体验,又想定制配置的需求。

但是问题还是不得不需要面对,对于新增配置的业务场景呢?

从最开始的不支持这种使用场景,到逐步开放一些内部配置,再到支持通过 webpack.config.js 以编码的方式进行配置,可以看出这是一个相当纠结的过程。其实大家都知道,支持通过 webpack.config.js 的方式基本可以肯定是后续想要暗箱升级,是不太可能了。本质上这和 atool-build 是一模一样的情况。

未来

经过那么多年的发展,各类方式的长和短差不多都已心知肚明。那如何根本上解决这个问题呢?

个人观点:

  1. 要放弃特定业务脚手架针对通用型构建配置进一步修改或者封装的这种方式。原因在于,一做为构建主体会很难升级,二业务会强绑定死某个版本,三业务很可能在某个阶段需要构建配置,目前脚手架这种一旦初始化生命周期就被终止的情况,很难把新的内容给予到老的业务。
  2. 抹杀 webpack.config.js 这种形式,至少要在所有正常归属业务中抹杀掉。
  3. 实现语义配置,用户只需要知道语义化的配置来实现配置的自定义。

那如何达成以上这三点呢?

我想到了 m-init 无线端脚手架的演化,以及 babel 的 preset.

分别来说说这两个看似没有关系事情。

m-init 诞生之初都由我一个人在维护,内部包含了通用型的离线包业务,react-native 业务,component 等,但是随着业务方自己在各方面的沉淀,在应用架构也好,在工具端的认知度也好,我慢慢开始享受 pr/mr 的过程。到目前 m-init 已经不再托管实际脚手架,而只是作为标准化业务脚手架输出的管理工具。简单的来说我的角色变成了审核以及 code review。

而 babel 的 preset 只是针对了特定的技术选型或者 transform 条件而集合在一起的一堆 babel-plugin。方便记忆和管理,便把它通过 preset 这种方式告知于用户。

所以我相信未来的配置应该是属于 preset 这种方式和方向的,而基于 m-init 中演化,我并不担心这整一套机制的在业务中落地的可行性。

那问题又来了,preset 到底是什么?如何看待 preset 和 普通开发者之间的关系呢?又如何实现 preset 这种机制呢?

我尝试用一些图来说明

构建因子:在这边额外引入了一个概念叫构建因子,白话来说它是可以被沉淀的一种针对某种构建场景的最小单元解决方案。而在具体实现中,构建因子需要符合因子的规范。在这边大家可以理解为所有的因子都是某个基础因子的 extension,在这些 extension 中需要用户实现对应的 hook 即钩子,如 pre、post、main、和 service。前三者应该不用过多解释,而 service 即提供了从用户端读取特定配置的功能,即语义化配置的最终来源。

preset: 业务 owner 即目前脚手架维护者,从构建因子中挑选已有的解决方案,形成一个业务级别的 preset,该 preset 会以 npm 包的形式存在,并最终被业务脚手架所引用。而作为业务普通开发者可以在约定配置文件中,针对 preset 做出选择性调整。任何不能适应当前 preset 的情况,都需要基于当前 preset 创建新的 preset。这么做的原因在于,业务一旦稳定,配置也会稳定,如果需要变更,可以理解为 1. 新的类型,那么创建新的 preset 2. 如果是新增 feature,则 告知维护当前业务 preset 成员新增因子,并由开发人员决定是否将其开启。

通过以上这些我相信能更好的解决如上提到的这些问题,业务也会从配置这个泥潭中出来。

同时这种处理方案还能解决:

  1. 配置更新的问题,因为 preset 是一个 npm 包,是一种描述性构建配置的包,任何的变动只需要发布一个版本,普通开发者通过更新便可以升级上来。
  2. 普通开发者,可以真正意义上享受到无痛升级,因为在这种方式下,对于用户的感知只是配置,而实际抹平这一层的是构建因子。而这一层并不会关系到普通开发者,所以理论上可以做到构建底层随意切换,某种意义上我们也实现了对于构建的收敛,只是我们收敛到了构建因子,比如 webpack1 webpack2。

再谈谈中心化和非中心化

虽然上文中已经提到了中心化和非中心化,但总感觉没讲彻底,所以故予以补充。

在我的理解中,中心化是 AllInOne 的代表,具体表现在工具时,当工具需要支持的业务类型足够多时,那需要内置的内容即解决方案就要足够多,如此一来,工具本身在尺寸上就会显得臃肿一些(比如当初 spm 尺寸达到了 400MB 以上),这是我想要表达的缺陷1: 尺寸大。由于要做到一面千用,那么势必可配置的内容也要足够的多,用户的灵活度提升了,但背后牺牲的是工具本身的灵活度和可维护性,因为往往配置到了一定程度后,就会存在配置同步与配置互斥的问题,这往往是面对后续新需求,但又无从下手的导火索,这是缺陷2:开发者维护性会越来越差。另外配置足够多是否意味着用户用的越 high 呢?在 spm 时代,对于构建我们大概有 20 多个 配置项,这是一个看起来并不起眼的数字,即使在文档充裕的情况下,用户也经常被配置困惑,很多时候一个字段很难把事情描述明白,另外在往常项目中,配置文件往往是收敛在 package.json或者某个 .rc 文件下,时间一长项目一交接工具一升级,等等这之后就没有人感动项目内的配置文件了,这都是发生的真实案例,这是我要表达的 缺陷3:可配置并不意味高用户体验度。

所以在个人观点中,我并不建议把需要面向多种业务形态的工具 - 构建和调试工具,做成 AllInOne 的形式。如果你的工具就面向一种业务形态,那么中心化或许可以提供更好的用户体验,甚至可以内置脚手架。

但是在面向多业务形态时,对于中心化,我们现在的做法是脚手架进行输出,也就是我们的脚手架内容其实是中心化的。它的作用并不单单初始化一个项目,而同时也输出了衔接各类其他前端工程类产品方式。

在如上图的架构设计中,构建因子是离散的即所谓的非中心化,它是单个功能的解决方案,用户可以在各个方案选择中可以实现热替换,而 preset 本质上是中心化的,它是规定某个业务对构建的完全描述。

preset 作为 npm 包的合理性

在线下沟通中,很多同学并不能理解未来工具体系如何做到收敛,如何做到更新,作为原有的构建实体 atool-build 又该承担怎么样的角色。在这里再重新梳理一下。

在上图的分层结构中,作为开发者应该活跃在构建因子层,而业务 owner 应该主要活跃在 preset 函数式配置层,而普通开发者应该主要活跃在 rc 语义层配置。在目标态中,一旦业务层 preset 确定 后普通业务开发者应该去动语义层配置的机会会很少。

如何理解 preset 层是需要函数式的呢,原因在于构建因子层在设计中本质是一个函数块,钩子的方法集,另外确保一定的灵活性,所以这里不得不是函数式的。同时 preset 对构建因子的组合方式也决定了用户 rc 语义层的配置,原因在于,构建因子存在对特定构建配置的读取权。

而 preset 要作为 npm 包它的出发点其实来源于收敛和更新。众所周知在以往我们把配置完完全全留在了项目端,而且该配置还是函数式的,这样太高灵活度导致的是构建实体没法升级,于此同时,一旦某一类业务发生变更那么就需要手动通知所有的该业务类型进行升级,如果没有很好的监控体系,这几乎是一个不可能完成的任务。而 preset 作为一个 npm 包由业务 owner 来进行维护,一旦该包发布版本,那么就可以普惠到所有的业务开发者。另外由于 preset 的形式本身就约束了用户端的灵活性所以在升级阻力上基本可以视为 0 阻力。

那又如何理解原有的 atool-build 将来会承担怎么样的角色呢?

在原先 atool-build 只是一个中性的构建工具,而往后由于实际描述构建能力的内容已经被 preset 替代,它更多程度上会转换为一个壳的角色,可以让自己的版本维持在一个相对稳定的版本。得益于 preset 机制,在壳上我们可以做到一些基于配置的性能优化分析。另外 atool-build 也将承载调度 preset,提醒 preset 升级等,一系列围绕在 preset 周边的功能。

@IMWeb前端社区

本文由作者pigcan 授权转发

https://github.com/pigcan/blog/issues/4

微信:IMWebTech

0 人点赞