带你解读 webpack 打包原理及vite的优势

7,226 阅读8分钟

ESMoudle

模块化规范有很多:AMD/CMD(浏览器), commonJS(服务端)

浏览器对 ES Module 标准的原生支持,改变了这种情况。目前大多数浏览器已经支持通过 <script type="module"> 的方式和 import 的方式加载标准的 ES 模块

模块只会执行一次并且默认为defer也支持async

传统的<script>如果引入的JS文件地址是一样的,则JS会执行多次。但是,对于type="module"<script>元素,即使模块地址一模一样,也只会执行一次。例如:

<!-- 1.mjs只会执行一次 -->
<script type="module" src="1.mjs"></script>
<script type="module" src="1.mjs"></script>
<script type="module">import "./1.mjs";</script>
<!-- 下面传统JS引入会执行2次 -->
<script src="2.js"></script>
<script src="2.js"></script>

打包工具出现的原因及作用

  • 前端模块化成为趋势:前端应用日益复杂开发模块化写法可以更灵活易于管理

  • 浏览器 对ES Modules 有兼容问题,

  • 其次模块化开发模式下,随着代码自然增长会有越来越多模块,模块越多浏览器要发起的请求数也就越多

所以打包工具诞生了,例如webpack 需要webpack把我们写好的模块代码进行处理,让浏览器识别我们写的模块代码。最后可以打包输出一个js文件,以一种串联的方式管理好了这些模块。

webpack 如何实现模块化打包的

例子:

//文件1:
exports.bar = function () {
    return 1;
}
//文件index.js:
const bar = require('./bar');function foo() {
 return bar.bar();
}

一、commonJS 方式

1.webpack通过模拟module,exports,require变量,将我们的模块代码打包成一个IIFE(立即执行函数),函数参数是我们写的各个模块被包装之后组成的数组,浏览器执行这个立即执行函数就可以运行我们的模块代码。

**

(function (modules) {
/* 省略函数内容 */
return __webpack_require__(__webpack_require__.s = 0);
 //启动入口模块
})
([function (module, exports, __webpack_require__) {
/* 模块1代码 */
},function (module, exports, __webpack_require__) {
/* 模块2的代码 */
}]);

**

2.立即执行函数的入参

webpack是怎样把我们写的模块包装成函数的呢?

require入口模块时,入口模块会收到收到三个参数,下面是入口模块代码:

参数说明:

module:当前缓存的模块,包含当前模块的信息和exports

exports: module.exports的引用

__webpack_require__ :require的实现

webpack函数入参如下所示:

[(function(module, exports, __webpack_require__) {
var bar = __webpack_require__(1);bar.bar();
}),
(function(module, exports, __webpack_require__) {
exports.bar = function () {return 1;}}
)]

3.立即执行函数的函数体内容:

