手写简易版webpack

详细实现收录在https://github.com/jdkwky/webstudydeep/tree/webstudydeep/webpackstudy 中,主要以webpack03、webpack04、pwebpack文件为主,如果觉得对您有帮助欢迎给个star。


npm link

使用 npm link 创建自己本地开发包然后通过npm link modelName 引入到需要引用该模块的项目中

举例:

pwebpack 是我们的本地打包js文件的项目,当前pwebpack是空项目;

  1. cd pwebpack & npm init;
  2. package.json 中的name字段为"pwebpack"(name字段可以更改但是后面引入的包名也会跟着这个名字的改变而改变)
  3. package.json 中 加入 "bin":"./bin/pwebpack.js";
  4. 在"./bin/pwebpack.js"文件中写如下代码
#! /usr/bin/env node

// 作用 是告诉系统此文件用node执行 并且引用那个环境下的node模块

console.log('start');
  1. 在需要引入pwebpack包中的文件中执行 npm link pwebpack;
  2. 测试 npx pwebpack; 输出 "start" 即成功,修改一下pwebpack.js文件中的输出字段,重新npx pwebpack一下更新新的数据。

手写简易版 webpack

  1. 入口文件
const path = require('path');
// 获取配置文件内容
const config = require(path.resolve('webpack.config.js'));
// 引用编译类
const Compiler = require('../lib/Compiler.js');
// 创建对象
const compiler = new Compiler(config);
// 调用run 方法
compiler.run();

  1. Compiler 类
const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const types = require('@babel/types');
const generator = require('@babel/generator').default;
const ejs = require('ejs');
const { SyncHook } = require('tapable');

class Compiler {
    constructor(config) {
        this.config = config || {};
        // 保存所有模块依赖
        this.modules = {};
        // 入口文件
        this.entry = config.entry;
        this.entryId = '';

        // 工作目录
        this.root = process.cwd();

    }

    // 获取资源
    getSource(modulePath) {
        let content = fs.readFileSync(modulePath, 'utf8');
        return content;
    }

    // 解析语法
    parse(source, parentPath) {
        // AST 语法树解析
        const ast = babylon.parse(source);

        // 依赖的数组
        const dependencies = [];
        traverse(ast, {
            CallExpression(p) {
                const node = p.node;
                if (node.callee.name == 'require') {
                    node.callee.name = '__webpack_require__';
                    let moduleName = node.arguments[0].value;
                    moduleName =
                        moduleName + (path.extname(moduleName) ? '' : '.js'); // ./a.js
                    moduleName = './' + path.join(parentPath, moduleName); // ./src/a/js
                    dependencies.push(moduleName);
                    node.arguments = [types.stringLiteral(moduleName)]; // 改掉源码
                }
            }
        });

        const sourceCode = generator(ast).code;
        return { sourceCode, dependencies };
    }

    // 构建模块
    buildModule(modulePath, isEntry) {
        // 模块路径  是否是入口文件
        // 拿到模块内容
        const source = this.getSource(modulePath);

        // 获取模块id 需要相对路径

        const moduleName = './' + path.relative(this.root, modulePath);

        if (isEntry) {
            this.entryId = moduleName;
        }
        // 解析代码块
        const { sourceCode, dependencies } = this.parse(
            source,
            path.dirname(moduleName)
        );

        this.modules[moduleName] = sourceCode;
        dependencies.forEach(dep => {
            this.buildModule(path.join(this.root, dep), false);
        });
    }
    // 发射文件
    emitFile() {
        // 输出到哪个目录下
        let main = path.join(
            this.config.output.path,
            this.config.output.filename
        );
        let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
        let code = ejs.render(templateStr, {
            entryId: this.entryId,
            modules: this.modules
        });
        // 可能打包多个
        this.assets = {};
        // 路径对应的代码
        this.assets[main] = code;
        fs.writeFileSync(main, this.assets[main]);
    }
    // 运行
    run() {
        // 执行创建模块的依赖关系
        // 得到入口文件的绝对路径
       
        this.buildModule(path.resolve(this.root, this.entry), true);
        this.emitFile();
    }
}

  1. 添加简易版 loader plugin(简易版都是同步钩子)解析

构造函数constructor函数中

// 插件
this.hooks = {
    entryOption: new SyncHook(),
    compile: new SyncHook(),
    afterCompile: new SyncHook(),
    afterPlugins: new SyncHook(),
    run: new SyncHook(),
    emit: new SyncHook(),
    done: new SyncHook()
};
// 解析plugins 通过tapable
const plugins = this.config.plugins;
if (Array.isArray(plugins)) {
     plugins.forEach(plugin => {
         plugin.apply(this);
     });
}
this.hooks.afterPlugins.call();

getSource函数中

// 解析loader
const rules = this.config.module.rules || [];

for (let i = 0; i < rules.length; i++) {
    let rule = rules[i];
    const { test, use } = rule || {};
    let len = use.length;
    if (test.test(modulePath)) {
        // 需要通过 loader 进行转化
        while (len > 0) {
            let loader = require(use[--len]);
            content = loader(content);
        }
    }
}

plugin 中 tapable钩子

同步钩子

const { SyncHook } = require('tapable');

const hook = new SyncHook(['name']);

hook.tap('hello', name => {
    console.log(`hello ${name}`);
});

