你是否曾经被Webpack的黑魔法吓到?今天我们不玩玄学,带你用通俗易懂的方式,手把手撸一个属于自己的Webpack!别眨眼,代码和原理全都给你,笑着学技术,快乐敲代码!😆
一、Webpack到底是个啥?
Webpack是前端界的“搬砖小能手”,它能把你项目里各种依赖文件(JS、CSS、图片等)全都打包成浏览器能识别的静态资源。就像把一堆零散的乐高拼成一辆炫酷跑车,方便你一键启动!
官方Webpack的打包流程
- 读取入口文件
- 递归分析依赖,生成AST(抽象语法树)
- 根据AST转换高版本JS为低版本(靠Babel)
- 打包输出
但官方Webpack太庞大,原理不明?自己撸一个,原理全掌握!
二、项目结构一览
我们的myWebpack目录结构如下:
myWebpack/
├── bundle.js # 手写Webpack主程序
├── dist/bundle.js # 打包输出文件
├── src/ # 源码目录
│ ├── add.js
│ ├── minus.js
│ ├── index.js
│ └── test.js
├── index.html # 演示页面
├── package.json # 项目配置
└── .gitignore # 忽略文件
三、手写Webpack核心源码全解析
1. 入口文件:bundle.js
下面我们一行一行拆解,注释+原理全都有!
const fs = require('fs'); // 文件系统模块,负责读写文件
const path = require('path'); // 路径处理模块
const parser = require('@babel/parser'); // 代码解析为AST
const traverse = require('@babel/traverse').default; // 遍历AST收集依赖
const babel = require('@babel/core'); // 转换高版本JS为低版本
// 获取模块信息(核心函数)
function getModuleInfo(file) {
// 读取文件内容
const body = fs.readFileSync(file, 'utf-8');
// 解析为AST
const ast = parser.parse(body, {
sourceType: 'module'
});
// 依赖收集
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
// 收集import语句依赖
const dirname = path.dirname(file);
const abspath = './' + path.join(dirname, node.source.value);
deps[node.source.value] = abspath;
}
});
// 使用Babel转换代码(降级处理)
const { code } = babel.transformFromAstSync(ast, body, {
presets: ['@babel/preset-env']
});
return {
file,
deps,
code
};
}
// 递归收集所有依赖模块
function parseModules(file) {
const entry = getModuleInfo(file);
const temp = [entry]; // 用数组模拟队列,存储所有模块信息
const depsGraph = {}; // 依赖图
for (let i = 0; i < temp.length; i++) {
const deps = temp[i].deps;
for (let key in deps) {
// 如果还没收集过该依赖,则递归收集
if (!depsGraph[deps[key]]) {
temp.push(getModuleInfo(deps[key]));
}
}
}
// 构建依赖图对象
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
};
});
return depsGraph;
}
// 生成最终打包代码
function bundle(file) {
const depsGraph = parseModules(file);
// 字符串模板,模拟Webpack打包后的代码结构
return `
(function(graph){
function require(file){
function absRequire(relPath){
return require(graph[file].deps[relPath]);
}
var exports = {};
(function(require, exports, code){
eval(code);
})(absRequire, exports, graph[file].code);
return exports;
}
require('${file}');
})(${JSON.stringify(depsGraph)})
`;
}
// 指定入口文件,生成bundle.js
const entry = './src/index.js';
const content = bundle(entry);
fs.writeFileSync('./dist/bundle.js', content, 'utf-8');
console.log('打包完成!快去dist/bundle.js看看成果吧!🎉');
2. 代码实现目的与原理讲解
- getModuleInfo:读取文件内容,解析AST,收集依赖,降级代码。
- parseModules:递归收集所有依赖,构建依赖图。
- bundle:生成打包后的代码,模拟Webpack的require机制。
- 入口与输出:指定入口文件,输出到dist目录。
这样一套流程下来,Webpack的核心原理你就全掌握了!
四、源码注释与趣味解读
每一步都加了详细注释,代码像段子一样易懂:
- 用
fs读文件,像翻箱倒柜找零食 - 用
@babel/parser解析AST,像把零食拆包分类 - 用
traverse收集依赖,像数清楚每种零食有几包 - 用
babel降级代码,像把辣条变成儿童版 - 最后用字符串模板拼出大礼包,所有零食一锅端!
五、打包流程可视化表格
| 步骤 | 作用说明 | 涉及API/库 |
|---|---|---|
| 读取入口文件 | 获取主模块内容 | fs |
| 解析AST | 代码转抽象语法树 | @babel/parser |
| 收集依赖 | 遍历AST找import语句 | @babel/traverse |
| 降级代码 | 高版本JS转低版本 | @babel/core |
| 构建依赖图 | 递归收集所有依赖 | 自己写的parseModules |
| 生成bundle | 输出打包代码 | 自己写的bundle |
六、和官方Webpack的对比
| 功能 | 官方Webpack | 手写Webpack |
|---|---|---|
| 支持文件类型 | 多种 | 仅JS |
| 插件系统 | 丰富 | 无 |
| 性能优化 | 多样 | 基础 |
| 原理透明度 | 黑盒 | 透明 |
| 学习成本 | 高 | 低 |
手写版虽然简陋,但原理清晰,适合学习和面试装X!😎
七、常见问题与优化建议
- 为什么每次都很慢? 因为每次都要递归分析所有依赖,重新生成代码,像每次做饭都从种地开始。
- 如何优化? 可以加缓存、只编译变动部分、用多线程等。
- 能否支持CSS/图片? 需要扩展解析器和loader,感兴趣可以继续深挖!
八、结语:手写Webpack,快乐敲代码!
通过本篇简易核心源码实战,你不仅能掌握Webpack的核心原理,还能用自己的代码打包项目,面试再也不怕被问底层实现!