共计 6238 个字符,预计需要花费 16 分钟才能阅读完成。
前言
require.context 其实是一个非常实用的 api。但是 3-4 年过去了,却依旧还有很多人不知道如何使用。
而这个 api 主要为我们做什么样的事情?它可以帮助我们动态加载我们想要的文件,非常灵活和强大 (可递归目录)。 可以做 import 做不到的事情。今天就带大家一起来分析一下,webpack 的 require.context
是如何实现的。
准备工作
在分析这个 api 之前呢,我们需要先了解一下一个最简单的文件,webpack 会编译成啥样。
-- src
-- index.ts
// index.ts
console.log(123)
编译之后,我们可以看见 webpack 会编译成如下代码
// 源码 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/bundle-only-index.js
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
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)
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;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
// define __esModule on exports
__webpack_require__.r = function(exports) {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;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {return Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.ts");
})
({"./src/index.ts": (function(module, exports) {console.log('123');
})
});
初次一看是很乱的,所以为了梳理结构,我帮大家去除一些跟本文无关紧要的。其实主要结构就是这样而已,代码不多为了之后的理解,一定要仔细看下每一行
// 源码地址 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/webpack-main.js
(function(modules) {// 缓存所有被加载过的模块(文件)
var installedModules = {};
// 模块 (文件) 加载器 moduleId 一般就是文件路径
function __webpack_require__(moduleId) {
// 走 cache
if (installedModules[moduleId]) {return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache) 解释比我清楚
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}});
// 执行我们的模块(文件) 目前就是 ./src/index.ts 并且传入 3 个参数
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;
}
// 开始加载入口文件
return __webpack_require__((__webpack_require__.s = './src/index.ts'));
})({'./src/index.ts': function(module, exports, __webpack_require__) {console.log('123');
}
});
__webpack_require__
就是一个模块加载器,而我们所有的模块都会以对象的形式被读取加载
modules = {'./src/index.ts': function(module, exports, __webpack_require__) {console.log('123');
}
}
我们把这样的结构先暂时称之为 模块结构对象
正片
了解了主体结构之后我们就可以写一段 require.context
来看看效果。我们先新增 2 个 ts 文件并且修改一下我们的 index.ts,以便于测试我们的动态加载。
--- src
--- demos
--- demo1.ts
--- demo2.ts
index.ts
// index.ts
// 稍后我们通过源码分析为什么这样写
function importAll(contextLoader: __WebpackModuleApi.RequireContext) {contextLoader.keys().forEach(id => console.log(contextLoader(id)));
}
const contextLoader = require.context('./demos', true, /\.ts/);
importAll(contextLoader);
查看我们编译后的源码,发现多了这样一块的 模块结构对象
// 编译后代码地址 https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/contex-sync.js#L82-L113
{'./src/demos sync recursive \\.ts': function(module, exports, __webpack_require__) {
var map = {
'./demo1.ts': './src/demos/demo1.ts',
'./demo2.ts': './src/demos/demo2.ts'
};
// context 加载器,通过之前的模块加载器 加载模块(文件)
function webpackContext(req) {var id = webpackContextResolve(req);
var module = __webpack_require__(id);
return module;
}
// 通过 moduleId 查找模块 (文件) 真实路径
// 个人在这不喜欢 webpack 内部的一些变量命名,moduleId 它都会编译为 request
function webpackContextResolve(req) {
// id 就是真实文件路径
var id = map[req];
// 说实话这波操作没看懂,目前猜测是 webpack 会编译成 0.js 1.js 这样的文件 如果找不到误加载就出个 error
if (!(id + 1)) {
// check for number or string
var e = new Error('Cannot find module"' + req + '".');
e.code = 'MODULE_NOT_FOUND';
throw e;
}
return id;
}
// 遍历得到所有 moduleId
webpackContext.keys = function webpackContextKeys() {return Object.keys(map);
};
// 获取文件真实路径方法
webpackContext.resolve = webpackContextResolve;
// 该模块就是返回一个 context 加载器
module.exports = webpackContext;
// 该模块的 moduleId 用于 __webpack_require__ 模块加载器
webpackContext.id = './src/demos sync recursive \\.ts';
}
我在源码中写了很详细的注释。看完这段代码就不难理解文档中所说的 require.context
会返回一个带有 3 个 API 的函数(webpackContext) 了。
接着我们看看编译后 index.ts
的源码
'./src/index.ts': function(module, exports, __webpack_require__) {function importAll(contextLoader) {contextLoader.keys().forEach(function(id) {
// 拿到所有 moduleId,在通过 context 加载器去加载每一个模块
return console.log(contextLoader(id));
});
}
var contextLoader = __webpack_require__('./src/demos sync recursive \\.ts');
importAll(contextLoader);
}
很简单,可以发现 require.context
编译为了 __webpack_require__
加载器并且加载了 id 为 ./src/demos sync recursive \\.ts
的模块,sync
表明我们是同步加载这些模块(之后我们在介绍这个参数),recursive
表示需要递归目录查找。自此,我们就完全能明白 webpack 是如何构建所有模块并且动态加载的了。
进阶深入探究 webpack 源码
我们知道 webpack 在 2.6 版本后,在加载模块时,可以指定 webpackMode
模块加载模式,我们能使用几种方式来控制我们要加载的模块。常用的 mode 一般为sync
lazy
lazy-once
eager
所以在 require.context 是一样适用的,我们如果查看一下 @types/webpack-env
就不难发现它还有第四个参数。
简要来说
-
sync
直接打包到当前文件,同步加载并执行 -
lazy
延迟加载会分离出单独的 chunk 文件 -
lazy-once
延迟加载会分离出单独的 chunk 文件,加载过下次再加载直接读取内存里的代码。 -
eager
不会分离出单独的 chunk 文件,但是会返回 promise,只有调用了 promise 才会执行代码,可以理解为先加载了代码,但是我们可以控制延迟执行这部分代码。
文档在这里 https://webpack.docschina.org…。
这部分文档很隐晦,也可能是文档组没有跟上,所以如果我们去看 webpack 的源码的话,可以发现真正其实是有 6 种 mode。
mode 类型定义
https://github.com/webpack/we…
那 webpack 到底是如何做到可递归获取我们的文件呢?在刚刚上面的源码地址里我们能发现这样一行代码。
这一看就是去寻找我们所需要的模块。所以我们跟着这行查找具体的源码。
这就是 require.context 是如何加载到我们文件的具体逻辑了。其实就是 fs.readdir
而已。最后获取到文件之后在通过 context 加载器来生成我们的模块结构对象。比如这样的代码就是负责生成我们 sync
类型的 context 加载器。大家可以具体在看别的 5 种类型。
6 种类型加载逻辑并且生成 context 加载器的模块结构对象
https://github.com/webpack/we…
总结
1. 学习了解 webpack 是如何组织加载一个模块的,webpack 的加载器如何运作,最后如何生成编译后的代码。
2. 本来仅仅只是想了解 require.context
如何实现的,却发现了它第三个参数有 6 种 mode,这部分却也是 webpack 文档上没有的。
3. 从一个实用的 API 出发,探索了该 api 的实现原理,并且一起阅读了部分 webpack 源码。
4.探索本质远比你成为 API 的搬运工更重要。只有你不断地探寻本质,才可以发现世界的奥秘。
最后抛砖引玉,可以按照这样的思路再去学习另外 6 种 mode 编译后的代码。
文章里编译后的代码,都在这里 >>> https://github.com/MeCKodo/re…
个人网站 >>> http://www.meckodo.com
最后常年招人
厦门 RingCentral 外企,福利待遇厦门顶尖
5 点半下班 5 点半下班 5 点半下班
有需要的联系我~