Skip to content

Latest commit

 

History

History
571 lines (402 loc) · 16.9 KB

20210312.md

File metadata and controls

571 lines (402 loc) · 16.9 KB

2021-03-12 技术周刊

【webpack 进阶】Webpack 打包后的代码是怎样的?

from: https://segmentfault.com/a/1190000039360348

注:这里分析的是 webpack v4 的打包后代码,v5 生成的代码略有不同。

准备工作

mkdir learn-webpack-output
cd learn-webpack-output
npm init -y
yarn add webpack webpack-cli -D
// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development', // 可以设置为 production
  // 执行的入口文件
  entry: './src/index.js',
  output: {
    // 输出的文件名
    filename: 'bundle.js',
    // 输出文件都放在 dist
    path: path.resolve(__dirname, './dist')
  },
  // 为了更加方便查看输出
  devtool: 'cheap-source-map'
}
// src/index.js
import sayHello from './sayHello';

console.log(sayHello, sayHello('Gopal'));
// src/sayHello.js
function sayHello(name) {
  return `Hello ${name}`;
}

export default sayHello;

分析主流程

其实就是一个 IIFE

其实总体的文件就是一个 IIFE——立即执行函数。

(function(modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};
    function __webpack_require__(moduleId) {
    // ...省略细节
    }
    // 入口文件
    return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
 "./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
  "./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__) {})
});

入参 modules 是一个对象,对象的 key 就是每个 js 模块的相对路径,value 就是一个函数(我们下面称之为模块函数)。IIFE 会先 require 入口模块。即上面就是 ./src/index.js

// 入口文件
return __webpack_require__(__webpack_require__.s = "./src/index.js");

入口模块会在执行时 require 其他模块例如 ./src/sayHello.js"

{
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
      var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
    console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
    Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
  })
}

重要的实现机制——__webpack_require__

  // 缓存模块使用
  var installedModules = {};

  // The require function
  // 模拟模块的加载,webpack 实现的 require
  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)
    // 没有则创建并放入缓存中,其中 key 值就是模块 Id,也就是上面所说的文件路径
    var module = installedModules[moduleId] = {
      i: moduleId, // Module ID
      l: false, // 是否已经执行
      exports: {}
    };

    // Execute the module function
    // 执行模块函数,挂载到 module.exports 上。this 指向 module.exports
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    // 标记这个 module 已经被加载
    module.l = true;

    // Return the exports of the module
    // module.exports通过在执行module的时候,作为参数存进去,然后会保存module中暴露给外界的接口,如函数、变量等
    return module.exports;
  }

所以这个__webpack_require__就是来加载一个模块,并在最后返回模块 module.exports 变量

webpack 是如何支持 ESM 的

  • esm export default sayHello;
 /***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

  "use strict";
  __webpack_require__.r(__webpack_exports__);
  /* harmony import */ var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sayHello */ "./src/sayHello.js");


  console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"], Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));

  /***/ }),

  /***/ "./src/sayHello.js":
  /*!*************************!*\
    !*** ./src/sayHello.js ***!
    \*************************/
  /*! exports provided: default */
  /***/ (function(module, __webpack_exports__, __webpack_require__) {

  "use strict";
  __webpack_require__.r(__webpack_exports__);
  function sayHello(name) {
    return `Hello ${name}`;
  }

  /* harmony default export */ __webpack_exports__["default"] = (sayHello);

  /***/ })
  • cjs: module.exports = sayHello;
/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

  "use strict";
  __webpack_require__.r(__webpack_exports__);
  /* harmony import */ var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sayHello */ "./src/sayHello.js");
  /* harmony import */ var _sayHello__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_sayHello__WEBPACK_IMPORTED_MODULE_0__);
  // src/index.js


  console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0___default.a, _sayHello__WEBPACK_IMPORTED_MODULE_0___default()('Gopal'));


  /***/ }),

  /***/ "./src/sayHello.js":
  /*!*************************!*\
    !*** ./src/sayHello.js ***!
    \*************************/
  /*! no static exports found */
  /***/ (function(module, exports) {

  // src/sayHello.js
  function sayHello(name) {
    return `Hello ${name}`;
  }

  module.exports = sayHello;


/***/ })

