-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
支付宝前端构建工具的发展和未来的选择 #4
Comments
写的非常赞!晚睡的鸟有文看 |
不明觉厉 |
这个架构看起来像似百度的FIS。插件化的编译。 |
@jincdream 资源文件的管理和组合,这个问题本质上我们抛给了 webpack,所有文件即是 module 而项目目录的合理化,则应该是针对不同的技术栈脚手架输出。 |
我有时候总看到优酷的地址栏总有类似?spm=a2hww.20023042.m_223465.5 |
@liuliangsir 没关系 那个据我所知是埋点相关 |
@pigcan https://github.com/egoist/poi |
@pigcan 你的理解感觉太狭隘了诶。
支付宝前端构建工具,如果只当做构建工具这样去看待是比较局限的,当前前端构建工具基本都无法很好地解决前端开发体验问题和前端资源部署问题。(当然部署是与当前前端团队的对应环境相关的) 项目目录的合理化项目目录的合理化解决的是前端团队的协作问题,也就是说问题不在于脚手架的层面,无论你用什么工具,或者自己写一个小工具、小插件,都能生成想要的开发项目目录。我觉得这是需要中心化的,无论所在公司的前端岗位是横向的,还是垂直的,如果想要前端开发直接的快速配合开发,这一点还是很有必要的。 资源文件的管理和组合资源文件的管理和组合,前端的资源是很特殊的,我们开发的时候可能是本地文件,但开发完进行部署的时候,文件的引用就可能变成了URL,如果不用编译工具插手的话,那这个URL的修改过程是非常痛苦,效率很低的。
对于资源的最终输出,我们就有这些(不完全)情况:
以上所说的都已经超越了工具的层面了,有些是webpack能解决的,但也不是能全部解决的。 |
一群 fis 吹,我用 fis 比你们都久没觉得这么好,楼主的代码还没看到你们就敢说话? 仔细学习下楼主文章,看到了构建的血泪史,也期待看到成品! |
mark |
这里只谈到构建工具,工程上的东西支付宝并未做输出,以后有可能会结合 egg 一起做一个方案。 |
赞! https://github.com/neikvon/fbi 我的理解是,脚手架五花八门,自然要去中心化,但一个脚手架的构建流程涉及效率和协作,需要中心化,至于配置,跟脚手架绑定就降低了复杂度,也不失灵活性。 |
@jincdream 在定义一个东西的时候,会尽可能的精简,因为定义的目的是定义某样东西的 core/base,而不是边界。所以 @pigcan 的定义并不是狭窄,而是希望能直接抓住问题的本质。
而你在 #4 (comment) 中,列出的各种使用场景差异,我们都知道。但这篇文章主要讨论的是:如何尽可能的降低构建配置的维护成本,并提高使用体验。 所以我觉得你的评论,离题了。当然,一篇好的文章应该激发读者的思想,提出文章主题以外的观点也正常。但我觉得还是有必要指出,你所讨论的是另外一个问题,而非当前文章的主题。 |
这篇刚出来的时候,我看了一遍,好像也没抓住文中的重点。昨晚 @dead-horse 贴了一篇内网的东西给我看,今天又看了一遍,貌似明白了。 楼上 @jincdream 说的那段,跟本文尝试要解决的问题不在一个层面。本文里面写的,应该确实是侧重应用脚手架和构建工具,为什么会跟fis有侧重点的差异,我的理解,是因为各自面临的应用场景不同。 如果你的产品前端形态存在大量的模板,很少有前端的组件化体系,“构建”本身要做的事情并不会特别复杂,那大部分问题确实是资源的管理,fis可能更适合。 但蚂蚁的场景是不一样的,蚂蚁面临的一大堆项目,前端的形态是客户端部分偏重,组件化复杂度高,没有什么模板的东西,各项目之间存在不少配置上的差异,也有很多构建过程要做的事情,而且还面临跟发布体系的集成,整个是产品开发和发布链条的其中一块环节。 在一个产品启动的时候,需要有脚手架给业务项目提供默认配置,给它一些可选的配置项,到底给哪些,不给哪些,是这篇要尝试解决的问题,要把不同类型的需求尽量覆盖到,并且还要把配置项抽象起来,收敛起来,避免各项目随意写自定义配置,导致后续升级之类的问题。 蚂蚁这边的项目特点是,业务项目的选型和构建发布过程都是受到强管控的,始终有一个中心化的大平台在支撑整件事情,所以一定不会期望一个业务项目的配置项和发布过程很特立独行,除非它在业务上就有其不可避免的特殊性。即使有那种情况,也会在平台这边提供选项,而不是放任其自己搞。 |
楼主用类似 DSL 的中间层,而非 webpack.config.js 的复写来确保构建脚手架的中心化和配置项的去中心化,在架构思路上值得借鉴。 看到楼上有聊到 fis 的,我这边也顺道分享下。
|
如果能放一些demo的配置代码参考一下就好了,光是这样文字描述外人看着就像空中楼阁一样。 |
下文说说我理解的支付宝前端构建工具发展史,从 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 对于开发者的视图是一个一个的散点,而散点是我们给出的解决方案,诸如
与此同时我们也尝试给前端工具下了一个定义:前端开发工具是一个把 规范化输入内容 转化为 规范化输出内容 的转化器。
在这个定义下,我们细化了各类业务场景做为规范化的输入,但是如何规范化呢,答案是 细分业务类型脚手架。
脚手架很好的解决了工具有点到面的过程,相对降低了工具的门槛。
所以要用一句话来概括 ant tool 可以归结为 ** 一套更加 面向社区 的、更加 轻薄灵活 的,并以 脚手架做为输出口径 的解决方案 **。
但随着脚手架方案的普及,弊端也随之而来了。
总结来说使用脚手架的问题是:
灵活的 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 是一模一样的情况。
未来
经过那么多年的发展,各类方式的长和短差不多都已心知肚明。那如何根本上解决这个问题呢?
个人观点:
要放弃特定业务脚手架针对通用型构建配置进一步修改或者封装的这种方式。原因在于,一做为构建主体会很难升级,二业务会强绑定死某个版本,三业务很可能在某个阶段需要构建配置,目前脚手架这种一旦初始化生命周期就被终止的情况,很难把新的内容给予到老的业务。
抹杀 webpack.config.js 这种形式,至少要在所有正常归属业务中抹杀掉。
实现语义配置,用户只需要知道语义化的配置来实现配置的自定义。
我想到了 m-init 无线端脚手架的演化,以及 babel 的 preset.
分别来说说这两个看似没有关系事情。
m-init 诞生之初都由我一个人在维护,内部包含了通用型的离线包业务,react-native 业务,component 等,但是随着业务方自己在各方面的沉淀,在应用架构也好,在工具端的认知度也好,我慢慢开始享受 pr/mr 的过程。到目前 m-init 已经不再托管实际脚手架,而只是作为标准化业务脚手架输出的管理工具。简单的来说我的角色变成了审核以及 code review。
而 babel 的 preset 只是针对了特定的技术选型或者 transform 条件而集合在一起的一堆 babel-plugin。方便记忆和管理,便把它通过 preset 这种方式告知于用户。
所以我相信未来的配置应该是属于 preset 这种方式和方向的,而基于 m-init 中演化,我并不担心这整一套机制的在业务中落地的可行性。
我尝试用一些图来说明
构建因子:在这边额外引入了一个概念叫构建因子,白话来说它是可以被沉淀的一种针对某种构建场景的最小单元解决方案。而在具体实现中,构建因子需要符合因子的规范。在这边大家可以理解为所有的因子都是某个基础因子的 extension,在这些 extension 中需要用户实现对应的 hook 即钩子,如 pre、post、main、和 service。前三者应该不用过多解释,而 service 即提供了从用户端读取特定配置的功能,即语义化配置的最终来源。
preset: 业务 owner 即目前脚手架维护者,从构建因子中挑选已有的解决方案,形成一个业务级别的 preset,该 preset 会以 npm 包的形式存在,并最终被业务脚手架所引用。而作为业务普通开发者可以在约定配置文件中,针对 preset 做出选择性调整。任何不能适应当前 preset 的情况,都需要基于当前 preset 创建新的 preset。这么做的原因在于,业务一旦稳定,配置也会稳定,如果需要变更,可以理解为 1. 新的类型,那么创建新的 preset 2. 如果是新增 feature,则 告知维护当前业务 preset 成员新增因子,并由开发人员决定是否将其开启。
通过以上这些我相信能更好的解决如上提到的这些问题,业务也会从配置这个泥潭中出来。
同时这种处理方案还能解决:
2017/5/8 更新
再谈谈中心化和非中心化
虽然上文中已经提到了中心化和非中心化,但总感觉没讲彻底,所以故予以补充。
在我的理解中,中心化是 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 周边的功能。
The text was updated successfully, but these errors were encountered: