如何编写一个WebPack的插件原理及实践

阅读目录javascript

一:webpack插件的基本原理css

webpack构建工具你们应该不陌生了,那么下面咱们来简单的了解下什么是webpack的插件。好比我如今写了一个插件叫 "kongzhi-plugin" 这个插件。那么这个插件在处理webpack编译过程当中会处理一些特定的任务。html

好比咱们如今在webpack.config.js 中引入了一个以下插件:java

// 引入打包html文件
const HtmlWebpackPlugin = require('html-webpack-plugin');

而后咱们须要以下使用该插件:node

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html' // 模版文件
    }),
  ]
};

如上就是一个 HtmlWebpackPlugin 插件 及在webpack中使用的方式了。如今咱们须要实现一个相似的webpack的插件。jquery

webpack打包是一种事件流的机制,它的原理是将各个插件串联起来。那么实现这一切的核心就是tapable,要想深刻了解 tapable的知识能够看我以前的一篇文章.webpack

tapable它能够暴露出挂载plugin的方法。可让咱们能将plugin控制在webpack事件流上运行。
tapable给咱们暴露了不少钩子类,能为咱们的插件提供挂载的钩子。
以下代码所示:git

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook
} = require('tapable');

如上各个钩子的含义及使用方式,能够看我以前这篇文章的介绍。github

下面咱们来看个简单的demo,咱们会定义一个 KongZhiClass 类,在内部咱们建立一个 hooks 这个对象,而后在该对象上分别建立同步钩子kzSyncHook及异步钩子 kzAsyncHook。 而后分别执行,代码以下:web

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

// 建立类 

class KongZhiClass {
  constructor() {
    this.hooks = {
      kzSyncHook: new SyncHook(['name', 'age']),
      kzAsyncHook: new AsyncParallelHook(['name', 'age'])
    }
  }
}

// 实例化
const myName = new KongZhiClass();

// 绑定同步钩子
myName.hooks.kzSyncHook.tap("eventName1", (name, age) => {
  console.log(`同步事件eventName1: ${name} this year ${age} 周岁了, 但是仍是单身`);
});

// 绑定一个异步Promise钩子
myName.hooks.kzAsyncHook.tapPromise('eventName2', (name, age) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`异步事件eventName2: ${name} this year ${age}周岁了,但是仍是单身`);
    }, 1000);
  });
});

// 执行同步钩子
myName.hooks.kzSyncHook.call('空智', 31);

// 执行异步钩子
myName.hooks.kzAsyncHook.promise('空智', 31).then(() => {
  console.log('异步事件执行完毕');
}, (err) => {
  console.log('异步事件执行异常:' + err);
}) 

执行结果以下:

如上是咱们使用的 tapable 的使用方式,如今咱们须要使用tapable的demo来和咱们的webpack的插件相关联起来,咱们要如何作呢?

咱们能够将上面的代码来拆分红两个文件:compiler.js、main.js. (main.js 是入口文件)

假如咱们的项目结构以下:

|--- tapable项目
| |--- node_modules  
| |--- public
| | |--- js
| | | |--- main.js
| | | |--- compiler.js
| |--- package.json
| |--- webpack.config.js

compiler.js 须要作的事情以下:

1. 定义一个 Compiler 类,接收一个options对象参数,该参数是从main.js中的MyPlugin类的实列对象。该对象下有 apply函数。

2. 在该类中咱们定义了run方法,咱们在main.js 中执行该run函数就能够自动执行对应的插件了。

代码以下:

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

class Compiler {
  constructor(options) {
    this.hooks = {
      kzSyncHook: new SyncHook(['name', 'age']),
      kzAsyncHook: new AsyncParallelHook(['name', 'age'])
    };
    let plugins = options.plugins;
    if (plugins && plugins.length > 0) {
      plugins.forEach(plugin => plugin.apply(this));
    }
  }
  run() {
    console.log('开始执行了---------');
    this.kzSyncHook('我是空智', 31);
    this.kzAsyncHook('我是空智', 31);
  }
  kzSyncHook(name, age) {
    this.hooks.kzSyncHook.call(name, age);
  }
  kzAsyncHook(name, age) {
    this.hooks.kzAsyncHook.callAsync(name, age);
  }
}

module.exports = Compiler;

main.js 须要作的事情以下:

1. 引入 compiler.js 文件。
2. 定义一个本身的插件,好比叫 MyPlugin 类,该类下有 apply 函数。该函数有一个 compiler 参数,该参数就是咱们的 compiler.js 中的实列对象。而后咱们会使用 compiler 实列对象去调用 compiler.js 里面的函数。所以就能够自动执行了。

代码以下所示:

const Compiler = require('./compiler');

class MyPlugin {
  constructor() {
    
  }
  apply(compiler) {
    compiler.hooks.kzSyncHook.tap("eventName1", (name, age) => {
      console.log(`同步事件eventName1: ${name} this year ${age} 周岁了, 但是仍是单身`);
    });
    compiler.hooks.kzAsyncHook.tapAsync('eventName2', (name, age) => {
      setTimeout(() => {
        console.log(`异步事件eventName2: ${name} this year ${age}周岁了,但是仍是单身`);
      }, 1000)
    });
  }
}

const myPlugin = new MyPlugin();

const options = {
  plugins: [myPlugin]
};

const compiler = new Compiler(options);
compiler.run();

最后执行的效果以下所示:

如上就是咱们仿照Compiler和webpack的插件原理逻辑实现的一个简单demo。也就是说在webpack源码里面也是经过相似的方式来作的。

上面只是一个简单实现的基本原理,可是在咱们的webpack当中咱们要如何实现一个插件呢?
在咱们的webpack官网中会介绍编写一个插件要知足以下条件, 官网地址

从官网得知:编写一个webpack插件须要由如下组成:

1. 一个javascript命名函数。
2. 在插件函数的prototype上定义一个 apply 方法。
3. 指定一个绑定到webpack自身的钩子函数。
4. 处理webpack内部实列的特定数据。
5. 功能完成后调用webpack提供的回调函数。

一个最基础的插件代码像以下这个样子:

// 一个javascript命名函数
function MyExampleWebpackPlugin() {
  
};
// 在插件函数的prototype上定义一个 apply 方法
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  // 指定一个挂载到webpack自身的事件钩子。
  compiler.plugin('webpacksEventHook', function(compilation, callback) {
    console.log('这是一个插件demo');

    // 功能完成后调用 webpack 提供的回调
    callback();
  })
}

// 导出plugin
module.exports = MyExampleWebpackPlugin;

在咱们使用该plugin的时候,相关调用及配置代码以下:

const MyExampleWebpackPlugin = require('./MyExampleWebpackPlugin');
module.exports = {
  plugins: [
    new MyExampleWebpackPlugin(options)
  ]
};

webpack启动后,在读取配置的过程当中会先执行 new MyExampleWebpackPlugin(options) 初始化MyExampleWebpackPlugin来得到一个实列。而后咱们会把该实列当作参数传递给咱们的Compiler对象,而后会实列化 Compiler类(这个逻辑能够结合看咱们上面实现了一个简单的demo中 的main.js和compiler.js的代码结合起来理解)。在Compiler类中,咱们会获取到options的这个参数,该参数是一个对象,该对象下有一个 plugins 这个属性。而后遍历该属性,而后依次执行 某项插件中的apply方法,即:myExampleWebpackPlugin.apply(compiler); 给插件传递compiler对象。插件实列获取该compiler对象后,就能够经过 compiler.plugin('事件名称', '回调函数'); 监听到webpack广播出来的事件.(这个地方咱们能够看咱们上面的main.js中的以下代码能够看到, 在咱们的main.js代码中有这样代码:compiler.hooks.kzSyncHook.tap("eventName1", (name, age) => {}));
如上就是一个简单的Plugin的插件原理(切记:结合上面的demo中main.js和compiller.js来理解效果会更好)。

二:理解 Compiler对象 和 Compilation 对象

在开发Plugin时咱们最经常使用的两个对象就是 Compiler 和 Compilation, 他们是Plugin和webpack之间的桥梁。

Compiler对象

Compiler 对象包含了Webpack环境全部的配置信息,包含options,loaders, plugins这些项,这个对象在webpack启动时候被实例化,它是全局惟一的。咱们能够把它理解为webpack的实列。

基本源码能够看以下:

// webpack/lib/webpack.js
const Compiler = require("./Compiler")