看的一头雾水?没关系...

先看看 __webpack_require__.r 函数,就是添加一个属性 __esModule,值为 true

__webpack_require__.r = function(exports) {
 object.defineProperty(exports, '__esModule', { value: true });
};

再看一个 __webpack_require__.n 的实现,判断module是否为es模块,当__esModule为 true 的时候,返回module.default,否则返回module

// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
  var getter = module && module.__esModule ?
    function getDefault() { return module['default']; } :
    function getModuleExports() { return module; };
  __webpack_require__.d(getter, 'a', getter);
  return getter;
};

最后看 __webpack_require__.d,主要的工作就是将上面的 getter 函数绑定到 exports 中的属性 agetter

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, {
            configurable: false,
            enumerable: true,
            get: getter
        });
    }
};

动态导入

常见的代码分割有以下几种方法:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。
// src/another.js
function Another() {
  return 'Hi, I am Another Module';
}

export { Another };
// src/index.js
import sayHello from './sayHello';

console.log(sayHello, sayHello('Gopal'));

// 单纯为了演示,就是有条件的时候才去动态加载
if (true) {
  import('./Another.js').then(res => console.log(res))
}

可以看到多出一个 0.bundle.js 文件,这个我们称它为动态加载的 chunkbundle.js 我们称为主 chunk

img

主 chunk 分析

if (true) {
  __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./Another.js */ "./src/Another.js")).then(res => console.log(res))
}
__webpack_require__.e ——使用 JSONP 动态加载
// 已加载的chunk缓存
var installedChunks = {
  "main": 0
};
// ...
__webpack_require__.e = function requireEnsure(chunkId) {
  // promises 队列,等待多个异步 chunk 都加载完成才执行回调
  var promises = [];

  // JSONP chunk loading for javascript
  var installedChunkData = installedChunks[chunkId];

  // 0 代表已经 installed
  if(installedChunkData !== 0) { // 0 means "already installed".
    // a Promise means "currently loading".
    // 目标chunk正在加载,则将 promise push到 promises 数组
    if(installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      // setup Promise in chunk cache
      // 利用Promise去异步加载目标chunk
      var promise = new Promise(function(resolve, reject) {
        // 设置 installedChunks[chunkId]
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });

      // i设置chunk加载的三种状态并缓存在 installedChunks 中,防止chunk重复加载
      // nstalledChunks[chunkId]  = [resolve, reject, promise]
      promises.push(installedChunkData[2] = promise);

      // start chunk loading
      // 使用 JSONP
      var head = document.getElementsByTagName('head')[0];
      var script = document.createElement('script');

      script.charset = 'utf-8';
      script.timeout = 120;

      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }

      // 获取目标chunk的地址,__webpack_require__.p 表示设置的publicPath,默认为空串
      script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";

      // 请求超时的时候直接调用方法结束,时间为 120 s
      var timeout = setTimeout(function(){
        onScriptComplete({ type: 'timeout', target: script });
      }, 120000);

      script.onerror = script.onload = onScriptComplete;

      // 设置加载完成或者错误的回调
      function onScriptComplete(event) {
        // avoid mem leaks in IE.
        // 防止 IE 内存泄露
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];

        // 如果为 0 则表示已加载,主要逻辑看 webpackJsonpCallback 函数
        if(chunk !== 0) {
          if(chunk) {
            var errorType = event && (event.type === 'load' ? 'missing' : event.type);
            var realSrc = event && event.target && event.target.src;
            var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
            error.type = errorType;
            error.request = realSrc;
            chunk[1](error);
          }
          installedChunks[chunkId] = undefined;
        }
      };

      head.appendChild(script);
    }
  }
  return Promise.all(promises);
};
  • 可以看出将 import() 转换成模拟 JSONP 去加载动态加载的 chunk 文件

  • 设置 chunk 加载的三种状态并缓存在installedChunks中,防止chunk重复加载。这些状态的改变会在 webpackJsonpCallback 中提到

    • installedChunks[chunkId]0,代表该 chunk 已经加载完毕
    • installedChunks[chunkId]undefined,代表该 chunk 加载失败、加载超时、从未加载过
    • installedChunks[chunkId]Promise对象,代表该 chunk 正在加载

