亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747


「干貨」揭秘webpack插件工作流程和原理

 

作者:frank

轉(zhuǎn)發(fā)鏈接:https://mp.weixin.qq.com/s/LI-SkBoPA94Ply6Qes92PA

前言

通過(guò)插件我們可以擴(kuò)展webpack,在合適的時(shí)機(jī)通過(guò)Webpack提供的 API 改變輸出結(jié)果,使webpack可以執(zhí)行更廣泛的任務(wù),擁有更強(qiáng)的構(gòu)建能力。 本文將嘗試探索 webpack 插件的工作流程,進(jìn)而去揭秘它的工作原理。同時(shí)需要你對(duì)webpack底層和構(gòu)建流程的一些東西有一定的了解。

想要了解 webpack 的插件的機(jī)制,需要弄明白以下幾個(gè)知識(shí)點(diǎn):

  1. 一個(gè)簡(jiǎn)單的插件的構(gòu)成
  2. webpack構(gòu)建流程
  3. Tapable是如何把各個(gè)插件串聯(lián)到一起的
  4. compiler以及compilation對(duì)象的使用以及它們對(duì)應(yīng)的事件鉤子。

插件基本結(jié)構(gòu)

plugins是可以用自身原型方法Apply來(lái)實(shí)例化的對(duì)象。apply只在安裝插件被Webpack compiler執(zhí)行一次。apply方法傳入一個(gè)webpck compiler的引用,來(lái)訪問(wèn)編譯器回調(diào)。

一個(gè)簡(jiǎn)單的插件結(jié)構(gòu):

class HelloPlugin{
  // 在構(gòu)造函數(shù)中獲取用戶給該插件傳入的配置
  constructor(options){
  }
  // Webpack 會(huì)調(diào)用 HelloPlugin 實(shí)例的 apply 方法給插件實(shí)例傳入 compiler 對(duì)象
  apply(compiler) {
    // 在emit階段插入鉤子函數(shù),用于特定時(shí)機(jī)處理額外的邏輯;
    compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
      // 在功能流程完成后可以調(diào)用 webpack 提供的回調(diào)函數(shù);
    });
    // 如果事件是異步的,會(huì)帶兩個(gè)參數(shù),第二個(gè)參數(shù)為回調(diào)函數(shù),在插件處理完任務(wù)時(shí)需要調(diào)用回調(diào)函數(shù)通知webpack,才會(huì)進(jìn)入下一個(gè)處理流程。
    compiler.plugin('emit',function(compilation, callback) {
      // 支持處理邏輯
      // 處理完畢后執(zhí)行 callback 以通知 Webpack 
      // 如果不執(zhí)行 callback,運(yùn)行流程將會(huì)一直卡在這不往下執(zhí)行 
      callback();
    });
  }
}

module.exports = HelloPlugin;

安裝插件時(shí), 只需要將它的一個(gè)實(shí)例放到Webpack config plugins 數(shù)組里面:

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

先來(lái)分析一下webpack Plugin的工作原理

  1. 讀取配置的過(guò)程中會(huì)先執(zhí)行 new HelloPlugin(options) 初始化一個(gè) HelloPlugin 獲得其實(shí)例。
  2. 初始化 compiler 對(duì)象后調(diào)用 HelloPlugin.apply(compiler) 給插件實(shí)例傳入 compiler 對(duì)象。
  3. 插件實(shí)例在獲取到 compiler 對(duì)象后,就可以通過(guò)compiler.plugin(事件名稱, 回調(diào)函數(shù)) 監(jiān)聽(tīng)到 Webpack 廣播出來(lái)的事件。 并且可以通過(guò) compiler 對(duì)象去操作 Webpack。

webapck 構(gòu)建流程

在編寫(xiě)插件之前,還需要了解一下Webpack的構(gòu)建流程,以便在合適的時(shí)機(jī)插入合適的插件邏輯。

