1. Webpack流程概括 #

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

1.1 钩子 #

2. 编写Webpack #

2.1. 创建项目 #

"bin": {"zfpxpack": "./bin/zfpxpack.js"},

2.2 创建可执行文件 #

#! /usr/bin/env node
const path = require('path');
const fs = require('fs');
const Compiler = require('../lib/Compiler');//引入Compiler构造函数
let root = process.cwd();
let options = require(path.resolve(root, 'webpack.config.js'));
let compiler = new Compiler(options);
compiler.hooks.entryOption.call(options);
compiler.run();

2.3 创建Compiler对象 #

const { SyncHook } = require('tapable');
const path = require('path');
const fs = require('fs');
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');

class Compiler {
    constructor(options) {
        this.options = options;//保存选项对象
        this.hooks = {
            entryOption: new SyncHook(['options']),//保存一个解析选项对象的钩子
            run: new SyncHook([]),
            afterPlugins: new SyncHook([]),
            compile: new SyncHook(),//编译
            afterCompile: new SyncHook(),//编译完成后
            emit: new SyncHook(),//发射
            done: new SyncHook()//完成
        }
        let plugins = options.plugins;//加载自定义插件
        if (plugins && plugins.length > 0) {
            plugins.forEach(plugin => {
                plugin.apply(this);
            });
        }
        this.hooks.afterPlugins.call();//发射加载完成事件
    }
    run() {

    }

}
module.exports = Compiler;

2.4 开始编译 #

const {SyncHook} = require('tapable');
const path = require('path');
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
const ejs = require('ejs');
const fs = require('fs');
class Compiler{
    constructor(options){
        this.options = options;
        this.hooks = {
            entryOption:new SyncHook(['compiler']),
            afterPlugins:new SyncHook(['compiler']),
            run:new SyncHook(['compiler']),
            compile:new SyncHook(['compiler']),
            afterCompile:new SyncHook(['compiler']),
            emit:new SyncHook(['compiler']),
            done:new SyncHook(['compiler'])
        }
        let plugins = options.plugins;
        plugins.forEach((plugin)=>{
            plugin.apply(this);
        });
        this.hooks.afterPlugins.call(this);
    }
    run(){
        const compiler = this;
        this.hooks.run.call(compiler);
        let root = process.cwd();//webpack所在目录的绝对路
        let {
            entry,
            output:{
                path:dist,
                filename
            },
            module:{
                rules
            }
        } = this.options;//入口 输出 目录 和文件名
        let modules = {};//存放所有的模块
        let entryId; //入口ID 所有的ID都是相对于root根目录的
        this.hooks.compile.call(compiler);
        parseModule(path.resolve(root,entry),true);//解析模块
        this.hooks.afterCompile.call(compiler);
        let tmpl = fs.readFileSync(path.join(__dirname,'main.ejs'),'utf8');
        let bundle = ejs.compile(tmpl)({modules,entryId});
        fs.writeFileSync(path.join(dist,filename),bundle);
        this.hooks.emit.call(compiler);
        this.hooks.done.call(compiler);

        function parseModule(modulePath,isEntry){//解析模块和依赖的模块 路径是一个绝对路径
            let source = fs.readFileSync(modulePath,'utf8');//读取此模块的文件内容
            for(let i=0;i<rules.length;i++){
                let rule = rules[i];
                if(rule.test.test(modulePath)){
                    let use = rule.use || rule.loader
                    if(use instanceof Array){
                        for(let j=use.length-1;j>=0;j--){
                            let loader = require(path.resolve(root,'node_modules',use[j]));
                            source = loader(source);
                        }
                    }else{
                        let loader = require(path.resolve(root,'node_modules',typeof use == 'string'?use:use.loader));
                    }
                    break;
                }
            }
            let moduleId = './'+path.relative(root,modulePath);
            if(isEntry) entryId = moduleId;
            let parseResult = parse(source,path.dirname(moduleId));
            let requires = parseResult.requires;//取得依赖的模块数组
            modules[moduleId] = parseResult.source;//记录ID和转换后代码的对应关系
            if(requires && requires.length>0){
                requires.forEach(require=>{
                    parseModule(path.join(root,require))
                });
            }
        }

        //源码和父路径
        function parse(source,parentPath){
           let ast = esprima.parse(source);
           let requires = [];
           estraverse.replace(ast,{
               enter(node,parent){
                    if(node.type == 'CallExpression' && node.callee.name == 'require'){
                        let name = node.arguments[0].value;//取得参数里的值
                        name += (name.lastIndexOf('.')>0?'':'.js');//添加后缀名
                        let moduleId = './'+path.join(parentPath,name);//取得此模块ID,也是相对于根路径的
                        requires.push(moduleId);
                        node.arguments = [{type:'Literal',value:moduleId}];
                        return node;
                    }
                   return node;
               }
           });
           source = escodegen.generate(ast);//重新生成代码
           return {source,requires};//转换后的代码和依赖的模块
        }
    }
}