hook.tap('Hello again', name => {
    console.log(`Hello ${name},again`);
});

hook.call('wky');
// 输出   hello wky , Hello wky , again

简易版实现

class SyncHook {
    constructor() {
        this.tasks = [];
    }
    tap(name, fn) {
        this.tasks.push(fn);
    }
    call(...args) {
        this.tasks.forEach(task => {
            task(...args);
        });
    }
}

const hook = new SyncHook(['name']);

hook.tap('hello', name => {
    console.log(`hello ${name}`);
});

hook.tap('Hello again', name => {
    console.log(`Hello ${name},again`);
});

hook.call('wky');

SyncBailHook 熔断性执行

const { SyncBailHook } = require('tapable');

const hook = new SyncBailHook(['name']);
hook.tap('node', function(name) {
    console.log('node', name);
    return '停止学习';
});
hook.tap('react', function(name) {
    console.log('react', name);
});

hook.call('wky');

// node wky 停止学习, 就不会返回执行下面的代码

实现原理

class SyncBailHook {
    constructor() {
        this.tasks = [];
    }
    tap(name, fn) {
        this.tasks.push(fn);
    }
    call(...args) {
        let index = 0,
            length = this.tasks.length,
            tasks = this.tasks;
        let result;
        do {
            result = tasks[index](...args);
            index++;
        } while (result == null && index < length);
    }
}

const hook = new SyncBailHook(['name']);
hook.tap('node', function(name) {
    console.log('node', name);
    return '停止学习';
});
hook.tap('react', function(name) {
    console.log('react', name);
});

hook.call('wkyyc');

同步瀑布钩子(上一个监听函数的值会传递给下一个监听函数)


const { SyncWaterfallHook } = require('tapable');

const hook = new SyncWaterfallHook(['name']);
hook.tap('node', function(name) {
    console.log('node', name);
    return 'node 学的还不错';
});
hook.tap('react', function(data) {
    console.log('react', data);
});

hook.call('wky');

实现原理

class SyncWaterfallHook {
    constructor() {
        this.tasks = [];
    }

    tap(name, fn) {
        this.tasks.push(fn);
    }
    call(...args) {
        const [firstFn, ...others] = this.tasks;
        others.reduce((sum, task) => task(sum), firstFn(...args));
    }
}

const hook = new SyncWaterfallHook(['name']);
hook.tap('node', function(name) {
    console.log('node', name);
    return 'node 学的还不错';
});
hook.tap('react', function(data) {
    console.log('react', data);
});

hook.call('wkyyc');

异步钩子, 并行执行的异步钩子,当注册的所有异步回调都并行执行完毕之后再执行callAsync或者promise中的函数

const { AsyncParallelHook } = require('tapable');
const hook = new AsyncParallelHook(['name']);

hook.tapAsync('hello', (name, cb) => {
    setTimeout(() => {
        console.log(`hello ${name}`);
        cb();
    }, 1000);
});

hook.tapAsync('hello again', (name, cb) => {
    setTimeout(() => {
        console.log(`Hello ${name} again`);
        cb();
    }, 2000);
});

hook.callAsync('wkyyc', () => {
    console.log('end');
});

实现原理

class AsyncParallelHook {
    constructor() {
        this.tasks = [];
    }
    tapAsync(name, fn) {
        this.tasks.push(fn);
    }
    callAsync(...args) {
        // 要最后执行的函数
        const callbackFn = args.pop();
        let index = 0;
        const next = () => {
            index++;
            if (index == this.tasks.length) {
                callbackFn();
            }
        };

        this.tasks.forEach(task => {
            task(...args, next);
        });
    }
}
const hook = new AsyncParallelHook(['name']);

hook.tapAsync('hello', (name, cb) => {
    setTimeout(() => {
        console.log(`hello ${name}`);
        cb();
    }, 1000);
});

hook.tapAsync('hello again', (name, cb) => {
    setTimeout(() => {
        console.log(`Hello ${name} again`);
        cb();
    }, 3000);
});

hook.callAsync('wkyyc', () => {
    console.log('end');
});

异步串行执行

const { AsyncSeriesHook } = require('tapable');
const hook = new AsyncSeriesHook(['name']);

hook.tapPromise('hello', name => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`hello ${name}`);
            resolve();
        }, 1000);
    });
});

hook.tapPromise('hello again', data => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`Hello ${data} again`);
            resolve();
        }, 1000);
    });
});

hook.promise('wkyyc').then(() => {
    console.log('end');
});

实现原理

class AsyncSeriesHook {
    constructor() {
        this.tasks = [];
    }

    tapPromise(name, fn) {
        this.tasks.push(fn);
    }
    promise(...args) {
        const [firstFn, ...others] = this.tasks;

        return others.reduce(
            (sum, task) =>
                sum.then(() => {
                    return task(...args);
                }),
            firstFn(...args)
        );
    }
}
const hook = new AsyncSeriesHook(['name']);
hook.tapPromise('hello', name => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`hello ${name}`);
            resolve();
        }, 1000);
    });
});

hook.tapPromise('hello again', data => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`Hello ${data} again`);
            resolve();
        }, 1000);
    });
});

hook.promise('wkyyc').then(() => {
    console.log('end');
});

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342