CommanderXL / Biu-blog

个人博客
432 stars 39 forks source link

Webpack module 模块系统 #38

Open CommanderXL opened 5 years ago

CommanderXL commented 5 years ago

在我们使用 webpack 作为我们的构建工具来进行日常的业务开发过程中,我们可以借助 babel 这样的代码编译转化工具来使用 js 新版本所实现的特性,其中 ES Module 模块标准是 ES6 当中实现的,在一些低版本的浏览器当中肯定是没法直接使用 ES Module 的。所以我们需要借助 webpack 来完成相关的模块加载、执行等相关的工作,使得我们在源码当中写的遵照 ES Module 规范的代码能在低版本的浏览器当中运行。这篇文章主要就是来介绍下 webpack 自身为了达到这样一个目的从而实现的自己的一套模块系统。

我们首先来看个简单的例子:

// a.js
import { add } from './b'

add(1, 2)
import(/* webpackChunkName: "c" */ './c.js').then(del => del(3, 4)) // 异步加载 c 模块
// b.js
export function add(n1, n2) {
  return n1 + n2
}
// c.js
export default function del(n1, n2) {
  return n1 - n2
}

webpack 输出到 dist 目标文件夹当中的代码可以这样分为这样3种:

其中 webpack runtime bootstrap 可以单独输出成一个 chunk,也可以使之包含于一个普通的 chunk 当中,这取决于你是否配置了相关的 chunk 优化策略,具体的内容参见webpack相关文档,在这里例子当中我们配置的是将 runtime bootstrap 单独打包输出一个 chunk。

其中在 runtime bootstrap 当中有个核心的方法:

/******/    // install a JSONP callback for chunk loading
/******/    function webpackJsonpCallback(data) {
/******/        var chunkIds = data[0];
/******/        var moreModules = data[1];
/******/        var executeModules = data[2];
/******/
/******/        // add "moreModules" to the modules object,
/******/        // then flag all "chunkIds" as loaded and fire callback
/******/        var moduleId, chunkId, i = 0, resolves = [];
/******/        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(data);
/******/
/******/        while(resolves.length) {
/******/            resolves.shift()();
/******/        }
/******/
/******/        // add entry modules from loaded chunk to deferred list
/******/        deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/        // run deferred modules when all chunks ready
/******/        return checkDeferredModules();
/******/    };
/******/    // 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 jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/    var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/    jsonpArray.push = webpackJsonpCallback;
/******/    jsonpArray = jsonpArray.slice();
/******/    for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/    var parentJsonpFunction = oldJsonpFunction;

__webpack_require__这个方法主要的功能就是首先判断 installedModules 上是否已经缓存了传入的 moduleId 对应的 module,如果有就直接返回这个 module.exports,即对应的 module 导出的内容。如果没有缓存过,那么首先初始化话一个新的 module 对象,并获取已经加载的 modules 上对应 moudleId 的 module 并执行(即实际每个模块的执行),传入module/module.exports/__webpack_require__这3个对象,这个 module 执行完之后就返回这个 module.exports 对象。

另外就是在 window 对象上定义了一个webpackJsonp数组对象。同时改写了这个数组的push方法为webpackJsonpCallback(这个方法的具体实现后面会讲)。

接下来我们就来看下不包含 runtime bootstrap 代码的 module 打包后是什么样的,我们看下需要异步加载的c.js最终打包出来的 chunk :

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[2],{

/***/ 2:
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "del", function() { return del; });
// import 'css-loader!./css-demo.css'

Promise.resolve(/* import() */).then(__webpack_require__.bind(null, 1)).then(function (add) {
  return add(1, 2);
});

function del(a, b) {
  return a - b;
}

/***/ })

}]);

可以看到的是在 chunk 的最外层调用window["webpackJsonp"]上的push(即webpackJsonpCallback)方法,这个方法接收了一个数组参数,其中第一项为这个 chunk 的 chunkId,第二项为这个 chunk 所包含的所有 module。在webpackJsonpCallback方法内部主要完成的工作就是收集 moduleId/module 之间的映射关系并缓存(这个时候这个 module 还未被执行,只有调用__webpack_require__方法的时候才会执行这个 module),此外就是将在异步加载 module 时创建 promise 对象的 resolve 函数收集至一个 resolves 数组,然后一一推出并执行,即将那些异步加载的 promise 的状态进行 resolve,那么也就会执行这个 promise 通过 then 方法传入的回调函数。此外我们可以看到这个 chunk 当中只包含了一个 moduleId 为 2 的 module,这个 module 为一个匿名的函数,接受了3个参数,即上文当中提到的有关每个 module 执行时所接收的。再回到刚才的那个例子,通过import语法引入了其他的模块,同时使用export语法导出了对应的方法或者对象。那么这个 module 通过 webpack 处理后变为一个匿名函数,原本模块当中使用的import语法会通过__webpack_require__方法来引入其他模块,原模块使用的export语法通过__webpack_exports__语法来导出相关的对象或者方法。