本文通过一个简单的例子,解析webpack runtime的工作原理

在现代前端开发的工作流中,webapckbabel这类是十分重要的工具,下面我们通过一个简单的例子,介绍webapck runtime或者说webpack模块系统工作原理。

主程序

为了了解工作原理,业务代码越少越好,我们的代码很简单,一个入口文件和一个被引用的模块,日常hello world#(滑稽)

// index.js
import { helloworld } from "./helloworld";
helloworld();
// helloworld.js
export const helloworld = () => console.log("hello world");

//webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanbWebpackPlugin = require("clean-webpack-plugin");

module.exports = {
  resolve: {
    alias: {
      "@src": path.resolve(__dirname, "src/utilities/"),
      "@components": path.resolve(__dirname, "src/components/"),
      "@assets": path.resolve(__dirname, "src/assets/"),
      "@utils": path.resolve(__dirname, "src/utils/")
    }
  },
  entry: {
    app: "./src/index.js"
  },
  // mode: 'development',
  mode: "none",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
    chunkFilename: "[name].[chunkhash].js"
  },

  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        use: "babel-loader",
        exclude: /node_modules/,
        include: path.resolve(__dirname, "src")
      },
      { test: /\.css$/, use: "css-loader" }
    ]
  },
  plugins: [
    new CleanbWebpackPlugin(["dist"]),
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "./src/index.html"
    })
  ],
  optimization: {
    noEmitOnErrors: true,
    namedModules: true,
    namedChunks: true,
    runtimeChunk: {
      name: "runtime"
    },
    splitChunks: {
      chunks: "all",
      minSize: 0,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 10,
      maxInitialRequests: 10,
      // auto generate chunk names
      automaticNameDelimiter: ".",
      name: true,
      cacheGroups: {
        vendor: {
          name: "vendor",
          test: /\/node_modules\//,
          priority: 0
        }
      }
    }
  }
};

构建结果

构建出的js脚本有两个:

  • runtime.js: webpack runtime

  • app.5a2b9a96b1bb2ab2ca91.js: 业务代码


Version: webpack 4.19.0
Time: 959ms
Built at: 2018-09-21 18:20:43
                      Asset       Size   Chunks             Chunk Names
app.5a2b9a96b1bb2ab2ca91.js  822 bytes      app  [emitted]  app
                 runtime.js   6.04 KiB  runtime  [emitted]  runtime
                 index.html  394 bytes           [emitted]