Webpack的基本構(gòu)建流程如下:

  1. 校驗(yàn)配置文件 :讀取命令行傳入或者webpack.config.js文件,初始化本次構(gòu)建的配置參數(shù)
  2. 生成Compiler對(duì)象:執(zhí)行配置文件中的插件實(shí)例化語(yǔ)句new MyWebpackPlugin(),為webpack事件流掛上自定義hooks
  3. 進(jìn)入entryOption階段:webpack開(kāi)始讀取配置的Entries,遞歸遍歷所有的入口文件
  4. run/watch:如果運(yùn)行在watch模式則執(zhí)行watch方法,否則執(zhí)行run方法
  5. compilation:創(chuàng)建Compilation對(duì)象回調(diào)compilation相關(guān)鉤子,依次進(jìn)入每一個(gè)入口文件(entry),使用loader對(duì)文件進(jìn)行編譯。通過(guò)compilation我可以可以讀取到module的resource(資源路徑)、loaders(使用的loader)等信息。再將編譯好的文件內(nèi)容使用acorn解析生成AST靜態(tài)語(yǔ)法樹(shù)。然后遞歸、重復(fù)的執(zhí)行這個(gè)過(guò)程, 所有模塊和和依賴分析完成后,執(zhí)行 compilation 的 seal 方法對(duì)每個(gè) chunk 進(jìn)行整理、優(yōu)化、封裝__webpack_require__來(lái)模擬模塊化操作.
  6. emit:所有文件的編譯及轉(zhuǎn)化都已經(jīng)完成,包含了最終輸出的資源,我們可以在傳入事件回調(diào)的compilation.assets上拿到所需數(shù)據(jù),其中包括即將輸出的資源、代碼塊Chunk等等信息。
// 修改或添加資源
compilation.assets['new-file.js'] = {
  source() {
    return 'var a=1';
  },
  size() {
    return this.source().length;
  }
};
  1. afterEmit:文件已經(jīng)寫(xiě)入磁盤(pán)完成
  2. done:完成編譯

奉上一張滴滴云博客的WebPack 編譯流程圖,不喜歡看文字講解的可以看流程圖理解記憶

WebPack 編譯流程圖原圖出自:https://blog.didiyun.com/index.php/2019/03/01/webpack/

看完之后,如果還是看不懂或者對(duì)縷不清webpack構(gòu)建流程的話,建議通讀一下全文,再回來(lái)看這段話,相信一定會(huì)對(duì)webpack構(gòu)建流程有很更加深刻的理解。

理解事件流機(jī)制 Tapable

webpack本質(zhì)上是一種事件流的機(jī)制,它的工作流程就是將各個(gè)插件串聯(lián)起來(lái),而實(shí)現(xiàn)這一切的核心就是Tapable。

Webpack 的 Tapable 事件流機(jī)制保證了插件的有序性,將各個(gè)插件串聯(lián)起來(lái), Webpack 在運(yùn)行過(guò)程中會(huì)廣播事件,插件只需要監(jiān)聽(tīng)它所關(guān)心的事件,就能加入到這條webapck機(jī)制中,去改變webapck的運(yùn)作,使得整個(gè)系統(tǒng)擴(kuò)展性良好。

Tapable也是一個(gè)小型的 library,是Webpack的一個(gè)核心工具。類(lèi)似于node中的events庫(kù),核心原理就是一個(gè)訂閱發(fā)布模式。作用是提供類(lèi)似的插件接口。

webpack中最核心的負(fù)責(zé)編譯的Compiler和負(fù)責(zé)創(chuàng)建bundles的Compilation都是Tapable的實(shí)例,可以直接在 Compiler 和 Compilation 對(duì)象上廣播和監(jiān)聽(tīng)事件,方法如下:

/**
* 廣播事件
* event-name 為事件名稱,注意不要和現(xiàn)有的事件重名
*/
compiler.apply('event-name',params);
compilation.apply('event-name',params);
/**
* 監(jiān)聽(tīng)事件
*/
compiler.plugin('event-name',function(params){});
compilation.plugin('event-name', function(params){});

