最近在看 webpack 的原理, 觉得可以分为两个方面来完成:
了解 webpack 打包出来的文件.
了解 webpack 流程并且自己写 loader 和 plugin.
当然看源码是可以的, 但是有点事倍功半并且没有必要, 个人觉得完成以上两部分就可以对 webpack 有不错的了解了. 本文主要关于 webpack 打包出来的文件的内容[希望能够提出不对或者可以补充的地方, 感觉说的不是很清晰, 欢迎指 tu 正 cao] .
配置以及待打包文件如下:
- // webpack.config.js
- const path = require('path');
- const webpack = require('webpack');
- module.exports = {
- entry: {
- bundle1: path.resolve(__dirname, 'src/index1.js'),
- bundle2: path.resolve(__dirname, 'src/index2.js')
- },
- output: {
- path: path.resolve(__dirname, 'dist'),
- filename: '[name].js'
- },
- plugins: [
- new webpack.optimize.CommonsChunkPlugin({
- name: 'manifest'
- })
- ]
- };
- // index1.js
- const test1 = require('./test1');
- const test3 = require('./test3')
- console.log(test1);
- console.log(test3);
- // test1.js
- const str = 'test1 is loaded';
- module.exports = str;
- // test3.js
- const str = 'test3 is loaded';
- module.exports = str;
- // index2.js
- setTimeout(function() {
- require.ensure([], function() {
- const test2 = require('./test2');
- console.log(test2);
- });
- }, 5000);
- // test2.js
- const str = 'test2 is async loaded';
- module.exports = str;
module 和 chunk
首先了解 module 和 chunk 的概念:
module 其实就是打包前, import 或者 require 的 js 文件, 如 test1.js 与 index1.js.
chunk 是打包后的文件, 即 bundle1.js,bundle2.js,0.js 和 manifest.js 文件, 这里需要注意 一个 chunk 可能包含若干 module.
三个核心的方法
打包结果文件(简化版本):
- // manifest.js
- (function(modules) { // webpackBootstrap
- // install a JSONP callback for chunk loading
- var parentJsonpFunction = window["webpackJsonp"];
- window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
- // add "moreModules" to the modules object,
- // then flag all "chunkIds" as loaded and fire callback
- var moduleId, chunkId, i = 0, resolves = [], result;
- for(;i < chunkIds.length; i++) {
- chunkId = chunkIds[i];
- if(installedChunks[chunkId]) {
- resolves.push(installedChunks[chunkId][0]);
- }
- installedChunks[chunkId] = 0;
- }
- for(moduleId in moreModules) {
- if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
- modules[moduleId] = moreModules[moduleId];
- }
- }
- if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
- while(resolves.length) {
- resolves.shift()();
- }
- if(executeModules) {
- for(i=0; i < executeModules.length; i++) {
- result = __webpack_require__(__webpack_require__.s = executeModules[i]);
- }
- }
- return result;
- };
- // The module cache
- var installedModules = {};
- // objects to store loaded and loading chunks
- var installedChunks = {
- 3: 0
- };
- // The require function
- function __webpack_require__(moduleId) {
- // Check if module is in cache
- if(installedModules[moduleId]) {
- return installedModules[moduleId].exports;
- }
- // Create a new module (and put it into the cache)
- var module = installedModules[moduleId] = {
- i: moduleId,
- l: false,
- exports: {}
- };
- // Execute the module function
- modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
- // Flag the module as loaded
- module.l = true;
- // Return the exports of the module
- return module.exports;
- }
- // This file contains only the entry chunk.
- // The chunk loading function for additional chunks
- __webpack_require__.e = function requireEnsure(chunkId) {
- var installedChunkData = installedChunks[chunkId];
- if(installedChunkData === 0) {
- return new Promise(function(resolve) { resolve(); });
- }
- // a Promise means "currently loading".
- if(installedChunkData) {
- return installedChunkData[2];
- }
- // setup Promise in chunk cache
- var promise = new Promise(function(resolve, reject) {
- installedChunkData = installedChunks[chunkId] = [resolve, reject];
- });
- installedChunkData[2] = promise;
- // start chunk loading
- var head = document.getElementsByTagName('head')[0];
- var script = document.createElement('script');
- script.type = 'text/javascript';
- script.charset = 'utf-8';
- script.async = true;
- script.timeout = 120000;
- if (__webpack_require__.nc) {
- script.setAttribute("nonce", __webpack_require__.nc);
- }
- script.src = __webpack_require__.p + ""+ chunkId +".js";
- var timeout = setTimeout(onScriptComplete, 120000);
- script.onerror = script.onload = onScriptComplete;
- function onScriptComplete() {
- // avoid mem leaks in IE.
- script.onerror = script.onload = null;
- clearTimeout(timeout);
- var chunk = installedChunks[chunkId];
- if(chunk !== 0) {
- if(chunk) {
- chunk[1](new Error('Loading chunk' + chunkId + 'failed.'));
- }
- installedChunks[chunkId] = undefined;
- }
- };
- head.appendChild(script);
- return promise;
- };
- })
- ([]);
manifest.js 先运行注入了一些方法, 下面三个是最核心的方法:
- webpackJsonp
- webpack_require
- webpack_require.e
这里只大致的说下大致的作用以及重点的部分.
webpackJsonp 方法, 接受三个参数 chunkIds, moreModules, executeModules. 这里要分清楚 chunk id 和 module id,chunk id 指的是一个打包后文件的标示, 而 module id 是每个打包前的 module 的唯一标示也就是 id. 这里需要分别用来表示各个 chunk 和 module 以及在之后的缓存过程中使用到.
chunkIds 指的是这个 chunk 文件加载后需要被加载的 chunk id 的数组, 所以默认会有自身 chunk 的 id. 如果有这个 chunk 会用到的 module 打包到的 chunk 需要被预加载的话, 对应的 chunk 的 id 也会在 chunkIds 中.
moreModules 指的是这个 chunk 加载后带来的 module 的数组, 其中的每个 module 被以函数的形式包裹实现作用域上的隔离, 其实和 node 的模块加载的机制很像.
以 bundle1.js 为例
- webpackJsonp([1],[
- /* 0 */
- /***/ (function(module, exports, __webpack_require__) {
- const test1 = __webpack_require__(1);
- const test3 = __webpack_require__(2)
- console.log(test1);
- console.log(test3);
- /***/ }),
- /* 1 */
- /***/ (function(module, exports) {
- const str = 'test1 is loaded';
- module.exports = str;
- /***/ }),
- /* 2 */
- /***/ (function(module, exports) {
- const str = 'test3 is loaded';
- module.exports = str;
- /***/ })
- ],[0]);
chunkIds 是 [1],moreModules 是中间的数组参数, executeModules 是[0](这里要分清楚,[1] 中的 1 是 chunk id, 而 [0] 中的 0 是 module id 指的是需要被执行的 module 的 id 这里指的就是打包之前的 index.js 文件)这里运行了 bundle1.js 相当于执行了打包之前的 index1.js 文件.
moreModules 数组中的元素举个栗子:
- /* 0 */
- /***/ (function(module, exports, __webpack_require__) {
- const test1 = __webpack_require__(1);
- const test3 = __webpack_require__(2)
- console.log(test1);
- console.log(test3);
- /***/ })
每个 module 接收三个参数, 第三个参数可选 (取决于该 module 是否依赖其他 module, 稍后说明) 其中的 module 和 exports 是在开发时的模块导出中经常遇到的. 我们在导出一个模块的时候的操作:
module.exports = balabala;
我们知道 javascript 的函数的参数是引用传参, 所以也就是变相的将 module 中 export 的内容挂载到了 module.export 对象上了. 在打包后的代码可以看到, 我们只要执行 moreModules 数组中对应的元素的函数, 就能够变相的将这个 module 想要 export 的内容挂载到输入到函数的 module 的 export 对象上.
在介绍完 module 的接收的三个参数后. 我们可以看到函数内部当需要引用某个 module 的时候, 会调用__webpack_require__方法参入对应的 module 的 id.
- // The require function
- function __webpack_require__(moduleId) {
- // Check if module is in cache
- if(installedModules[moduleId]) {
- return installedModules[moduleId].exports;
- }
- // Create a new module (and put it into the cache)
- var module = installedModules[moduleId] = {
- i: moduleId,
- l: false,
- exports: {}
- };
- // Execute the module function
- modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
- // Flag the module as loaded
- module.l = true;
- // Return the exports of the module
- return module.exports;
- }
__webpack_require__方法比较简单, 其实就是传入 moduleId 首先判断下 installedModules 中是否有缓存(也就是之前加载过), 有的话直接返回输出的内容, 没有的话, 就执行 modules[moduleId].call(module.exports, module, module.exports, webpack_require); 将 module 输出的内容挂载到 module.exports 对象上, 同时缓存到 installedModules 中, 结果就是:
每个 module 只会在最开始依赖到的时候加载一次, 之后会从 installedModules 直接获取不在加载. 如果 module 依赖的 module 继续依赖其他 module 的话, 上述的过程会递归的执行下去, 但是加载过的依赖值会加载一次.
这里可以看到如果依赖的 module 被打包到独立的 chunk, 并且这个 chunk 还没有被执行的话, 这个时候 modules[moduleId]就是 undefined 了. 我觉得这个时候就和 webpackJsonp 的第一个参数有关了, 也就是这个 chunk 依赖的 chunk 的 id 需要在 chunkIds 参数中. 这里牵扯到另一个概念 "循环依赖" 的处理, 这个打算之后再另一篇文章中专门介绍.
简单来说__webpack_require__的作用就是加载执行对应的 module, 并且缓存起来.
在了解了__webpack_require__后, 回头看下每个 chunk 都有的 IIFE 的对应的 webpackJsonp 方法, webpackJsonp 做的事情其实简单来说:
标记了每个 chunk 是否加载过了.
缓存了每个加载的 chunk 带来的 module 到 modules 对象中.
将需要异步加载的 chunk 的回调 (promise 的 resolve) 统一收集并且执行.(这部分具体的过程感觉理解的还不是很透彻)
按顺序加载 executeModules 中的 module, 加载的过程就已经执行了对应的 module, 返回最后的 module 的执行结果.
这里还需要注意 **webpack_require.e** 方法, 这个方法对应的是 require.ensure 方法, 这个方法的作用是可以实现按需加载, 也就是对应的 module 会被打包成独立的 chunk, 在执行 require.ensure 回调中的方法的时候才下载对应的 chunk, 从而实现 chunk 的按需加载.
从__webpack_require__.e 的代码中可以看到, ta 大致的思想是: 判断对应的 chunk 是否已经加载过了, 如果已经加载过了, 就 return 一个 resolve 了的 promise, 然后执行对应的回调函数的内容. 如果 chunk 没有加载过则用动态添加 script 标签的方式加载对应的 chunk(所以这里的方法名叫做 webpackJsonp 也是有道理的, 异步加载的方式和 jsonp 的方法有类似的地方). 然后标注 chunk 已加载, 这里还对如果 chunk 下载失败的情况下抛错警告 chunk 加载失败.
总结
至此算是大致的介绍了 webpack 打包后文件的内容, 可以看到 webpack 打包后的文件, 自己实现了一套模块加载的机制, 这样方便实现比如代码分割等功能.
在了解了 webpack 的打包之后文件的结构, 我们并知道 webpack 的打包过程依靠不同 loader 和 plugin 组合来进行. loader 负责将不同类型的文件进行转换成 js 类型的数据, plugin 在打包过程中注册对应的事件, webpack 在对应的阶段执行不同插件来实现对文件的处理.
在知道了这些之后, 实现 loader 和 plugin 来真正的使用 webpack.
参考资料:
深入理解 webpack 文件打包机制 https://github.com/happylindz/blog/issues/6
简单易懂的 webpack 打包后 JS 的运行过程
来源: https://juejin.im/post/5ad8c96ff265da0ba062b190