all articles

learn webpack 4 - create plugin system(lspack)

2017-04-18 @sunderls

webpack js

1. tapable

tapable是webpack使用的plugin实现方式。具体我们来看例子:

var Tapable = require("tapable");
function MyClass() {
    Tapable.call(this);
}

MyClass.prototype = Object.create(Tapable.prototype);

MyClass.prototype.method = function() {};

上述code执行过后,MyClass就变成了一个支持plugin的class,其将拥有apply()plugin(),来看Tapable的代码:

function Tapable() {
    this._plugins = {};
}
Tapable.prototype.plugin = function plugin(name, fn) {
    if(Array.isArray(name)) {
        name.forEach(function(name) {
            this.plugin(name, fn);
        }, this);
        return;
    }
    if(!this._plugins[name]) this._plugins[name] = [fn];
    else this._plugins[name].push(fn);
};

Tapable.prototype.apply = function apply() {
    for(var i = 0; i < arguments.length; i++) {
        arguments[i].apply(this);
    }
};

plugin()就是注册一个plugin,plugin以名字为key维护在_plugins数组中,apply()接受一个plugin数组,然后调用arguments[i].apply(this),也就是执行plugin。回想在上一篇文章,创建plugin的时候需要写apply方法,也就是会被这里调用,也就是说compiler实际上是mixin了Tapable。

嘛,简单来看就是维护一个数组而已了,没什么难度,也不一定非要这么写。

2. lspack的plugin

上一篇文章中的结尾总结中,我们可以看到对于lspack而言,每次build都有以下几个步骤:

  1. webpack的compiler根据entry的设定,compile后得到所需要的files
  2. 根据loader的定义,将files转化为modules
  3. 再根据entry的设置,分析modules的关系,生成对应的chunks
  4. chunks输出成assets。

对于webpack而言可以阅读这里的API查看其可以hook的地方,lspack只是搞着玩,所以暂定如下几个可以hook的点:

  1. filesCollected: 用到了哪些sourceFile,这里可以hook到进行更改
  2. modulesWrapped: sourceFile被包好成为了module,这里可以增删modules。
  3. chunksGenerated: 根据entry设置得到的chunks graph得到确定。这里可以增删一些chunks
  4. assetsGenerated: 最终生成的assets 数据,包含了最终打包资源的文本格式。可以增删。
  5. assetsOutputted: compilation结束。

命名和实现上和webpack就有些不一样了,不过思想感觉是差不多的(汗。webpack分了compiler和compilation两个重要概念,用compiler来指代webpack编译环境,用compilation来指代每一次发生的编译。lspack将不做太多区分(免得麻烦,只是学习而已)

2.1 lspack的plugin配置

总之先实现一个filelist plugin好了。

lspack.config.js

class FileListPlugin {
    apply(lspack) {
        lspack.plugin('assetsGenerated', (assets) => {
            const assetsNow = assets.slice(0);
            assets.push({
                source: () => {
                    return assetsNow.map(asset => lspack.output(asset.output)).join('\n');
                },
                output: {
                    fileName: 'filelist.md'
                }
            });
        });
    }
}

module.exports = {
    entry: {
        app: './src/index.js'
    },
    output: {
        path: './dist/',
        fileName: '[name].js'
    },
    rules: [
        {
            reg: /\.ls$/,
            loader: 'ls'
        }
    ],
    plugins: [
        new FileListPlugin()
    ]
}

在config上,类似于webpack,new一个plugin,plugin需要支持apply方法,apply接受lspack传入,然后调用lspack的plugin方法hook进去一个function。

另外为了方便查看log,example中,增加log.js将log内容直接打印在dom中。github commit

2.2 index.js 分解

分解成不同的步骤,挂载不同的hook,有几个数组需要维护:

  1. files: string[] 用到的不同的source
  2. modules: function[] 不同source转换而来的module function
  3. chunks: Object[] 根据entry得到的chunks,每一个chunk包含一个到多个module
  4. assets: Object[] 最终生成的assets,包含文本source和文件输出路径。

plugin就是对上述数组进行操作。首先把index.js分解成上述如下几个步骤:

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const parseEntry = require('./lib/parseEntry');
const parseOutput = require('./lib/parseOutput');
const collectFiles = require('./lib/collectFiles');
const wrapToModules = require('./lib/wrapToModules');
const generateChunks = require('./lib/generateChunks');
const generateAssets = require('./lib/generateAssets');
const outputAssets = require('./lib/outputAssets');

// read lspack.config.js and parse
const config = require(path.resolve('./lspack.config.js'));

const entry = parseEntry(config.entry);

const files = collectFiles(entry);

const moduleGraph = wrapToModules(files, config.rules);

const chunks = generateChunks(entry, moduleGraph);

const assets = generateAssets(chunks, moduleGraph);

const output = parseOutput(config.output);

outputAssets(assets, output);

具体没有做新增内容,只是把之前的一个文件中的内容进行了分解,注意collectFiles和wrapToModules分别进行了一次parse,感觉有点多余,这个以后再优化好了。这次更新的内容在这里

2.3 封装compilation

考虑到文件变化的时候需要重新compile,所以上面几个方法需要封装称称为compiler class。 compiler执行方法的每一个阶段,都跑一遍plugin。 plugin实现的很土,就针对每一个event用一个数组维护其对应的plugin。在compiler的构造方法中接受config传过来的plugin并注册。

compiler.js

cfunction Compiler(config) {
    this.entry = parseEntry(config.entry);
    this.output = parseOutput(config.output);
    this.rules = config.rules;
    this.plugins = {};

    config.plugins.forEach(plugin => {
        plugin.apply(this);
    });
}

Compiler.prototype = {
    compile(){
        const files = collectFiles(this.entry);
        this.applyPlugins('filesCollected', files);
        const moduleGraph = wrapToModules(files, this.rules);
        this.applyPlugins('modulesWrapped', moduleGraph);
        const chunks = generateChunks(this.entry, moduleGraph);
        this.applyPlugins('chunksGenerated', chunks);
        const assets = generateAssets(chunks, moduleGraph);
        this.applyPlugins('assetsGenerated', assets);
        outputAssets(assets, this.output);
        this.applyPlugins('assetsOutputted', assets);
    },

    plugin(event, callback) {
        if (!this.plugins[event]) {
            this.plugins[event] = [];
        }

        this.plugins[event].push(callback);
    },

    applyPlugins(event, params) {
        if (this.plugins[event]) {
            this.plugins[event].forEach(plugin => {
                plugin(params);
            });
        }
    }
}

module.exports = Compiler;

当然还有一些其他的调整

2.4 试一下

> npm link
> cd example
> lspack

可以看到dist/中新增了filelist.md文件,其只有一行,是app.js的地址。

./dist/app.js

嗯,感觉上走得通。

3 接下来呢?

lspack的plugin实现非常粗糙,不过能说明一点点问题。接下来的webpack学习过程中,我会继续根据实际需求或者问题来优化lspack,比如很重要的code splitting,以及hot module replacement等,敬请期待。