Skip to content

Webpack 核心原理

CommonJS 与 AMD

CommonJS

  • js 原生没有模块化规范
  • nodejs 推出了由 requiremodule.exports 实现的模块化规范,即 CommonJS。

AMD

  • Web 环境为满足模块化需求,受 CommonJS 启发,推出 AMD,Asynchronous Module Definition
  • RequireJS 提供 define 定义模块,require 引入依赖的模块
  • RequireJS 利用 JsonP 来对模块文件进行异步加载

UMD

  • 可运行与 nodejs 环境和 Web 环境
  • 没有自己专有的规范,是集结了 CommonJS、AMD 的规范于一身
  • UMD 的实现如下:
((root, factory) => {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['jquery'], factory);
  } else if (typeof exports === 'object') {
    // CommonJS
    var $ = require('jquery');
    module.exports = factory($);
  } else {
    root.testModule = factory(root.jQuery);
  }
})(this, ($) => {
  // TO DO
});

ESModule

  • ES6 之后,在语言层面,通过关键词 importexport 对模块导入导出。
  • JS 解释器(JS Core) 在不同宿主环境是一样的;NodeJS(global、process),浏览器(document、window),小程序(wx、swan)在解释器的基础之上,封装一些环境相关的 API。
  • ESModule 属于 JS Core 层的规范,而 CommonJS,AMD 是运行环境层面的规范。

webpack 打包流程

  • 1) 读取入口文件内容
  • 2) 分析入口文件,递归读取模块所依赖的文件内容,生产 AST 语法树
    • 通过 @babel/parser 转成 AST
    • 通过 @babel/traverser 遍历 AST
    • 通过 @babel/core@babel/preset-env,ES6 转 ES5
  • 3) 根据 AST 语法树,生成浏览器可运行代码

简单实现模块解析

  • 以下代码是 webpack 主要流程的简单实现
  • 入口文件 ./src/index.js
  • node bundle.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser'); // 转成 AST
const traverse = require('@babel/traverse').default; // 遍历 AST
const babel = require('@babel/core');

// 1. 解析模块
function getModuleInfo(file) {
  let body = fs.readFileSync(file, 'utf-8');
  // 转成 AST
  let ast = parser.parse(body, {
    sourceType: 'module' // 解析 es6
  });
  // console.dir(ast, ast.program.body);

  // 遍历 AST,收集依赖
  let deps = {};
  traverse(ast, {
    ImportDeclaration: (nodePath) => {
      // console.log(nodePath)
      let val = nodePath.node.source.value;
      let dirname = path.dirname(file);
      deps[val] = './' + path.join(dirname, val);
    }
  });

  // 将 ast 转换成 es5
  let { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env']
  });

  return { file, code, deps };
}

// 2. 收集所有依赖
function parseModules(file) {
  const depsGraph = {};

  // 广度优先遍历
  let queue = [getModuleInfo(file)];
  for(let i = 0; info = queue[i++];) {
    depsGraph[info.file] = {
      code: info.code,
      deps: info.deps
    };

    for(let k in info.deps) {
      let file = info.deps[k];
      if (!depsGraph[file]) { // 没有访问过
        queue.push(getModuleInfo(file));
      }
    }
  }

  return depsGraph;
}

// 3. 生成打包代码
function bundle(file) {
  let graph = JSON.stringify(parseModules(file));

  let codeStr = `
  (function(depsGraph) {
    var installedModules = {}; // 模块缓存对象,避免循环引用

    function _require(file) {
      if (installedModules[file]) {
        return installedModules[file].exports;
      }

      var exports = {};
      installedModules[file] = {
        i: file,
        exports
      }

      function absRequire(path) {
        let absPath = depsGraph[file].deps[path];
        return _require(absPath);
      }

      (function(require, exports, code){ // 递归执行 require
        eval(code);
      })(absRequire, exports, depsGraph[file].code)

      return exports;
    }

    console.log(installedModules);
    _require('${file}'); // 入口文件
  })(${graph});
  `;

  return codeStr;
}

let content = bundle('./src/index.js');
// console.log(content);

// 4. 写入文件
if (!fs.existsSync('./dist')) {
  fs.mkdirSync('./dist');
}
fs.writeFileSync('./dist/bundle.js', content);

同步模块之 require

  1. 读取所有的模块,存入 modeuleList
  2. 收集依赖,保存在 modeuleDepMapList
  3. 从入口文件开始递归 require(0)
(function() {
  // const modeulePathIdMap = {};
  // STEP 1. 同步模块
  const modeuleList = [
    // index.js
    function(require, module, exports) {
      const moduleA = require('./moduleA.js'); // this is a parentModule
      console.log('this is index.js', moduleA);
    },
    // moduleA.js
    function(require, module, exports) {
      console.log('this is moduleA.js');
      module.exports = 123;
    }
  ];
  const modeuleDepMapList = [
    // index.js 模块的依赖
    {
      './moduleA.js': 1
    },
    // moduleA.js 模块的依赖
    {}
  ];

  function require(id, parentModuleId) {
    var currentModuleId = parentModuleId !== undefined ? modeuleDepMapList[parentModuleId][id] : id;
    var moduleFunc = modeuleList[currentModuleId];
    console.log(id, moduleFunc);

    var module = {
      exports: {}
    };
    var _require = id => require(id, currentModuleId); // 递归先执行依赖的模块
    _require.ensure = require.ensure;
    moduleFunc(_require, module, module.exports);
    return module.exports;
  }

  require(0); // 测试同步模块入口
})()