Tapable類(lèi)暴露了tap、tapAsync和tapPromise方法,可以根據(jù)鉤子的同步/異步方式來(lái)選擇一個(gè)函數(shù)注入邏輯。

tap 同步鉤子

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

tapAsync 異步鉤子,通過(guò)callback回調(diào)告訴Webpack異步執(zhí)行完畢tapPromise 異步鉤子,返回一個(gè)Promise告訴Webpack異步執(zhí)行完畢

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");
「干貨」揭秘webpack插件工作流程和原理

 

tapable

簡(jiǎn)單實(shí)現(xiàn)一個(gè) SyncHook

class Hook{
    constructor(args){
        this.taps = []
        this.interceptors = [] // 這個(gè)放在后面用
        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插件關(guān)聯(lián)的?

Compiler.js

const { AsyncSeriesHook ,SyncHook } = require("tapable");
//創(chuàng)建類(lèi)
class Compiler {
    constructor() {
        this.hooks = {
           run: new AsyncSeriesHook(["compiler"]), //異步鉤子
           compile: new SyncHook(["params"]),//同步鉤子
        };
    },
    run(){
      //執(zhí)行異步鉤子
      this.hooks.run.callAsync(this, err => {
         this.compile(onCompiled);
      });
    },
    compile(){
      //執(zhí)行同步鉤子 并傳參
      this.hooks.compile.call(params);
    }
}
module.exports = Compiler

MyPlugin.js

const Compiler = require('./Compiler')

class MyPlugin{
    apply(compiler){//接受 compiler參數(shù)
        compiler.hooks.run.tap("MyPlugin", () => console.log('開(kāi)始編譯...'));
        compiler.hooks.compile.tapAsync('MyPlugin', (name, age) => {
          setTimeout(() => {
            console.log('編譯中...')
          }, 1000)
        });
    }
}

//這里類(lèi)似于webpack.config.js的plugins配置
//向 plugins 屬性傳入 new 實(shí)例

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(負(fù)責(zé)編譯)

開(kāi)發(fā)插件首先要知道compiler和 compilation 對(duì)象是做什么的

Compiler 對(duì)象包含了當(dāng)前運(yùn)行Webpack的配置,包括entry、output、loaders等配置,這個(gè)對(duì)象在啟動(dòng)Webpack時(shí)被實(shí)例化,而且是全局唯一的。Plugin可以通過(guò)該對(duì)象獲取到Webpack的配置信息進(jìn)行處理。

如果看完這段話,你還是沒(méi)理解compiler是做啥的,不要怕,接著看。 運(yùn)行npm run build,把compiler的全部信息輸出到控制臺(tái)上console.log(Compiler)。

「干貨」揭秘webpack插件工作流程和原理

 

compiler

// 為了能更直觀的讓大家看清楚compiler的結(jié)構(gòu),里面的大量代碼使用省略號(hào)(...)代替。
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對(duì)象包含了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 {
    //監(jiān)聽(tīng)文件變化列表信息
     ...
  }
}

Compiler源碼精簡(jiǎn)版代碼解析

源碼地址(948行):https://github.com/webpack/webpack/blob/master/lib/Compiler.js

const { SyncHook, SyncBailHook, AsyncSeriesHook } = require("tapable");
class Compiler {
  constructor() {
    // 1. 定義生命周期鉤子
    this.hooks = Object.freeze({
      // ...只列舉幾個(gè)常用的常見(jiàn)鉤子,更多hook就不列舉了,有興趣看源碼
      done: new AsyncSeriesHook(["stats"]),//一次編譯完成后執(zhí)行,回調(diào)參數(shù):stats
      beforeRun: new AsyncSeriesHook(["compiler"]),
      run: new AsyncSeriesHook(["compiler"]),//在編譯器開(kāi)始讀取記錄前執(zhí)行
      emit: new AsyncSeriesHook(["compilation"]),//在生成文件到output目錄之前執(zhí)行,回調(diào)參數(shù): compilation
      afterEmit: new AsyncSeriesHook(["compilation"]),//在生成文件到output目錄之后執(zhí)行
      compilation: new SyncHook(["compilation", "params"]),//在一次compilation創(chuàng)建后執(zhí)行插件
      beforeCompile: new AsyncSeriesHook(["params"]),
      compile: new SyncHook(["params"]),//在一個(gè)新的compilation創(chuàng)建之前執(zhí)行
      make:new AsyncParallelHook(["compilation"]),//完成一次編譯之前執(zhí)行
      afterCompile: new AsyncSeriesHook(["compilation"]),
      watchRun: new AsyncSeriesHook(["compiler"]),
      failed: new SyncHook(["error"]),
      watchClose: new SyncHook([]),
      afterPlugins: new SyncHook(["compiler"]),
      entryOption: new SyncBailHook(["context", "entry"])
    });
    // ...省略代碼
  }
  newCompilation() {
    // 創(chuàng)建Compilation對(duì)象回調(diào)compilation相關(guān)鉤子
    const compilation = new Compilation(this);
    //...一系列操作
    this.hooks.compilation.call(compilation, params); //compilation對(duì)象創(chuàng)建完成 
    return compilation
  }
  watch() {
    //如果運(yùn)行在watch模式則執(zhí)行watch方法,否則執(zhí)行run方法
    if (this.running) {
      return handler(new ConcurrentCompilationError());
    }
    this.running = true;
    this.watchMode = true;
    return new Watching(this, watchOptions, handler);
  }
  run(callback) {
    if (this.running) {
      return callback(new ConcurrentCompilationError());
    }
    this.running = true;
    process.nextTick(() => {
      this.emitAssets(compilation, err => {
        if (err) {
          // 在編譯和輸出的流程中遇到異常時(shí),會(huì)觸發(fā) failed 事件 
          this.hooks.failed.call(err)
        };
        if (compilation.hooks.needAdditionalPass.call()) {
          // ...
          // done:完成編譯
          this.hooks.done.callAsync(stats, err => {
            // 創(chuàng)建compilation對(duì)象之前   
            this.compile(onCompiled);
          });
        }
        this.emitRecords(err => {
          this.hooks.done.callAsync(stats, err => {

          });
        });
      });
    });

    this.hooks.beforeRun.callAsync(this, err => {
      this.hooks.run.callAsync(this, err => {
        this.readRecords(err => {
          this.compile(onCompiled);
        });
      });
    });

  }
  compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      this.hooks.compile.call(params);
      const compilation = this.newCompilation(params);
      //觸發(fā)make事件并調(diào)用addEntry,找到入口js,進(jìn)行下一步
      this.hooks.make.callAsync(compilation, err => {
        process.nextTick(() => {
          compilation.finish(err => {
            // 封裝構(gòu)建結(jié)果(seal),逐次對(duì)每個(gè)module和chunk進(jìn)行整理,每個(gè)chunk對(duì)應(yīng)一個(gè)入口文件
            compilation.seal(err => {
              this.hooks.afterCompile.callAsync(compilation, err => {
                // 異步的事件需要在插件處理完任務(wù)時(shí)調(diào)用回調(diào)函數(shù)通知 Webpack 進(jìn)入下一個(gè)流程,
                // 不然運(yùn)行流程將會(huì)一直卡在這不往下執(zhí)行
                return callback(null, compilation);
              });
            });
          });
        });
      });
    });
  }
  emitAssets(compilation, callback) {
    const emitFiles = (err) => {
      //...省略一系列代碼
      // afterEmit:文件已經(jīng)寫(xiě)入磁盤(pán)完成
      this.hooks.afterEmit.callAsync(compilation, err => {
        if (err) return callback(err);
        return callback();
      });
    }

    // emit 事件發(fā)生時(shí),可以讀取到最終輸出的資源、代碼塊、模塊及其依賴,并進(jìn)行修改(這是最后一次修改最終文件的機(jī)會(huì))
    this.hooks.emit.callAsync(compilation, err => {
      if (err) return callback(err);
      outputPath = compilation.getPath(this.outputPath, {});
      mkdirp(this.outputFileSystem, outputPath, emitFiles);
    });
  }
  // ...省略代碼
}