异步 Chunk

// window["webpackJsonp"] 实际上是一个数组,添加一个元素。
// 这个元素也是一个数组,其中数组的第一个元素是chunkId,第二个对象,跟传入到 IIFE 中的参数一样
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

  /***/ "./src/Another.js":
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
  	// ...
  /***/ })

  }]);
  //# sourceMappingURL=0.bundle.js.map

这个 window['webpackJsonp'] 在哪里会用到呢?我们回到主 chunk 中。在 return __webpack_require__(__webpack_require__.s = "./src/index.js"); 进入入口之前还有一段

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];

// 保存原始的 Array.prototype.push 方法
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);

// 将 push 方法的实现修改为 webpackJsonpCallback
// 这样我们在异步 chunk 中执行的 window['webpackJsonp'].push
// 其实是 webpackJsonpCallback 函数。
jsonpArray.push = webpackJsonpCallback;

jsonpArray = jsonpArray.slice();
// 对已在数组中的元素依次执行webpackJsonpCallback方法
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
webpackJsonpCallback ——加载完动态 chunk 之后的回调
var installedChunks = {
  "main": 0
};

function webpackJsonpCallback(data) {
  // window["webpackJsonp"] 中的第一个参数——即[0]
  var chunkIds = data[0];
  // 对应的模块详细信息,详见打包出来的 chunk 模块中的 push 进 window["webpackJsonp"] 中的第二个参数
  var moreModules = data[1];

  // 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];
    // 所以此处是找到那些未加载完的chunk,他们的value还是[resolve, reject, promise]
    // 这个可以看 __webpack_require__.e 中设置的状态
    // 表示正在执行的chunk,加入到 resolves 数组中
    if(installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    // 标记成已经执行完
    installedChunks[chunkId] = 0;
  }
  // 挨个将异步 chunk 中的 module 加入主 chunk 的 modules 数组中
  for(moduleId in moreModules) {
    if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId];
    }
  }
  // parentJsonpFunction: 原始的数组 push 方法,将 data 加入 window["webpackJsonp"] 数组。
  if(parentJsonpFunction) parentJsonpFunction(data);
  // 等到 while 循环结束后,__webpack_require__.e 的返回值 Promise 得到 resolve
  // 执行 resolove
  while(resolves.length) {
    resolves.shift()();
  }
};

当我们 JSONP 去加载异步 chunk 完成之后,就会去执行 window["webpackJsonp"] || []).push,也就是 webpackJsonpCallback。主要有以下几步

  • 遍历要加载的 chunkIds,找到未执行完的 chunk,并加入到 resolves 中
for(;i < chunkIds.length; i++) {
  chunkId = chunkIds[i];
  // 所以此处是找到那些未加载完的chunk,他们的value还是[resolve, reject, promise]
  // 这个可以看 __webpack_require__.e 中设置的状态
  // 表示正在执行的chunk,加入到 resolves 数组中
  if(installedChunks[chunkId]) {
    resolves.push(installedChunks[chunkId][0]);
  }
  // 标记成已经执行完
  installedChunks[chunkId] = 0;
}
  • 这里未执行的是非 0 状态,执行完就设置为0

  • installedChunks[chunkId][0] 实际上就是 Promise 构造函数中的 resolve

    // __webpack_require__.e
    var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    
  • 挨个将异步 chunk 中的 module 加入主 chunkmodules 数组中

  • 原始的数组 push 方法,将 data 加入 window["webpackJsonp"] 数组

  • 执行各个 resolves 方法,告诉 __webpack_require__.e 中回调函数的状态

动态导入小结

流程图

总结

本篇文章分析了 webpack 打包主流程以及和动态加载情况下输出代码,总结如下

  • 总体的文件就是一个 IIFE——立即执行函数
  • webpack 会对加载过的文件进行缓存,从而优化性能
  • 主要是通过 __webpack_require__ 来模拟 import 一个(esm)模块,并在最后返回模块 export 的变量
  • webpack 是如何支持 ES Module (和 cjs)的
  • 动态加载 import() 的实现主要是使用 JSONP 动态加载模块,并通过 webpackJsonpCallback 判断加载的结果

新闻&推荐阅读