require.context 实际上是一个很是实用的 api。可是 3-4 年过去了,却依旧还有不少人不知道如何使用。javascript
而这个 api 主要为咱们作什么样的事情?它能够帮助咱们动态加载咱们想要的文件,很是灵活和强大(可递归目录)。 能够作 import 作不到的事情。今天就带你们一块儿来分析一下,webpack 的 require.context
是如何实现的。java
在分析这个 api 以前呢,咱们须要先了解一下一个最简单的文件,webpack 会编译成啥样。webpack
-- src
-- index.ts
复制代码
// index.ts console.log(123) 复制代码
编译以后,咱们能够看见 webpack 会编译成以下代码git
// 源码 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'); }) }); 复制代码
初次一看是很乱的,因此为了梳理结构,我帮你们去除一些跟本文可有可无的。其实主要结构就是这样而已,代码很少为了以后的理解,必定要仔细看下每一行github
// 源码地址 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__
就是一个模块加载器,而咱们全部的模块都会以对象的形式被读取加载web
modules = { './src/index.ts': function(module, exports, __webpack_require__) { console.log('123'); } } 复制代码
咱们把这样的结构先暂时称之为 模块结构对象
api
了解了主体结构以后咱们就能够写一段require.context
来看看效果。咱们先新增 2 个 ts 文件而且修改一下咱们的 index.ts,以便于测试咱们的动态加载。promise
--- 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)了。markdown
接着咱们看看编译后 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 在 2.6 版本后,在加载模块时,能够指定 webpackMode
模块加载模式,咱们能使用几种方式来控制咱们要加载的模块。经常使用的 mode通常为sync
lazy
lazy-once
eager
因此在 require.context 是同样适用的,咱们若是查看一下@types/webpack-env
就不难发现它还有第四个参数。
简要来讲
sync
直接打包到当前文件,同步加载并执行lazy
延迟加载会分离出单独的 chunk 文件lazy-once
延迟加载会分离出单独的 chunk 文件,加载过下次再加载直接读取内存里的代码。eager
不会分离出单独的 chunk 文件,可是会返回 promise,只有调用了 promise 才会执行代码,能够理解为先加载了代码,可是咱们能够控制延迟执行这部分代码。这部分文档很隐晦,也多是文档组没有跟上,因此若是咱们去看 webpack 的源码的话,能够发现真正实际上是有 6 种 mode。
mode类型定义 github.com/webpack/web…
那 webpack 究竟是如何作到可递归获取咱们的文件呢?在刚刚上面的源码地址里咱们能发现这样一行代码。
这一看就是去寻找咱们所须要的模块。因此咱们跟着这行查找具体的源码。
这就是 require.context 是如何加载到咱们文件的具体逻辑了。其实就是fs.readdir
而已。最后获取到文件以后在经过 context 加载器来生成咱们的模块结构对象。好比这样的代码就是负责生成咱们sync
类型的context加载器。你们能够具体在看别的5种类型。
6种类型加载逻辑而且生成 context 加载器的模块结构对象 github.com/webpack/web…
1.学习了解 webpack 是如何组织加载一个模块的,webpack 的加载器如何运做,最后如何生成编译后的代码。
2.原本仅仅只是想了解 require.context
如何实现的,却发现了它第三个参数有 6 种mode,这部分却也是 webpack 文档上没有的。
3.从一个实用的 API 出发,探索了该 api 的实现原理,而且一块儿阅读了部分 webpack 源码。
4.探索本质远比你成为 API 的搬运工更重要。只有你不断地探寻本质,才能够发现世界的奥秘。
最后抛砖引玉,能够按照这样的思路再去学习另外 6 种 mode 编译后的代码。
文章里编译后的代码,都在这里 >>> github.com/MeCKodo/req…
我的网站 >>> www.meckodo.com
厦门 RingCentral 外企,福利待遇厦门顶尖
5点半下班 5点半下班 5点半下班
有须要的联系我~