打包结果
webpack打包后输出默认是dist/main.js;其中的每个js模块都会进行编译和进行依赖图谱graphMap记录。如下源码
// src/index.js
import { a } from './a.js'
console.log(a)
//////////////////////////////////////
// src/a.js
import { b } from './b.js'
export const a = 'a' + b
/////////////////////////////////////
// src/b.js
export const b = 'b'
编译生成graphMap大概是这样子
graphMap: {
'src/index.js': {
code: xxx,
dependencies: {'./a.js': 'src/a.js'}
},
'src/a.js': {
code: xxx,
dependencies: {'./b.js': 'src/b.js'}
},
'src/b.js': {
code: xxx,
dependencies: {}
}
}
现在我们尝试去研究一下整个流程是怎么样的。
webpack的Compiler如何编译代码
首先要有一个bundle.js; 它的作用主要是提供模块编译和生成依赖图谱的功能。
// bundle.js
const fs = require('fs');
const path = require('path');
const babelParser = require('@babel/parser');
const babelTraverse = require('@babel/traverse').default;
const babel = require('@babel/core');
// 模块依赖分析,传入入口,返回一个数组表示各个模块的相对路径和绝对路径
const moduleAnalyser = fileName => {
// 拿到源码内容
const content = fs.readFileSync(fileName, 'utf-8')
// 生成ast
const ast = babelParser.parse(content, {
sourceType: 'module'
})
// 分析ast 拿到每个依赖的相对路径和绝对路径
const dependencies = {}
babelTraverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(fileName)
const absolutePath = path.join('./', dirname, node.source.value) // 绝对路径
dependencies[node.source.value] = absolutePath
}
})
// 对ast进行转换为es5代码
const { code } = babel.transformFromAst(ast, null, {
"presets": [
"@babel/preset-env"
]
})
// 返回入口路径、依赖关系、翻译好的es5源码
return {
fileName,
dependencies,
code
}
}
// 从入口开始寻找依赖,生成依赖图谱
const makeModuleGraph = entry => {
const graph = {}
const analyser = entry => {
const { dependencies, code, fileName } = moduleAnalyser(entry)
graph[fileName] = {
code,
dependencies
}
if (Object.getOwnPropertyNames(dependencies).length) {
for (let i in dependencies) {
// 防止循环引用!
if (!graph[dependencies[i]]) {
analyser(dependencies[i])
}
}
}
}
analyser(entry)
return graph
}
module.exports = makeModuleGraph;
-
模块编译分三个步骤
- 通过babelParser将源码转换成ast
- 通过 babelTraverse遍历ast, 把import的相对路径提取出来,并且生成基于src目录的绝对路径
- 再通过babel.transformFromAst把ast转换为es5源码 code
模块编译得到该模块的3个信息 fileName, dependencies, code
-
生成依赖图谱分为两个步骤
- 准备一个空对象graph,从入口文件开始进行分析模块并把模块分析后的3个信息记录到graph中:
fileName: { dependencies, code}
- 遇到有dependencies的,递归解析模块
- 准备一个空对象graph,从入口文件开始进行分析模块并把模块分析后的3个信息记录到graph中:
然后再提供一个编译类Compiler去进行编译整合工作
// Compiler.js
const makeModuleGraph = require('./bundle.js')
class Compiler{
constructor(config) {
this.entry = config.entry // 入口
this.output = config.output // 出口
}
// 生成最终打包输出的代码
generateCode (entry) {
const graph = makeModuleGraph(entry)
const stringifyGraph = JSON.stringify(graph)
const code = `
;(function(graph){
function require(relativePath) {
function getFullPath (path) {
return require(graph[relativePath].dependencies[path]);
}
var exports = {};
(function(require, code) {
eval(code);
})(getFullPath, graph[relativePath].code)
return exports;
}
require('${entry}');
})(${stringifyGraph});
`
return code
}
run() {
const code = this.generateCode(this.entry)
console.log(code)
}
}
module.exports = Compiler
其中最烧脑的部分就是generateCode 的模板字符串的那块逻辑,这得仔细品尝了
配置与打包
最后配置一个webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
与一个index.js来运行打包逻辑
const Compiler = require('./Compiler.js')
const config = require('./webpack.config.js')
const compiler= new Compiler(config)
compiler.run();
目录结构如下,及打包命令 "build": "node ./index.js"
把打包输出的code复制粘贴到控制台进行验证,根据我们的预期应该是输出'ab'字符串的 npm run build.png
符合预期!
网友评论