一个为了让console.log写起来更偷懒的webpack-plugin

做为一个敲码5分钟,调试两小时的bug大叔,天天和console.log打的交道天然很多,人到中年,愈来愈懒,因而想把console.log('bug: ', bug)变成log.bug来让个人懒癌病发得更加完全。因而硬着头皮看了下webpack插件的写法,在此记录一下webpack系统的学习笔记。webpack

跟着webpack源码摸石过河

去吧!皮卡丘!!!git

  1. webpack初始化
// webpack.config.js

const webpack = require('webpack');
const WebpackPlugin = require('webpack-plugin');

const options = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'index.bundle.js'
  },
  module: {
    rules: []
  },
  plugins: [
    new WebpackPlugin()
  ]
  // ...
};

webpack(options);
复制代码
// webpack.js

// 引入Compiler类
const Compiler = require("./Compiler");

// webpack入口函数
const webpack = (options, callback) => {

  // 建立一个Compiler实例
  compiler = new Compiler(options.context);
  
  // 实例保存webpack的配置对象
  compiler.options = options
  
  // 依次调用webpack插件的apply方法,并传入compiler引用
  for (const plugin of options.plugins) {
    plugin.apply(compiler);
  }
  
  //执行实例的run方法
  compiler.run(callback)
  
  //返回实例
  return compiler
}

module.exports = webpack
复制代码
  • 最开始,不管咱们在控制台输入webpack指令仍是使用Node.js的API,都是调用了webpack函数(源码),并传入了webpack的配置选项,建立了一个 compiler 实例。
  • compiler 是什么?—— 明显发现compiler保存了完整的webpack的配置参数options。因此官方说:

compiler 对象表明了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性创建,并配置好全部可操做的设置,包括 options,loader 和 plugin。可使用 compiler 来访问 webpack 的主环境。github

  • 全部 webpack-plugin 也在这里经过提供一个叫 apply 的方法给webpack调用,以完成初始化的工做,而且接收到刚建立的 compiler 的引用。
  1. compiler、compilation与tapable
// Compiler.js

const {
  Tapable,
  SyncHook,
  SyncBailHook,
  AsyncParallelHook,
  AsyncSeriesHook
} = require("tapable");

const Compilation = require("./Compilation");

class Compiler extends Tapable {

  hooks = [
    hook1: new SyncHook(),
    hook2: new AsyncSeriesHook(),
    hook3: new AsyncParallelHook()
  ]

  run () {
    // ...
    // 触发钩子回调执行
    this.hooks[hook1].call()
    this.hooks[hook2].callAsync(() => {
      this.hooks[hook4].call()
    })

    // 进入编译流程
    this.compile()
    // ...
    this.hooks[hook3].promise(() => {})
  }

  compile () {
    // 建立一个Compilation实例
    const compilation = new Compilation(this)
  }
}
复制代码
  • 研究下Compiler.js这个文件,它引入了一个 tapable 类(源码)的库,这个库提供了一些 Hook 类,内部实现了相似的 事件订阅/发布系统web

  • 哪里用到 Hook 类?—— 在 Compiler 类(Compiler.js源码) 里拥有不少有意思的hook,这些hook表明了整个编译过程的各个关键事件节点,它们都是继承于 Hook 类 ,因此都支持 监听/订阅,只要咱们的插件提早作好事件订阅,那么编译流程进行到该事件点时,就会执行咱们提供的 事件回调 ,作咱们想作的事情了。express

  • 如何进行订阅呢?——在上文中每一个webpack-plugin中都有一个apply方法。其实注册的代码就藏在里面,经过如下相似的代码实现。任何的webpack插件均可以订阅hook1,所以hook1维护了一个taps数组,保存着全部的callback。npm

    compiler.hooks.hook1.tap(name, callback) // 注册/订阅api

    compiler.hooks.hook1.call() // 触发/发布数组

  • 准备工做作好了以后,当 run() 方法开始被调用时,编译就正式开始了。在该方法里,执行 call/callAsync/promise 这些事件时(他们由webpack内部包括一些官方使用的webpack插件进行触发的管理,无需开发者操心),相应的hook就会把本身的taps里的函数均执行一遍。大概的逻辑以下所示。 promise

  • 其中,hooks的执行是按照编译的流程顺序来的,hooks之间有彼此依赖的关系。bash

  • compilation 实例也在这里建立了,它表明了一次资源版本构建。每当检测到文件变化,就会建立一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。并且compilation也和compiler相似,拥有不少hooks(Compilation.js源码),所以一样提供了不少事件节点给予咱们订阅使用。

  1. webpack插件体系的使用套路