apply方法中插入鉤子的一般形式如下:

// compiler提供了compiler.hooks,可以根據(jù)這些不同的時(shí)刻去讓插件做不同的事情。
compiler.hooks.階段.tap函數(shù)('插件名稱', (階段回調(diào)參數(shù)) => {

});
compiler.run(callback)

理解Compilation

Compilation對(duì)象代表了一次資源版本構(gòu)建。當(dāng)運(yùn)行 webpack 開(kāi)發(fā)環(huán)境中間件時(shí),每當(dāng)檢測(cè)到一個(gè)文件變化,就會(huì)創(chuàng)建一個(gè)新的 compilation,從而生成一組新的編譯資源。一個(gè) Compilation 對(duì)象表現(xiàn)了當(dāng)前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態(tài)信息,簡(jiǎn)單來(lái)講就是把本次打包編譯的內(nèi)容存到內(nèi)存里。Compilation 對(duì)象也提供了插件需要自定義功能的回調(diào),以供插件做自定義處理時(shí)選擇使用拓展。

簡(jiǎn)單來(lái)說(shuō),Compilation的職責(zé)就是構(gòu)建模塊和Chunk,并利用插件優(yōu)化構(gòu)建過(guò)程。

和 Compiler 用法相同,鉤子類(lèi)型不同,也可以在某些鉤子上訪問(wèn) tapAsync和 tapPromise。

