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

栏目: JavaScript · 发布时间: 6年前

内容简介:作为一个敲码5分钟,调试两小时的bug大叔,每天和console.log打的交道自然不少,人到中年,越来越懒,于是想把console.log('bug: ', bug)变成log.bug来让我的懒癌病发得更加彻底。于是硬着头皮看了下webpack插件的写法,在此记录一下webpack系统的学习笔记。去吧!皮卡丘!!!compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。可以

作为一个敲码5分钟,调试两小时的bug大叔,每天和console.log打的交道自然不少,人到中年,越来越懒,于是想把console.log('bug: ', bug)变成log.bug来让我的懒癌病发得更加彻底。于是硬着头皮看了下webpack插件的写法,在此记录一下webpack系统的学习笔记。

跟着webpack源码摸石过河

去吧!皮卡丘!!!

  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 的主环境。

  • 所有 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 类,内部实现了类似的 事件订阅/发布系统

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

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

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

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

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

    一个为了让console.log写起来更偷懒的webpack-plugin
  • 其中,hooks的执行是按照编译的流程顺序来的,hooks之间有彼此依赖的关系。

  • 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字段: ,加入 文件路径 ,轻松支持 打印颜色 效果,相同文件的日志信息 可折叠 ,给你一个简洁方便的调试环境。

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

以下为源码,欢迎测试反馈。 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)
        
      })
    })

  }
}
复制代码

以上所述就是小编给大家介绍的《一个为了让console.log写起来更偷懒的webpack-plugin》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Linux Device Drivers

Linux Device Drivers

Jonathan Corbet、Alessandro Rubini、Greg Kroah-Hartman / O'Reilly Media / 2005-2-17 / USD 39.95

Device drivers literally drive everything you're interested in--disks, monitors, keyboards, modems--everything outside the computer chip and memory. And writing device drivers is one of the few areas ......一起来看看 《Linux Device Drivers》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具