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