模拟实现webpack

  • 项目目录如下:
project/
  ├── dist/
      └── bundle.js    ← 输出文件
  ├── src/
      └── foo.json
      └── index.js
      └── message.js
  └── mini-webpack-config.js
  • 依赖:
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env chokidar@2
  • 功能项:
功能项 实现目标
1. 入口文件 从入口文件出发,递归分析依赖
2. AST 转换 使用 Babel 解析代码,提取依赖
3. 模块依赖图 构建依赖图
4. Loader 模拟 将 .json 文件转换为 JS 对象; 模拟处理图片的 loader,实际情况下图片会被转为 base64 或上传至 CDN。
5. Plugin 系统 实现生命周期钩子compile、emit,支持tap和tapAsync操作
6. Compiler 负责处理构建生命周期中的各种事件,初始化和启动构建过程,结合插件系统 在相应的生命周期钩子中执行操作
7. Compilation 负责在每次构建中处理模块(模块解析、加载、编译等)
8. HMR 通过chokidar,实现在文件发生变化时,生成新的 Compilation 实例
9. 模块打包 将多个模块打包成一个 bundle
10. 输出文件 生成最终 JS 文件并写入磁盘
  • 示例源码

src/index.js

import message, {val} from './message.js';
import foo from './foo.json';

console.log('Message is:', message, val);

console.log(foo);

src/message.js

export default 'Hello from Mini Webpack!';

export const val = "thank you";

src/foo.json

{
  "foo": "suc"
}

mini-webpack-config.js

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 chokidar = require('chokidar'); // 用于监听文件变化

// 同步执行,按注册顺序立即执行
class SyncHook {
    constructor(args) {
        this.args = args; // 参数列表(如 ['name', 'age'])
        this.taps = [];   // 存储注册的回调函数
    }

    // 注册回调
    tap(name, callback) {
        this.taps.push({ name, callback });
    }

    // 触发回调
    call(...args) {
        // 依次执行所有回调
        for (const tap of this.taps) {
            tap.callback(...args);
            console.log(tap.name, '执行同步钩子', new Date());
        }
    }
}

// 异步串行执行,按注册顺序依次执行,等待前一个完成
class AsyncSeriesHook {
    constructor(args) {
        this.args = args;
        this.taps = [];
    }

    // 注册同步钩子
    tap(name, fn) {
        this.taps.push({ name, type: 'sync', fn });
    }

    // 注册异步钩子(回调风格)
    tapAsync(name, fn) {
        this.taps.push({ name, type: 'async', fn });
    }

    // 触发异步钩子
    callAsync(...args) {
        const finalCallback = args.pop();
        const context = {};
        let index = 0;

        const next = (err) => {
            if (err) return finalCallback(err);
            const tap = this.taps[index++];
            if (!tap) return finalCallback();

            try {
                if (tap.type === 'sync') {
                    tap.fn(...args, context);
                    console.log(tap.name, '执行同步钩子', new Date());
                    next();
                } else if (tap.type === 'async') {
                    console.log(tap.name, '执行异步钩子', new Date());
                    tap.fn(...args, next);
                }
            } catch (error) {
                next(error);
            }
        };

        next();
    }
}

// 模拟 JSON loader
function jsonLoader(content) {
    return `export default ${content}`;
}

// 模拟图片 loader(这里只是简单地将图片路径作为模块内容)
function imageLoader(content) {
    return `export default '${content}'`;
}

// 解析模块
function parseModule(filename) {
    let content = fs.readFileSync(filename, 'utf-8');

    // 处理不同类型的文件
    if (filename.endsWith('.json')) {
        content = jsonLoader(content);
    } else if (filename.match(/\.(jpg|png|gif)$/)) {
        content = imageLoader(content);
    }

    const ast = parser.parse(content, { sourceType: 'module' });
    const dependencies = [];
    const mapping = {}; // 这里用来存储模块的依赖关系

    traverse(ast, {
        ImportDeclaration({ node }) {
            dependencies.push(node.source.value);
            const relativePath = node.source.value;
            // 解析相对路径为绝对路径
            const absolutePath = path.resolve(path.dirname(filename), relativePath);
            dependencies.push(absolutePath); // 使用绝对路径存储依赖
            mapping[relativePath] = absolutePath; // 保存相对路径与绝对路径的映射关系
        }
    });

    const { code } = babel.transformFromAstSync(ast, null, {
        presets: ['@babel/preset-env'],
    });

    return {
        filename,
        dependencies,
        code,
        mapping
    };
}

// 模拟 Compilation 类:负责模块的解析和构建
class Compilation {
    constructor(modules, plugins) {
        this.modules = modules; // 存储所有的模块
        this.plugins = plugins; // 存储所有插件
    }

    buildModule(filename) {
        // 通过解析模块,构建模块内容
        const module = parseModule(filename);
        this.modules.push(module);
    }

