支付宝小程序构建经历了以下两大过程:
-
Webpack 化
-
去 Webpack 化
乍一看就好像一开始走错了路,但其实,并没有清晰的界限可以判定某一技术变更是对的还是错的,因为所有的决定都取决于当下的状态。
Webpack 化
Webpack 化
先来说一说为什么 Webpack 化。个人觉得 Webpack 最大的魅力是视万物为 module 的能力。它高阶的扩展能力,带来了极高的个性化能力;极其活跃的社区或多或少可以让我们少走路,也少走弯路;另外不得不着重提一下 CRA,CRA 在优化开发体验上下的功夫非常深,目前绝大多数基于 Webpack 的上层封装,都会有 CRA 的影子。总而言之,Webpack :
-
优异的 Add-on 设计
-
成熟的社区配套
-
活跃的社区氛围
-
CRA 提供了优异的开发体验模块
这些对于新生业务来说有着致命的诱惑。
另外由于之前对 Ant tool 的维护,我成为了 Webpack 死忠粉。所以我们自然而然地采用了 Webpack 做为构建的内核,也基于它的 Add-on 能力做了足够多的个性化能力输出。
一切看上去都没什么大问题,除了 Webpack 内置的缓存优化方案坑了我们一次之外,没有任何的意外。
然而凡事都有个但是,小程序一步步铺开,开发者的个性化需求也见涨。不出所料,越来越多的同学希望我们能开放核心的构建能力、更多的参数配置,因此 webpack.config.js 又被搬上了台面。如果业务取向稳定可控,那么 webpack.config.js 绝对是滋生恶魔的来源,
由于 Webpack 内置支持 pollyfill nodejs buildin 的模块,作为社区大库他们自然会使用一些 module,通常他们也会提供一个 dist 目录,以存储可以纯跑在浏览器端的文件。而作为开发者,在我所看到的上千的项目中,几乎 90% 的开发者都没有这个意识,他们习惯「拿来即用」。
import a from 'a';const hello = require('./hello');const d = {
x: 1,};module.exports.c = function c() {};
exports.b = {};export { d };
不知道大家有没有想过元罪是什么?是 Webpack 这一类 universal 方案么? 还是 NPM 把前端模块也引入了进来了?这是一个开放的命题,没有标准答案。
回到正题,因为 Webpack 强大的包容性(当然这也怪我们当初没限死),慢慢的我们发现在小程序业务中,居然出现了一类 cheerio 的依赖,看到越来越多的案例把传统 PC、Node 开发思路用到了小程序之上。就我个人来说,这是我不想看到的。
另外,在调试环节,小程序研发流程中用户会设置编译模式(即只调试固定页),Webpack 贪婪式的编译模式(全量构建)是否真的契合小程序的调试模式?
此外,随着 Web-IDE 的盛行,我们也琢磨着把编译流程整合到浏览器端,Webpack 的厚重让可能性显得比较渺茫。
不过最最最重要的原因还是,Webpack 的效率仍略显局促,特别是在与友商的对比下。不过在这里要给 Webpack 抱不平的是,Webpack 的能力是远大于友商的,而这种能力,就像现在的手机,它的算力是过剩的,而这种过剩的算力导致了时间的上累。
调研之路,友商带来的启发
友商在安全的防护上做的是相当到位的,我很难通过 hack 的方式来窥视整个流程。
探究的过程中留给我最深刻的印象是对于克制的理解。产品层是克制的,功能是克制的,开放度是克制的。要真正在浮躁的互联网中做到如此「克制」还是挺难的。
接下来我们单纯从技术层面来说一说。注意:以下都是我个人的理解,未必对。
友商的技术选型,按我的理解应该遵循四点:轻,快,可管控,够用就好。这几点在框架、DSL、构建服务等方面均有体现。
框架层面,友商相比蚂蚁,做的还是很薄的,蚂蚁背靠 react 生态,但这很有可能是把双刃剑,小程序真的 “小” 吗?我们是否做好了开放所带来的的管控问题?我觉得这些都是棘手的问题。而友商的轻薄,虽然在管控性上做的比较极致,但是面对开发者天马行空的需求,或许他们的问题更多的是如何来支持。典型的案例就是 NPM 支持,在蚂蚁这套技术体系下,我们是纯天然就支持现有的前端研发链路的,所以在开发习惯的延续性上基本没有任何问题,但友商最初的做法是,让开发者在小程序生态外自己创建一个 NPM 使用流程,这也就是为什么在社区里面会有很多这类方案的原因,而后续友商发布支持 NPM,但仔细琢磨,其实友商 NPM 更加偏向于是 component,而不是标准前端意义上的模块,比如不支持 nodejs module shim 就可以看出。反观我们的 NPM 目前已被玩坏。
另外在梳理过程中发现,友商在处理 xml 文件和 css 文件时,它是解耦的,贪婪的。稍微深入就可以看到,他有专门的二进制编译工具负责此类文件的编译,利用编译工具可以批处理 xml 和 css 文件。这和我们之前基于 Webpack 的方案有着巨大的差异,我们的编译本质上是有上下文的,即比如 component 样式会有作用域提升,进而影响 page,另外这当中大量依赖了 Webpack 的语法分析 和 loader 机制,与此同时我们还依赖了一个虽不属于构建却又能影响构建的外部过程,所以从根本上来说,在现有的技术架构上我们很难真正意义上超越友商。
另外,友商在框架层应该就考虑了模块加载,所以友商对于模块的加载模式优化或者内部模块间的管控,比如控制 exports 有着更加灵活的空间,但我们在这一层依赖 Webpack 提供的 runtime,正因为这种模式,如果友商想要往比如 Web-IDE 靠拢,其实更加可行。对于这一层的认知主要是我接触到了 Stackblitz,以及之后慢慢了解到了 Systemjs。这部分内容我放到下节。
通过对友商的学习,我感受到了小而美的力量,没有厚重的堆积感,确实能称得上 “小程序”。
CodeSandbox、Stackblitz 带来的启发
对于我来说,我的命题就是尽可能的让开发者以最快的速度来完成开发环境的初始化。我做过非常多基于 Webpack 的尝试,熟人常知的 happypack,cache-loader,thread-loader,hardsource,以及如何尽可能的让缓存增加有效性,以及甚至基于 Webpack 的 memory fs hack 了一套自己的逻辑等等等等,但效果并不让人满意,甚至很多优化反而会导致很奇怪的问题。
很神奇的是,我有一次无意看到了友商在 worker 端代码的加载过程,它并没有真正意义上的 bundle 过程,而是对所有的 worker 进行了全量的 http 请求,我的第一个反应是 http 有同源策略,为什么他们还敢这样做,难道并发量不会成为瓶颈吗?这种方式我见不到相对 Webpack 的优势到底是什么?!
这困扰了我很长时间,直至 Stackblitz 走到了我的眼前——最初就是那篇 turboCDN 文章。我基本上挖坟了所有有关 Stackblitz 的 Issue Twitter。实际上 CodeSandbox 在这一块上也用着类似的方案,但 CodeSandbox 的问题是,他在这块的实现在当时与其业务实现深度耦合,所以我更加倾选择 systemjs,至少它是独立的,且有自己的生态。随着了解的深入,我逐渐对他如何在浏览器内实现伪 bundle 和 NPM 如何跑在浏览器端有了一定的了解。当时我的最大想法就是,我可以基于 systemjs 来实现一套动态和按需的加载方案,在本地开发阶段省去所有 bundle 过程,文件只有在真正用到时再进行编译,甚至通过一些方式,比如在浏览器端实现 fs,那么这套方案就可以被移植到 Web-IDE 上。基于这样的构想,我开始了很长时间的尝试,从 18 年 10 月初我写下第一行代码,代号被取为 Gravity,更多 Gravity 设计的内容我会放在下一段 。但在浏览器端实现 fs 和利用 web worker 来实现 compile 上我碰了很多壁,因为浏览器端没有 nodejs 环境,我要解决所有的 pollyfill 的问题,以及文件 resolve 的差异性。然而时间上并不允许我做过多的技术性探究,所以第一阶段我退而求其次,把这部分内容架设在了本地,通过启动一个 koajs 实例来解决,即浏览器端发生资源请求时,通过中间件(所处 nodejs 环境)来实现真正的编译过程。仅仅只是这一步尝试,就把我原先在 mac 上花费的 40s+ 编译时间降到了 8s 左右,说实话我自己都没法相信。
学习 CodeSandbox、Stackblitz 带来的启发是,利用纯浏览器带来的架构上的变更或许是另外一种出路。
再来谈谈 bundler 历史
把时间倒退到六年前,那会儿我们对于打包的概念应该还是在 grunt 或 gulp 流式的任务处理,对资源文件的处理也仅仅是压缩和拼接。而后前端界兴起了模块化浪潮,模块化后的代码放在哪儿,又如何被引入,相信这个问题是那会儿前端们最为关注的事情,所以又要翻翻老账 - seajs 有自己的源服务器来承载模块化后的模块也是理所当然的事情。但大家也都知道,世界上最大的代码源服务是 NPM,但如上文中提到的起始时 NPM = “Node.js Package Manager”,它并不真正意义上服务于前端浏览器。但是开发者对此的诉求实在是太大了,而众所周知 Node.js 使用的是 CJS 模块规范,所以不经过打包根本不可能运行于浏览器中,而诸多的模块定义,也给了 Browserify, Webpack 等发挥的空间,特别是 Webpack universal 的概念非常契合大众诉求。
当然作者观点更多是当下已然 2019 了,我们应该往前看,因为在浏览器端已经支持了 ESM。对我而言,我觉得这种想要跳出困局寻求突破的精神是更加值得学习的。另外也带来一个问题,撇开作者提供的思路或实现,本地 bundle 基于现有的技术架构,能否有所破局。
新技术方案 Gravity
在谈新方案 Gravity 前,还要来回首下我在几年前写的一篇文章 - 支付宝前端构建工具的发展和未来的选择,那会儿我们最大的困扰是配置带来的不可控性。所以那会儿我提出了构建因子以及 preset 的想法。出于对配置的敬畏,在 Gravity 中我把这一套想法完全实现了出来(其实在那篇文章几个月后就有一版实现,但不幸的是没有继续深入就流产了,另外也因为我工作内容而被动调整了)。
另外对于我看到了市面上各个公司都想往小程序上走,大家在小程序架构上都是大同小异,是不是有这么一种可能性,能做一套构建底层来适配所有的小程序业务。这是我做 Gravity 的第二个念想。
所以 Gravity 最 base 的架构思路是让 Gravity 变成构建工具的工厂,让各种业务形态的小程序构建变成 Gravity 的一种上层实现。要实现如上这个想法也就意味着,Gravity 必须要有好的插件机制。这个时候 tapable 自然而然成为了我的最佳选择,对于 tapable 渊源还要从我解析 Webpack watch 实现说起,当然这不是我们今天的重点。重点是 Webpack 就是基于 tapable 实现出来的,它的灵活性健壮性毋庸置疑,另外我彻彻底底研究过 tapable,真的是喜欢到不行。还有非常重要的一点是,我用 tapable 来设计插件机制,可以对开发者非常友好,因为几乎不需要学习。
另外还有一个好处是基于 tapable 我可以非常轻松实现一种时序,比如说我现在要实现一个 css 文件的加载 loader,在上文中我大概说了因为在时间上的原因我并未在一期就尝试把所有流程都丢到浏览器内完成,而是把一部分工作丢给了 koa 的中间件,所以在文件处理上(Webpack 中叫 loader),我实现了一套动态生成中间件的方案,原因在于实现一个 css 文件的加载可能需要经过多个加载器,比如 post-css,css,style,这其中就有时序的问题,所以借由 tapable 我可以很方便来根据描述文件(类 Webpack rules 设计)动态创建一个时序,转而变成一个中间件。
在设计 Gravity loader 时,和 CodeSandbox 一样,我们把 API 尽可能的往 Webpack 靠了。原因就在于我想要有复用 Webpack loader 的可能性。另外对于上层实现,也会更加友好,因为基本可以做到和 Webpack 长得一样,用的一样。
另外还有很多细节我这里就不在阐述了。
Gravity 会进一步维护,也会在合适的时候开源。它的目标也非常明确,成为真正意义上浏览器端方案,在上层实现层可以对接到更多的业务场景。最终通过一个 Web-IDE 把所有的事情都串联起来。
总结
抛弃偏见,我相信 Cloud IDE 一定是未来,而面向 Web 的架构一定是当务之急。