webpack打包原理初解析

1,838 阅读8分钟
  • webpack是一个打包工具,它可以根据模块的依赖关系,结合指定的规则,生成对应的静态文件

webpack打包的文件

toStringTag

  • 通常用来表示该对象的自定义类型标签
let obj = {};
Object.defineProperty(obj, Symbol.toStringTag, { value: "Module" });
console.log(Object.prototype.toString.call(obj)); // [Object Module]

打包后bundle.js基本结构

(function (modules) {

  var installedModules = {}; // 模块的缓存

  function __webpack_require__(moduleId) { // webpack自定义的__webpack_require__函数
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports; //检测模块是否在缓存中存在,如果存在则直接返回模块的的exports,否则创建新的模块
    }
    //创建一个新的模块并且放到模块的缓存中
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    });

    //执行模块函数
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );

    //把模块设置为已经加载
    module.l = true;

    //返回模块的导出对象
    return module.exports;
  }

  //加载入口模块并且返回导出对象
  return __webpack_require__((__webpack_require__.s = "./src/index.js"));
})({
  "./src/index.js": function (module, exports, __webpack_require__) {
    var title = __webpack_require__("./src/title.js");
    console.log(title);
  },
  "./src/title.js": function (module, exports) {
    module.exports = "title";
  },
});
  • 打包的结果是一个自执行函数,参数是代码块所以来的模块,返回是入口模块的导出对象
  • 定义一个自己实现的__webpack_require__函数
  • 先加载入口模块
  • 判断模块在不在缓存中,如果在,则直接返回缓存模块的module.exports,
  • 如果不在,创建一个新的模块,并且让对应的函数执行
  • 将模块设置为已经加载
  • 返回模块的module.exports

__webpack_require__下的各种方法

// 在打包文件中的结构
function __webpack_require__() {}
__webpack_require__.o = function (object, property) {
    ...
};
__webpack_require__.d = function (exports, name, getter) {
  ....
};
...

webpack_require.o判断对象身上是否存在某属性

function __webpack_require__() {}
__webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
};
'注意':object.hasOwnProperty(property)会存在覆盖的情况,因此使用原型
...

webpack_require.d定义getter函数

function __webpack_require__() {}
__webpack_require__.d = function (exports, name, getter) {
  if (!__webpack_require__.o(exports, name)) {
    Object.defineProperty(exports, name, { enumerable: true, get: getter });
  }
};

let obj = {};
__webpack_require__.d(obj, "name", function () {
  return "xxx";
});
...

webpack_require.r为模块添加__esmodule: true

__webpack_require__.r = function() {
   if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    }
    Object.defineProperty(exports, "__esModule", { value: true }); //定义__esModule属性
}

webpack_require.n获取默认导出的函数

  __webpack_require__.n = function (module) {
    var getter =
      module && module.__esModule // 判断是否有__esmodule属性
        ? function getDefault() {
            return module["default"];
          }
        : function getModuleExports() {
            return module;
          };
    __webpack_require__.d(getter, "a", getter); // 给getter上定义a 的getter函数。如果是esModule,返回module['default'],否则返回module
    return getter;
  };
  
  测试:
  1. let obj = {name: 'xxx'}
  let result = __webpack_require__.n(obj)
  console.log(result.a) // {name: 'xxx'}
  
  2. let obj = {__esmodule: true, default: {name: 'xxx'}}
    let result = __webpack_require__.n(obj)
  console.log(result.a) // {name: 'xxx'}
  • 对于es6模块的默认导出,会放在module.exports['default'],其他放在module.exports对象上

webpack_require.t创建一个命名对象,把模块转换为es6模块

/*
* mode & 1 value是模块ID直接用__webpack_require__加载
* mode & 2 把所有的属性合并到命名空间ns上
* mode & 4 已经是ns对象了,可以直接返回值
* mode & 8|1 行为类似于require
import('xxxx').then(r => r)之后一定是个es6模块,
进行互相组合
*/
  __webpack_require__.t = function (value, mode) {
    if (mode & 1) value = __webpack_require__(value); // 是一个模块ID,需要引入
    if (mode & 8) return value;
    if (mode & 4 && typeof value === "object" && value && value.__esModule) // 如果value是一个esmodule,直接返回
      return value;
    var ns = Object.create(null); //如果不是es6转换成es6模块,定义一个空对象
    __webpack_require__.r(ns); 给ns定义__esmodule属性为true
    Object.defineProperty(ns, "default", { enumerable: true, value: value }); // 让ns.default属性为value
    if (mode & 2 && typeof value != "string") // 将value上的属性定义在ns上
      for (var key in value)
        __webpack_require__.d(
          ns,
          key,
          function (key) {
            return value[key];
          }.bind(null, key)
        );
    return ns; // 返回ns
    
  };
  • 如果mode&1,说明value是一个模块ID,先倒入该模块
  • 如果是4,说明value已经是个es6模块了,直接返回value
  • 定义一个空对象作为命名对象
  • 为ns定义__esmodules属性
  • 设置ns.default,并将值设面得到的value
  • 如果mode&2,将value上的属性,映射到ns上并且返回ns

