本文主要介绍了深度webpack打包的原理以及loader和plugin的实现。通过示例代码进行了非常详细的介绍,对于大家的学习或者工作都有一定的参考价值。有需要的朋友下面跟边肖学习。
本文的核心内容如下:
网络包装的基本原理
如何自己实现一个加载器和插件
注:本文使用的webpack版本为v4.43.0,webpack-cli版本为v3.3.11,节点版本为v12.14.1,npm版本为v6.13.4(喜欢yarn的可以做),chrome浏览器版本为81.0.4044.129(正式版)
1. webpack打包基本原理
webpack的核心功能之一就是将我们编写的模块化代码打包,然后生成可以在浏览器中运行的代码。这里也从简单开始,一步步探索webpack的打包原理。
1.1 一个简单的需求
首先,我们建立一个空项目,用npm init -y快速初始化一个package.json,然后安装webpack webpack-cli。
接下来在根目录下创建src目录,在根目录下创建index.js,add.js,minus.js,index.html,其中index.js在index.html引入,add.js,minus.js在index.js引入
目录结构如下:
该文件的内容如下:
//add.js
导出默认值(a,b)={
返回a b
}
//minus.js
导出常数减=(a,b)={
返回a - b
}
//index.js
“导入添加自”。/add.js
从导入{ minus }。/minus.js
const sum=add(1,2)
常数除法=minus(2,1)
console.log(sum ,sum)
console.log(分部,分部)
!index.html
!文档类型html
html lang=en
头
meta charset=UTF-8
meta name= viewport content= width=device-width,initial-scale=1.0
title演示/标题
/头
身体
脚本src=。/src/index.js/script
/body
/html
这样直接引入index.html的index.js的代码显然无法在浏览器中运行,你会看到这样的错误。
未捕获的语法错误:不能在模块外使用import语句
是的,我们不能在脚本引入的js文件中使用es6模块化语法。
1.2 实现webpack打包核心功能
首先,我们在项目的根目录下设置了另一个bundle.js,用于封装我们刚刚编写的模块化js代码文件。
先来看webpack官网对其打包过程的描述:
内部构建了一个依赖图,它映射了项目需要的每个模块并生成了一个或多个包(Web Pack将在内部构建一个依赖图,它将映射项目需要的每个模块并生成一个或多个包)
在正式开始之前,我们包装工作的基本流程如下,通过分析结合以上webpack官网的描述:
首先,我们需要读取门户文件的内容(即index.js的内容)。其次,我们分析门户文件,递归地读取模块所依赖的文件的内容,并生成依赖图。最后,根据依赖图,我们可以生成浏览器可以运行的最终代码。1.处理单个模块(以门户为例)。1.获取模块内容。
由于我们需要使用node.js的核心模块fs来读取文件内容,所以我们先来看看我们读取的内容:
//bundle.js
const fs=require(fs )
const getModuleInfo=file={
const body=fs.readFileSync(文件,“utf-8”)
console.log(body)
}
getModuleInfo(。/src/index.js )
我们定义了一个方法getModuleInfo。在这个方法中,我们读出文件内容并打印出来。输出结果如下:
我们可以看到导入文件index.js的所有内容都是以字符串的形式输出的。接下来,我们可以使用正则表达式或者其他方法,从中提取导入导出的内容以及对应的路径文件名,从而分析导入文件的内容,获取有用的信息。但是如果进出口的内容非常多,那将是一个非常麻烦的过程。这里我们用巴别塔。
提供完成导入文件分析的功能。
1.2 分析模块内容
我们安装了@babel/parser,演示时安装的版本号是7.9.6。
这个babel模块的作用就是把我们js文件的代码内容转换成js对象的形式,这种js对象叫做抽象语法树(AST)。
//bundle.js
const fs=require(fs )
const parser=require( @ babel/parser )
const getModuleInfo=file={
const body=fs.readFileSync(文件,“utf-8”)
const ast=parser.parse(body,{
//表示我们正在解析es6模块。
源类型:“模块”
})
控制台.日志(ast)
控制台.日志(ast.program.body)
}
getModuleInfo(。/src/index.js )
使用@babel/parser的解析方法,导入文件转换被称为ast,我们打印出了AST。注意文件内容在ast.program.body中,如下图所示:
导入文件的内容放在一个数组中,总共有六个节点。我们可以看到,每个节点都有一个type属性,其中前两个type属性是ImportDeclaration,对应于我们导入文件的两个import语句,每个type属性都是ImportDeclaration的节点,它的source.value
属性是引入该模块的相对路径,这样我们就可以在导入文件中获得对打包有用的重要信息。
接下来,我们应该处理ast并返回一个结构化数据供后续使用。
1.3 对模块内容做处理
ast.program.body的一些数据的获取和处理本质上就是这个数组的遍历,数据处理是在循环中完成的。这里还引入了一个babel模块@babel/traverse来完成这项工作。
安装@babel/traverse。演示期间安装的版本号是7.9.6。
const fs=require(fs )
const path=require(path )
const parser=require( @ babel/parser )
const traverse=require( @ babel/traverse )。系统默认值
const getModuleInfo=file={
const body=fs.readFileSync(文件,“utf-8”)
const ast=parser.parse(body,{
源类型:“模块”
})
const deps={}
遍历(ast,{
ImportDeclaration({ node }) {
const dirname=path . dirname(file);
const absPath=。/ path.join(目录名,节点.源.值)
deps[node . source . value]=absPath
}
})
console.log(deps)
}
getModuleInfo(。/src/index.js )
创建一个对象deps,用于收集模块本身引入的依赖关系。traverse用于遍历ast。我们只需要处理ImportDeclaration的节点。注意,我们所做的处理实际上是将相对路径转换为绝对路径。这里,我用的是Mac系统。如果是windows系统,注意斜线的区别。
获得依赖关系后,我们需要对ast做语法转换,将es6的语法转换成es5的语法,这是通过使用babel核心模块@babel/core和@babel/preset-env来完成的。
安装@babel/core @babel/preset-env,演示时安装的版本号都是7.9.6。
const fs=require(fs )
const path=require(path )
const parser=require( @ babel/parser )
const traverse=require( @ babel/traverse )。系统默认值
const babel=require( @ babel/core )
const getModuleInfo=file={
const body=fs.readFileSync(文件,“utf-8”)
const ast=parser.parse(body,{
源类型:“模块”
})
const deps={}
遍历(ast,{
ImportDeclaration({ node }) {
const dirname=path . dirname(file);
const absPath=。/ path.join(目录名,节点.源.值)
deps[node . source . value]=absPath
}
})
const { code }=babel . transformfromast(ast,null,{
预设:[@babel/preset-env]
})
const moduleInfo={ file,deps,code }
console.log(moduleInfo)
返回模块信息
}
getModuleInfo(。/src/index.js )
如下图所示,我们最终将一个模块的代码转换成对象形式的信息,其中包含了文件的绝对路径,文件所依赖的模块的信息,巴别塔转换后模块内部的代码。
2.递归获取所有模块的信息
这个过程,也就是获取依赖图的过程,从入口模块开始,调用getModuleInfo方法分析每个模块及其依赖模块,最后返回一个包含所有模块信息的对象。
const parseModules=file={
//定义依赖图
const depsGraph={}
//首先获取入口的信息
const entry=getModuleInfo(file)
常数温度=[条目]
对于(设I=0;我温度长度;i ) {
常数item=temp[i]
const deps=item.deps
如果(部门){
//遍历模块的依赖,递归获取模块信息
对于(deps中的常量键){
if (deps.hasOwnProperty(key)) {
在…之时push(getModuleInfo(deps[key])
}
}
}
}
temp.forEach(moduleInfo={
depsGraph[moduleInfo.file]={
deps: moduleInfo.deps,
代码:moduleInfo.code
}
})
console.log(depsGraph)
返回文件
}
parseModules(./src/index.js )
获得的笔录对象如下图:
我们最终得到的模块分析数据如上图所示,接下来,我们就要根据这里获得的模块分析数据,来生产最终浏览器运行的代码。
3.生成最终代码
在我们实现之前,观察上一节最终得到的依赖图,可以看到,最终的密码里包含出口以及需要这样的语法,所以,我们在生成最终代码时,要对出口和需要做一定的实现和处理
我们首先调用之前说的解析模块方法,获得整个应用的依赖图对象:
const bundle=file={
const deps graph=JSON。stringify(解析模块(文件))
}
接下来我们应该把依赖图对象中的内容,转换成能够执行的代码,以字符串形式输出。我们把整个代码放在自执行函数中,参数是依赖图对象
const bundle=file={
const deps graph=JSON。stringify(解析模块(文件))
返回`(函数(图形){
功能要求(文件){
定义变量导出={ };
退货出口
}
需要(“${file}”)
})($ { depsGraph })` 0
}
接下来内容其实很简单,就是我们取得入口文件的密码信息,去执行它就好了,使用评价评价函数执行,初步写出代码如下:
const bundle=file={
const deps graph=JSON。stringify(解析模块(文件))
返回`(函数(图形){
功能要求(文件){
定义变量导出={ };
(功能(代码){
评估(代码)
})(图[文件]。代码)
退货出口
}
需要(“${file}”)
})($ { depsGraph })` 0
}
上面的写法是有问题的,我们需要对文件做绝对路径转化,否则图形[文件]。密码是获取不到的,定义adsRequire方法做相对路径转化为绝对路径
const bundle=file={
const deps graph=JSON。stringify(解析模块(文件))
返回`(函数(图形){
功能要求(文件){
定义变量导出={ };
函数absRequire(relPath){
返回要求(图形[文件])。deps[relPath])
}
(函数(要求、导出、代码){
评估(代码)
})(absRequire,exports,graph[file].代码)
退货出口
}
需要(“${file}”)
})($ { depsGraph })` 0
}
接下来,我们只需要执行捆方法,然后把生成的内容写入一个Java脚本语言文件即可
const content=bundle( ./src/index.js )
//写入到dist/bundle.js
fs.mkdirSync( ./dist’)
fs.writeFileSync( ./dist/bundle.js ,内容)
最后,我们在index.html引入这个. dist/bundle.js文件,我们可以看到控制台正确输出了我们想要的结果
bundle.js的完整代码
const fs=require(fs )
const path=require(path )
const parser=require( @ babel/parser )
const traverse=require( @ babel/traverse ).系统默认值
const babel=require( @ babel/core )
const getModuleInfo=file={
const body=fs.readFileSync(文件,“utf-8”)
console.log(body)
const ast=parser.parse(body,{
源类型:"模块"
})
//console.log(ast.program.body)
const deps={}
遍历(ast,{
ImportDeclaration({ node }) {
const dirname=path。dirname(文件);
const absPath= ./ path.join(目录名,节点。源。值)
deps[节点。来源。value]=absPath
}
})
const { code }=babel。transformfromast(ast,null,{
预设:[@babel/preset-env]
})
const moduleInfo={ file,deps,code }
返回模块信息
}
const parseModules=file={
//定义依赖图
const depsGraph={}
//首先获取入口的信息
const entry=getModuleInfo(file)
常数温度=[条目]
对于(设I=0;我温度长度;i ) {
常数item=temp[i]
const deps=item.deps
如果(部门){
//遍历模块的依赖,递归获取模块信息
对于(deps中的常量键){
if (deps.hasOwnProperty(key)) {
在…之时push(getModuleInfo(deps[key])
}
}
}
}
temp.forEach(moduleInfo={
depsGraph[moduleInfo.file]={
deps: moduleInfo.deps,
代码:moduleInfo.code
}
})
//console.log(depsGraph)
返回文件
}
//生成最终可以在浏览器运行的代码
const bundle=file={
const deps graph=JSON。stringify(解析模块(文件))
返回`(函数(图形){
功能要求(文件){
定义变量导出={ };
函数absRequire(relPath){
返回要求(图形[文件])。deps[relPath])
}
(函数(要求、导出、代码){
评估(代码)
})(absRequire,exports,graph[file].代码)
退货出口
}
需要(“${file}”)
})($ { depsGraph })` 0
}
const build=file={
常量内容=捆绑包(文件)
//写入到dist/bundle.js
fs.mkdirSync( ./dist’)
fs.writeFileSync( ./dist/bundle.js ,内容)
}
构建(。/src/index.js )
2. 手写 loader 和 plugin
2.1 如何自己实现一个装货设备
装货设备本质上就是一个函数,这个函数会在我们在我们加载一些文件时执行
2.1.1 如何实现一个同步装货设备
首先我们初始化一个项目,项目结构如图所示:
其中索引。射流研究…和网络包。配置。射流研究…的文件内容如下:
//index.js
console.log(我要学好前端,因为学好前端可以: )
//网页包。配置。射流研究…
const path=require(path )
模块。导出={
模式:"开发",
条目:{
主要:。/src/index.js
},
输出:{
path: path.resolve(__dirname, dist ),
文件名:[名称]。js
}
}
我们在根目录下创建syncLoader.js,用来实现一个同步的装载机,注意这个函数必须返回一个缓冲器或者线
//syncloader.ja
模块.导出=函数(源){
console.log(source ,源)
返回源
}
同时,我们在网络包。配置。射流研究…中使用这个装载机,我们这里使用resolveLoader配置项,指定装货设备查找文件路径,这样我们使用装货设备时候可以直接指定装货设备的名字
const path=require(path )
模块。导出={
模式:"开发",
条目:{
主要:。/src/index.js
},
输出:{
path: path.resolve(__dirname, dist ),
文件名:[名称]。js
},
resolveLoader: {
//加载程序路径查找顺序从左往右
模块:[node_modules , ./]
},
模块:{
规则:[
{
测试:/。js$/,
使用:"同步加载程序"
}
]
}
}
接下来我们运行打包命令,可以看到命令行输出了来源内容,也就是装货设备作用文件的内容。
接着我们改造我们的装载机:
模块.导出=函数(源){
source=升值加薪
返回源
}
我们再次运行打包命令,去观察打包后的代码:
这样,我们就实现了一个简单的装载机,为我们的文件增加一条信息。我们可以尝试在装货设备的函数里打印这个,发现输出结果是非常长的一串内容,这个上有很多我们可以在装货设备中使用的有用信息,所以,对于装货设备的编写,一定不要使用箭头函数,那样会改变这
的指向。
一般来说,我们会去使用官方推荐的加载程序-实用程序包去完成更加复杂的装货设备的编写
我们继续安装loader-utils,版本是^2.0.0
我们首先改造webpack.config.js:
const path=require(path )
模块。导出={
模式:"开发",
条目:{
主要:。/src/index.js
},
输出:{
path: path.resolve(__dirname, dist ),
文件名:[名称]。js
},
resolveLoader: {
//加载程序路径查找顺序从左往右
模块:[node_modules , ./]
},
模块:{
规则:[
{
测试:/。js$/,
使用:{
加载程序:"同步加载程序",
选项:{
消息: 升值加薪
}
}
}
]
}
}
注意到,我们为我们的装货设备增加了选择配置项,接下来在装货设备函数里使用加载程序-实用程序获取配置项内容,拼接内容,我们依然可以得到与之前一样的打包结果
//syncLoader.js
const loader utils=require( loader-utils )
module.exports=函数(源){
const options=loader utils . get options(this)
console.log(选项)
源=选项.消息
//可以传递更详细的信息
this.callback(null,source)
}
这样,我们就完成了一个简单的同步加载器的编写。
2.1.2如何实现异步加载器?
非常类似于同步加载器的编写方式,我们在根目录下设置一个文件asyncLoader.js,内容如下:
const loader utils=require( loader-utils )
module.exports=函数(源){
const options=loader utils . get options(this)
const asyncfunc=this.async()
setTimeout(()={
Source=开始颠覆生活
asyncfunc(null,res)
}, 200)
}
注意这里的this.async()。正式来说,它意味着告诉加载程序运行程序,加载程序打算回调一个同步。返回this.callback。这是为了让webpack知道这个加载程序正在异步运行,并返回this。与同步使用一致的回调。
接下来,我们修改webpack.config.js
const path=require(path )
模块.导出={
模式:“开发”,
条目:{
主要:。/src/index.js
},
输出:{
path: path.resolve(__dirname, dist ),
文件名:[名称]。js
},
resolveLoader: {
//加载程序路径搜索顺序从左到右
模块:[node_modules ,。/]
},
模块:{
规则:[
{
测试:/。js$/,
使用:[
{
加载程序:“同步加载程序”,
选项:{
寄语:‘走向人生巅峰’
}
},
{
加载程序:“异步加载程序”
}
]
}
]
}
}
注意加载器执行顺序是从离线开始的,所以先给正文写升值涨薪,再写走向人生巅峰
到目前为止,我们已经简单介绍了如何手工编写加载程序。在实际项目中,可以考虑一些常见的简单逻辑,可以通过编写一个加载器来完成(比如国际文本替换)。
2.2如何自己实现一个插件?
插件通常在webpack打包的某个时间做一些操作。当我们使用plugin时,通常是以new Plugin()的形式。所以,首先应该明确plugin应该是一个类。
我们初始化与上次实现加载器时相同的项目,并在根目录中创建一个demo-webpack-plugin.js文件。我们首先在webpack.config.js中使用它
const path=require(path )
const DemoWebpackPlugin=require(。/plugins/demo-web pack-plugin’)
模块.导出={
模式:“开发”,
条目:{
主要:。/src/index.js
},
输出:{
path: path.resolve(__dirname, dist ),
文件名:[名称]。js
},
插件:[
新DemoWebpackPlugin()
]
}
看看demo-webpack-plugin.js的实现。
DemoWebpackPlugin类{
构造函数(){
console.log(插件初始化)
}
应用(编译器){
}
}
module . exports=DemoWebpackPlugin
我们在DemoWebpackPlugin的构造函数中打印一条消息,当我们执行package命令时,这条消息就会被输出。插件类中需要实现一个apply方法。当webpack被打包时,插件的aplly方法将被调用来执行插件的逻辑。这个方法将一个编译器作为参数,这个编译器是webpack的一个实例。
plugin的核心是在执行apply方法的时候,你可以操作这次webpack的每个时间节点(钩子,也就是生命周期钩子),在不同的时间节点做一些操作。
关于webpack编译过程的生命周期钩子,请参考编译器钩子。
类似地,这些钩子是同步和异步的。下面是如何编写编译器挂钩。对于一些重要的观点,请参考评论:
DemoWebpackPlugin类{
构造函数(){
console.log(插件初始化)
}
//编译器是webpack实例。
应用(编译器){
//创建新编译后(同步)
//compilation表示每个包的独立编译。
compiler . hooks . compile . tap( DemoWebpackPlugin ,compilation={
console.log(编译)
})
//在将资源生成到输出目录之前(异步)
compiler . hooks . emit . tap async( DemoWebpackPlugin ,(compilation,fn)={
console.log(编译)
编译。资产[索引。MD ]={
//文件内容
来源:函数(){
返回"这是插件的演示"
},
//文件尺寸
size: function () {
返回25
}
}
fn()
})
}
}
模块。exports=DemoWebpackPlugin
我们的这个插件的作用就是,打包时候自动生成一个钔文档,文档内容是很简单的一句话
上述异步钩住的写法也可以是以下两种:
//第二种写法(承诺)
编译器。钩子。发射。tappromise( DemoWebpackPlugin ,(编译)={
返回新承诺((解决,拒绝)={
setTimeout(()={
解决()
}, 1000)
}).然后(()={
console.log(编译。资产)
编译。资产[索引。MD ]={
//文件内容
来源:函数(){
返回"这是插件的演示"
},
//文件尺寸
size: function () {
返回25
}
}
})
})
//第三种写法(异步等待)
编译器。钩子。发射。tappromise( DemoWebpackPlugin ,async(编译)={
等待新的承诺((解决,拒绝)={
setTimeout(()={
解决()
}, 1000)
})
console.log(编译。资产)
编译。资产[索引。MD ]={
//文件内容
来源:函数(){
返回"这是插件的演示"
},
//文件尺寸
size: function () {
返回25
}
}
})
最终的输出结果都是一样的,在每次打包时候生成一个钔文档
到此为止,本文介绍了网络包打包的基本原理,以及自己实现装货设备和插件的方法。希望本文内容能对大家对网络包的学习,使用带来帮助。更多相关网络包打包装货设备和插件内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!