抖音前端团队里的前端工程化
开篇语:抖音前端团队通过几年的实践,积累了一些前端研发中的经验与理念,计划通过比较轻松、简洁的语言记录下来分享给大家,希望对大家有帮助,有理解不妥的不吝赐教。
作为前端架构的一部分,工程化是个永恒的话题,我们之所以老生常谈这个事情,核心原因在于浏览器这个环境所支持的三种语言没有在语言层面提供统一的模块化支持,这使得大家都在考虑如何添加这些模块特性,更好的支持好开发。而之所以模块本身非常重要,是因为我们知道分而治之的道理。
以上这段话怎么理解呢,想象一下你所接触的最基本语言 HTML、CSS、JS,对于这三种浏览器端标准语言在早前是不支持模块引入的,如果你想对 JS 进行模块拆分,你就得依靠 HTML 来串起来执行,当然 CSS 是具备这个能力的,HTML 在现在的标准中也逐步有了这样的能力。HTML、CSS、JS 组合式的出现,这又让我们了解到,我们需要定义一类 UI 模块,这里通常的叫法是 UI 组件。它们包含了 HTML、CSS、JS,我们通过工程化的手段来达到组件的工程化支持。现在的浏览器已经逐步支持 Web Component 来逐步磨平这些个不足。
为了实现分而治之,我们不得不实现一套组件化体系,这套体系一方面约束开发,一方面完美地演绎分而治之的精髓。组件化体系解决了开发过程中复用的问题,当然得想办法解决运行的问题。比如当我们通过组件的方式去完成一个站点的拆分工作,会发现拆分后的组件很多,达到了分而治之的目的但同时给运行带来了不少的麻烦,这个麻烦我们是需要去解决的。所以我们的工程化体系里面有了打包(package)这个名词,这个名词几乎是我们工程化体系中无法抹去的一个话题,也同样是性能优化体系里面一个非常重要的手段。
在组件化的体系里面,我们可以回想一下静态编译语言或者支持模块化的语言对于模块的一个特性,比如继承、比如扩展、再比如依赖。同样我们构建的组件化体系中统一要解决这些问题。当你在开发静态编程语言 c 语言的时候,在编译阶段会有依赖错误产生,这时候你可能非常好解决这个问题,你可以按照错误去解决。另一个比较难以解决的问题是运行时发现有依赖的基础库错误,这时候就不好解决了。同样在前端的组件化体系中也同样有这样的问题。
解决依赖是个非常麻烦的事情,一般我们会在构建编译的阶段去搞定这个事情,如果静态编译想搞定这个事情需要有一套可识别的标志。这就是刚才提到的组件化体系里面对于组件的引用的设定更准确一些就叫语法(require、define、import),这也是组件化规范的一部分(比如 CommonJS、AMD、CMD、UMD),在当下 ES6 默认支持。一般都会使用路径引入模块。构建时通过分析这些语法,来获得依赖关系。依赖关系有两个用途,一方面是在合并打包的时候作为参考,另一方面在浏览器端运行时提前去加载依赖;为了解决依赖问题,有很多名词经常出现,比如依赖前置。
每个构建工具在解决依赖这块做法是不太一样的,有些是从根节点搜索一棵树,有些是先分析出依赖树,最后再去根据收集到的依赖决策打包。实现方式有正则方式、也有 AST 方式,当然这些都大同小异,如果感兴趣也可以自己创造一种新的方式。
加载组件以及组件的依赖,在浏览器执行阶段可以有两种方式,一种是同步加载,对于同步加载的情况一般的做法是在 HTML 上 script 标签进行外链,另外一种方式是通过 JS 动态生成 script 节点来外链或者直接 Ajax 或者 Fetch 请求代码片段再执行。这种方式也同样适用于 CSS。加载工作主要是 Loader 来完成的,所以在组件化体系中有个 JS Loader 的概念。require.js、seajs 等都有这方面的支持。再往大里面延展 Loader 的功能,解决加载、处理依赖、保证执行、保证作用域、保证各种方式的执行代码的都由一个 JS 框架完成。
分治是为了更好的开发、维护、调试,其最核心优化的是研发的效率,合并(打包)最核心优化的是浏览器端运行的效率。所以,我们工程化其中一个核心的目标就是优化效率。
开发阶段,我们需要的是开发体验的提升,希望用抽象的思维去看待一切,绝不去浪费时间在重复的事情上。这样的情况下孕育了各种各样的工具。比如脚手架、模拟服务、LiveReload、HMR 等等。
我们希望覆盖整个研发流程,从方方面面进行效率的提升。这就是工具这个方向的使命。
站在浏览器的视角,运行快速、准确是最终我们要达到的目标。为了这个目标,根据网页的特性,我们需要优化整个链路,包括资源的加载、代码的运行、浏览器渲染的研究、数据的获取等等。积攒了非常多的经验后,把这些经验融合到我们的工程化体系里面,把这些优化在业务开发中不可见,按照某种方式去做,然后就可以获取到最佳性能,而不需要做额外的工作。这就是架构本身的魅力,做到这一点需要有规范。
站在研发的视角,开发快、易于调试、快速上线这些是我们的目标,对于这些目标,我们同样需要有一定的规范。规范我们代码如何写更易于维护,规范我们用什么语法更快速的能够开发。
不管是组件化体系、运行时效率、研发效率都需要规范,这些规范整体归纳为我们的研发规范体系;除了规范体系,还有更多的基础设施围绕着效率展开,比如更好写的语言、更高纬度的抽象、更高效的框架等等。
开发的场景里面,CSS 不好写,我们就有了 Sass;JS 弱类型,我们就有了 Typescript;另外为了开发的更高效,我们有了一堆的框架,React、Vue、Angular 等等。
我们围绕研发、浏览器两个视角不停的在构建我们的工程体系,希望于把一些繁杂的事情化简、把一些固化的优化对于研发透明。
固化的方式是有可能限制创造的,所以为了达到激发创造的可能,松耦合的工程架构体系才更适用。松耦合的工程体系会造成更多的个性化,个性化就可能降低效率。所以做工程也是一个反复折中的过程,无法一蹴而就,一劳永逸。