从零实现一个迷你Webpack
大厂技术坚持周更精选好文
本文为来自字节跳动国际化电商S项目的文章,已授权ELab发布。
webpack是当前使用较多的一个打包工具,将众多代码组织到一起使得在浏览器下可以正常运行,下面以打包为目的,实现一个简易版webpack,支持单入口文件的打包,不涉及插件、分包等。
前置知识
举个,先来看看下面这个demo,例子很简单,一个index。js,里面引用了一个文件a。js,a。js内部引入了b。js,通过webpack最简单的配置,将index。js文件作为入口进行打包。
来看看打包后的内容是怎样的index。js
require(。a。js);
console。log(entryload);
a。js
require(。b。js);
consta1;
console。log(aload);
module。exportsa;
b。js
console。log(bload);
constb1;
module。exportsb;
可以看到打包产物是一个立即执行函数,函数初始先定义了多个module,每个module是实际代码中被require的文件内容,同时由于浏览器不支持require方法,webpack内部自行实现了一个webpackrequire,并将代码中的require全部替换为该函数(从打包结果可看出)。
在webpackrequire定义之后,便开始执行入口文件,同时可以看出,webpack的打包过程便是通过入口文件,将直接依赖和间接依赖以module的形式组织到一起,并通过自行实现的require实现模块的同步加载。
了解了打包产物后,便可以开始实现简易版的webpack,最终打包产物与webpack保持一致。
初始化参数
根据Node接口webpack中文文档〔1〕可以知道,webpacknodeapi对外暴露出了webpack方法,通过调用webpack方法传入配置,返回compiler对象,compiler对象包含run方法可执行编译,即constwebpackrequire(webpack);引用webpack
constcompilerwebpack(options);传入配置生成compiler对象
compiler。run((err,stats){执行编译,传入回调
});
因此,首先需要实现一个webpack方法,同时该方法支持传入webpack配置,返回compiler实例,webpack官方支持了以cli的形式运行webpack命令和指定参数、配置文件,这一部分暂时简单实现,我们暴露出一个方法,方法接收用户的配置。miniwebpackcoreindex。js
functionstrongtoutiaooriginspanclasshighlighttextwebpackstrong{
创建compiler对象
constcompilernewCompiler(options);
}
module。exportswebpack;
如上,实现了一个webpack方法,可传入一个options参数,包括用户指定的打包入口entry、output等。webpack({
entry:。index。js,
output:{
path:path。resolve(dirname,dist),
filename:〔name〕。js,
},
module:{
rules:
}
})
编译
上面已经实现了webpack配置的传入,compiler的创建,接下来还需要实现Compiler类,该类内部暴露一个run方法,用于执行编译。
首先需要明确编译过程需要做的事情。
读取入口文件,将入口文件交给匹配的loader处理,返回处理后的代码
开始编译loader处理完的代码
若代码中依赖了其他文件,则对require函数替换为webpack自行实现的webpackrequire,保存该文件的处理结果,同时让其他文件回到第1步进行处理,不断循环。
编译结束后,每个文件都有其对应的处理结果,将这些文件的编译结果从初始的入口文件开始组织到一起。
入口文件loader处理
读取入口文件,将入口文件交给匹配的loader处理miniwebpackcompiler。js
constfsrequire(fs);
classCompiler{
constructor(options){
this。optionsoptions{};
保存编译过程编译的module
this。modulesnewSet;
}
run(callback){
constentryChunkthis。build(path。join(process。cwd,this。options。entry));
}
build(modulePath){
letoriginCodefs。readFileSync(modulePath);
originCodethis。dealWidthLoader(modulePath,originCode。toString);
returnthis。dealDependencies(originCode,modulePath);
}
将源码交给匹配的loader处理
dealWidthLoader(modulePath,originCode){
〔。。。this。options。module。rules〕。reverse。forEach(item{
if(item。test(modulePath)){
constloaders〔。。。item。use〕。reverse;
loaders。forEach(loaderoriginCodeloader(originCode))
}
})
returnoriginCode
}
}
module。exportsCompiler;
入口文件处理
这里需要开始处理入口文件的依赖,将其require转换成自定义的webpackrequire,同时将其依赖收集起来,后续需要不断递归处理其直接依赖和间接依赖,这里用到了babel进行处理。调用webpack处理依赖的代码
dealDependencies(code,modulePath){
constfullPathpath。relative(process。cwd,modulePath);
创建模块对象
constmodule{
id:fullPath,
dependencies:该模块所依赖模块绝对路径地址
};
处理require语句,同时记录依赖了哪些文件
constastparser。parse(code,{
sourceType:module,
ast:true,
});
深度优先遍历语法Tree
traverse(ast,{
CallExpression:(nodePath){
constnodenodePath。node;
if(node。callee。namerequire){
获得依赖的路径
constrequirePathnode。arguments〔0〕。value;
constmoduleDirNamepath。dirname(modulePath);
constfullPathpath。relative(path。join(moduleDirName,requirePath),requirePath);
替换require语句为webpack自定义的require方法
node。calleet。identifier(webpackrequire);
将依赖的路径修改成以当前路行为基准
node。arguments〔t。stringLiteral(fullPath)〕;
constexitModule〔。。。this。modules〕。find(itemitem。idfullPath)
该文件可能已经被处理过,这里判断一下
if(!exitModule){
记录下当前处理的文件所依赖的文件(后续需逐一处理)
module。dependencies。push(fullPath);
}
}
},
});
根据新的ast生成代码
const{code:compilerCode}generator(ast);
保存处理后的代码
module。sourcecompilerCode;
返回当前模块对象
returnmodule;
}
依赖处理
到这里为止便处理完了入口文件,但是在处理文件过程,还收集了入口文件依赖的其他文件未处理,因此,在dealDependencies尾部,加入以下代码调用webpack处理依赖的代码
dealDependencies(code,modulePath){
。。。
。。。
。。。
为当前模块挂载新的生成的代码
module。sourcecompilerCode;
递归处理其依赖
module。dependencies。forEach((dependency){
constdepModulethis。build(dependency);
同时保存下编译过的依赖
this。modules。add(depModule);
});
。。。
。。。
。。。
返回当前模块对象
returnmodule;
}
Chunk
在上面的步骤中,已经处理了入口文件、依赖文件,但目前它们还是分散开来,在webpack中,是支持多个入口,每个入口是一个chunk,这个chunk将包含入口文件及其依赖的moduleminiwebpackcompiler。js
constfsrequire(fs);
classCompiler{
constructor(options){
this。optionsoptions{};
保存编译过程编译的module
this。modulesnewSet;
}
run(callback){
constentryModulethis。build(path。join(process。cwd,this。options。entry));
constentryChunkthis。buildChunk(entry,entryModule);
}
build(modulePath){
}
将源码交给匹配的loader处理
dealWidthLoader(modulePath,originCode){
}
调用webpack处理依赖的代码
dealDependencies(code,modulePath){
}
buildChunk(entryName,entryModule){
return{
name:entryName,
入口文件编译结果
entryModule:entryModule,
所有直接依赖和间接依赖编译结果
modules:this。modules,
};
}
}
module。exportsCompiler;
文件生成
至此我们已经将入口文件和其所依赖的所有文件编译完成,现在需要将编译后的代码生成对应的文件。
根据最上面利用官方webpack打包出来的产物,保留其基本结构,将构造的chunk内部的entryModule的source以及modules的souce替换进去,并根据初始配置的output生成对应文件。miniwebpackcompiler。js
constfsrequire(fs);
classCompiler{
constructor(options){
this。optionsoptions{};
保存编译过程编译的module,下面会讲解到
this。modulesnewSet;
}
run(callback){
constentryModulethis。build(path。join(process。cwd,this。options。entry));
constentryChunkthis。buildChunk(entry,entryModule);
this。generateFile(entryChunk);
}
build(modulePath){
}
将源码交给匹配的loader处理
dealWidthLoader(modulePath,originCode){
}
调用webpack处理依赖的代码
dealDependencies(code,modulePath){
}
buildChunk(entryName,entryModule){
}
generateFile(entryChunk){
获取打包后的代码
constcodethis。getCode(entryChunk);
if(!fs。existsSync(this。options。output。path)){
fs。mkdirSync(this。options。output。path);
}
写入文件
fs。writeFileSync(
path。join(
this。options。output。path,
this。options。output。filename。replace(〔name〕,entryChunk。name)
),
code
);
}
getCode(entryChunk){
return
({
webpackBootstrap
varwebpackmodules{
{entryChunk。modules。map(module
{module。id}:(module,unusedwebpackexports,webpackrequire){
{module。source}
}
)。join(,)}
};
varwebpackmodulecache{};
functionwebpackrequire(moduleId){
Checkifmoduleisincache
varcachedModulewebpackmodulecache〔moduleId〕;
if(cachedModule!undefined){
returncachedModule。exports;
}
Createanewmodule(andputitintothecache)
varmodule(webpackmodulecache〔moduleId〕{
exports:{},
});
Executethemodulefunction
webpackmodules〔moduleId〕(
module,
module。exports,
webpackrequire
);
Returntheexportsofthemodule
returnmodule。exports;
}
varwebpackexports{};
ThisentryneedtobewrappedinanIIFEbecauseitneedtobeisolatedagainstothermodulesinthechunk。
({
{entryChunk。entryModule。source};
});
})
;
}
}
module。exportsCompiler;
试试在浏览器下跑一下生成的代码
符合预期,至此便完成了一个极简的webpack,针对单入口文件进行打包。当然真正的webpack远非如此简单,这里仅仅只是实现其一个打包思路。
谢谢支持
以上便是本次分享的全部内容,希望对你有所帮助
欢迎关注公众号ELab团队收货大厂一手好文章字节跳动校社招内推码:WWCM1TA
投递链接:https:job。toutiao。comsrj1fwQW
可凭内推码投递字节跳动国际化电商S项目团队相关岗位哦
参考资料
〔1〕
Node接口webpack中文文档:https:webpack。docschina。orgapinodewebpack
END