控制臺(tái)輸出console.log(compilation)

「干貨」揭秘webpack插件工作流程和原理

 

通過(guò) Compilation 也能讀取到 Compiler 對(duì)象。

源碼2000多行,看不動(dòng)了- -,有興趣的可以自己看看。 https://github.com/webpack/webpack/blob/master/lib/Compilation.js

介紹幾個(gè)常用的Compilation Hooks

buildModule(SyncHook):在模塊開(kāi)始編譯之前觸發(fā),可以用于修改模

succeedModule(SyncHook):在模塊開(kāi)始編譯之前觸發(fā),可以用于修改模塊

finishModules(AsyncSeriesHook):當(dāng)所有模塊都編譯成功后被調(diào)用

seal(SyncHook):當(dāng)一次compilation停止接收新模塊時(shí)觸發(fā)

optimizeDependencies(SyncBailHook):在依賴優(yōu)化的開(kāi)始執(zhí)行

optimize(SyncHook):在優(yōu)化階段的開(kāi)始執(zhí)行

optimizeModules(SyncBailHook):在模塊優(yōu)化階段開(kāi)始時(shí)執(zhí)行,插件可以在這個(gè)鉤子里執(zhí)行對(duì)模塊的優(yōu)化,回調(diào)參數(shù):modules

optimizeChunks(SyncBailHook):在代碼塊優(yōu)化階段開(kāi)始時(shí)執(zhí)行,插件可以在這個(gè)鉤子里執(zhí)行對(duì)代碼塊的優(yōu)化,回調(diào)參數(shù):chunks

optimizeChunkAssets(AsyncSeriesHook):優(yōu)化任何代碼塊資源,這些資源存放在compilation.assets 上。一個(gè) chunk 有一個(gè) files 屬性,它指向由一個(gè)chunk創(chuàng)建的所有文件。任何額外的 chunk 資源都存放在 compilation.additionalChunkAssets 上。回調(diào)參數(shù):chunks

optimizeAssets(AsyncSeriesHook):優(yōu)化所有存放在 compilation.assets的所有資源。回調(diào)參數(shù):assets |

Compiler 和 Compilation 的區(qū)別

Compiler 代表了整個(gè) Webpack 從啟動(dòng)到關(guān)閉的生命周期,而 Compilation只是代表了一次新的編譯,只要文件有改動(dòng),compilation就會(huì)被重新創(chuàng)建。

常用 API

