揭秘webpack插件工作流程和原理(一)

简介: 揭秘webpack插件工作流程和原理

前言

通过插件我们可以扩展webpack,在合适的时机通过Webpack提供的 API 改变输出结果,使webpack可以执行更广泛的任务,拥有更强的构建能力。 本文将尝试探索 webpack 插件的工作流程,进而去揭秘它的工作原理。同时需要你对webpack底层和构建流程的一些东西有一定的了解。

想要了解 webpack 的插件的机制,需要弄明白以下几个知识点:

  1. 一个简单的插件的构成
  2. webpack构建流程
  3. Tapable是如何把各个插件串联到一起的
  4. compiler以及compilation对象的使用以及它们对应的事件钩子。

插件基本结构

plugins是可以用自身原型方法apply来实例化的对象。apply只在安装插件被Webpack compiler执行一次。apply方法传入一个webpck compiler的引用,来访问编译器回调。

一个简单的插件结构:

class HelloPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }
  // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    // 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
    compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
      // 在功能流程完成后可以调用 webpack 提供的回调函数;
    });
    // 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
    compiler.plugin('emit',function(compilation, callback) {
      // 支持处理逻辑
      // 处理完毕后执行 callback 以通知 Webpack 
      // 如果不执行 callback,运行流程将会一直卡在这不往下执行 
      callback();
    });
  }
}
module.exports = HelloPlugin;

安装插件时, 只需要将它的一个实例放到Webpack config plugins 数组里面:

const HelloPlugin = require('./hello-plugin.js');
var webpackConfig = {
  plugins: [
    new HelloPlugin({options: true})
  ]
};

先来分析一下webpack Plugin的工作原理

  1. 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 获得其实例。
  2. 初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象。
  3. 插件实例在获取到 compiler 对象后,就可以通过compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack

webapck 构建流程

在编写插件之前,还需要了解一下Webpack的构建流程,以便在合适的时机插入合适的插件逻辑。

Webpack的基本构建流程如下:

  1. 校验配置文件 :读取命令行传入或者webpack.config.js文件,初始化本次构建的配置参数
  2. 生成Compiler对象:执行配置文件中的插件实例化语句new MyWebpackPlugin(),为webpack事件流挂上自定义hooks
  3. 进入entryOption阶段:webpack开始读取配置的Entries,递归遍历所有的入口文件
  4. run/watch:如果运行在watch模式则执行watch方法,否则执行run方法
  5. compilation:创建Compilation对象回调compilation相关钩子,依次进入每一个入口文件(entry),使用loader对文件进行编译。通过compilation我可以可以读取到moduleresource(资源路径)、loaders(使用的loader)等信息。再将编译好的文件内容使用acorn解析生成AST静态语法树。然后递归、重复的执行这个过程, 所有模块和和依赖分析完成后,执行 compilationseal 方法对每个 chunk 进行整理、优化、封装__webpack_require__来模拟模块化操作.
  6. emit:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。
// 修改或添加资源
compilation.assets['new-file.js'] = {
  source() {
    return 'var a=1';
  },
  size() {
    return this.source().length;
  }
};
  1. afterEmit:文件已经写入磁盘完成
  2. done:完成编译

奉上一张滴滴云博客的WebPack 编译流程图,不喜欢看文字讲解的可以看流程图理解记忆

WebPack 编译流程图原图出自:https://blog.didiyun.com/index.php/2019/03/01/webpack/

看完之后,如果还是看不懂或者对缕不清webpack构建流程的话,建议通读一下全文,再回来看这段话,相信一定会对webpack构建流程有很更加深刻的理解。

理解事件流机制 Tapable

webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。

WebpackTapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webapck机制中,去改变webapck的运作,使得整个系统扩展性良好。

Tapable也是一个小型的 library,是Webpack的一个核心工具。类似于node中的events库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。

webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例,可以直接在 CompilerCompilation 对象上广播和监听事件,方法如下:

/**
* 广播事件
* event-name 为事件名称,注意不要和现有的事件重名
*/
compiler.apply('event-name',params);
compilation.apply('event-name',params);
/**
* 监听事件
*/
compiler.plugin('event-name',function(params){});
compilation.plugin('event-name', function(params){});

