01月18, 2018

解析webpack plugin的生命周期,书写自己的第一个plugin

引子

想要了解webpack plugin如何编写,首先要了解其应用场景和作用。

可以先浏览这三篇文章

how-to-write-a-plugin

compiler API

plugins API

除此之外,在这里我和webpack loader进行了简单的对比。

plugin & loader

plugin

顾名思义,webpack plugin是作为webpack的一个插件机制存在,将webpack提供的处理方法暴露给第三方(开发者)来开发。在整个项目架构中,往往起宏观上的作用。例如HtmlWebpackPlugin,修改一些文件,inject一些用户的资源,这些资源往往是经过loader处理过的资源,比如jsx文件,css文件。

loader

而loader用于对开发者源代码的转换,功能而言,跟webpack本身并没有强耦合的关系。例如,强大的babel-loader可以使用浏览器暂不支持的JavaScript语法(糖),css-loaderstyles-loader用来处理你的css

总之,Loader的这些工作不需要开发者去干涉,只需相应配置全权交个loader去处理。而plugin往往需要用户先预备好已经有的资源,再去对资源进行宏观上的操作,并不会在内容细节上处理。

场景的明确

我们需要明确一些plugin场景来进行实际开发的模拟。比如,抽离公共模块(CommonsChunkPlugin),控制模块的输出方式,或者输出内容(这里可能体现比较直观的是UglifyJsPlugin),复制一些为经过webpack处理的静态文件(copyWebpackPlugin)。

Compiler and Compilation

在了解生命周期之前,必须要了解Compiler and Compilation两个概念,我通常会翻译成编译器编译集合

Compiler(编译器)

翻译为编译器,是因为往往编译器在开发者的眼中是整个源代码所处的编译环境(预设环境),是一个静态场景。webpack通过Compiler提供了webpack配置内容的所有配置项和插件相关的调用函数,在这里,你可以随意获得你想要的某个配置,并且根据相应的配置书写相应的plugin代码逻辑。下面展示了compiler中用到的一些生命周期和有关webpack配置的代码。

_plugins: { 'before-run': [ [Function] ], done: [ [Function] ] },
options:
   { entry: './index.js',
     output:
      { path: '/Users/beace/Documents/beace/github/webpack/custom-plugins/first-plugin',
        filename: 'bundle.js',
        chunkFilename: '[id].bundle.js',
        library: '',
        hotUpdateFunction: 'webpackHotUpdate',
        jsonpFunction: 'webpackJsonp',
        libraryTarget: 'var',
        sourceMapFilename: '[file].map[query]',
        hotUpdateChunkFilename: '[id].[hash].hot-update.js',
        hotUpdateMainFilename: '[hash].hot-update.json',
        crossOriginLoading: false,
        chunkLoadTimeout: 120000,
        hashFunction: 'md5',
        hashDigest: 'hex',
        hashDigestLength: 20,
        devtoolLineToLine: false,
        strictModuleExceptionHandling: false },
     plugins: [ HelloWorldPlugin {}, MyPlugin {} ],
     context: '/Users/beace/Documents/beace/github/webpack/custom-plugins/first-plugin',
     devtool: false,
     cache: true,
     target: 'web',
     module:
      { unknownContextRequest: '.',
        unknownContextRegExp: false,
        unknownContextRecursive: true,
        unknownContextCritical: true,
        exprContextRequest: '.',
        exprContextRegExp: false,
        exprContextRecursive: true,
        exprContextCritical: true,
        wrappedContextRegExp: /.*/,
        wrappedContextRecursive: true,
        wrappedContextCritical: false,
        strictExportPresence: false,
        strictThisContextOnImports: false,
        unsafeCache: true },
     node:
      { console: false,
        process: true,
        global: true,
        Buffer: true,
        setImmediate: true,
        __filename: 'mock',
        __dirname: 'mock' },
     performance: { maxAssetSize: 250000, maxEntrypointSize: 250000, hints: false },
     resolve:
      { unsafeCache: true,
        modules: [Array],
        extensions: [Array],
        mainFiles: [Array],
        aliasFields: [Array],
        mainFields: [Array],
        cacheWithContext: false },
     resolveLoader:
      { unsafeCache: true,
        mainFields: [Array],
        extensions: [Array],
        mainFiles: [Array],
        cacheWithContext: false } },
  context: '/Users/beace/Documents/beace/github/webpack/custom-plugins/first-plugin',
}