module.exports = Compiler;

2.5 产出文件 #

/******/ (function(modules) { // webpackBootstrap
/******/     // The module cache
/******/     var installedModules = {};
/******/
/******/     // The require function
/******/     function require(moduleId) {
/******/
/******/         // Check if module is in cache
/******/         if(installedModules[moduleId]) {
/******/             return installedModules[moduleId].exports;
/******/         }
/******/         // Create a new module (and put it into the cache)
/******/         var module = installedModules[moduleId] = {
/******/             i: moduleId,
/******/             l: false,
/******/             exports: {}
/******/         };
/******/
/******/         // Execute the module function
/******/         modules[moduleId].call(module.exports, module, module.exports, require);
/******/
/******/         // Flag the module as loaded
/******/         module.l = true;
/******/
/******/         // Return the exports of the module
/******/         return module.exports;
/******/     }
/******/     // Load entry module and return exports
/******/     return require(require.s = "<%-entryId%>");
/******/ })
/************************************************************************/
/******/ ({
<%
for(let id in modules){
    let source = modules[id];%>
/***/ "<%-id%>":
/***/ function(module, exports,require) {

eval(`<%-source%>`);

/***/ },
<%}%>
/******/ });

2.5 支持loader #

less-loader

var less = require('less');
module.exports = function (source) {
    let css;
    less.render(source, (err, output) => {
        css = output.css;
    });
    return css.replace(/\n/g,'\\n','g');
}

css-loader

var less = require('less');
module.exports = function (source) {
    return `let style = document.createElement('style');style.innerHTML = ${JSON.stringify(source)};document.head.appendChild(style);`;
}

2.6. 支持插件 #

2.7 测试用例 #

index.js

let name = require('./a/a1');
require('./index.less');
alert(name);

index.less

body{color:red;}

webpack.config.js

const path = require('path');
const AfterCompilerWebpackPlugin = require('./src/plugins/after-compiler-webpack-plugin');
const CompilerWebpackPlugin = require('./src/plugins/compiler-webpack-plugin');
const DoneWebpackPlugin = require('./src/plugins/done-webpack-plugin');
const EmitWebpackPlugin = require('./src/plugins/emit-webpack-plugin');
const OptionWebpackPlugin = require('./src/plugins/option-webpack-plugin.js');
const RunWebpackPlugin = require('./src/plugins/run-webpack-plugin');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve('dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.less/,
                use: ['style-loader', 'less-loader']
            }
        ]
    },
    plugins: [
        new AfterCompilerWebpackPlugin(),
        new CompilerWebpackPlugin(),
        new DoneWebpackPlugin(),
        new EmitWebpackPlugin(),
        new OptionWebpackPlugin(),
        new RunWebpackPlugin()
    ]
}

plugins

class Plugin{
    apply(compiler) {
        compiler.hooks.entryOption.tap('Plugin',(option) => {
            console.log('run-webpack-plugin');
        });
    }
}
module.exports=Plugin;

class Plugin{
    apply(compiler) {
        compiler.hooks.entryOption.tap('Plugin',(option) => {
            console.log('run-webpack-plugin');
        });
    }
}
module.exports=Plugin;

class Plugin{
    apply(compiler) {
        compiler.hooks.entryOption.tap('Plugin',(option) => {
            console.log('compiler');
        });
    }
}
module.exports=Plugin;


class Plugin{
    apply(compiler) {
        compiler.hooks.entryOption.tap('Plugin',(option) => {
            console.log('after compiler');
        });
    }
}
module.exports=Plugin;

class Plugin{
    apply(compiler) {
        compiler.hooks.entryOption.tap('Plugin',(option) => {
            console.log('emit');
        });
    }
}
module.exports=Plugin;

class Plugin{
    apply(compiler) {
        compiler.hooks.entryOption.tap('Plugin',(option) => {
            console.log('done');
        });
    }
}
module.exports=Plugin;