异步模块之 require.ensure

  1. 异步模块的代码打包会成单独的 chunk
  2. 定义一个 window.__JSONP 函数,入参为 chunkId 和 回调函数 moduleFunc
  3. require.ensure 加载 chunk 的异步代码,同时创建一个 promise,缓存在全局 cache
  4. chunk 加载完后,立即执行 __JSONP() 函数,并传入定义好的回调函数
  5. __JSONP() 函数内部,会获取缓存起来的 promise,执行回调函数 moduleFunc,然后 resolve 掉。

异步 chunk 文件

// 文件名: 1.chunk.js
__JSONP(1, function(require, module, exports) {
  console.log('this is in 1.chunk.js');
  module.exports = 456;
});

require.ensure

(function() {
  // const modeulePathIdMap = {};
  // STEP 1. 同步模块
  const modeuleList = [
    // async-index.js
    function(require, module, exports) {
      require.ensure('1').then(content => {
        console.log(content);
      });
    }
  ];
  const modeuleDepMapList = [{}];

  function require(id, parentModuleId) {
    var currentModuleId = parentModuleId !== undefined ? modeuleDepMapList[parentModuleId][id] : id;
    var moduleFunc = modeuleList[currentModuleId];
    console.log(id, moduleFunc);

    var module = {
      exports: {}
    };
    var _require = id => require(id, currentModuleId); // 递归先执行依赖的模块
    _require.ensure = require.ensure;
    moduleFunc(_require, module, module.exports);
    return module.exports;
  }

  // STEP 2. 异步模块
  const cache = {}; // 全局缓存模块状态
  // const chunkModeulePathIdMap = {};
  // const chuckModeuleList = [];

  window.__JSONP = function(chunkId, moduleFunc) {
    var currentChunkPromise = cache[chunkId];
    var resolve = currentChunkPromise[0];

    var module = {
      exports: {}
    };
    moduleFunc(require, module, module.exports);
    resolve(module.exports);
  }

  require.ensure = function(chunkId, parentModuleId) {
    var currentModuleId = parentModuleId !== undefined ? modeuleDepMapList[parentModuleId][chunkId] : chunkId;
    var chunkPromise = cache[currentModuleId];

    // 没缓存
    if (chunkPromise === undefined) {
      var $script = document.createElement('script');
      $script.src = currentModuleId + '.chunk.js';
      document.head.append($script);

      var promise = new Promise(function(resolve, reject) {
        cache[currentModuleId] = [resolve]; // 缓存 resolve 和 promise
      });
      cache[currentModuleId].push(promise);

      return promise;
    }

    // 有缓存
    if (chunkPromise) {
      return chunkPromise[1];
    }
  }

  require(0); // 测试异步模块入口
})()

习题

说一下模块打包运行原理?

打包流程

  • 1、读取 webpack 的配置参数;

  • 2、启动 webpack,创建 Compiler 对象并开始解析项目;

  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;

  • 4、对不同文件类型的依赖模块文件使用对应的 Loader 进行编译,最终转为 Javascript 文件;

  • 5、整个过程中 webpack 会通过发布订阅模式,向外抛出一些 hooks,而 webpack 的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

核心模块

文件的解析与构建, 主要依赖于两个核心对象: compilercompilation

  • compiler 对象是一个全局单例,他负责把控整个 webpack 打包的构建流程。
  • compilation 对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息。每次热更新和重新构建,compiler 都会重新生成一个新的 compilation 对象,负责此次更新的构建过程。
  • 而每个模块间的依赖关系,则依赖于 AST 语法树。 每个模块文件在通过Loader解析完成之后,会通过 acorn 库 生成模块代码的 AST 语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。

打包结果

整个 立即执行函数 里边只有三个变量和一个函数方法:

  • __webpack_modules__ 存放了编译后的各个文件模块的JS内容;
  • __webpack_module_cache__ 用来做模块缓存;
  • __webpack_require__ 是Webpack内部实现的一套依赖引入函数。
  • 最后一句则是代码运行的起点,从入口文件开始,启动整个项目。
// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
    var __webpack_modules__ = ({
        'file-A-path': ((modules) => { // ... })
        'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
    })

    // The module cache
    var __webpack_module_cache__ = {};

    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
                return cachedModule.exports;
        }
        // Create a new module (and put it into the cache)
        var module = __webpack_module_cache__[moduleId] = {
                // no module.id needed
                // no module.loaded needed
                exports: {}
        };

        // Execute the module function
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");

        // Return the exports of the module
        return module.exports;
    }

    // startup
    // Load entry module and return exports
    // This entry module can't be inlined because the eval devtool is used.
    var __webpack_exports__ = __webpack_require__("./src/index.js");
})