前端的交付基于浏览器,资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,是前端发展中一直探索的难题。
模块化 这个词大家一定有所熟知。模块的产生就是为了解决前端日趋复杂,从而加载越来越多资源而产生的问题。最终目的是为了提高生产力!
前端模块发展历程:前端模块化系统
模块化发展到今天,其基本的范式为:利用 bundle 工具(如 webpack)将源码打包成浏览器可识别的 bundle。
范式从本质上讲是一种理论体系、理论框架。范式具备一定程度内的公认性,被人们普遍接受。
该范式(Bundle 模式)下,随着项目体积增大,开发阶段一次性将源代码和第三方依赖编译处理打包到一起的耗时会显著增加;成千上万个模块导致首次 dev server 启动耗时在几分钟甚至十几分钟,严重影响了开发效率与体验。
解决思路: 从减少 webpack 模块数量角度考量,剔除 node_modules 下的第三方依赖,仅对业务代码打包。
针对该方式常见的方法是将第三方库在 Webpack 构建时配置 External, HTML 中直接通过 Script 标签引入 UMD 产物。但 UMD 会带来副作用:① 不支持 UMD 的包进行改造(额外工作量);② 全局变量污染,甚至互相覆盖;③ 需要增加 UMD 额外的兼容代码。
显然,UMD 不是最佳方式。
随着 ECMAScript 2015
提出 ECMAScript Module
规范,各个浏览器都在积极地推进着浏览器模块系统的实现,前端模块化有了原生支持方式。
未来的构建范式? 两个方向:
- 构建产出 ESModule 模块
- 直接将 npm 仓库上的包转化成支持 ESModule 的版本(ESM 包的分发)
构建出 ESModule 模块
典型的示例:Snowpack、Vite
Snowpack 是首次提出利用浏览器原生 ESM 能力的工具。开发过程中,Snowpack 为你的应用程序提供 unbundled server。每个文件只需要构建一次,就可以永久缓存。文件更改时,Snowpack 会重新构建该单个文件。在重新构建每次变更时没有任何的时间浪费,只需要在浏览器中进行 HMR 更新。
对比一下 bundle 和 ESM 两者的区别:
浏览器请求前将全部资源进行转换打包处理生成 bundle,然后浏览器加载相关 bundle。
浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
依附于 ESM import
和 export
可以单独加载依赖项。因此对于单文件构建速度、调试、缓存等优势明显。每个文件都是单独构建并无限期缓存。开发环境永远不会多次构建文件,浏览器永远不会下载文件两次(直到它发生变化)。
使用 ESM 构建的核心特点:
node_modules
完全不需要参与到构建过程,构建效率提升明显- 构建复杂度非常低,修改任何内容都只需做单文件编译(不需要重新构建和重新打包应用程序的整个bundle),时间复杂度永远是 O(1),reload 时间与项目大小无关
- 借助 ESM 的能力,模块化交给浏览器端,不存在资源重复加载问题,如果不是涉及到 jsx 或者 typescript 语法,甚至可以不用编译直接运行;同时这种原生的 TreeShaking 还可以做到访问文件时再编译,做到单文件级别的按需构建
- 生产环境仍需要打包,为了获得最佳的加载性能,同时将代码进行 tree-shaking、懒加载和 chunk 分割,以获得更好的缓存
ESM 包的分发
典型的示例:esm.sh、skypack
将 NPM 仓库上的包转化成支持 ESModule 的版本并通过 URL 来进行分发。
有了 ESM 分发:
- 可以更好的利用以往用
CMD
或者AMD
规范开发的众多 NPM 包; - 可以替换掉之前使用 UMD 加载组件库(或其他包)的场景;
- 可以借助 CDN ,对一个特定版本的 NPM 包 转化而来的 ESM 包做永久存储,达到在线加载速度最大化。
原理: 将传统的 ADM/CMD/UMD 语法,通过 AST 的解析,将其转化为 ESModule 语法。 难点: 这种转换属于语法升级,需要做向上兼容处理。