require.context其实是一个非常实用的api。但是3-4年过去了,却依旧还有很多人不知道如何使用。
而这个api主要为我们做什么样的事情?它可以帮助我们动态加载我们想要的文件,非常灵活和强大(可递归目录)。可以做进口做不到的事情。今天就带大家一起来分析一下,webpack的require.context是如何实现的。
准备工作
在分析这个api之前呢,我们需要先了解一下一个最简单的文件,webpack会编译成啥样。
-src
-索引。TS
//index.ts
控制台。日志(123)
编译之后,我们可以看见webpack会编译成如下代码
//源码https://github.com/MeCKodo/require-context-sourece/blob/master/simple-dist/bundle-only-index.js
(功能(模块){//webpackBootstrap
//模块高速缓冲存储器
VAR installedModules={};
//require函数
功能__webpack_require__(的moduleId){
//检查模块是在缓存中
,如果(installedModules[的moduleId]){
返回installedModules[的moduleId。出口;
}
//创建一个新的模块(并把它进入缓存)
var module=installedModules[moduleId]={
i:moduleId,
l:false,
exports:{}
};
//执行模块功能
模块[moduleId]。调用(模块。出口,模块,模块,产品出口,__webpack_require__);
//将模块标记为已加载
模块。l=真;
//返回模块
返回模块的导出。出口;
}
//公开模块对象(__webpack_modules__)
__ webpack_require__。m=模块;
//公开模块缓存
__webpack_require__。c=installedModules;
//为和谐输出定义getter函数
__webpack_require__。d=function(exports,name,getter){
if(!__ webpack_require__。o(出口,名称)){
对象。defineProperty(exports,name,{
configurable:false,
enumerable:true,
get:getter
});
}
};
//在__webpack_require__上定义
__esModule。r=function(exports){
Object。defineProperty(exports,’__ myModule’,{value:是的});
};
//getDefaultExport函数,用于兼容非和谐模块
__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);
返回吸气;
};
//Object.prototype.hasOwnProperty.call
__webpack_require__。o=function(object,property){return Object。原型。hasOwnProperty。呼(对象,财产);};
//__webpack_public_path__
__webpack_require__。p=“”;
//加载输入模块并返回导出
返回__webpack_require__(__ webpack_require__。s=“。/src/index.ts”);
})
({
“./src/index.ts”:(功能(模块,出口){
控制台。日志(’123’);
})
});
初次一看是很乱的,所以为了梳理结构,我帮大家去除一些跟本文无关紧要的。其实主要结构就是这样而已,代码不多为了之后的理解,一定要仔细看下每一行
//源码地址https://github.com/MeCKodo/require-context-sourece/
blob/master/simple-dist/webpack-main.js
(function(modules){
//缓存所有被加载过的模块(文件)
var installedModules={};
//模块(文件)加载器moduleId一般就是文件路径
功能__webpack_require__(的moduleId){
//走缓存
如果(installedModules[的moduleId]){
返回installedModules[的moduleId。出口;
}
//创建一个新的模块(和将其放入缓存)解释比我清楚
变种模块=(installedModules[moduleId]={
i:moduleId,
l:false,
exports:{}
});
//执行我们的模块(文件)目前就是./src/index.ts并且传入3个参数
模块[moduleId]。调用(
模块。出口,
模块,
模块,产品出口,
__webpack_require__
);
//将模块标记为已加载解释比我清楚
模块。l=真;
//返回模块的出口解释比我清楚
返回模块。出口;
}
//开始加载入口文件
return __webpack_require __((__ webpack_require__。s=’。/src/index.ts’));
})({
”./src/index.ts’:功能(模块,出口,__webpack_require__){
控制台。登录(’123’);
}
});
__webpack_require__就是一个模块加载器,而我们所有的模块都会以对象的形式被读取加载
modules={
‘。/src/index.ts’:function(module,exports,__ webpack_require__){
console。log(’123’);
}
}
我们把这样的结构先暂时称之为模块结构对象
正片
了解了主体结构之后我们就可以写一段require.context来看看效果。我们先新增2个ts文件并且修改一下我们的index.ts,以便于测试我们的动态加载。
—src
—演示
—demo1。ts
—demo2。ts
指数。TS
//index.ts
//稍后我们通过源码分析为什么这样写
功能importAll(的ContextLoader:__WebpackModuleApi。RequireContext){
的ContextLoader。键()。的forEach(ID=>控制台。日志(的ContextLoader(ID)));
}
常量的ContextLoader=需要。context(’./demos’,true,/\。if/);
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={
‘。/dev1.ts’:’。/src/demos/demo1.ts’,
‘。/demo2.ts’:’。/src/demos/demo2.ts’
};
//context加载器,通过之前的模块加载器加载模块(文件)
function webpackContext(req){
var id=webpackContextResolve(req);
var module=__webpack_require __(id);
返回模块;
}
//通过moduleId查找模块(文件)真实路径
//个人在这不喜欢webpack内部的一些变量命名,moduleId它都会编译为请求
函数webpackContextResolve(req){
//id就是真实文件路径
var id=map[req];
//说实话这波操作没看懂,目前猜测是webpack会编译成0.js 1.js这样的文件如果找不到误加载就出个错
if if(!(id+1)){
//check对于数字或字符串
var e=new Error(’找不到模块”+req+’“。’);
e。code=’MODULE_NOT_FOUND’;
扔é;
}
return id;
}
//遍历得到所有moduleId
webpackContext。keys=function webpackContextKeys(){
return Object。键(地图);
};
//获取文件真实路径方法
webpackContext。resolve=webpackContextResolve;
//该模块就是返回一个上下文加载器
模块。exports=webpackContext;
//该模块的moduleId用于__webpack_require__模块加载器
webpackContext。id=’。/src/demos sync recursive\\。ts’;
}
我在源码中写了很详细的注释。看完这段代码就不难理解文档中所说的require.context会返回一个带有3个API的函数(webpackContext)了。
image.png
我们接着看看compile-后index.ts的源码
”./src/index.ts’:功能(模块,出口,__webpack_require__){
函数importAll(的ContextLoader){
的ContextLoader。键()。的forEach(函数(ID){
//拿到所有的moduleId,在通过上下文加载器去加载每一个模块
返回控制台。日志(的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模块加载模式,我们能使用几种方式来控制我们要加载的模块。常用的模式一般为sync lazy lazy-once eager
所以在require.context是一样适用的,我们如果查看一下 types/webpack-env就不难发现它还有第四个参数。
image.png
简要来说
sync直接打包到当前文件,同步加载并执行
lazy延迟加载会分离出单独的chunk文件
lazy-once延迟加载会分离出单独的chunk文件,加载过下次再加载直接读取内存里的代码。
eager不会分离出单独的chunk文件,但是会返回promise,只有调用了承诺才会执行代码,可以理解为先加载了代码,但是我们可以控制延迟执行这部分代码。
文档在这里https://webpack.docschina.org/api/module-methods/#magic-comments。
这部分文档很隐晦,也可能是文档组没有跟上,所以如果我们去看webpack的源码的话,可以发现真正其实是有6种模式。
模式类型定义
https://github.com/webpack/webpack/blob/master/lib/ContextModule.js#L13
那个webpack到底是如何做到可递归获取我们的文件呢?在刚刚上面的源码地址里我们能发现这样一行代码。
image.png
这一看就是去寻找我们所需要的模块。所以我们跟着这行查找具体的源码。
image.png
这就是require.context是如何加载到我们文件的具体逻辑了。其实就是fs.readdir而已。最后获取到文件之后在通过context加载器来生成我们的模块结构对象。比如这样的代码就是负责生成我们sync类型的上下文加载器。大家可以具体在看别的5种类型。
image.png
6种类型加载逻辑并且生成context加载器的模块结构对象
https://github.com/webpack/webpack/blob/master/lib/ContextModule.js
总结
1.学习了解webpack是如何组织加载一个模块的,webpack的加载器如何运作,最后如何生成编译后的代码。
2.本来仅仅是想想解require.context如何实现的,却发现了它第三个参数有6种模式,这部分却也是webpack文档上没有的。
3.从一个实用的API出发,探索了该api的实现原理,并且一起阅读了部分webpack源码。
4.学会一个小小的API使用很简单,但是如果你去探寻本质,可以发现许多边边角角有意思的东西。探索本质远比你成为API的搬运工更重要。
最后留个作业,大家可以按照这样的思路再去学习另外6种模式编译后的代码。
文章里编译后的代码,都在这里>>>https://github.com/MeCKodo/require-context-sourece