模块打包处理

  • 在 webpack 打包模块中,默认import和require是一样的,最终都是转化成__webpack_require__。

commonJS引commonJS

// title.js 加载commonJS
exports.name = " v";
exports.age = "18";

// index.js采取commonJS
let title = require("./title");
console.log(title.name);
console.log(title.age);

/*
 打包的bundle.js,因为webpack打包之后,将模块都转为commonJS
 将commonJS的require换成自己实现的__webpack_require__,按照commonJS的包装,其他不变
*/
{
"./src/index.js":
  (function(module, exports, __webpack_require__) {
    var title = __webpack_require__("./src/title.js");
    console.log(title.name);
    console.log(title.age);
  }),
"./src/title.js":
  (function(module, exports) {
    exports.name = 'title_name';
    exports.age = 'title_age';
  })
}
  • 打包的bundle.js,因为webpack打包之后,将模块都转为commonJS
  • 将commonJS的require换成自己实现的__webpack_require__,按照commonJS的包装,其他不变

commonJS加载es6

// title.js,采取es6模块
export default 'xxx'
export const age = 18

// index.js commonJS
const title = require('./title.js')
console.log(title.name);
console.log(title.age);

//bundle.js
{
 "./src/index.js":
 (function(module, exports, __webpack_require__) {
    var title = __webpack_require__("./src/title.js");
    console.log(title["default"]);
    console.log(title.age);
 }),
 "./src/title.js":
 (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);//__esModule=true
    __webpack_require__.d(__webpack_exports__, "age", function() { return age; });
    __webpack_exports__["default"] = 'title_name';
    var age = 'title_age';
 }

  • 打包后的commonJS模块,将原来的require转为__webpack_require__
  • 针对es6模块
    • 通过__webpack_require__.r给模块添加__esmodule属性为true,标记为es6Module
    • 批量导出,通过__webpack_require__.d定义对应的属性getter
    • 对于默认导出,将变量放在__webpack_require['default']上

es6加载es6

// title.js
export const age = 18
export default 'xxx'

// index.js
import name, { age } from './title.js'
console.log(name) // 'xxx'
console.log(age) // 18

// bundle.js
{
 "./src/index.js":
 (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);//__esModule=true // 标记为es6模块
    var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/title.js");
    console.log(_title__WEBPACK_IMPORTED_MODULE_0__["default"]); // 默认导出从导出对象的‘default’属性取值
    console.log(_title__WEBPACK_IMPORTED_MODULE_0__["age"]); // 批量导出直接从导出对象中取值
 }),
 "./src/title.js":
 (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);//__esModule=true // 标记为es6模块
    __webpack_require__.d(__webpack_exports__, "age", function() { return age; }); // 对于export,批量导出采用__webpack_require__.d在导出对象上定义属性的getter函数
    __webpack_exports__["default"] = 'title_name'; // 默认导出在__webpack_require__['default']上赋值
    var age = 'title_age';
 })
}

import
  • 针对es6模块,遇到import,先使用__webpack_require__.r标记模块为es6模块
  • 然后通过__webpack_require__引入相应的模块
  • 从导出对象或者['default']属性取值
export
  • 将模块标记为es6模块
  • 批量导出采用__webpack_require__.d在导出对象上定义属性的getter函数
  • 默认导出在__webpack_require__['default']上赋值

es6模块加载commonJS

// title.js
module.exports = {
  name: xxx
  age: 18,
};

// index.js
import name, { age } from "./title";
console.log(name); // {name: "xxx", age: 18} 相当于默认导出
console.log(age); // 

// 打包bundle.js
{
  "./src/index.js": function (module,__webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__);
    var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
      "./src/title.js"
    );
    var _title__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_title__WEBPACK_IMPORTED_MODULE_0__);

    console.log(_title__WEBPACK_IMPORTED_MODULE_0___default.a); //因为要取default,所以要用n包装一下
    console.log(_title__WEBPACK_IMPORTED_MODULE_0__["age"]); //age是正常取属性就可以
    },

    "./src/title.js": function (module, exports) {
      module.exports = {
        name: "title_name",
        age: "title_age",
      };
    },
  "./src/title.js": function (module, exports) {
    module.exports = {
      name: "title_name",
      age: "title_age",
    };
  },
}

  • 针对es6模块
    • 先标记为es6模块
    • 通过__webpack_require__()拿到对应文件的导出对象
    • 通过__webpack_require__.n加工导出对象,因为加载的模块不是esModule,所以指甲返回module,拿到的是{ name: "title_name", age: "title_age", },默认导出
    • 如果要获取通过{age}引入的属性,就可以在导出对象上直接取值
    • name就要通过n包装,得到引入的module.exports
  • commonJS模块几乎不变
为什么es6加载commonJS需要经过n方法处理
  • n最主要的原因是处理 esModule的default + common引入common,没有default的问题,esModule引入esModule,default在各自模块内部处理,使用的时候直接 moduleName.default就可以了。common引入esModule和es引入es一样。只有es引入common,commonJs中是没有default的,但是在es中使用import的变量时,我们会默认为它就是 default,所以才用 .n处理了一下

