从零撸Webpack

103 阅读4分钟

你是否曾经被Webpack的黑魔法吓到?今天我们不玩玄学,带你用通俗易懂的方式,手把手撸一个属于自己的Webpack!别眨眼,代码和原理全都给你,笑着学技术,快乐敲代码!😆


一、Webpack到底是个啥?

Webpack是前端界的“搬砖小能手”,它能把你项目里各种依赖文件(JS、CSS、图片等)全都打包成浏览器能识别的静态资源。就像把一堆零散的乐高拼成一辆炫酷跑车,方便你一键启动!

官方Webpack的打包流程

  1. 读取入口文件
  2. 递归分析依赖,生成AST(抽象语法树)
  3. 根据AST转换高版本JS为低版本(靠Babel)
  4. 打包输出

但官方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的核心原理,还能用自己的代码打包项目,面试再也不怕被问底层实现!