webpack打包的基本原理
核心功能就是把我们写的模块化代码转换成浏览器能够识别运行的代码,话不多说我们一起来了解它
首先我们建一个空项目用 npm init -y 创建一个初始化的,在跟目录下创建src文件夹,src下创建index.js,add.js,square.js,tip.js。在根目录下创建index.html
index.html引入index.js,index.js引入add.js、square.js。square.js引入tip.js
//add.js
export default (a, b) => {
return a + b;
};
//square.js
import tip from "tip.js";
const square = (a) => {
console.log(tip);
return a * a;
};
export { square };
//tip.js
export default "我是提示-----";
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script src="./src/index.js"></script>
</html>
运行index.html会报错,不能识别es6的模块化导入
让我们来实现webpack打包的核心功能
实现webpack打包核心功能
首先在根目录下建一个bundle.js用来对刚刚写的index.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)
根据上面的说明进行分析,打包的工作基本流程如下
- 读取入口文件中的内容(也就是index.js文件)
- 分析入口文件,递归读取模块所依赖的文件 内容,生成依赖图
- 根据依赖图生成浏览器能运行的代码
1、处理单个模块内容
const fs = require("fs");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, "utf-8");
//获取文件内容,是字符串
console.log(body);
};
getModuleInfo("./src/index.js");
打印出来是文件内容的字符串
借助安装@babel/parser 把,js文件代码转换成js对象,叫做抽象语法树(ast)
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.program.body);
};
getModuleInfo("./src/index.js");
打印ast.program.body的结构:
[
Node {
type: 'ImportDeclaration',
start: 0,
end: 27,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
specifiers: [ [Node] ],
source: Node {
type: 'StringLiteral',
start: 16,
end: 26,
loc: [SourceLocation],
extra: [Object],
value: './add.js'
}
},
Node {
type: 'ImportDeclaration',
start: 29,
end: 66,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
specifiers: [ [Node] ],
source: Node {
type: 'StringLiteral',
start: 52,
end: 65,
loc: [SourceLocation],
extra: [Object],
value: './square.js'
}
},
Node {
type: 'VariableDeclaration',
start: 68,
end: 90,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
declarations: [ [Node] ],
kind: 'const'
},
Node {
type: 'VariableDeclaration',
start: 92,
end: 114,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
declarations: [ [Node] ],
kind: 'const'
},
Node {
type: 'ExpressionStatement',
start: 116,
end: 143,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
expression: Node {
type: 'CallExpression',
start: 116,
end: 142,
loc: [SourceLocation],
callee: [Node],
arguments: [Array]
}
},
Node {
type: 'ExpressionStatement',
start: 145,
end: 172,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
expression: Node {
type: 'CallExpression',
start: 145,
end: 171,
loc: [SourceLocation],
callee: [Node],
arguments: [Array]
}
}
]
type属性是ImportDeclaration的节点,其source.value属性是引入这个模块的相对路径,上面打印出两个ImportDeclaration节点,说明对应的是入口文件中的两个import
对ast.program.body做处理,本质上是对这个数组遍历,循环做处理,借助安装@babel/traverse来完成这项工作
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const getModuleInfo = (file) => {
//1、把入口文件字符串ast对象
const body = fs.readFileSync(file, "utf-8");
const ast = parser.parse(body, {
//表示我们要解析的是es6模块
sourceType: "module",
});
//2、获取类型为ImportDeclaration的所依赖模块的信息地址
const deps = {};
//创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
let absPath = "./" + path.join(dirname, node.source.value);
absPath = absPath.replace("\\", "/");
deps[node.source.value] = absPath;
},
});
console.log(deps);
};
getModuleInfo("./src/index.js");
deps打印出入口文件引入的依赖地址
获取依赖后,需要对ast做语法转换,把es6转成es5的语法,安装@babel/core以及@babel/preset-env完成
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) => {
//1、把入口文件字符串ast对象
const body = fs.readFileSync(file, "utf-8");
const ast = parser.parse(body, {
//表示我们要解析的是es6模块
sourceType: "module",
});
//2、获取类型为ImportDeclaration的所依赖模块的信息地址
const deps = {};
//创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
let absPath = "./" + path.join(dirname, node.source.value);
absPath = absPath.replace("\\", "/");
deps[node.source.value] = absPath;
},
});
//3、把es6语法转换成es5
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = { file, deps, code };
console.log(moduleInfo);
return moduleInfo;
};
getModuleInfo("./src/index.js");
moduleInfo 打印结果为:
最终把一个模块的代码转化为一个对象形式的信息,这个对象包含文件的绝对路径,文件所依赖模块的信息,以及模块内部经过babel转化后的代码
接下去需要递归查找所有的模块,比如square.js里引用了tip.js。
这个过程也是获取依赖图(dependency graph)的过程
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) => {
//1、把入口文件字符串ast对象
const body = fs.readFileSync(file, "utf-8");
const ast = parser.parse(body, {
//表示我们要解析的是es6模块
sourceType: "module",
});
//2、获取类型为ImportDeclaration的所依赖模块的信息地址
const deps = {};
//创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
let absPath = "./" + path.join(dirname, node.source.value);
absPath = absPath.replace("\\", "/");
deps[node.source.value] = absPath;
},
});
//3、把es6语法转换成es5
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = { file, deps, code };
return moduleInfo;
};
//4、递归获取所有模块信息和之间的依赖关系
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");
获得的depsGraph对象如下:
{
file: './src/index.js',
deps: { './add.js': './src/add.js', './square.js': './src/square.js' },
code: '"use strict";\n' +
'\n' +
'var _add = _interopRequireDefault(require("./add.js"));\n' +
'var _square = require("./square.js");\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'var sum = (0, _add["default"])(2, 3);\n' +
'var sqr = (0, _square.square)(4);\n' +
'console.log("sum===", sum);\n' +
'console.log("sqr===", sqr);'
}
PS C:\Users\keyuan04\Desktop\webpack> node bundle.js
{
'./src/index.js': {
deps: { './add.js': './src/add.js', './square.js': './src/square.js' },
code: '"use strict";\n' +
'\n' +
'var _add = _interopRequireDefault(require("./add.js"));\n' +
'var _square = require("./square.js");\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'var sum = (0, _add["default"])(2, 3);\n' +
'var sqr = (0, _square.square)(4);\n' +
'console.log("sum===", sum);\n' +
'console.log("sqr===", sqr);'
},
'./src/add.js': {
deps: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
'var _default = function _default(a, b) {\n' +
' return a + b;\n' +
'};\n' +
'exports["default"] = _default;'
},
'./src/square.js': {
deps: { 'tip.js': './src/tip.js' },
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports.square = void 0;\n' +
'var _tip = _interopRequireDefault(require("tip.js"));\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'var square = function square(a) {\n' +
' console.log(_tip["default"]);\n' +
' return a * a;\n' +
'};\n' +
'exports.square = square;'
},
'./src/tip.js': {
deps: {},
code: '"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
'var _default = "我是提示-----";\n' +
'exports["default"] = _default;'
}
}
我们最终得到的模块分析数据如上图所示,接下来,我们就要根据这里获得的模块分析数据,来生产最终浏览器运行的代码。
上面打印的依赖图可以看到,最终的code里包含exports以及require这样的语法,所以,我们在生成最终代码时,要对exports和require做一定的实现和处理,把依赖图对象中的内容转换成能够执行的代码,以字符串形式输出。 我们把整个代码放在自执行函数中,参数是依赖图对象
把取得入口文件 的code信息,去执行。使用eval函数执行。
最后把生成的内容写入到dist文件中
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) => {
//1、把入口文件字符串ast对象
const body = fs.readFileSync(file, "utf-8");
const ast = parser.parse(body, {
//表示我们要解析的是es6模块
sourceType: "module",
});
//2、获取类型为ImportDeclaration的所依赖模块的信息地址
const deps = {};
//创建一个对象deps,用来收集模块自身引入的依赖,用traverse遍历需要的ImportDeclaration节点做处理,把相对路径转成绝对路径
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
let absPath = "./" + path.join(dirname, node.source.value);
absPath = absPath.replace("\\", "/");
deps[node.source.value] = absPath;
},
});
//3、把es6语法转换成es5
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = { file, deps, code };
return moduleInfo;
};
//4、递归获取所有模块信息和之间的依赖关系
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 content = bundle("./src/index.js");
fs.rmdirSync("./dist", { recursive: true });
// 写入到dist/bundle.js
fs.mkdirSync("./dist");
fs.writeFileSync("./dist/build.js", content);
最后在index.html中入这个./dist/bundle.js,在控制台就可以看到正确的输出结果