class MyPlugin {
  apply(compiler) {
    // 设置回调来访问 compilation 对象:
    compiler.hooks.compilation.tap('myPlugin', (compilation) => {
      // 如今,设置回调来访问 compilation 中的任务点:
      compilation.hooks.optimize.tap('myPlugin', () => {
        console.log('Hello compilation!');
      });
    });
  }
}

module.exports = MyPlugin;
复制代码
  • 订阅 compilercomplation 的事件节点都在webpack-plugin中的 apply 方法里书写,具体的演示如上。固然你想拿到编译过程当中的什么资源,首先得要找出能提供该资源引用的对应的compiler事件节点进行订阅(上帝先创造了亚当,再有夏娃)。每一个compiler时间节点能提供什么参数,在hook的实例化时已经作了说明(以下),更多可查看源码
this.hooks = {
  // 拿到compilation和params
  compilation: new SyncHook(["compilation", "params"]),
  // 拿到stats
  done: new AsyncSeriesHook(["stats"]),
  // 拿到compiler
  beforeRun: new AsyncSeriesHook(["compiler"])
}
复制代码

梳理webpack流程

通过以上的初步探索,写webpack插件须要了解的几个知识点应该有了大概的掌握:

  1. 插件提供 apply 方法供 webpack 调用进行初始化
  2. 使用 tap 注册方式钩入 compilercompilation 的编译流程
  3. 使用 webpack 提供的 api 进行资源的个性化处理。

写插件的套路已经知道了,如今还剩如何找出合适的钩子,修改资源这件事。在webpack系统里,钩子即流程,是编译构建工做的生命周期 。固然,想要了解全部 tapable 实例对象的钩子的具体做用,须要探索webpack全部的内部插件如何使用这些钩子,作了什么工做来进行总结,想一想就复杂,因此只能抽取重要流程作思路归纳,借用淘宝的一张经典图示。![webpack_flow.jpg](file:///Users/Ando/Documents/webpack-plugin/webpack_flow.jpg) 整个编译流程大概分红三个阶段,如今从新整理一下:

  1. 准备阶段 ,webpack的初始化
  • webpack依次调用开发者自定义的插件的 apply 方法,让插件们作好事件注册。
  • WebpackOptionsApply 接收组合了 命令行webpack.config.js 的配置参数,负责webpack内部基础流程使用的插件和compiler上下文环境的初始化工做。(*Plugin均为webpack内部使用的插件)
    // webpack.js
      // 开发者自定义的插件初始化
      if (options.plugins && Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
          if (typeof plugin === "function") {
            plugin.call(compiler, compiler)
          } else {
            plugin.apply(compiler)
          }
        }
      }
      // ...
      // webpack内部使用的插件初始化
      compiler.options = new WebpackOptionsApply().process(options, compiler)
    
      // WebpackOptionsApply.js
      class WebpackOptionsApply extends OptionsApply {
        process (options, compiler) {
          // ...
          new WebAssemblyModulesPlugin({
              mangleImports: options.optimization.mangleWasmImports
          }).apply(compiler);
    
          new EntryOptionPlugin().apply(compiler);
          compiler.hooks.entryOption.call(options.context, options.entry);
    
          new CompatibilityPlugin().apply(compiler);
          new HarmonyModulesPlugin(options.module).apply(compiler);
          new AMDPlugin(options.module, options.amd || {}).apply(compiler);
          // ...
        }
      }
    复制代码
  • 执行 run / watch(一次打包/监听打包模式)触发 编译 阶段
  1. 编译阶段,生成modulechunk资源。

    runcompile 编译 → 建立 compilation 对象。compilation 的建立是一次编译的起步,即将对全部模块加载(load)封存(seal)优化(optimiz)分块(chunk)哈希(hash)从新建立(restore)

    module.exports = class Compiler extends Tapable {
    
        run () {
          // 声明编译结束回调
          function onCompiled () {}
          // 触发run钩子
          this.hooks.run.callAsync(this, err => {
            this.compile(onCompiled)
          })
        }
    
        compile(callback) {
          // ...
          // 编译开始前,触发beforeCompile钩子
          this.hooks.beforeCompile.callAsync(params, err => {
            // 编译开始,触发compile钩子
            this.hooks.compile.call(params);
            // 建立compilation实例
            const compilation = this.newCompilation(params);
            // 触发make钩子
            this.hooks.make.callAsync(compilation, err => {
              // 模块解析完毕,执行compilation的finish方法
              compilation.finish();
              // 资源封存,执行seal方法
              compilation.seal(err => {
                // 编译结束,执行afterCompile钩子
                this.hooks.afterCompile.callAsync(compilation, err => {
    
                  // ...
                });
              });
            });
          });
        }
      }
    复制代码
  • 加载模块make钩子触发 → DllEntryPlugin 内部插件调用compilation.addEntrycompilation维护了一些资源生成工厂方法 compilation.dependencyFactories ,负责把入口文件及其(循环)依赖转换成 modulemodule 的解析过程会应用匹配的 loader )。每一个 module 的解析过程提供 buildModule / succeedModule 等钩子, 全部 module 解析完成后触发 finishModules 钩子。
  • 封存seal 方法包含了 优化/分块/哈希 , 编译中止接收新模块,开始生成chunks。此阶段依赖了一些webpack内部插件对module进行优化,为本次构建生成的chunk加入hash等。createChunkAssets()会根据chunk类型使用不一样的模板进行渲染。此阶段执行完毕后就表明一次编译完成,触发 afterCompile钩子
    • 优化BannerPlugin
      compilation.hooks.optimizeChunkAssets.tapAsync('MyPlugin', (chunks, callback) => {
          chunks.forEach(chunk => {
        	chunk.files.forEach(file => {
        	  compilation.assets[file] = new ConcatSource(
        	    '\/**Sweet Banner**\/',
        	    '\n',
        	    compilation.assets[file]
        	  );
        	});
          });
      
          callback();
        });
      复制代码
    • 分块:用来分割chunk的 SplitChunksPlugin 插件监听了optimizeChunksAdvanced钩子
    • 哈希createHash
  1. 文件生成阶段
  • 编译完成后,触发 emit ,遍历 compilation.assets 生成全部文件。