const webpack = (options, callback) => {
  ...
  // 初始化 webpack 各配置参数
  options = new WebpackOptionsDefaulter().process(options);

  // 初始化 compiler 对象,这里 options.context 为 process.cwd()
  let compiler = new Compiler(options.context);

  compiler.options = options                               // 往 compiler 添加初始化参数

  new NodeEnvironmentPlugin().apply(compiler)              // 往 compiler 添加 Node 环境相关方法

  for (const plugin of options.plugins) {
    plugin.apply(compiler);
  }
  ...
}

源码能够点击这里查看官网能够看这里

如上咱们能够看到,Compiler对象包含了全部的webpack可配置的内容。开发插件时,咱们能够从 compiler 对象中拿到全部和 webpack 主环境相关的内容。

compilation 对象

compilation 对象包含了当前的模块资源、编译生成资源、文件的变化等。当webpack在开发模式下运行时,每当检测到一个文件发生改变的时候,那么一次新的 Compilation将会被建立。从而生成一组新的编译资源。

Compiler对象 与 Compilation 对象 的区别是:Compiler表明了是整个webpack从启动到关闭的生命周期。Compilation 对象只表明了一次新的编译。
Compiler对象的事件钩子,咱们能够看官网. 或者咱们也能够查看它的源码也能够看获得,查看源码

咱们能够了解常见的事件钩子:下面是一些比较常见的事件钩子及做用:

钩子               做用                     参数               类型
after-plugins     设置完一组初始化插件以后    compiler          sync
after-resolvers   设置完 resolvers 以后     compiler          sync
run               在读取记录以前             compiler          async
compile           在建立新 compilation以前  compilationParams  sync
compilation       compilation 建立完成      compilation        sync
emit              在生成资源并输出到目录以前  compilation        async
after-emit        在生成资源并输出到目录以后  compilation        async
done              完成编译                  stats              sync

理解webpack中的事件流

咱们能够把webpack理解为一条生产线,须要通过一系列处理流程后才能将源文件转换成输出结果。
这条生产线上的每一个处理流程的职责都是单一的,多个流程之间会存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

咱们的插件就像一个插入到生产线中的一个功能,在特定的时机对生产线上的资源会作处理。webpack它是经过 Tapable来组织这条复杂的生产线的。

webpack在运行的过程当中会广播事件,插件只须要关心监听它的事件,就能加入到这条生产线中。而后会执行相关的操做。
webpack的事件流机制它能保证了插件的有序性,使整个系统的扩展性好。事件流机制使用了观察者模式来实现的。好比以下代码:

/*
 * 广播事件
 * myPlugin-name 为事件名称
 * params 为附带的参数
*/

compiler.apply('myPlugin-name', params);

/*
 * 监听名称为 'myPlugin-name' 的事件,当 myPlugin-name 事件发生时,函数就会执行。
*/

compiler.hooks.myPlugin-name.tap('myPlugin-name', function(params) {
  
});

三:插件中经常使用的API

1. 读取输出资源、模块及依赖

在咱们的emit钩子事件发生时,表示的含义是:源文件的转换和组装已经完成了,在这里事件钩子里面咱们能够读取到最终将输出的资源、代码块、模块及对应的依赖文件。而且咱们还能够输出资源文件的内容。好比插件代码以下:

class MyPlugin {
  apply(compiler) {
    compiler.plugin('emit', function(compilation, callback) {
      // compilation.chunks 是存放了全部的代码块,是一个数组,咱们须要遍历
      compilation.chunks.forEach(function(chunk) {
        /*
         * chunk 表明一个代码块,代码块它是由多个模块组成的。
         * 咱们能够经过 chunk.forEachModule 能读取组成代码块的每一个模块
        */
        chunk.forEachModule(function(module) {
          // module 表明一个模块。
          // module.fileDependencies 存放当前模块的全部依赖的文件路径,它是一个数组
          module.fileDependencies.forEach(function(filepath) {
            console.log(filepath);
          });
        });
        /*
         webpack 会根据chunk去生成输出的文件资源,每一个chunk都对应一个及以上的输出文件。
         好比在 Chunk中包含了css 模块而且使用了 ExtractTextPlugin 时,
         那么该Chunk 就会生成 .js 和 .css 两个文件
        */
        chunk.files.forEach(function(filename) {
          // compilation.assets 是存放当前全部即将输出的资源。
          // 调用一个输出资源的 source() 方法能获取到输出资源的内容
          const source = compilation.assets[filename].source();
        });
      });
      /*
       该事件是异步事件,所以要调用 callback 来通知本次的 webpack事件监听结束。
       若是咱们没有调用callback(); 那么webpack就会一直卡在这里不会日后执行。
      */
      callback();
    })
  }
}