插件可以用來(lái)修改輸出文件、增加輸出文件、甚至可以提升 Webpack 性能、等等,總之插件通過(guò)調(diào)用Webpack 提供的 API 能完成很多事情。 由于 Webpack提供的 API 非常多,有很多 API 很少用的上,又加上篇幅有限,下面來(lái)介紹一些常用的 API。

讀取輸出資源、代碼塊、模塊及其依賴

有些插件可能需要讀取 Webpack 的處理結(jié)果,例如輸出資源、代碼塊、模塊及其依賴,以便做下一步處理。 在 emit 事件發(fā)生時(shí),代表源文件的轉(zhuǎn)換和組裝已經(jīng)完成,在這里可以讀取到最終將輸出的資源、代碼塊、模塊及其依賴,并且可以修改輸出資源的內(nèi)容。 插件代碼如下:

class Plugin {
  apply(compiler) {
    compiler.plugin('emit', function (compilation, callback) {
      // compilation.chunks 存放所有代碼塊,是一個(gè)數(shù)組
      compilation.chunks.forEach(function (chunk) {
        // chunk 代表一個(gè)代碼塊
        // 代碼塊由多個(gè)模塊組成,通過(guò) chunk.forEachModule 能讀取組成代碼塊的每個(gè)模塊
        chunk.forEachModule(function (module) {
          // module 代表一個(gè)模塊
          // module.fileDependencies 存放當(dāng)前模塊的所有依賴的文件路徑,是一個(gè)數(shù)組
          module.fileDependencies.forEach(function (filepath) {
          });
        });

        // Webpack 會(huì)根據(jù) Chunk 去生成輸出的文件資源,每個(gè) Chunk 都對(duì)應(yīng)一個(gè)及其以上的輸出文件
        // 例如在 Chunk 中包含了 css 模塊并且使用了 ExtractTextPlugin 時(shí),
        // 該 Chunk 就會(huì)生成 .js 和 .css 兩個(gè)文件
        chunk.files.forEach(function (filename) {
          // compilation.assets 存放當(dāng)前所有即將輸出的資源
          // 調(diào)用一個(gè)輸出資源的 source() 方法能獲取到輸出資源的內(nèi)容
          let source = compilation.assets[filename].source();
        });
      });

      // 這是一個(gè)異步事件,要記得調(diào)用 callback 通知 Webpack 本次事件監(jiān)聽(tīng)處理結(jié)束。
      // 如果忘記了調(diào)用 callback,Webpack 將一直卡在這里而不會(huì)往后執(zhí)行。
      callback();
    })
  }
}

監(jiān)聽(tīng)文件變化

Webpack 會(huì)從配置的入口模塊出發(fā),依次找出所有的依賴模塊,當(dāng)入口模塊或者其依賴的模塊發(fā)生變化時(shí), 就會(huì)觸發(fā)一次新的 Compilation。

在開(kāi)發(fā)插件時(shí)經(jīng)常需要知道是哪個(gè)文件發(fā)生變化導(dǎo)致了新的 Compilation,為此可以使用如下代碼:

// 當(dāng)依賴的文件發(fā)生變化時(shí)會(huì)觸發(fā) watch-run 事件
compiler.hooks.watchRun.tap('MyPlugin', (watching, callback) => {
  // 獲取發(fā)生變化的文件列表
  const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
  // changedFiles 格式為鍵值對(duì),鍵為發(fā)生變化的文件路徑。
  if (changedFiles[filePath] !== undefined) {
    // filePath 對(duì)應(yīng)的文件發(fā)生了變化
  }
  callback();
});

默認(rèn)情況下 Webpack 只會(huì)監(jiān)視入口和其依賴的模塊是否發(fā)生變化,在有些情況下項(xiàng)目可能需要引入新的文件,例如引入一個(gè) HTML 文件。 由于 JAVAScript 文件不會(huì)去導(dǎo)入 HTML 文件,Webpack 就不會(huì)監(jiān)聽(tīng) HTML 文件的變化,編輯 HTML 文件時(shí)就不會(huì)重新觸發(fā)新的 Compilation。 為了監(jiān)聽(tīng) HTML 文件的變化,我們需要把 HTML 文件加入到依賴列表中,為此可以使用如下代碼:

