Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它的核心功能是将各种资源(JavaScript、CSS、图片等)作为模块进行打包,并生成优化后的输出文件。本文将结合具体例子和部分经典源码,详细讲解 Webpack 的打包原理。
基本概念
在深入了解打包原理之前,我们需要先了解一些基本概念:
- Entry(入口):Webpack 从一个或多个入口文件开始构建依赖图。
- Output(输出):Webpack 将打包后的文件输出到指定的目录。
- Loaders(加载器):Webpack 使用加载器来处理非 JavaScript 文件(如 CSS、图片)。
- Plugins(插件):Webpack 插件用于执行范围更广的任务(如打包优化、资源管理)。
打包流程
Webpack 的打包流程可以分为以下几个步骤:
- 初始化:读取配置文件,合并默认配置,初始化 Compiler 对象。
- 构建模块依赖图:从入口文件开始,递归解析所有依赖模块,生成依赖图。
- 转换模块:使用 Loaders 转换模块内容。
- 生成代码块:将模块分组为代码块(Chunks)。
- 输出文件:将代码块转换为文件,并输出到指定目录。
具体例子
我们通过一个简单的例子来演示 Webpack 的打包过程。
1. 项目结构
my-webpack-project/
├── src/
│ ├── index.js
│ └── module.js
├── dist/
├── webpack.config.js
└── package.json
2. 代码示例
src/index.js:
import { greet } from './module';
greet('World');
src/module.js:
export function greet(name) {
console.log(`Hello, ${name}!`);
}
webpack.config.js:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
};
3. 打包过程
运行以下命令进行打包:
npm install webpack webpack-cli babel-loader @babel/core @babel/preset-env --save-dev
npx webpack
Webpack 源码解析
我们通过分析部分经典源码,进一步理解 Webpack 的打包原理。
1. 初始化
Webpack 通过 webpack-cli 读取配置文件,并初始化 Compiler 对象:
const webpack = require('webpack');
const config = require('./webpack.config.js');
const compiler = webpack(config);
2. 构建模块依赖图
Compiler 对象调用 run 方法,开始构建模块依赖图:
compiler.run((err, stats) => {
if (err) {
console.error(err);
return;
}
console.log(stats.toString({
chunks: false,
colors: true
}));
});
在 run 方法中,Webpack 从入口文件开始,递归解析所有依赖模块:
const entry = './src/index.js';
const modules = [];
function buildModule(filename) {
const content = fs.readFileSync(filename, 'utf-8');
const dependencies = parseDependencies(content);
modules.push({ filename, content, dependencies });
dependencies.forEach(dep => {
buildModule(dep);
});
}
buildModule(entry);
关于parseDependencies的基本原理,可见 juejin.cn/post/743622…
3. 转换模块
Webpack 使用 Loaders 转换模块内容,例如使用 babel-loader 转换 ES6 代码:
const babel = require('@babel/core');
function transform(content) {
return babel.transform(content, {
presets: ['@babel/preset-env']
}).code;
}
4. 生成代码块
Webpack 将模块分组为代码块(Chunks),并生成最终的输出文件:
const bundle = modules.map(module => {
return `
// ${module.filename}
function(module, exports, require) {
${module.content}
}
`;
}).join(',');
const result = `
(function(modules) {
function require(filename) {
const fn = modules[filename];
const module = { exports: {} };
fn(module, module.exports, require);
return module.exports;
}
require('${entry}');
})({${bundle}})
`;
fs.writeFileSync('./dist/bundle.js', result);
自己实现一个简单的打包器
为了更好地理解 Webpack 的打包原理,我们可以尝试自己实现一个简单的打包器。
1. 项目结构
my-bundler/
├── src/
│ ├── index.js
│ ├── add.js
│ └── minus.js
├── dist/
├── bundle.js
└── package.json
2. 代码示例
src/add.js:
export default (a, b) => {
return a + b;
}
src/minus.js:
export const minus = (a, b) => {
return a - b;
}
src/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="./dist/bundle.js"></script>
</body>
</html>
3. 实现打包器
我们首先在项目根目录下创建 bundle.js,这个文件用来对我们刚刚写的模块化 JS 代码文件进行打包。
3.1 处理单个模块
我们需要读取文件内容,并将其转换为 AST(抽象语法树),然后提取依赖关系并转换代码。
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"]
});
return { file, deps, code };
};
3.2 递归获取所有模块的信息
我们需要递归地获取所有模块的信息,构建依赖图。
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
};
});
return depsGraph;
};
3.3 生成最终代码
我们需要根据依赖图生成最终的代码,并写入到 dist/bundle.js 文件中。
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);
fs.mkdirSync('./dist');
fs.writeFileSync('./dist/bundle.js', content);
};
build('./src/index.js');