2. 监听文件变化

webpack读取文件的时候,它会从入口模块去读取,而后依次找出全部的依赖模块。当入口模块或依赖的模块发生改变的时候,那么就会触发一次新的 Compilation。

在咱们开发插件的时候,咱们须要知道是那个文件发生改变,致使了新的Compilation, 咱们能够添加以下代码进行监听。

// 当依赖的文件发生改变的时候 会触发 watch-run 事件
class MyPlugin {
  apply(compiler) {
    compiler.plugin('watch-run', (watching, callback) => {
      // 获取发生变换的文件列表
      const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
      // changedFiles 格式为键值对的形式,当键为发生变化的文件路径
      if (changedFiles[filePath] !== undefined) {
        // 对应的文件就发生了变化了
      }
      callback();
    });

    /*
     默认状况下Webpack只会监听入口文件或其依赖的模块是否发生变化,可是在有些状况下好比html文件发生改变的时候,那么webpack
     就会去监听html文件的变化。所以就不会从新触发新的 Compilation。所以为了监听html文件的变化,咱们须要把html文件加入到
     依赖列表中。所以咱们须要添加以下代码:
    */
    compiler.plugin('after-compile', (compilation, callback) => {
      /*
       以下的参数filePath是html文件路径,咱们把HTML文件添加到文件依赖表中,而后咱们的webpack会去监听html模块文件,
       html模板文件发生改变的时候,会从新启动下从新编译一个新的 Compilation.
      */
      compilation.fileDependencies.push(filePath);
      callback();
    })
  }
}

3. 修改输出资源

咱们在第一点说过:在咱们的emit钩子事件发生时,表示的含义是:源文件的转换和组装已经完成了,在这里事件钩子里面咱们能够读取到最终将输出的资源、代码块、模块及对应的依赖文件。所以若是咱们如今要修改输出资源的内容的话,咱们能够在emit事件中去作修改。那么全部输出的资源会存放在 compilation.assets中,compilation.assets是一个键值对,键为须要输出的文件名,值为文件对应的内容。以下代码:

class MyPlugin {
  apply(compiler) {
    compiler.plugin('emit', (compilation, callback) => {
      // 设置名称为 fileName 的输出资源
      compilation.assets[fileName] = {
        // 返回文件内容
        source: () => {
          // fileContent 便可以表明文本文件的字符串,也能够是表明二进制文件的buffer
          return fileContent;
        },
        // 返回文件大小
        size: () => {
          return Buffer.byteLength(fileContent, 'utf8');
        }
      };
      callback();
    });
    // 读取 compilation.assets 代码以下:
    compiler.plugin('emit', (compilation, callback) => {
      // 读取名称为 fileName 的输出资源
      const asset = compilation.assets[fileName];
      // 获取输出资源的内容
      asset.source();
      // 获取输出资源的文件大小
      asset.size();
      callback();
    });
  }
}

4. 判断webpack使用了哪些插件

在咱们开发一个插件的时候,咱们须要根据当前配置是否使用了其余某个插件,咱们能够经过读取webpack某个插件配置的状况,好比来判断咱们当前是否使用了 HtmlWebpackPlugin 插件。代码以下:

/*
 判断当前配置使用了 HtmlWebpackPlugin 插件。
 compiler参数即为 webpack 在 apply(compiler) 中传入的参数
*/

function hasHtmlWebpackPlugin(compiler) {
  // 获取当前配置下全部的插件列表
  const plugins = compiler.options.plugins;
  // 去plugins中寻找有没有 HtmlWebpackPlugin 的实列
  return plugins.find(plugin => plugin.__proto__.constructor === HtmlWebpackPlugin) !== null;
}

四:编写插件实战

 假如如今咱们的项目的目录结构以下:

|--- webpack-plugin-demo
| |--- node_modules
| |--- js
| | |--- main.js               # js 的入口文件
| |--- plugins
| | |--- logWebpackPlugin.js   # 编写的webpack的插件,主要做用是打印日志功能
| |--- styles
| |--- index.html
| |--- package.json
| |--- webpack.config.js

1. 实现一个打印日志的LogWebpackPlugin插件

代码以下:

class LogWebpackPlugin {
  constructor(doneCallback, emitCallback) {
    this.emitCallback = emitCallback
    this.doneCallback = doneCallback
  }
  apply(compiler) {
    compiler.hooks.emit.tap('LogWebpackPlugin', () => {
      // 在 emit 事件中回调 emitCallback
      this.emitCallback();
    });
    compiler.hooks.done.tap('LogWebpackPlugin', (err) => {
      // 在 done 事件中回调 doneCallback
      this.doneCallback();
    });
    compiler.hooks.compilation.tap('LogWebpackPlugin', () => {
      // compilation('编译器'对'编译ing'这个事件的监听)
      console.log("The compiler is starting a new compilation...")
    });
    compiler.hooks.compile.tap('LogWebpackPlugin', () => {
      // compile('编译器'对'开始编译'这个事件的监听)
      console.log("The compiler is starting to compile...")
    });
  }
}

// 导出插件
module.exports = LogWebpackPlugin;

下面咱们在webpack中引入该插件;以下代码:

// 引入LogWebpackPlugin 插件
const LogWebpackPlugin = require('./public/plugins/logWebpackPlugin');

module.exports = {
  plugins: [
    new LogWebpackPlugin(() => {
      // Webpack 模块完成转换成功
      console.log('emit 事件发生啦,全部模块的转换和代码块对应的文件已经生成好~')
    } , () => {
      // Webpack 构建成功,而且文件输出了后会执行到这里,在这里能够作发布文件操做
      console.log('done 事件发生啦,成功构建完成~')
    })
  ]
}

而后执行结果以下所示:

能够看到咱们执行成功了,执行了对应的回调函数。如上代码中的 compiler 这个我这边就不讲解了,上面已经讲过了。那么 compiler.hooks 表明的是对外 暴露了多少事件钩子,具体那个钩子是什么含义,咱们能够来看下官网

如上面代码,咱们使用两个钩子事件,分别是 compiler.hooks.emit 和 compiler.hooks.done, compiler.hooks.emit 钩子事件的含义是: 在生成资源并输出到目录以前。这个事件就会发生。 compiler.hooks.done 的含义是:编译完成,该事件就会发生。所以上面截图咱们能够看到先触发 emit事件,所以会打印 'done 事件发生啦,成功构建完成~', 而后会触发 done事件,所以会打印 "emit 事件发生啦,全部模块的转换和代码块对应的文件已经生成好~" 执行这个回调函数。
github代码查看

2. 编写去除生成 bundle.js 中多余的注释的插件

项目结构以下:

|--- webpack-plugin-demo
| |--- node_modules
| |--- public
| | |--- js
| | | |--- main.js                     # 入口文件             
| | |--- plugins                       # 存放全部的webpack插件
| | | |--- AsyncPlugin.js 
| | | |--- AutoExternalPlugin.js
| | | |--- DonePlugin.js
| | | |--- FileListPlugin.js
| | | |--- MyPlugin.js
| | | |--- OptimizePlugin.js
| | |--- styles                        # 存放css样式文件
| | |--- index.html                    # index.html模板
| |--- package.json        
| |--- webpack.config.js 

项目结构如上所示;上面在 public/plugins 中一共有6个插件,咱们分别来看下6个插件的代码:

1. public/plugins/AsyncPlugin.js 代码以下:

class AsyncPlugin {
  constructor() {

  }
  apply(compiler) {
    // 监听emit事件,编译完成后,文件内容输出到硬盘上 触发该事件
    compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, callback) => {
      setTimeout(() => {
        console.log('文件将要被写入到硬盘中');
        callback();
      }, 2000)
    })
  }
}

module.exports = AsyncPlugin;

如上该插件代码没有什么实际做用,无非就是监听 emit 异步事件钩子,emit事件钩子咱们从官网 

上能够看到具体的含义为:'在生成资源并输出到目录以前',会执行该事件钩子中函数代码,这边无非就是在控制台中打印一些提示信息的,没有什么实际做用的。
2. public/plugins/DonePlugin.js 代码以下:

class DonePlugin {
  constructor() {

  }
  apply(compiler) {
    compiler.hooks.done.tapAsync('DonePlugin', (name, callback) => {
      console.log('所有编译完成');
      callback();
    })
  }
}

module.exports = DonePlugin;

如上代码也是一个意思,当编译完成后,就会执行 done的事件钩子的回调函数,也是在命令中提示做用的。

3. public/plugins/OptimizePlugin.js 代码以下:

class OptimizePlugin {
  constructor() {

  }
  apply(compiler) {
    // 监听 compilation 事件
    compiler.hooks.compilation.tap('OptimizePlugin', (compilation) => {
      compilation.hooks.optimize.tap('OptimizePlugin', () => {
        console.log('compilation 完成,正在优化,准备输出');
      });
    });
  }
}

module.exports = OptimizePlugin;

也是同样监听 compilation 事件的,每当检测到一个文件发生改变的时候,那么一次新的 Compilation将会被建立。从而生成一组新的编译资源。

4. public/plugins/FileListPlugin.js 代码以下:

class FileListPlugin {
  constructor() {

  }
  apply(compiler) {
    compiler.hooks.compilation.tap('FileListPlugin', (compilation) => {
      compiler.hooks.emit.tap('FileListPlugin', () => {
        let content = '生成的文件列表\r\n';
        content = Object.keys(compilation.assets).reduce((current, prev) => current + '- ' + prev + '\r\n', content);
        console.log(content);
        compilation.assets['README.md'] = {
          source() {
            return content;
          },
          size() {
            return content.length;
          }
        }
      })
    })
  }
}
module.exports = FileListPlugin;

生成文件列表的时候,就会触发该文件的代码。

5. public/plugins/AutoExternalPlugin.js 代码以下:

const ExternalModules = require('webpack/lib/ExternalModule');

class AutoExternalPlugin {
  constructor(options) {
    this.options = options;
    this.externalModules = {};
  }
  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {
      // parser 将代码转换为语法书 判断有无 import
      normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', (parser, parserOptions) => {
        parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
          if (this.options[source]) {
            this.externalModules[source] = true;
          }
        })
      })
      // factory 是建立模块的方法
      // data 是建立模块的参数
      normalModuleFactory.hooks.factory.tap('AutoExternalPlugin', factory => (data, callback) => {
        const dependencies = data.dependencies;
        const value = dependencies[0].request; // jquery
        if (this.externalModules[value]) {
          const varName = this.options[value].varName;
          callback(null, new ExternalModules(varName, 'window'));
        } else {
          factory(data, callback);
        }
      })
    });
    compiler.hooks.compilation.tap('InlinePlugin', (compilation) => {
      compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('AutoExternalPlugin', (htmlPluginData, callback) => {
        Object.keys(this.options).forEach(key => {
          this.externalModules[key] = this.options[key];
          htmlPluginData.body.unshift(this.processTags(compilation, htmlPluginData, this.options[key]))
        });
        callback(null, htmlPluginData); 
      });
    });
  }
  processTags(compilation, htmlPluginData, value) {
    var tag;
    return tag = {
      tagName: 'script',
      closeTag: true,
      attributes: {
        type: 'text/javascript',
        src: value.url
      }
    }
  }
}

module.exports = AutoExternalPlugin;

如上该插件的代码的做用是能够解决外部的js引用,好比我在webpack中以下使用该插件:

const AutoExternalPlugin = require('./public/plugins/AutoExternalPlugin');
module.exports = {
  plugins:[
    new AutoExternalPlugin({
      jquery:{
        varName:'jQuery',
        url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'
      }
    })
  ]
}

这样我就能够在页面中使用jquery插件了;以下代码所示:

import $ from 'jquery';
console.log($);

而后在咱们的页面中引入的是 该 jquery库文件,它会把该库文件自动生成到 index.html 上去,以下index.html 代码变成以下了:

<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
  <link rel="manifest" href="/public/manifest.json" />
<link href="main.css" rel="stylesheet"></head>
<body>
  <div id="app">222226666</div>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.1.0/jquery.js"></script><script type="text/javascript" src="bundle.js"></script></body>
</html>

咱们能够来简单的分析下 AutoExternalPlugin.js 的代码:

在apply方法内部会生成一个 compiler 实列,而后咱们监听 normalModuleFactory 事件,该事件的做用咱们能够看下官网就知道了。

compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', (normalModuleFactory) => {
  // parser 将代码转换为语法书 判断有无 import
  normalModuleFactory.hooks.parser.for('javascript/auto').tap('AutoExternalPlugin', (parser, parserOptions) => {
    parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
      if (this.options[source]) {
        this.externalModules[source] = true;
      }
    })
  })
}

如上 parser 实例,是用来解析由 webpack 处理过的每一个模块。parser 也是扩展自 tapable 的 webpack 类,而且提供多种 tapable 钩子,插件做者可使用它来自定义解析过程。官网解释能够看这里

如上代码,咱们调用 parser.hooks.import 钩子函数, 而后返回的 source 就是咱们的在 咱们的main.js 中调用插件名。如main.js 代码以下:

import $ from 'jquery';

所以在咱们的webpack.config.js 中会以下初始化插件 

new AutoExternalPlugin({
  jquery:{
    varName:'jQuery',
    url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'
  }
});

所以 source 返回的值 就是 'jquery'; 其余的代码能够本身稍微看看就好了。这里暂时先不讲了,因为时间问题。

6. public/plugins/MyPlugin.js 代码以下:

class MyPlugin {
  constructor(options) {
    this.options = options;
    this.externalModules = {};
  }
  apply(compiler) {
    var reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g;
    compiler.hooks.emit.tap('CodeBeautify', (compilation) => {
      Object.keys(compilation.assets).forEach((data) => {
        console.log(data);
        let content = compilation.assets[data].source(); // 获取处理的文本
        content = content.replace(reg, function (word) { // 去除注释后的文本
          return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word;
        });
        compilation.assets[data] = {
          source() {
            return content;
          },
          size() {
            return content.length;
          }
        }
      });
    });
  }
}
module.exports = MyPlugin;

这个js代码的真正的含义才是咱们今天要讲到的,这个插件最主要做用是 去除注释后的文本。

1. 第一步,咱们使用 compiler.hooks.emit 钩子函数。在生成资源并输出到目录以前触发该函数,也就是说将编译好的代码发射到指定的stream中就会触发,而后咱们从回调函数返回的 compilation 对象上能够拿到编译好的 stream.

2. 访问compilation对象,compilation内部会返回不少内部对象,这边先不打印了,由于打印的话直接会卡死掉,要等很长时间才会打印出来,大家本身能够试试;而后咱们遍历 assets.

Object.keys(compilation.assets).forEach((data) => {
  console.log(compilation.assets);
  console.log(8888)
  console.log(data);
});

以下图所示:

1) assets 数组对象中的key是资源名。在如上代码,咱们经过 Object.key()方法拿到了。以下所示:

main.css
bundle.js
index.html

2) 而后咱们调用 compilation.assets[data].source(); 能够获取资源的内容。

3) 使用正则,去掉注释,以下代码:

Object.keys(compilation.assets).forEach((data) => {
  let content = compilation.assets[data].source(); // 获取处理的文本
  content = content.replace(reg, function (word) { // 去除注释后的文本
    return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? "" : word;
  });
});

4) 更新 compilation.assets[data] 对象,以下代码:

compilation.assets[data] = {
  source() {
    return content;
  },
  size() {
    return content.length;
  }
}

而后咱们就能够在webpack中引入该全部的插件:

const DonePlugin = require('./public/plugins/DonePlugin');
const OptimizePlugin = require('./public/plugins/OptimizePlugin');
const AsyncPlugin = require('./public/plugins/AsyncPlugin');
const FileListPlugin = require('./public/plugins/FileListPlugin');
const AutoExternalPlugin = require('./public/plugins/AutoExternalPlugin');
const MyPlugin = require('./public/plugins/MyPlugin');

调用方式以下:

module.exports = {
  plugins:[
    new DonePlugin(),
    new OptimizePlugin(),
    new AsyncPlugin(),
    new FileListPlugin(),
    new MyPlugin(),
    new AutoExternalPlugin({
      jquery:{
        varName:'jQuery',
        url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'
      }
    })
  ]
}

而后咱们进行打包运行效果以下所示:

github源码查看

相关文章
相关标签/搜索