写一个加强console.log调试体验的webpack插件 simple-log-webpack-plugin

一张效果图先上为敬。(对照图片)只需写 log.a ,经过本身的webpack插件自动补全字段标识 a字段: ,加入 文件路径 ,轻松支持 打印颜色 效果,相同文件的日志信息 可折叠 ,给你一个简洁方便的调试环境。

如下为源码,欢迎测试反馈。github npm

const ColorHash = require('color-hash')
const colorHash = new ColorHash()

const Dependency = require('webpack/lib/Dependency');

class LogDependency extends Dependency {

  constructor(module) {
    super();
    this.module = module;
  }
}

LogDependency.Template = class {
  apply(dep, source) {

    const before = `;console.group('${source._source._name}');`
    const after = `;console.groupEnd();`
    const _size = source.size()

    source.insert(0, before)

    source.replace(_size, _size, after)

  }
};


module.exports = class LogPlugin {

  constructor (opts) {
    this.options = {
      expression: /\blog\.(\w+)\b/ig,
      ...opts
    }
    this.plugin = { name: 'LogPlugin' }
    
  }

  doLog (module) {

    if (!module._source) return
    let _val = module._source.source(),
        _name = module.resource;

    const filedColor = colorHash.hex(module._buildHash)

    // 判断是否须要加入
    if (this.options.expression.test(_val)) {
      module.addDependency(new LogDependency(module));
    }

    _val = _val.replace(
      this.options.expression,
      `console.log('%c$1字段:%c%o, %c%s', 'color: ${filedColor}', 'color: red', $1, 'color: pink', '${_name}')`
    )

    return _val

  }

  apply (compiler) {

    compiler.hooks.compilation.tap(this.plugin, (compilation) => {

      // 注册自定义依赖模板
      compilation.dependencyTemplates.set(
        LogDependency,
        new LogDependency.Template()
      );

      // modlue解析完毕钩子
      compilation.hooks.succeedModule.tap(this.plugin, module => {
        // 修改模块的代码
        module._source._value = this.doLog(module)
        
      })
    })

  }
}
复制代码
相关文章
相关标签/搜索