Tapable类暴露了taptapAsynctapPromise方法,可以根据钩子的同步/异步方式来选择一个函数注入逻辑。

tap 同步钩子

compiler.hooks.compile.tap('MyPlugin', params => {
  console.log('以同步方式触及 compile 钩子。')
})

tapAsync 异步钩子,通过callback回调告诉Webpack异步执行完毕tapPromise 异步钩子,返回一个Promise告诉Webpack异步执行完毕

compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
  console.log('以异步方式触及 run 钩子。')
  callback()
})
compiler.hooks.run.tapPromise('MyPlugin', compiler => {
  return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
    console.log('以具有延迟的异步方式触及 run 钩子')
  })
})

Tabable用法

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

13154dec5eafa6d1f323abf44b0dff1c_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpgtapable

简单实现一个 SyncHook

class Hook{
    constructor(args){
        this.taps = []
        this.interceptors = [] // 这个放在后面用
        this._args = args 
    }
    tap(name,fn){
        this.taps.push({name,fn})
    }
}
class SyncHook extends Hook{
    call(name,fn){
        try {
            this.taps.forEach(tap => tap.fn(name))
            fn(null,name)
        } catch (error) {
            fn(error)
        }
    }
}

tapable是如何将webapck/webpack插件关联的?

Compiler.js

const { AsyncSeriesHook ,SyncHook } = require("tapable");
//创建类
class Compiler {
    constructor() {
        this.hooks = {
           run: new AsyncSeriesHook(["compiler"]), //异步钩子
           compile: new SyncHook(["params"]),//同步钩子
        };
    },
    run(){
      //执行异步钩子
      this.hooks.run.callAsync(this, err => {
         this.compile(onCompiled);
      });
    },
    compile(){
      //执行同步钩子 并传参
      this.hooks.compile.call(params);
    }
}
module.exports = Compiler

MyPlugin.js

const Compiler = require('./Compiler')
class MyPlugin{
    apply(compiler){//接受 compiler参数
        compiler.hooks.run.tap("MyPlugin", () => console.log('开始编译...'));
        compiler.hooks.compile.tapAsync('MyPlugin', (name, age) => {
          setTimeout(() => {
            console.log('编译中...')
          }, 1000)
        });
    }
}
//这里类似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例
const myPlugin = new MyPlugin();
const options = {
    plugins: [myPlugin]
}
let compiler = new Compiler(options)
compiler.run()

想要深入了解tapable的文章可以看看这篇文章:

webpack4核心模块tapable源码解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html

理解Compiler(负责编译)

开发插件首先要知道compilercompilation 对象是做什么的

Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该对象获取到Webpack的配置信息进行处理。

如果看完这段话,你还是没理解compiler是做啥的,不要怕,接着看。 运行npm run build,把compiler的全部信息输出到控制台上console.log(Compiler)

59a473c65ce4ecce6cd0348357216e51_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpgcompiler

// 为了能更直观的让大家看清楚compiler的结构,里面的大量代码使用省略号(...)代替。
Compiler {
  _pluginCompat: SyncBailHook {
    ...
  },
  hooks: {
    shouldEmit: SyncBailHook {
     ...
    },
    done: AsyncSeriesHook {
     ...
    },
    additionalPass: AsyncSeriesHook {
     ...
    },
    beforeRun: AsyncSeriesHook {
     ...
    },
    run: AsyncSeriesHook {
     ...
    },
    emit: AsyncSeriesHook {
     ...
    },
    assetEmitted: AsyncSeriesHook {
     ...
    },
    afterEmit: AsyncSeriesHook {
     ...
    },
    thisCompilation: SyncHook {
     ...
    },
    compilation: SyncHook {
     ...
    },
    normalModuleFactory: SyncHook {
     ...
    },
    contextModuleFactory: SyncHook {
     ...
    },
    beforeCompile: AsyncSeriesHook {
      ...
    },
    compile: SyncHook {
     ...
    },
    make: AsyncParallelHook {
     ...
    },
    afterCompile: AsyncSeriesHook {
     ...
    },
    watchRun: AsyncSeriesHook {
     ...
    },
    failed: SyncHook {
     ...
    },
    invalid: SyncHook {
     ...
    },
    watchClose: SyncHook {
     ...
    },
    infrastructureLog: SyncBailHook {
     ...
    },
    environment: SyncHook {
     ...
    },
    afterEnvironment: SyncHook {
     ...
    },
    afterPlugins: SyncHook {
     ...
    },
    afterResolvers: SyncHook {
     ...
    },
    entryOption: SyncBailHook {
     ...
    },
    infrastructurelog: SyncBailHook {
     ...
    }
  },
  ...
  outputPath: '',//输出目录
  outputFileSystem: NodeOutputFileSystem {
   ...
  },
  inputFileSystem: CachedInputFileSystem {
    ...
  },
  ...
  options: {
    //Compiler对象包含了webpack的所有配置信息,entry、module、output、resolve等信息
    entry: [
      'babel-polyfill',
      '/Users/frank/Desktop/fe/fe-blog/webpack-plugin/src/index.js'
    ],
    devServer: { port: 3000 },
    output: {
      ...
    },
    module: {
      ...
    },
    plugins: [ MyWebpackPlugin {} ],
    mode: 'production',
    context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin',
    devtool: false,
    ...
    performance: {
      maxAssetSize: 250000,
      maxEntrypointSize: 250000,
      hints: 'warning'
    },
    optimization: {
      ... 
    },
    resolve: {
      ...
    },
    resolveLoader: {
      ...
    },
    infrastructureLogging: { level: 'info', debug: false }
  },
  context: '/Users/frank/Desktop/fe/fe-blog/webpack-plugin',//上下文,文件目录
  requestShortener: RequestShortener {
    ...
  },
  ...
  watchFileSystem: NodeWatchFileSystem {
    //监听文件变化列表信息
     ...
  }
}

目录
相关文章
|
5天前
|
API 开发工具 开发者
webpack热更新原理
Webpack的Hot Module Replacement(HMR)提升开发效率,无需刷新页面即可更新模块。开启HMR需在配置中设`devServer.hot: true`。Webpack构建时插入HMR Runtime,通过WebSocket监听并处理文件变化。当模块改变,Webpack发送更新到浏览器,HMR Runtime找到对应模块进行热替换,保持应用状态。开发者可利用`module.hot` API处理热替换逻辑。
|
15天前
|
前端开发
【专栏】`webpack` 的 `DefinePlugin` 插件用于在编译时动态定义全局变量,实现环境变量差异化、配置参数动态化和条件编译
【4月更文挑战第29天】`webpack` 的 `DefinePlugin` 插件用于在编译时动态定义全局变量,实现环境变量差异化、配置参数动态化和条件编译。通过配置键值对,如 `ENV: JSON.stringify(process.env.NODE_ENV)`,可以在代码中根据环境执行相应逻辑。实际应用包括动态加载资源、动态配置接口地址和条件编译优化代码。注意变量定义的合法性和避免覆盖,解决变量未定义或值错误的问题,以提升开发效率和项目质量。
|
3月前
|
缓存 前端开发 算法
Webpack 进阶:深入理解其工作原理与优化策略
Webpack 进阶:深入理解其工作原理与优化策略
53 2
|
5月前
|
自然语言处理 JavaScript 前端开发
webpack 的热更新是如何做到的?原理是什么?
webpack 的热更新是如何做到的?原理是什么?
39 0
|
5月前
|
前端开发 JavaScript 中间件
React Proxy 详细流程与配置方式(webpack、setupProxy.js、package.json)
React Proxy 详细流程与配置方式(webpack、setupProxy.js、package.json)
72 0
|
5月前
|
前端开发 JavaScript API
webpack插件开发必会Tapable
webpack插件开发必会Tapable
55 0
|
5月前
|
JSON 监控 测试技术
《Webpack5 核心原理与应用实践》学习笔记-> 提升插件健壮性
《Webpack5 核心原理与应用实践》学习笔记-> 提升插件健壮性
54 0
|
5月前
|
缓存 前端开发 API
《Webpack5 核心原理与应用实践》学习笔记-> webpack插件开发基础
《Webpack5 核心原理与应用实践》学习笔记-> webpack插件开发基础
70 0
|
5月前
|
前端开发 JavaScript 测试技术
《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader运行与调试
《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader运行与调试
37 0
|
5月前
|
存储 缓存 JavaScript
《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader开发技巧
《Webpack5 核心原理与应用实践》学习笔记-> webpack的loader开发技巧
45 1