compiler.hooks.afterCompile.tap('MyPlugin', (compilation, callback) => {
  // 把 HTML 文件添加到文件依賴列表,好讓 Webpack 去監(jiān)聽(tīng) HTML 模塊文件,在 HTML 模版文件發(fā)生變化時(shí)重新啟動(dòng)一次編譯
  compilation.fileDependencies.push(filePath);
  callback();
});

3、修改輸出資源

有些場(chǎng)景下插件需要修改、增加、刪除輸出的資源,要做到這點(diǎn)需要監(jiān)聽(tīng)emit 事件,因?yàn)榘l(fā)生 emit 事件時(shí)所有模塊的轉(zhuǎn)換和代碼塊對(duì)應(yīng)的文件已經(jīng)生成好, 需要輸出的資源即將輸出,因此emit事件是修改 Webpack 輸出資源的最后時(shí)機(jī)。

所有需要輸出的資源會(huì)存放在 compilation.assets中,compilation.assets 是一個(gè)鍵值對(duì),鍵為需要輸出的文件名稱,值為文件對(duì)應(yīng)的內(nèi)容。

設(shè)置 compilation.assets 的代碼如下:

// 設(shè)置名稱為 fileName 的輸出資源
  compilation.assets[fileName] = {
    // 返回文件內(nèi)容
    source: () => {
      // fileContent 既可以是代表文本文件的字符串,也可以是代表二進(jìn)制文件的 Buffer
      return fileContent;
      },
    // 返回文件大小
      size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  };
  callback();

判斷webpack使用了哪些插件

// 判斷當(dāng)前配置使用使用了 ExtractTextPlugin,
// compiler 參數(shù)即為 Webpack 在 apply(compiler) 中傳入的參數(shù)
function hasExtractTextPlugin(compiler) {
  // 當(dāng)前配置所有使用的插件列表
  const plugins = compiler.options.plugins;
  // 去 plugins 中尋找有沒(méi)有 ExtractTextPlugin 的實(shí)例
  return plugins.find(plugin=>plugin.__proto__.constructor === ExtractTextPlugin) != null;
}

以上4種方法來(lái)源于文章: [Webpack學(xué)習(xí)-Plugin] :http://wushaobin.top/2019/03/15/webpackPlugin/

管理 Warnings 和 Errors

做一個(gè)實(shí)驗(yàn),如果你在 apply函數(shù)內(nèi)插入 throw new Error("Message"),會(huì)發(fā)生什么,終端會(huì)打印出 Unhandled rejection Error: Message。然后 webpack 中斷執(zhí)行。 為了不影響 webpack 的執(zhí)行,要在編譯期間向用戶發(fā)出警告或錯(cuò)誤消息,則應(yīng)使用 compilation.warnings 和 compilation.errors。

compilation.warnings.push("warning");
compilation.errors.push("error");

文章中的案例demo代碼展示

https://github.com/6fedcom/fe-blog/tree/master/webpack/plugin

webpack打包過(guò)程或者插件代碼里該如何調(diào)試?

  1. 在當(dāng)前webpack項(xiàng)目工程文件夾下面,執(zhí)行命令行:
node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress

其中參數(shù)--inspect-brk就是以調(diào)試模式啟動(dòng)node:

終端會(huì)輸出:

Debugger listening on ws://127.0.0.1:9229/1018c03f-7473-4d60-b62c-949a6404c81d
For help, see: https://nodejs.org/en/docs/inspector
  1. 谷歌瀏覽器輸入 chrome://inspect/#devices
「干貨」揭秘webpack插件工作流程和原理

 

點(diǎn)擊inspect

  1. 然后點(diǎn)一下Chrome調(diào)試器里的“繼續(xù)執(zhí)行”,斷點(diǎn)就提留在我們?cè)O(shè)置在插件里的debugger斷點(diǎn)了。
「干貨」揭秘webpack插件工作流程和原理

 


 

分享到:
標(biāo)簽:插件 webpack
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定