Entrypoint app = runtime.js app.5a2b9a96b1bb2ab2ca91.js
[./src/helloworld.js] 87 bytes {app} [built]
[./src/index.js] 56 bytes {app} [built]
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html] 502 bytes {0} [built]
    [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 509 bytes {0} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 519 bytes {0} [built]
        + 1 hidden module
✨  Done in 2.19s.Version: webpack 4.19.0

webpack runtime工作原理分析

接下来我要贴代码了…


/******/ (function(modules) { // webpackBootstrap
/******/     // 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()();
/******/         }
/******/
/******/         // 执行入口文件,我们的入口文件通常不会有什么导出,只是将别处的模块导入然后执行一遍
/******/         deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/         // run deferred modules when all chunks ready
/******/         return checkDeferredModules();
/******/     };
/******/     function checkDeferredModules() {
/******/         var result;
/******/         for(var i = 0; i < deferredModules.length; i++) {
/******/             var deferredModule = deferredModules[i];
/******/             var fulfilled = true;
/******/             for(var j = 1; j < deferredModule.length; j++) {
/******/                 var depId = deferredModule[j];
/******/                 if(installedChunks[depId] !== 0) fulfilled = false;
/******/             }
/******/             if(fulfilled) {
/******/                 deferredModules.splice(i--, 1);
/******/                 result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/             }
/******/         }
/******/         return result;
/******/     }
/******/
/******/     // 缓存了已经下载过的模块
/******/     var installedModules = {};
/******/
/******/     // object to store loaded and loading chunks
/******/     // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/     // Promise = chunk loading, 0 = chunk loaded
/******/     var installedChunks = {
/******/         "runtime": 0
/******/     };
/******/
/******/     var deferredModules = [];
/******/
/******/     // 核心的require函数
/******/     function __webpack_require__(moduleId) {
/******/
/******/         // 如果模块已经加载并执行过了,直接返回
/******/         if(installedModules[moduleId]) {
/******/             return installedModules[moduleId].exports;
/******/         }
/******/         // 创建新的模块
/******/         var module = installedModules[moduleId] = {
/******/             // 模块的id,默认是按照模块依赖关系以数字递增的方式标识的,为了在开发环境中保持稳定,webpack4以前的版本
/******/             // 需要手动引入NamedModulesPlugin,这样每次代码修改后,原有模块的id不会变,保证热更新效率
/******/             i: moduleId,
/******/             l: false,// 模块是否已经加载了
/******/             exports: {}// module.exports commonjs模块导出的定义
/******/         };
/******/
/******/         // 执行模块的定义函数,module.exports就是我们导出的对象
/******/         // 传入__webpack_require__,因为这个模块可能会依赖其他模块
/******/         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/         // 表示这个模块已经加载了
/******/         module.l = true;
/******/
/******/         // 返回模块的导出
/******/         return module.exports;
/******/     }
/******/
/******/
/******/     // modules: Map<ModuleID: Module> 记录了moduleId和module的Map
/******/     __webpack_require__.m = modules;
/******/
/******/     // 已经加载的模块
/******/     __webpack_require__.c = installedModules;
/******/
/******/     // 通过Object.defineProperty给module.exports添加单个导出的getter
/******/     __webpack_require__.d = function(exports, name, getter) {
/******/         if(!__webpack_require__.o(exports, name)) {
/******/             Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/         }
/******/     };
/******/
/******/     // 设置模块的__esModule属性,将其表示为ES6模块
/******/     __webpack_require__.r = function(exports) {
/******/       // toString的时候返回Module
/******/         if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/             Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/         }
/******/         Object.defineProperty(exports, '__esModule', { value: true });
/******/     };
/******/
/******/     // create a fake namespace object
/******/     // mode & 1: value is a module id, require it
/******/     // mode & 2: merge all properties of value into the ns
/******/     // mode & 4: return value when already ns object
/******/     // mode & 8|1: behave like require
/******/     __webpack_require__.t = function(value, mode) {
/******/         if(mode & 1) value = __webpack_require__(value);
/******/         if(mode & 8) return value;
/******/         if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/         var ns = Object.create(null); // 创建一个命名空间,__proto__ = null
/******/         __webpack_require__.r(ns);
/******/       // 定义exports.default
/******/         Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/         // getter转换
/******/         if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/         return ns;
/******/     };
/******/
/******/     // getDefaultExport function for compatibility with non-harmony modules
/******/     __webpack_require__.n = function(module) {
/******/         var getter = module && module.__esModule ?
/******/             function getDefault() { return module['default']; } : // ES6模块exports.default
/******/             function getModuleExports() { return module; }; // commonjs默认导出exports
/******/         __webpack_require__.d(getter, 'a', getter);
/******/         return getter;
/******/     };
/******/
/******/     // hasOwnProperty
/******/     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/     // 这玩意就是publicPath
/******/     __webpack_require__.p = "";
/******/
/******/     var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/     var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);// 将旧的push绑定到原数组,保存
/******/     // 在数组实例上定义push函数,prototype上的push会被shadow
/******/     jsonpArray.push = webpackJsonpCallback;
/******/     jsonpArray = jsonpArray.slice();
/******/     for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/     var parentJsonpFunction = oldJsonpFunction;
/******/
/******/
/******/     // run deferred modules from other chunks
/******/     checkDeferredModules();
/******/ })
/************************************************************************/
/******/ ([]);



// app.5a2b9a96b1bb2ab2ca91.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["app"],{

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

"use strict";
__webpack_require__.r(__webpack_exports__);
// 这里定义了模块的导出
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "helloworld", function() { return helloworld; });
var helloworld = function helloworld() {
  return console.log('hello world');
};

/***/ }),

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

"use strict";
__webpack_require__.r(__webpack_exports__);
// 这个模块并没有导出,仅有执行而已
// 这里依赖其他模块所以需要先require
/* harmony import */ var _helloworld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloworld.js");
// 执行之前导入的模块
Object(_helloworld__WEBPACK_IMPORTED_MODULE_0__["helloworld"])();
/***/ })

},[["./src/index.js","runtime"]]]);

这么长,怎么看呀!?

粗略看一眼,runtimeapp都是IIFE,这两个IIFE如何沟通?自然是通过在window上定义全局的变量。在app业务模块中,调用了window["webpackJsonp"],这玩意就是runtime脚本执行后暴露出来的webpack模块加载器