Compilation(编译集合)

Compilation虽然继承自Compiler,但是对于本身作用来讲,因为他包含了chunks,modules,cache,assets,是动态的资源集合。动态的原因是,在某个编译阶段,产生的编译资源是不相同的。

编译会显示有关模块资源,编译资源,更改的文件以及监视的依赖项当前状态的信息。编译还提供了许多插件可以选择执行自定义操作的回调点。

每一个版本执行的编辑逻辑(开发者),决定了上述特点。下面选取了部分关于chunks和assets中的内容

chunks:
   [ Chunk {
       id: 0,
       ids: [Array],
       debugId: 1000,
       name: 'main',
       _modules: [SortableSet],
       entrypoints: [Array],
       chunks: [],
       parents: [],
       blocks: [],
       origins: [Array],
       files: [Array],
       rendered: true,
       entryModule: [NormalModule],
       hash: 'bfe5f97a4642c50a5286f6a28486186a',
       renderedHash: 'bfe5f97a4642c50a5286' } ],

{ 'bundle.js':
   CachedSource {
     _source: ConcatSource { children: [Array] },
     _cachedSource: undefined,
     _cachedSize: undefined,
     _cachedMaps: {},
     node: [Function],
     listMap: [Function] } }

生命周期

简历一个简单的项目

通过以下简单的配置,我将一个index.js简单的进行webpack打包,输出bundle.js。并在根目录下创建my-plugin.js文件,作为即将开发的插件。代码如下。

const path = require('path');
const webpack = require('webpack');
const MyPlugin = require('./my-plugin');

module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname),
    filename: 'bundle.js',
  },
  plugins: [
    new MyPlugin({ options: true }),
  ]
}

根据webpack的要求,插件必须要在其原型上创建apply对象。

因为当webpack命令执行时,插件将被创建,而webpack将通过调用apply来安装插件,并将引用传递给webpack编译对象。

反观表现,通过创建apply对象以及apply的参数,可以调用webpack底层的方法。在my-plugin.js中写入

function MyPlugin(options) {}

MyPlugin.prototype.apply = function(compiler) {}

从执行顺序看生命周期

如果非常粗暴的将plugin的几个关键的生命周期输出出来,执行顺序是将会是这样的

    // 1
  compiler.plugin("compile", function(params) {
    console.log("The compile is starting to compile...", params);
  });
  // 2
  compiler.plugin("compilation", function(compilation, params) {
    console.log("The compile is starting a new compilation...");
    // 4
    compilation.plugin("optimize", function() {
      console.log("The compilation is starting to optimize file...");
    });
  });
  // 3
  compiler.plugin("make", function(compiler, callback){
    console.log("the compile is making file...");
    callback();
  });
  // 5
  compiler.plugin("after-compile", function(compilation) {
    console.log("The compile has aleardy compiled");
  });
    // 6
    compiler.plugin("emit", function(compilation, callback) {
    console.log("The compilation is going to emit files...");
    callback();
  });
    // 7
    compiler.plugin('after-emit', function(compilation) {
    console.log('The compliation has aleardy emitted');
  })

代码的注释,代表了执行的顺序,可以看下命令行中的输出

webpack

从上述代码的执行顺序来看,plugin的生命周期如下:

  1. Compile 开始进入编译环境,开始编译
  2. Compilation 即将产生第一个版本
  3. make任务开始
  4. optimize作为Compilation的回调方法,优化编译,在Compilation回调函数中可以为每一个新的编译绑定回调。
  5. after-compile编译完成
  6. emit准备生成文件,开始释放生成的资源,最后一次添加资源到资源集合的机会
  7. after-emit文件生成之后,编译器释放资源

从源码中看生命周期

咦,好像漏了两条,当编译完成时,可以看到命令行里面并没有文件的输出,回去查看项目中的代码,也并没有bundle.js文件。6、7步到底执行了么?

答案当然是没有执行。因为没有看到资源释放的结果。

让我们在源码中一探究竟。找到Compile所在的源码。