模块的异步加载

  • webpack ensure有人称他为异步加载,也有人称为代码切割,他其实就是将js模块给独立导出一个.js文件,然后使用这个模块的时候,webpack会构造script dom元素,由浏览器异步请求这个js文件,然后写个回调函数,让请求到的js文件做一些业务操作
准备
  var installedModules = {};
  var installedChunks = { main: 0 }; // 用来存放加载过的和加载中的代码块
  /*
  * 0已经加载完成
  * undefined,未加载
  * promise 加载中
  * null 预加载
  */ 
webpack_require.e
  • 异步加载主要通过__webpack_require__.e实现
function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + chunkId + ".bundle.js";
}
__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    var installedChunkData = installedChunks[chunkId];
    if (installedChunkData !== 0) {
      if (installedChunkData) {
        promises.push(installedChunkData[2]);// 模块正在加载
      } else { // 未加载
        var promise = new Promise(function (resolve, reject) {
          installedChunkData = installedChunks[chunkId] = [resolve, reject]; // installedChunkData是一个数组,第一个是成功的回调,第二个是的回调
        });
        promises.push((installedChunkData[2] = promise)); // 将新生成的promise放入promises队列中,并将新生成的promise当作installedChunkData数组的第三个参数
        var script = document.createElement("script"); // 创建script标签,给script标签添加标签,并将script标签插入head中去请求其他文件
        script.charset = "utf-8";
        script.timeout = 120;
    
        script.src = jsonpScriptSrc(chunkId);
        document.head.appendChild(script);
      }
    }
    return Promise.all(promises);
};
  • 传入一个chunkId,在installedChunks中找到当前模块的状态
  • 如果状态是0,说明模块已经加载成功,直接让promise成功
  • 如果状态不是0
    • 状态是个promise,则说明模块正在加载中
    • 如果是undefined,则表示模块未加载,此时创建一个promise,放入promises队列中,并将生成的promise作为installedChunkData的第三项,此时installedChunkData存放的是[reslove, reject, promise]
  • 创建script标签,去请求文件(Jsonp)
  • 返回promise

总结: webpack_require.e做的事情就是,根据传入的chunkId,去加载这个chunkId对应的异步 chunk 文件,它返回一个promise。通过jsonp的方式使用script标签去加载。这个函数调用多次,还是只会发起一次请求 js 的请求。若已加载完成,这时候异步的模块文件已经被注入到立即执行函数的入参modules变量中了,这个时候和同步执行import调用__webpack_require__的效果就一样了(这个注入由webpackJsonpCallback函数完成)。此时,在promise的回调中再调用__webpack_require__.bind(null, "./src/c.js"") 就能拿到对应的模块,并且执行相关逻辑了。

JSONP返回的文件
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  ["title"],
  {
    "./src/title.js": function (module, exports) {
      exports.name = "title_name";
      exports.age = "title_age";
    },
  },
]);

对照上边的返回

(function (modules) {
    ...
    
    
    
    function webpackJsonpCallback(data) {
        var chunkIds = data[0];  // 拿到chunkids: title
        var moreModules = data[1]; //' moreModules =   {"./src/title.js": fn}'

        var moduleId,
          chunkId,
          i = 0,
          resolves = [];
        for (; i < chunkIds.length; i++) {
          chunkId = chunkIds[i];
          if (installedChunks[chunkId]) { // installedChunks = [reslove, reject, promise]
            resolves.push(installedChunks[chunkId][0]); // 将对应的reslove放在数组中
          }
          installedChunks[chunkId] = 0; // 将模块状态设置为0
        }
        for (moduleId in moreModules) {
          modules[moduleId] = moreModules[moduleId]; // 将额外的模块合并到之前的modules,之前只有一个main.js
        }
        while (resolves.length) {
          resolves.shift()(); // 让所有的promise编程成功态,并且触发回调,这里代表__webpack_require__.e方法中的promise.all成功,就可以接着使用then
        }
    }
    
    
    
    var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []); // 创建一个数组
    var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
    jsonpArray.push = webpackJsonpCallback; // (window["webpackJsonp"] = window["webpackJsonp"] || []).push赋值一个函数
    jsonpArray = jsonpArray.slice();
    for (var i = 0; i < jsonpArray.length; i++)
        webpackJsonpCallback(jsonpArray[i]);
    var parentJsonpFunction = oldJsonpFunction;
    return __webpack_require__((__webpack_require__.s = "./src/main.js"));
})
  • 在__webpack_require__中重新定义定义(window["webpackJsonp"] = window["webpackJsonp"] || [])的push方法(webpackJsonpCallback)
  • 让script请求文件回来,会执行webpackJsonpCallback,去加载额外的模块
  • 将额外的模块合并到入口的modules,并且让所有的promise变成成功态,让代码块的状态变成0,至此使得__webpack_require_.e返回的promise.all成功,接着进行then时通过__webpack_require__就可以对对应的模块进行获取了