    generateOutput() {
        // 生成最终的打包代码
        let modules = '';
        this.modules.forEach(mod => {
            modules += `"${mod.filename}": [
        function(require, module, exports) {
          ${mod.code}
        },
        ${JSON.stringify(mod.mapping).replace(/\\\\/g,'\\')},
      ],`;
        });

        return `
      (function(modules) {
        function require(id) {
          const [fn, mapping] = modules[id];
          function localRequire(name) {
            return require(mapping[name]);
          }

          const module = { exports: {} };
          fn(localRequire, module, module.exports);
          return module.exports;
        }

        require("${this.modules[0].filename}");
      })({${modules}})
    `;
    }
}

// 模拟 Compiler 类:负责整体的构建生命周期管理
class Compiler {
    constructor({entry, plugins, outputPath}) {
        this.entry = entry; // 入口文件
        this.plugins = plugins || []; // 插件列表
        this.outputPath = outputPath; // 输出路径
        this.modules = []; // 存储所有模块
        this.hooks = {};
        this.initHooks();
    }

    initHooks() {
        this.hooks.compile = new SyncHook();
        this.hooks.emit = new AsyncSeriesHook();
    }

    // 执行插件生命周期钩子
    applyPlugins() {
        this.plugins.forEach(plugin => plugin.apply(this));
    }

    // 执行构建过程
    run() {
        this.applyPlugins();

        // 执行compile钩子相关回调
        this.hooks.compile.call();

        const compilation = new Compilation(this.modules, this.plugins);

        // 解析入口模块
        compilation.buildModule(path.resolve(this.entry));

        // 解析其他模块(依赖)
        for (const module of this.modules) {
            const dirname = path.dirname(module.filename);
            module.dependencies.forEach(relativePath => {
                const absPath = path.resolve(dirname, relativePath);
                compilation.buildModule(absPath);
            });
        }

        // 执行emit钩子相关回调
        this.hooks.emit.callAsync(compilation, () => {
            console.log('emit插件执行完成。')

            // 最终生成输出文件
            const output = compilation.generateOutput();

            // 输出到文件
            const outDir = this.outputPath;
            if (!fs.existsSync(outDir)) fs.mkdirSync(outDir);
            fs.writeFileSync(path.join(outDir, 'bundle.js'), output, 'utf-8');
            console.log('✅ 构建成功:dist/bundle.js');
        });
    }

    // 监视文件变化,重新构建
    watch() {
        const watcher = chokidar.watch(this.entry, { persistent: true });
        watcher.on('change', (filename) => {
            console.log(`文件变化:${filename}`);
            // 清空之前的模块,重新执行构建
            this.modules = [];
            this.run();
        });
    }
}

// 模拟插件
class ExamplePlugin1 {
    apply(compiler) {
        // 使用 tap 注册同步钩子
        compiler.hooks.compile.tap('ExamplePlugin1', (params) => {
            console.log('ExamplePlugin1,同步钩子:编译开始!', new Date());
        });

        // 使用 tapAsync 注册异步钩子
        compiler.hooks.emit.tapAsync('ExamplePlugin1', (compilation, callback) => {
            console.log('ExamplePlugin1,异步钩子:开始 emitting 文件...', new Date());
            setTimeout(() => {
                console.log('ExamplePlugin1,异步操作完成', new Date());
                callback(); // 完成异步操作并触发回调
            }, 1000);
        });
    }
}

class ExamplePlugin2 {
    apply(compiler) {
        // 使用 tap 注册同步钩子
        compiler.hooks.compile.tap('ExamplePlugin2', (params) => {
            console.log('ExamplePlugin2,同步钩子:编译开始!', new Date());
        });

        // 使用 tapAsync 注册异步钩子
        compiler.hooks.emit.tapAsync('ExamplePlugin2', (compilation, callback) => {
            console.log('ExamplePlugin2,异步钩子:开始 emitting 文件...', new Date());
            setTimeout(() => {
                console.log('ExamplePlugin2,异步操作完成', new Date());
                callback(); // 完成异步操作并触发回调
            }, 1000);
        });
    }
}

const compiler = new Compiler({
    entry: './src/index.js',
    plugins: [new ExamplePlugin1(), new ExamplePlugin2()],
    outputPath: path.resolve(__dirname, 'dist')
});

// 启动构建并监视文件变化
compiler.run();
compiler.watch();

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Study Notes[https://wuner.gitee.io/wuner-notes/fed-e-task...
    Wuner阅读 535评论 0 0
  • 第一部分:概念 概念 webpack 是一个现代的 JavaScript 应用程序的模块打包器(module bu...
    吴佳浩阅读 3,099评论 0 2
  • 1. 入门(一起来用这些小例子让你熟悉webpack的配置) 1.1 初始化项目 新建一个目录,初始化npm we...
    kkgo_阅读 2,071评论 0 40
  • 1. 简介 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bu...
    anearseeyou阅读 367评论 0 0
  • 前言: 2020年是多灾多难的一年,疫情持续至今,到目前,全世界的经济都受到不同程序的影响,各大公司裁员,在这样一...
    西巴撸阅读 2,221评论 0 11