compile(callback) {
        const params = this.newCompilationParams();
        this.applyPluginsAsync("before-compile", params, err => {
            if(err) return callback(err);
            // 1
            this.applyPlugins("compile", params);
            // 2
            const compilation = this.newCompilation(params);
            // 3
            this.applyPluginsParallel("make", compilation, err => {
                if(err) return callback(err);

                compilation.finish();
                // 4
                compilation.seal(err => {
                    if(err) return callback(err);
                    // 5
                    this.applyPluginsAsync("after-compile", compilation, err => {
                        if(err) return callback(err);

                        return callback(null, compilation);
                    });
                });
            });
        });
    }

很明显,当编译完成时,webpack Seal 资源完毕后直接将callback return,所以当我们在调用after-compile没有进行任何处理,阻止了接下来的return。将my-plugin.js中的代码注释掉after-compile这一步骤或者添加新的参数callback并执行。

// my-plugin.js
compiler.plugin("after-compile", function(compilation, callback) {
    console.log("The compile has aleardy compiled");
    callback();
  });

这时再运行webpack,命令行中可以看到输出了The compilation is going to emit files,并且输出了bundle.js

webpack

编写自己的插件

上面截图可以看到,Hash上面的一行输出All compilers have done.,其实这也是在webpack plugin的生命周期的范围,done是所有工作结束后,会执行的最后一个步骤。并且,当webpack plugin watch到某个过程出错的时候,也会执行done。如以下源代码,可以看到每次执行错误之后,都会走done 流程。

this.compiler.applyPluginsAsync("watch-run", this, err => {
            if(err) return this._done(err);
            const onCompiled = (err, compilation) => {
                if(err) return this._done(err);
                if(this.invalid) return this._done();

                if(this.compiler.applyPluginsBailResult("should-emit", compilation) === false) {
                    return this._done(null, compilation);
                }

                this.compiler.emitAssets(compilation, err => {
                    if(err) return this._done(err);
                    if(this.invalid) return this._done();

                    this.compiler.emitRecords(err => {
                        if(err) return this._done(err);

                        if(compilation.applyPluginsBailResult("need-additional-pass")) {
                            compilation.needAdditionalPass = true;

                            const stats = new Stats(compilation);
                            stats.startTime = this.startTime;
                            stats.endTime = Date.now();
                            this.compiler.applyPlugins("done", stats);

                            this.compiler.applyPluginsAsync("additional-pass", err => {
                                if(err) return this._done(err);
                                this.compiler.compile(onCompiled);
                            });
                            return;
                        }
                        return this._done(null, compilation);
                    });
                });
            };
            this.compiler.compile(onCompiled);
        });

因此,为了简单而言,我们此次编写的插件也是基于done来进行。

编写plugin

接下来将要编写一个在生成bundle.js文件之后,在第一行添加时间注释,在最后一行添加自己姓名注释,并重新输出bundle.js

compiler.plugin("done", function(stats) {
    console.log('All compilers have done.');
    const fileData = fs.readFileSync(path.join(path.resolve(__dirname), 'bundle.js'), {encoding: 'utf-8'});
    console.log(fileData);
    const prefix = '/*2018*/';
    const author = '/* ——By Beace Lee */';
    const finalFileData = `${prefix}\n${fileData}\n${author}`;
    fs.writeFileSync(
      path.join(path.resolve(__dirname), 'bundle.js'),
      finalFileData
    );
  })

通过以上代码可以看出,在done这个步骤中,通过读取emitbundle.js文件(因为这个时候资源已经释放,可以直接使用资源),以utf-8的格式读取,读取完毕后在整个字符串的前后添加两行注释并换行,再写到最终文件里。


// bundle.js
/*2018*/
...
/* 0 */
/***/ (function(module, exports) {
...
console.log('this is a entry js file');
...
/***/ })
/* ——By Beace Lee */

总结

此种方式,其实是调用了node的fs的API去实现,看起来除了生命周期之外,并没有和webpack plugin有什么太大关系,我们其实是操作了文件,当有大量文件存在的时候,该插件显得捉襟见肘。

除此之外,前面说过可以操作compiler的assets集合, 暂时写到这里,下回再聊。

本文链接:https://beacelee.com/post/webpack-plugin-custome-plugin.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。