跳到主要内容

webpack 打包 bundle 原理分析

const compier = webpack(options)
compier.run()

通过 webpack 生命周期概率,可以知道打包大致过程

  • 读取到配置的入口
    • 入口模板的路径
    • 模块分析
      • 模块依赖(依赖的路径)
        • 可以用递归的方式处理依赖模块
      • 内容处理(处理后的内容代码)
      • 依赖图谱对象
    • webpackBootstrap 入口函数
  • 读取到配置的出口
    • 生产文件
      • 文件存放的位置

根据以上的分析,编写一个自定义 webpack 打包文件

编写自定义 webpack 打包项目

新增 webpack.config.js, 跟 webpack 默认配置一样

const path = require('path')

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'), // 输出的文件目录,必须是绝对路径
filename: 'main.js' // 输出的文件名称
},
mode: 'development'
}

在此之前,先安装 @babel 库

npm i -D @babel/parser @babel/traverse @babel/core @babel/preset-env

@babel/parser 来支持解析内容转 ast 树方便解析
@babel/traverse 来可以过滤 ast
@babel/core@babel/preset-envast 转为 js
Ast 抽象语法书:以 node 节点为单位的 tree 树结构

新建 /lib/webpack.js, 打包的核心文件

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const {transformFromAst} = require('@babel/core')

module.exports = class webpack {
constructor(options) {
// console.log(options)
const {entry, output} = options
this.entry = entry
this.output = output
this.modules = []
}

run() {
const info = this.parse(this.entry)

// 输出依赖图谱
this.modules.push(info)
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i]
const {yilai} = item

for (let j in yilai) {
this.modules.push(this.parse(yilai[j]))
}
}

// 数据结构转换 arr->obj
const obj = {}
this.modules.forEach(item => {
obj[item.entryFile] = item
})

this.file(obj)
}

parse(entryFile) {
// 读取入口文件
const content = fs.readFileSync(entryFile, 'utf-8')

// 解析内容为 ast 树
const ast = parser.parse(content, {sourceType: 'module'})

// 过滤 ast 树
const dirname = path.dirname(entryFile)

//分析依赖 以及依赖路径
// 内容处理
const yilai = {}
traverse(ast, {
ImportDeclaration({node}) {
const newPathName = './' + path.join(dirname, node.source.value)
yilai[node.source.value] = newPathName
}
})

// ast 转 js
const {code} = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})

return {
entryFile,
yilai,
code
}
}

file(code) {
// 生产文件,放入到 dist 的目录
const filePath = path.join(this.output.path, this.output.filename)

// 生产 bundle 文件内容
const newCode = JSON.stringify(code)

const bundle = `(function (modules) {
function require(module) {
function pathRequire(relaPath) {
// relaPath: ./a.js -> ./src/a.js
return require(modules[module].yilai[relaPath])
}
const exports = {};
(function (require,exports, code) {
eval(code)
})(pathRequire,exports, modules[module].code)

return exports
}

require('${this.entry}')
})(${newCode})`

// webpackBootstrap
fs.writeFileSync(filePath, bundle, 'utf-8')
}
}

以下测试代码,新建 src/b.js

export const strb = 'b.js'

新建 src/a.js

import {strb} from "./b.js";
export const stra = 'a.js' + strb

新建入口文件 src/index.js

import {stra} from './a.js'
console.log(stra + 'index.js')

最后新建打包文件 bundle.js

const webapck = require('./lib/webpack')
const options = require('./webpack.config')

new webapck(options).run();

执行 node bundle.js, 即可生产 ./dist/main.js

(function (modules) {
function require(module) {
function pathRequire(relaPath) {
// relaPath: ./a.js -> ./src/a.js
return require(modules[module].yilai[relaPath])
}
const exports = {};
(function (require,exports, code) {
eval(code)
})(pathRequire,exports, modules[module].code)

return exports
}

require('./src/index.js')
})({"./src/index.js":{"entryFile":"./src/index.js","yilai":{"./a.js":"./src\\a.js"},"code":"\"use strict\";\n\nvar _a = require(\"./a.js\");\n\nconsole.log(_a.stra + 'index.js');"},"./src\\a.js":{"entryFile":"./src\\a.js","yilai":{"./b.js":"./src\\b.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.stra = void 0;\n\nvar _b = require(\"./b.js\");\n\nvar stra = 'a.js' + _b.strb;\nexports.stra = stra;"},"./src\\b.js":{"entryFile":"./src\\b.js","yilai":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.strb = void 0;\nvar strb = 'b.js';\nexports.strb = strb;"}})

运行以上代码结果

a.jsb.jsindex.js