_webpack_require__方法 require的实现

  • 定义了__webpack_require__ 这个函数,函数参数为模块的id。这个函数用来实现模块的require。

  • __webpack_require__ 函数首先会检查是否缓存了已加载的模块,如果有则直接返回缓存模块的exports

  • 如果没有缓存,也就是第一次加载,则首先初始化模块,并将模块进行缓存。

  • 一个动态绑定,改变this指向module.exports,将模块函数的调用对象绑定为module.exports

  • 调用完成后,模块标记为已加载。

  • 返回模块exports的内容。

    {
    // 1、模块缓存对象
    var installedModules = {};
    // 2、webpack实现的require
    function __webpack_require__(moduleId) {
    // 3、判断是否已缓存模块
    if(installedModules[moduleId]) {return installedModules[moduleId].exports;}
    // 4、缓存模块
    var module = installedModules[moduleId] = {i: moduleId,l: false,exports: {}};
    // 5、调用模块函数
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 6、标记模块为已加载
    module.l = true;
    // 7、返回module.exports
    return module.exports;
    }
    

二、ES Moudle 方式

文件1

export default function bar () {return 1;};
export function foo () {return 2;}

文件index.js

import bar, {foo} from './m';
bar();
foo();

同commonJs 方式一样webpack生成的代码是一个IIFE,这个IIFE完成一系列初始化工作后,就会通过__webpack_require__(0)启动入口模块。

1.代码模块的封装函数不同

步骤:

  • 不能识别原生expoerts,所以需要改写原来的方法并模拟exports。并且export default和export都被转换成了类似于commonjs的exports.xxx

  • index模块首先通过Object.defineProperty__webpack_exports__上添加属性__esModule ,值为true,表明这是一个es模块。所有引入的模块属性都会用Object()包装成对象,这是为了保证像Boolean、String、Number这些基本数据类型转换成相应的类型对象。

  •   _webpack_require__(1); 将下一个模块引入就是文件1
      
      [
      (function(module, __webpack_exports__, __webpack_require__) {
      Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
      var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);
      Object(__WEBPACK_IMPORTED_MODULE_0__m__["a"])();
       // 方法bar()Object(__WEBPACK_IMPORTED_MODULE_0__m__["b"])();
       // 方法 foo()}
      ),
      (
      function(module, __webpack_exports__, __webpack_require__){
      __webpack_exports__["a"] = bar;
      __webpack_exports__["b"] = foo;
      function bar () {return 1;};
      function foo () {return 2;}}
      )
      ]
      
    

结论:那么知道webpack工作的原理也就能得出结论:代码量和打包时间一定是成正比

既然已经有了 Webpack,尤大再整一个 Vite 到底有啥用呢?

webpack 无法避免的问题:

  • 本地开发环境webpack也是需要先打包,然后服务器运行的是打包后的文件,所以代码量很大的项目就会有启服务很慢的现象,
  • 热更新:Webpack 的热更新会以当前修改的文件为入口重新 build 打包,所有涉及到的依赖也都会被重新加载一次。虽然webpack 也采用的是局部热更新并且是有缓存机制的,但是还是需要重新打包所以很大的代码项目是真的有卡顿的现象(亲身经历,例如集成很多子平台的大型项目)

具有了快速冷启动、按需编译、模块热更新的 Vite

**Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间。
**

  • 依赖预构建:依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。参考文章:zhuanlan.zhihu.com/p/379164359

这个过程有两个目的:

  • CommonJS 和 UMD 兼容性: 开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块

  • Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。

  • 快速冷启动:只启动一台静态页面的服务器,对文件代码不打包,服务器会根据客户端的请求加载不同的模块处理(利用的是浏览器对esMoudle的原生支持),所以节省了webpack 那一套打包转化封装的逻辑。所以大型项目不会再出现热更新卡顿,起服务慢的情况(理论上,尚未找到合适项目实践)

与其它非打包解决方案比较

  • **按需编译、模块热更新:**采用立即编译当前修改文件的办法。同时 vite 还会使用缓存机制( http 缓存 => vite 内置缓存 )是基于缓存的热更新

    文件缓存:Vite 会将预构建的依赖缓存到node_modules/.vite。它根据几个源来决定是否需要重新运行预构建步骤:package.json 中的 dependencies 列表, package-lock等

    浏览器缓存:解析后的依赖请求会以 HTTP 头 max-age=31536000,immutable 强缓存,以提高在开发时的页面重载性能。一旦被缓存,这些请求将永远不会再到达开发服务器

快速开始

Vite 的原理

接下来开始说一下 Vite 实现的核心——拦截浏览器对模块的请求并返回处理后的结果。

浏览器发起的第一个请求自然是请求 localhost:3000/,这个请求发送到 Vite 后端之后经过静态资源服务器的处理,会进而请求到 index.html,此时 Vite 就开始对这个请求做拦截和处理了。

内联元素:

如下图2 你会惊奇的发现也并非读取我们的源码,而是构建后的产物。会把node-modules 里的这些引用重新构建成node-modules的绝对路径这个跟webpack一样,但是源码部分并没有像webpack 做函数转换封装。而是直接用了源码

链接引入:没有在 script 标签内部直接写 import,而是用 src 的形式引用,那么就会在浏览器发起对 main.jsx 请求的时候进行处理。只是会替换import node-modules 中的路径

Vue :

这样就把原本一个 .vue 的文件拆成了2个请求(分别对应 template和scriptstyle ) ,浏览器会先收到包含 script和template 逻辑的 App.vue 的响应,然后解析到 style 的路径后,会再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。

支持使用插件:

需要将它添加到项目的 devDependencies 并在 vite.config.js 配置文件中的 plugins 数组中引入它。

强制插件排序:默认在vite 之后调用,可以强制改变顺序调整到vite之前

Loader:

静态资源引用,直接使用不必再使用loader 转换

imgUrl 在开发时会是 /img.png,在生产构建后会是 /assets/img.2d8efhg.png

行为类似于 Webpack 的 file-loader。区别在于导入既可以使用绝对公共路径(基于开发期间的项目根路径),也可以使用相对路径。

单页面&多页面

单页面:

当需要将应用部署到生产环境时,只需运行 vite build 命令。默认情况下,它使用 <root>/index.html 作为其构建入口点,

多页面:

为什么生产环境仍需打包,为啥不直接将 entry.js 文件使用 标签引用

  1. 尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)

  2. 从 entry.js 到所有依赖的模块代码,全部采用 ES Module 方案实现,我们的依赖管理是采用 npm 的,而 npm 包大部分是采用 CommonJS 标准而未兼容 ES 标准的。

未来可期

无论vite 可以走多远,但是这种本地无需打包利用浏览器去解析当前请求的模块,还有热更新时候只编译也不再打包的概念都是我所期待的。永远摆脱了代码越多打包越慢的噩梦!!!