webpack 源码笔记

前言

为了了解清楚webpack的每个环节,及其对应的工作、使用的技术。最好能实现一个自己的打包器:不同类型模块的加载,不同loader,插件的使用

webpack 构建流程

image.png

  • 初始化参数:根据命令窗口输入参数以及 webpack.config.js 配置,得到最终配置
  • 开始编译:根据最终配置初始化一个 compiler 对象,注册所有插件 plugins,并开始监听 webpack 构建过程中生命周期环节(事件),不同环节会有相应处理,然后开始编译。
  • 确定入口:根据 webpack.config.js 中 entry 入口,开始解析文件构建 AST 语法树,找出依赖,递归下去。
  • 编译模块:根据文件类型和 loader 配置,取对应 loader 进行转换处理,并找出依赖模块,再递归下去,直到所有依赖模块都处理完成。
  • 编译过程:不同插件,执行不同的事。例如:clean-webpack-plugin ,会在结果输出前清除 dist 目录
  • 完成编译并输出:递归结束,得到每个文件结果,根据 entry 和 output 等配置生成 chunk 代码块
  • 打包完成:根据 output 输出所有的 chunk 到相应的文件目录

打包后文件 bundle.js分析

整体结构

(function (modules) { // webpackBootstrap...
})
/************************************************************************/
  ({
    "./src/index.js":
      /*! no static exports found */
      (function (module, exports) {
        console.log('index.js内容')
        module.exports = '入口文件导出内容'
      })
  });
复制代码

以上是单文件引入打包后的文件,可以分析:

  1. webpack打包后,整体来说,其实是一个自执行函数
  2. 传参是一个对象,键为入口文件路径:./src/index.js,值为打包完成输出的模块代码,并用一个函数包裹。并传递给上面的 function (modules){} 执行

那么,接下来我们看看,function (modules){}函数中每个部分对应的大体功能

(function(modules) {
  // 缓存已加载过的模块
  var installedModules = {};

  // webpack 自定义的一个加载方法,核心功能就是返回被加载模块中导出的内容(具体内部是如何实现的,后续再分析)
  function __webpack_require__(moduleId) {

    // 检查是否在缓存中,在则返回模块内容
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 创建一个新的模块,并放入缓存中
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // 判断是否存在 指定属性
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // 模块是否已加载过 flag
    module.l = true;

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

  // 将模块定义保存一份,通过 m 属性挂载到自定义的方法身上
  __webpack_require__.m = modules;

  // 将缓存通过 c 属性,挂载到自定义方法上
  __webpack_require__.c = installedModules;

  // 检测对象 有无指定属性 **** ,如果有则返回 true 
  __webpack_require__.o = function (object, property) { 
      return   Object.prototype.hasOwnProperty.call(object, property); 
  };

  // 检测exports 有无name 属性,没有则加上,并可通过getter 访问name属性
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };
  
  // 给 exports加上标记,通过标记,即可知道是否为 esModule
  __webpack_require__.r = function (exports) {
    // 下面的条件如果成立就说明是一个  esModule 
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      // Object.prototype.toString.call(exports)
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    // 如果条件不成立,我们也直接在 exports 对象的身上添加一个 __esModule 属性,它的值就是true 
    Object.defineProperty(exports, '__esModule', { value: true });
  };

  // 创建一个伪命名空间对象
  // 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) {
    // 01 拿到被加载模块中的内容 value 
    // 02 这里 value 可能会直接返回,也可能会处理之后再返回
    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);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    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;
  };

  // 定义一个getter ,并返回
  __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.config 配置文件中的 output 的 public_path 值会体现在这里
  __webpack_require__.p = "";
  

  // 导出模块,s 属性用于缓存,模块主入口的模块Id值
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
复制代码

webpack 编译流程

webpack 的编译过程可大致分为以下3个阶段:

  1. 配置初始化
  2. 内容编译
  3. 输出编译后内容

而这3个阶段的整体执行过程,就可以看做是一次事件驱动型事件工作流机制,它就可以将不同的插件串联起来,然后完成所有的工作。其中最为核心的是负责编译的compiler,负责创建bundles的 compilation。 它俩也是tapable的实例对象。

tapable本身是一个独立的库

  • 实例化hook注册事件监听
  • 通过hook触发时间监听
  • 执行懒编译生成的可执行代码

tapable 中 Hook 执行特点

  • Hook: 普通钩子,监听器间互不干扰
  • BailHook:熔断钩子,当某个监听返回非undefined时,后续监听不执行
  • WaterfallHook:瀑布钩子,上一个监听的返回值可传递到下一个
  • LoopHook: 循环钩子,如果没有返回false 则一直执行
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享