深⼊webpack打包原理及loader和plugin的实现
本⽂讨论的核⼼内容如下:
webpack进⾏打包的基本原理
如何⾃⼰实现⼀个loader和plugin
注:本⽂使⽤的webpack版本是v4.43.0 , webpack-cli版本是v3.3.11,node版本是v12.14.1,npm版本v6.13.4 (如果你喜欢yarn也是可以的),演⽰⽤的chrome浏览器版本81.0.4044.129(正式版本)(64 位)
1. webpack打包基本原理
webpack的⼀个核⼼功能就是把我们写的模块化的代码,打包之后,⽣成可以在浏览器中运⾏的代码,我们这⾥也是从简单开始,⼀步步探索webpack的打包原理
1.1 ⼀个简单的需求
我们⾸先建⽴⼀个空的项⽬,使⽤npm init -y快速初始化⼀个package.json,然后安装webpack webpack-cli
接下来,在根⽬录下创建src⽬录,src⽬录下创建index.js,add.js,minus.js,根⽬录下创建index.html,其中index.html引⼊index.js,在index.js引⼊add.js,minus.js,
⽬录结构如下:
⽂件内容如下:
// add.js
export default (a, b) => {
return a + b
}
// minus.js
export const minus = (a, b) => {
return a - b
}
// index.js
import add from './add.js'
import { minus } from './minus.js'
const sum = add(1, 2)
const division = minus(2, 1)
console.log('sum>>>>>', sum)
console.log('division>>>>>', division)
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
这样直接在index.html引⼊index.js的代码,在浏览器中显然是不能运⾏的,你会看到这样的错误
Uncaught SyntaxError: Cannot use import statement outside a module
是的,我们不能在script引⼊的js⽂件⾥,使⽤es6模块化语法
1.2 实现webpack打包核⼼功能
我们⾸先在项⽬根⽬录下再建⽴⼀个bundle.js,这个⽂件⽤来对我们刚刚写的模块化js代码⽂件进⾏打包
我们⾸先来看webpack官⽹对于其打包流程的描述:
it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack会在内部构建⼀个依赖图(dependency graph),此依赖图会映射项⽬所需的每个模块,并⽣成⼀个或多个 bundle)
在正式开始之前,结合上⾯webpack官⽹说明进⾏分析,明确我们进⾏打包⼯作的基本流程如下:
⾸先,我们需要读到⼊⼝⽂件⾥的内容(也就是index.js的内容)其次,分析⼊⼝⽂件,递归的去读取模块所依赖的⽂件内容,⽣成依赖图最后,根据依赖图,⽣成浏览器能够运⾏的最终代码 1. 处理单个模块(以⼊⼝为例) 1.1 获取模块内容
既然要读取⽂件内容,我们需要⽤到node.js的核⼼模块fs,我们⾸先来看读到的内容是什么:
// bundle.js
const fs = require('fs')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
console.log(body)
}
getModuleInfo('./src/index.js')
我们定义了⼀个⽅法getModuleInfo,这个⽅法⾥我们读出⽂件内容,打印出来,输出的结果如下图:
我们可以看到,⼊⼝⽂件index.js的所有内容都以字符串形式输出了,我们接下来可以⽤正则表达式或者其它⼀些⽅法,从中提取到import以及export的内容以及相应的路径⽂件名,来对⼊⼝⽂件内容进⾏分析,获取有⽤的信息。但是如果import和export的内容⾮常多,这会是⼀个很⿇烦的过程,这⾥我们借助babel
提供的功能,来完成⼊⼝⽂件的分析
1.2 分析模块内容
我们安装@babel/parser,演⽰时安装的版本号为^7.9.6
这个babel模块的作⽤,就是把我们js⽂件的代码内容,转换成js对象的形式,这种形式的js对象,称做抽象语法树(Abstract Syntax Tree, 以下简称AST)
// bundle.js
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
// 表⽰我们要解析的是es6模块
sourceType: 'module'
})
console.log(ast)
console.log(ast.program.body)
webpack打包流程 面试}
getModuleInfo('./src/index.js')
使⽤@babel/parser的parse⽅法把⼊⼝⽂件转化称为了AST,我们打印出了ast,注意⽂件内容是在ast.program.body中,如下图所⽰:
⼊⼝⽂件内容被放到⼀个数组中,总共有六个Node节点,我们可以看到,每个节点有⼀个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').default
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
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').default
const babel = require('@babel/core')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
const { code } = ansformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
console.log(moduleInfo)
return moduleInfo
}
getModuleInfo('./src/index.js')
如下图所⽰,我们最终把⼀个模块的代码,转化为⼀个对象形式的信息,这个对象包含⽂件的绝对路径,⽂件所依赖模块的信息,以及模块内部经过babel转化后的代码
2. 递归的获取所有模块的信息
这个过程,也就是获取依赖图(dependency graph)的过程,这个过程就是从⼊⼝模块开始,对每个模块以及模块的依赖模块都调⽤getModuleInfo⽅法就⾏分析,最终返回⼀个包含所有模块信息的对象
const parseModules = file => {
// 定义依赖图
const depsGraph = {}
// ⾸先获取⼊⼝的信息
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const item = temp[i]
const deps = item.deps
if (deps) {
// 遍历模块的依赖,递归获取模块信息
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: de
}
})
console.log(depsGraph)
return depsGraph
}
parseModules('./src/index.js')
获得的depsGraph对象如下图:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。