你不知道的webpack-代码拼装工厂

167 阅读5分钟

这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

引言:本篇文章主要针对webpack打包之后的产物进行分析,类似dist打包目录下的main.js文件。最开始阅读这部分代码时觉得很高大上,一堆英文注释和闭包。其实耐心读起来发现代码执行过程也挺简单,这也多亏了webpack开发人员的高水平代码开发和组织能力,下面先从一个最简单的经webpack打包好的文件开始,删除多余注释(不然仅仅注释就占了一半的代码。。。)

截屏2022-02-16 下午3.51.06.png

一、编译结果分析

  1. 最外层是个典型的自执行函数只有一个入参:modules(webpack模块编译流程最终的产物) (function(modules) {})(modules)

  2. 自执行函数闭包内部创建两个变量:

  • installedModules

  • __webpack_require__

  1. 最后在函数内部执行__webpack_require__,将入口模块路径作为参数传入
return __webpack_require__(__webpack_require__.s = entry);

二、变量具体分析

  1. modules: webpack模块编译、优化过程结束之后,进入代码组装环节。创建全局变量modules去保存每个模块编译后的结果,保存方式是把模块路径作为key,模块编译后的可执行函数作为value。modules其实就是webpack最后组装成自执行函数时的唯一入参,这样当浏览器请求服务端接口后得到该main.js文件,会自动执行脚本处理dom得到最终页面。

  2. installedModules: 保存已执行模块的导出结果,导出结果其实也是模块的概念,不过模块的属性是固定的(i, l, exports)。先初始化一个module对象,然后通过moduleId把这个对象关联到installedModules对象上,作为当前模块执行后结果的引用。

var module = installedModules[moduleId] = {
  i: moduleId,  // 模块Id
  l: false,     // 该模块是否被加载过
  exports: {}   // 该模块执行后导出的结果
}

意义:一个模块同时被多个其他模块引用时可以直接从缓存对象中读取执行后的export结果,提升程序执行效率。可以联想到webpack配置时会使用提取公共代码块到单独文件的优化手段,具体到代码执行层面就是采用这种缓存的方式。

if(installedModules[moduleId]) {
  return installedModules[moduleId].exports;
}
  1. __webpack_require__ :webpack自己重定义了require方法去获取并执行每个模块,只需要传入一个模块Id(moduleId),就可以根据modules[moduleId]定位到具体可执行函数,然后通过Function.call()方法去执行函数。
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)
    //初始化新的模块,并在模块执行之后将结果存放到installedModules
    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__的实现代码粘贴出来,是想让大家关注一个与commonjs相关的知识点。我们在nodejs中使用模块导入、导出的时候,经常会遇到下面这样很神奇的事情:

// lib.js
exports.a = 'Tom';
exports.add = function(a, b) {
    return a + b;
}
exports.b = {
    name: 'Jack'
}

module.exports = function sub(a, b) {
    return a - b;
}
// index.js
let lib = require('lib.js')
// What is lib ?

这里如果有另外一个文件index.js需要引入lib.js会得到什么呢?答案是只有sub!!!因为lib.js文件最后使用module.exports进行赋值,这样会覆盖前面的exports对象。原理其实也很简单,可以参考webpack编译后每个模块的实际运行代码块,modules[moduleId].call(module.exports, module, module.exports, __webpack_require__),明显exports就是module对象上的一个属性,所以在lib.js内部,module.exports完全等于exports

上面这段代码其实还有个很高明的地方,不得不感叹Webpack开发者的巧妙设计。每个模块的执行结果不仅会通过exports被其他模块引入使用,还通过module对象引用又被保存到了installedModule数组中。这样来看,巧妙使用js语言中复杂类型通过引用传值的特性,也可以设计出很好的程序。尽管日常需求开发中经常因为引用传值出现bug。。。

分享到这里就结束了,感兴趣的小伙伴可以自己在本地写几个测试文件,通过模块之间的导出和引入设计可执行程序,最后使用webpack进行打包,记得将mode设置成"development",devtool设置为"source-map",这样方便学习理解webpack打包后的代码。其实webpack最后的自执行函数内部处理还有很多优化操作,webpack为__webpack_require__函数对象又新增了不少属性,包括入参modules和installedModules变量的引用,这样看的话__webpack_require__才是隐藏最深的boss。

四、彩蛋

最后分享一个彩蛋,webpack既可以打包前端页面项目,又可以打包nodejs后端服务项目,但是两者的模块机制其实是不一样的,前端使用esModule,nodejs使用commonjs,那么在webpack内部到底是怎样兼容处理的呢?

这里我先放出一段webpack编译后的代码,留着下篇文章继续分享!

ES6新增内置对象的Symbol.toStringTag属性值:Module 对象M[Symbol.toStringTag]:'Module'

  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
  // 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;
  };