注册

深入webpack打包原理

本文讨论的核心内容如下:



  1. webpack进行打包的基本原理
  2. 如何自己实现一个loaderplugin

注: 本文使用的webpack版本是v4.43.0, webpack-cli版本是v3.3.11node版本是v12.14.1npm版本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.jsadd.jsminus.js,根目录下创建index.html,其中index.html引入index.js,在index.js引入add.jsminus.js


目录结构如下:


4403361856b0157afda1b5309175e098.png

文件内容如下:

// 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>

<<span class="hljs-attribute">span</span> class=<span class="hljs-string">"hljs-attribute"</span>>demo</span>

这样直接在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官网说明进行分析,明确我们进行打包工作的基本流程如下:



  1. 首先,我们需要读到入口文件里的内容(也就是index.js的内容)
  2. 其次,分析入口文件,递归的去读取模块所依赖的文件内容,生成依赖图
  3. 最后,根据依赖图,生成浏览器能够运行的最终代码

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')

df092a2d81aa916abd8ea35c3c827079.png

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)
}
getModuleInfo('./src/index.js')

1eb8d5ebf8892bcae5422cfd2c9de8ab.png


入口文件内容被放到一个数组中,总共有六个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 } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
console.log(moduleInfo)
return moduleInfo
}
getModuleInfo('./src/index.js')

115ff0c27f7271be77be308613cb405f.png

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: moduleInfo.code
}
})
console.log(depsGraph)
return depsGraph
}
parseModules('./src/index.js')

b27cfd4551a403e175345f24be885b7d.png

3. 生成最终代码


在我们实现之前,观察上一节最终得到的依赖图,可以看到,最终的code里包含exports以及require这样的语法,所以,我们在生成最终代码时,要对exports和require做一定的实现和处理


我们首先调用之前说的parseModules方法,获得整个应用的依赖图对象:


const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
}

接下来我们应该把依赖图对象中的内容,转换成能够执行的代码,以字符串形式输出。
我们把整个代码放在自执行函数中,参数是依赖图对象


const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
return exports
}
require('${file}')
})(${depsGraph})`
}

接下来内容其实很简单,就是我们取得入口文件的code信息,去执行它就好了,使用eval函数执行,初步写出代码如下:


const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
(function(code){
eval(code)
})(graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}

上面的写法是有问题的,我们需要对file做绝对路径转化,否则graph[file].code是获取不到的,定义adsRequire方法做相对路径转化为绝对路径


const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}

接下来,我们只需要执行bundle方法,然后把生成的内容写入一个JavaScript文件即可


const content = bundle('./src/index.js')
// 写入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)

4. bundle.js的完整代码

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')
console.log(body)
const ast = parser.parse(body, {
sourceType: 'module'
})
// console.log(ast.program.body)
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 } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
return moduleInfo
}

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: moduleInfo.code
}
})
// console.log(depsGraph)
return depsGraph
}


// 生成最终可以在浏览器运行的代码
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}


const build = file => {
const content = bundle(file)
// 写入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
}

build('./src/index.js')



0 个评论

要回复文章请先登录注册