all articles

learn webpack 3 - surface of plugin

2017-04-15 @sunderls

webpack js

在上一篇文章中,我们理解了一下loader,接下来我们看看Plugin - 另外一个非常重要的概念。实际上我个人在使用的时候,对于plugin也没有理解的特别深入,因为每一个plugin都基本上有特定的目的,而目的都比较明确简单,所以只知道了用某些plugin,对于plugin的实现并不怎么了解,在这篇文章中,我通过lspack中的plugin实现,来更加深入地理解一下。

非loader完成的工作都由plugin完成

官网上的原话:

A webpack plugin is a JavaScript object that has an apply property. This apply property is called by the webpack compiler, giving access to the entire compilation lifecycle.

也就是,plugin 会hook到compiler中,在特定的阶段完成特定的动作,然后影响编译结果。

plugin的使用

官网上给出了两个plugin的例子,一个是webpack自带,一个是第三方的

const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins
const path = require('path');

const config = {
  //...
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

可以看到plugin是new了过后,传递给config.plugins。为什么要new呢?因为简单的object的话,就不够灵活了,new的话可以传递参数,比如上面的template。这两个plugin的作用分别是:

  1. UglifyJsPlugin: 很明显,uglify js代码。
  2. HtmlWebpackPlugin: 我去查了一下,这个plugin生成一个html文件,包含了打包后的资源的引用。比如打包后的js文件名包含hash的话,可以通过这个自动化完成修改。

可以看到,第一个文件是影响输出结果,第二个插件是产生副作用干点其他事情。

plugin的例子

plugin的实现说明在这里, 看完了就可以看看其具体的API,我们直接看实例说明、一个简单的plugin:

function HelloWorldPlugin(options) {
  // Setup the plugin instance with options...
}

HelloWorldPlugin.prototype.apply = function(compiler) {
  compiler.plugin('done', function() {
    console.log('Hello World!'); 
  });
};

module.exports = HelloWorldPlugin;

看代码就明白了,HelloWorldPlugin 挂载到compiler的done 事件上,然后打印了个hello world,注意用的是compiler是,是webpack运行环境,没有涉及到具体的source变换。

再看一个涉及到source变换的例子:

function HelloCompilationPlugin(options) {}

HelloCompilationPlugin.prototype.apply = function(compiler) {

  // Setup callback for accessing a compilation:
  compiler.plugin("compilation", function(compilation) {

    // Now setup callbacks for accessing compilation steps:
    compilation.plugin("optimize", function() {
      console.log("Assets are being optimized.");
    });
  });
};

module.exports = HelloCompilationPlugin;

这个plugin挂载到了compiler的compilation上,然后compilation中挂载到了optimize事件上。 我们将lspack的example(除去ls loader部分)用webpack+上述plugin实验结果,得到了一条打印。

> webpack

Assets are being optimized.
Hash: 21cafce1d447ae1838ad
Version: webpack 2.2.0
Time: 73ms
    Asset     Size  Chunks             Chunk Names
bundle.js  3.58 kB       0  [emitted]  main
   [0] ./src/moduleA.js 113 bytes {0} [built]
   [1] ./src/moduleB.js 55 bytes {0} [built]
   [2] ./src/index.js 112 bytes {0} [built]

再看一个具体的例子

下面的plugin 生成一个markdown,包含所有的resource。

function FileListPlugin(options) {}

FileListPlugin.prototype.apply = function(compiler) {
  compiler.plugin('emit', function(compilation, callback) {
    // 创建md文件头
    var filelist = 'In this build:\n\n';

    // 在emit事件中,可以通过compilation的assets来获取所有文件。
    for (var filename in compilation.assets) {
      filelist += ('- '+ filename +'\n');
    }

    // 在compilation.assets中增加一个文件
    compilation.assets['filelist.md'] = {
      source: function() {
        return filelist;
      },
      size: function() {
        return filelist.length;
      }
    };

    callback();
  });
};

module.exports = FileListPlugin;

也就是说,webpack是加载source,然后compile,分析依赖关系,最后优化,bundle。其中每一个阶段都提供了hook,作为plugin,可以通过这些hook注册自己的行为,来影响最为核心的compilation。 嗯,有点清楚了。

再用lspack的example实验上述例子,得到:

> webpack

Hash: 21cafce1d447ae1838ad
Version: webpack 2.2.0
Time: 69ms
      Asset      Size  Chunks             Chunk Names
  bundle.js   3.58 kB       0  [emitted]  main
filelist.md  28 bytes          [emitted]
   [0] ./src/moduleA.js 113 bytes {0} [built]
   [1] ./src/moduleB.js 55 bytes {0} [built]
   [2] ./src/index.js 112 bytes {0} [built]

查看filelist.md

In this build:

- bundle.js

也就是说, compilation.assets包含最终的输出结果了,compilation还有另外一些属性,我们利用上述plugin打印出来看看,新建一个debugPlugin:

const CircularJSON = require('circular-json');

function DebugCompilationPlugin(options) {};

DebugCompilationPlugin.prototype.apply = function(compiler) {
  compiler.plugin('emit', function(compilation, callback) {
    const items = ['modules', 'fileDependencies', 'chunks', 'assets'];

    items.forEach(item => {
      // use CircularJSON to avoid circular reference
      const source = CircularJSON.stringify(compilation[item],null,4);
      compilation.assets[`compilation.${item}.json`] = {
        source: function() {
          return source;
        },
        size: function() {
          return source.length;
        }
      };
    });

    callback();
  });
};

module.exports = DebugCompilationPlugin;

注意由于出现了循环引用,打印的时候用了circular-json。build过后诞生了4个新文件,一个一个来看:

1. compilation.modules

按照文档说明:

An array of modules (built inputs) in the compilation. Each module manages the build of a raw file from your source library.

这应该就是最开始维护的那个module list

gist;

注意类似~0~dependencies~1~module~chunks~0~modules~2这样的语句是从circular-json来的,代表了:

root[0].dependencies[0].module.chunks[0].modules[2].

2. compilation.fileDependencies

An array of source file paths included into a module. This includes the source JavaScript file itself (ex: index.js), and all dependency asset files (stylesheets, images, etc) that it has required. Reviewing dependencies is useful for seeing what source files belong to a module.

[
    "/Users/sunderls/Documents/code/src/test-webpack/src/index.js",
    "/Users/sunderls/Documents/code/src/test-webpack/src/moduleA.js",
    "/Users/sunderls/Documents/code/src/test-webpack/src/moduleB.js"
]

就是简单的一个array而已。 gist

3. compilation.chunks

An array of chunks (build outputs) in the compilation. Each chunk manages the composition of a final rendered assets.

感觉和assets 很像啊。

[
    {
        "id": 0,
        "ids": [
            0
        ],
        "debugId": 1000,
        "name": "index",
        "modules": [
          ...
        ],
        "entrypoints": [
            {
                "name": "index",
                "chunks": [
                    "~0"
                ]
            }
        ],
        "chunks": [],
        "parents": [],
        "blocks": [],
        "origins": [
            {
                "module": "~0~modules~0~reasons~0~module",
                "name": "index"
            }
        ],
        "files": [
            "index.js"
        ],
        "rendered": true,
        "entryModule": "~0~modules~0~reasons~0~module",
        "hash": "d1c723b6646cfe7790bd86faa7db5de7",
        "renderedHash": "d1c723b6646cfe7790bd"
    }
]

中间的modules是这个chunk用到的module,chunk.files是指这个chunk生成的file(这里是index.js)。

gitst

4. compilation.assets

最终结果,assets包含了输出的文件。

{
    "index.js": {
        "_source": {
            "children": [
                ...
                ";"
            ]
        },
        "_cachedMaps": {}
    },
    "compilation.modules.json": {},
    "compilation.fileDependencies.json": {},
    "compilation.chunks.json": {}
}

gits

迷迷糊糊,继续看例子-

一个检测文件变化的plugin

function MyPlugin() {
  this.startTime = Date.now();
  this.prevTimestamps = {};
}

MyPlugin.prototype.apply = function(compiler) {
  compiler.plugin('emit', function(compilation, callback) {

    var changedFiles = Object.keys(compilation.fileTimestamps).filter(function(watchfile) {
      return (this.prevTimestamps[watchfile] || this.startTime) < (compilation.fileTimestamps[watchfile] || Infinity);
    }.bind(this));

    this.prevTimestamps = compilation.fileTimestamps;
    callback();
  }.bind(this));
};

module.exports = MyPlugin;

1 这里用到了compilation.fileTimestamps,把这个属性加到debug plugin中,然后webpack --watch`,并且save任何一个文件过后,会如下fileTimestamps:

{
    "/Users/sunderls/Documents/code/src/test-webpack/src/index.js": 1492232636000,
    "/Users/sunderls/Documents/code/src/test-webpack/src/moduleA.js": 1492227827000,
    "/Users/sunderls/Documents/code/src/test-webpack/src/moduleB.js": 1492227840000
}

根据文档所述,compilation会在每次save的时候重新触发,可以动态的在compilation.fileDependencies中,注入新的file依赖。

这里来理解一下chunk,chunk就是打包后的module。webpack将所有的资源看作module,有一个module池,最终可以两种方式打包:

  1. 打包成一个js,那么chunk就只有一个,compilation.chunks.json就会是: [ { files: ['bundle.js']}]
  2. 分割成多个块,那么就会有多个chunk,[{files: ['bundle.js']}, {files: ['bundle2.js']}]

和asset的区别在于:

  1. chunk还只是一个抽象的module graph, 可以生成一个asset
  2. asset可以由plugin额外生成,可以和module无关。

至于经常用到的CommonsChunkPlugin,就是在modules到chunks阶段的时候,分析了依赖图找到了公用的module,然后额外追加了一个chunk到chunks,最终生成了额外的asset。

总结

东一下西一下扯了这么多废话,总结一下(可能还有错误):

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

其中第1,2,4步,都可以通过plugin的方式hook进去,修改files、chunks抑或assets。

嗯,这次就到这里,下一次lspack会尝试实现这个plugin系统。