rollup 和 webpack 打包原理简述版

rollup

简单来说就是一开始会生成一个Bundle实例,实例在 build 的时候,会从入口触发,每个文件生成一个module实例,包含模块的源代码、路径、模块的抽象语法树 ast,然后将语法树语句展开,并作相应的转换,最后调用generate生成最终的代码,拼接所有语句,输出bundle

webpack

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。

  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。

  3. 确定入口:根据配置中的 entry 找出所有的入口文件。

  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。

  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。

  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。

  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

CommonJS 和 ES6 Module 的区别

  1. 导出方式的不同
  2. 动态与静态
  3. 值拷贝与动态映射
  4. 循环依赖

2. 动态与静态

CommonJS 与 ES6 Module 最本质的区别是:动态 vs 静态

  • CommonJS 对模块依赖的解决是“动态的”(只能在运行时,分析出对应的依赖关系)

  • ES6 Module 是“静态的”(可以在编译时,就分析出对应的依赖关系,才能做 tree shaking)

CommonJS 例子

1
2
3
4
5
6
// calculator.js
module.exports = {
name: "calculator",
};
// index.js
const name = require("./calculator.js").name;

在上面介绍 CommonJS 的部分时我们提到过,当模块 A 加载模块 B 时(在上面的例子中是 index.js 加载 calculator.js),会执行 B 中的代码,并将其 module.exports 对象作为 require 函数的返回值进行返回。

  • require 的模块路径可以动态指定,支持传入一个表达式,我们甚至可以通过 if 语句判断是否加载某个模块。

  • 因此,在 CommonJS 模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。

ES6Module 的写法

1
2
3
4
// calculator.js
export const name = "calculator";
// index.js
import { name } from "./calculator.js";

特点:

  1. es6 module 的导入、导出语句都是声明式的,它不支持导入的路径是一个表达式
  2. 并且导入、导出语句必须位于模块的顶层作用域(比如不能放在 if 语句中)。 因此我们说,es6 module 是一种静态的模块结构,在 es6 代码的编译阶段就可以分析出模块的依赖关系。

es6 module 相比于 commonjs 具备以下几点优势

  1. 死代码检测和排除。(tree shaking)
    • 我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。
  2. 模块变量类型检查
    • javascript 属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。es6 module 的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。
  3. 编译器优化
    • 在 commonjs 等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而 es6 module 支持直接导入变量,减少了引用层级,程序效率更高。

3. 值拷贝与动态映射

在导入一个模块时

  1. 对于 commonjs 来说获取的是一份导出值的拷贝
  2. 而在 ES6 Module 中则是值的动态映射,并且这个映射是只读的

4. 循环依赖

循环依赖是指模块 A 依赖于模块 B,同时模块 B 依赖于模块 A

  • 一般来说工程中应该尽量避免循环依赖的产生

ES6 Module 的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值。

总结 rollup cs webpack

rollup 诞生在 esm 标准出来后

  • 出发点就是希望开发者去写 esm 模块,这样适合做代码静态分析,可以做 tree shaking 减少代码体积,也是浏览器除了 script 标签外,真正让 JavaScript 拥有模块化能力。是 js 语言的未来
  • rollup 完全依赖高版本浏览器原生去支持 esm 模块,所以无额外代码注入,打包后的代码结构也是清晰的(不用像 webpack 那样 iife)
    • 目前浏览器支持模块化只有 3 种方法:
      • ①script 标签(缺点没有作用域的概念)
      • ②script 标签 + iife + window + 函数作用域(可以解决作用域问题。webpack 的打包的产物就这样)
      • ③esm (什么都好,唯一缺点 需要高版本浏览器)

webpack 诞生在 esm 标准出来前,commonjs 出来后

  • 当时的浏览器只能通过 script 标签加载模块

    • script 标签加载代码是没有作用域的,只能在代码内 用 iife 的方式 实现作用域效果,
    • 这就是 webpack 打包出来的代码 大结构都是 iife 的原因
    • 并且每个模块都要装到 function 里面,才能保证互相之间作用域不干扰。
    • 这就是为什么 webpack 打包的代码为什么乍看会感觉乱,找不到自己写的代码的真正原因
  • 关于 webpack 的代码注入问题,是因为浏览器不支持 cjs,所以 webpack 要去自己实现 require 和 module.exports 方法(才有很多注入)

    • 这么多年了,甚至到现在 2022 年,浏览器为什么不支持 cjs?
    • cjs 是同步的,运行时的,node 环境用 cjs,node 本身运行在服务器,无需等待网络握手,所以同步处理是很快的
    • 浏览器是 客户端,访问的是服务端资源,中间需要等待网络握手,可能会很慢,所以不能 同步的 卡在那里等服务器返回的,体验太差
  • 后续出来 esm 后,webpack 为了兼容以前发在 npm 上的老包(并且当时心还不够决绝,导致这种“丑结构的包”越来越多,以后就更不可能改这种“丑结构了”),所以保留这个 iife 的结构和代码注入,导致现在看 webpack 打包的产物,乍看结构比较乱且有很多的代码注入,自己写的代码都找不到

顺便提一嘴 parcel

parcel 相比于 webpackrollup 来说是比较新的打包工具了,它最大的特点就是作者宣称的 0 配置打包

  1. parcel 默认支持打包所有前端会用到的文件格式,比如css,sass,less,typescript等等,完全不需要手动去写配置文件,几乎开发中所有需要用到的 loader 官网都已经在配置文件中帮我们写好了,我们只需要下载一下编译环境,比如 sass 就需要使用npm install -D sass下载 sass 编译环境,less 甚至都不需要安装,当 Parcel 检测到 less 文件时会自动进行安装编译环境!就是这么神奇!,

  2. parcel 检测到相关的文件,比如.jsx,.vue文件,会自动帮你安装相关的依赖,所有不用自己安装依赖,直接打包就行。

总结: parcel 就是简单易用.

参考文献

rollup 打包产物解析及原理
开箱即用的 web 应用打包工具-parcel
前端模块标准之 CommonJS 和 ES6Module 的区别