<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【W(wǎng)ebpack】879- 簡單易懂的 webpack 插件原理分析

          共 13026字,需瀏覽 27分鐘

           ·

          2021-02-28 08:56

          • 本文作者:lzg9527

          • 本文鏈接:juejin.cn/post/6901210575162834958


          在 webpack 中,專注于處理 webpack 在編譯過程中的某個特定的任務(wù)的功能模塊,可以稱為插件。它和 loader 有以下區(qū)別:
          1. loader 是一個轉(zhuǎn)換器,將 A 文件進(jìn)行編譯成 B 文件,比如:將 A.less 轉(zhuǎn)換為 A.css,單純的文件轉(zhuǎn)換過程。webpack 自身只支持 js 和 json 這兩種格式的文件,對于其他文件需要通過 loader 將其轉(zhuǎn)換為 commonJS 規(guī)范的文件后,webpack 才能解析到。

          2. plugin 是一個擴(kuò)展器,它豐富了 webpack 本身,針對是 loader 結(jié)束后,webpack 打包的整個過程,它并不直接操作文件,而是基于事件機(jī)制工作,會監(jiān)聽 webpack 打包過程中的某些節(jié)點(diǎn),執(zhí)行廣泛的任務(wù)。

          plugin 的特征

          webpack 插件有以下特征

          • 是一個獨(dú)立的模塊。
          • 模塊對外暴露一個 js 函數(shù)。
          • 函數(shù)的原型 (prototype) 上定義了一個注入 compiler 對象的 apply 方法。
          • apply 函數(shù)中需要有通過 compiler 對象掛載的 webpack 事件鉤子,鉤子的回調(diào)中能拿到當(dāng)前編譯的 compilation 對象,如果是異步編譯插件的話可以拿到回調(diào) callback。
          • 完成自定義子編譯流程并處理 complition 對象的內(nèi)部數(shù)據(jù)。
          • 如果異步編譯插件的話,數(shù)據(jù)處理完成后執(zhí)行 callback 回調(diào)。
          class HelloPlugin {
            // 在構(gòu)造函數(shù)中獲取用戶給該插件傳入的配置
            constructor(options) {}
            // Webpack 會調(diào)用 HelloPlugin 實(shí)例的 apply 方法給插件實(shí)例傳入 compiler 對象
            apply(compiler) {
              // 在emit階段插入鉤子函數(shù),用于特定時機(jī)處理額外的邏輯;
              compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
                // 在功能流程完成后可以調(diào)用 webpack 提供的回調(diào)函數(shù);
              })
              // 如果事件是異步的,會帶兩個參數(shù),第二個參數(shù)為回調(diào)函數(shù),
              compiler.plugin('emit'function (compilation, callback{
                // 處理完畢后執(zhí)行 callback 以通知 Webpack
                // 如果不執(zhí)行 callback,運(yùn)行流程將會一直卡在這不往下執(zhí)行
                callback()
              })
            }
          }

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

          事件流機(jī)制

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

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

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

          //  廣播事件
          compiler.apply('event-name', params)
          compilation.apply('event-name', params)

          // 監(jiān)聽事件
          compiler.plugin('event-name'function (params{})
          compilation.plugin('event-name'function (params{})

          我們來看下 Tapable

          function Tapable() {
            this._plugins = {}
          }
          //發(fā)布name消息
          Tapable.prototype.applyPlugins = function applyPlugins(name{
            if (!this._plugins[name]) return
            var args = Array.prototype.slice.call(arguments1)
            var plugins = this._plugins[name]
            for (var i = 0; i < plugins.length; i++) {
              plugins[i].apply(this, args)
            }
          }
          // fn訂閱name消息
          Tapable.prototype.plugin = function plugin(name, fn{
            if (!this._plugins[name]) {
              this._plugins[name] = [fn]
            } else {
              this._plugins[name].push(fn)
            }
          }
          //給定一個插件數(shù)組,對其中的每一個插件調(diào)用插件自身的apply方法注冊插件
          Tapable.prototype.apply = function apply() {
            for (var i = 0; i < arguments.length; i++) {
              arguments[i].apply(this)
            }
          }

          Tapable 為 webpack 提供了統(tǒng)一的插件接口(鉤子)類型定義,它是 webpack 的核心功能庫。webpack 中目前有十種 hooks,在 Tapable 源碼中可以看到,他們是:

          exports.SyncHook = require('./SyncHook')
          exports.SyncBailHook = require('./SyncBailHook')
          exports.SyncWaterfallHook = require('./SyncWaterfallHook')
          exports.SyncLoopHook = require('./SyncLoopHook')
          exports.AsyncParallelHook = require('./AsyncParallelHook')
          exports.AsyncParallelBailHook = require('./AsyncParallelBailHook')
          exports.AsyncSeriesHook = require('./AsyncSeriesHook')
          exports.AsyncSeriesBailHook = require('./AsyncSeriesBailHook')
          exports.AsyncSeriesLoopHook = require('./AsyncSeriesLoopHook')
          exports.AsyncSeriesWaterfallHook = require('./AsyncSeriesWaterfallHook')

          Tapable 還統(tǒng)一暴露了三個方法給插件,用于注入不同類型的自定義構(gòu)建行為:

          • tap:可以注冊同步鉤子和異步鉤子。
          • tapAsync:回調(diào)方式注冊異步鉤子。
          • tapPromise:Promise 方式注冊異步鉤子。

          webpack 里的幾個非常重要的對象,CompilerCompilation 和 JavascriptParser 都繼承了 Tapable 類,它們身上掛著豐富的鉤子。

          編寫一個插件

          一個 webpack 插件由以下組成:

          • 一個 JavaScript 命名函數(shù)。
          • 在插件函數(shù)的 prototype 上定義一個 apply 方法。
          • 指定一個綁定到 webpack 自身的事件鉤子。
          • 處理 webpack 內(nèi)部實(shí)例的特定數(shù)據(jù)。
          • 功能完成后調(diào)用 webpack 提供的回調(diào)。

          下面實(shí)現(xiàn)一個最簡單的插件

          class WebpackPlugin1 {
            constructor(options) {
              this.options = options
            }
            apply(compiler) {
              compiler.hooks.done.tap('MYWebpackPlugin', () => {
                console.log(this.options)
              })
            }
          }

          module.exports = WebpackPlugin1

          然后在 webpack 的配置中注冊使用就行,只需要在 webpack.config.js 里引入并實(shí)例化就可以了:

          const WebpackPlugin1 = require('./src/plugin/plugin1')

          module.exports = {
            entry: {
              index: path.join(__dirname, '/src/main.js'),
            },
            output: {
              path: path.join(__dirname, '/dist'),
              filename'index.js',
            },
            plugins: [new WebpackPlugin1({ msg'hello world' })],
          }

          此時我們執(zhí)行一下 npm run build 就能看到效果了

          Compiler 對象 (負(fù)責(zé)編譯)

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

          compiler 上暴露的一些常用的鉤子:

          下面來舉個例子

          class WebpackPlugin2 {
            constructor(options) {
              this.options = options
            }
            apply(compiler) {
              compiler.hooks.run.tap('run', () => {
                console.log('開始編譯...')
              })

              compiler.hooks.compile.tap('compile', () => {
                console.log('compile')
              })

              compiler.hooks.done.tap('compilation', () => {
                console.log('compilation')
              })
            }
          }

          module.exports = WebpackPlugin2

          此時我們執(zhí)行一下 npm run build 就能看到效果了

          有一些編譯插件中的步驟是異步的,這樣就需要額外傳入一個 callback 回調(diào)函數(shù),并且在插件運(yùn)行結(jié)束時執(zhí)行這個回調(diào)函數(shù)

          class WebpackPlugin2 {
            constructor(options) {
              this.options = options
            }
            apply(compiler) {
              compiler.hooks.beforeCompile.tapAsync('compilation', (compilation, cb) => {
                setTimeout(() => {
                  console.log('編譯中...')
                  cb()
                }, 1000)
              })
            }
          }

          module.exports = WebpackPlugin2

          Compilation 對象

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

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

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

          Compilation 上暴露的一些常用的鉤子:

          Compiler 和 Compilation 的區(qū)別

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

          手寫插件 1:文件清單

          在每次 webpack 打包之后,自動產(chǎn)生一個一個 markdown 文件清單,記錄打包之后的文件夾 dist 里所有的文件的一些信息。

          思路:

          1.通過 compiler.hooks.emit.tapAsync() 來觸發(fā)生成資源到 output 目錄之前的鉤子 2.通過 compilation.assets 獲取文件數(shù)量 3.定義 markdown 文件的內(nèi)容,將文件信息寫入 markdown 文件內(nèi)
          4.給 dist 文件夾里添加一個資源名稱為 fileListName 的變量
          5.寫入資源的內(nèi)容和文件大小
          6.執(zhí)行回調(diào),讓 webpack 繼續(xù)執(zhí)行

          class FileListPlugin {
            constructor(options) {
              // 獲取插件配置項(xiàng)
              this.filename = options && options.filename ? options.filename : 'FILELIST.md'
            }

            apply(compiler) {
              // 注冊 compiler 上的 emit 鉤子
              compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
                // 通過 compilation.assets 獲取文件數(shù)量
                let len = Object.keys(compilation.assets).length

                // 添加統(tǒng)計(jì)信息
                let content = `# ${len} file${len > 1 ? 's' : ''} emitted by webpack\n\n`

                // 通過 compilation.assets 獲取文件名列表
                for (let filename in compilation.assets) {
                  content += `- ${filename}\n`
                }

                // 往 compilation.assets 中添加清單文件
                compilation.assets[this.filename] = {
                  // 寫入新文件的內(nèi)容
                  sourcefunction () {
                    return content
                  },
                  // 新文件大小(給 webapck 輸出展示用)
                  sizefunction () {
                    return content.length
                  },
                }

                // 執(zhí)行回調(diào),讓 webpack 繼續(xù)執(zhí)行
                cb()
              })
            }
          }

          module.exports = FileListPlugin

          手寫插件 2:去除注釋

          開發(fā)一個插件能夠去除打包后代碼的注釋,這樣我們的 bundle.js 將更容易閱讀

          思路:

          1.通過 compiler.hooks.emit.tap() 來觸發(fā)生成文件后的鉤子
          2.通過 compilation.assets 拿到生產(chǎn)后的文件,然后去遍歷各個文件
          3.通過 .source() 獲取構(gòu)建產(chǎn)物的文本,然后用正則去 replace 調(diào)注釋的代碼
          4.更新構(gòu)建產(chǎn)物對象
          5.執(zhí)行回調(diào),讓 webpack 繼續(xù)執(zhí)行

          class RemoveCommentPlugin {
            constructor(options) {
              this.options = options
            }
            apply(compiler) {
              // 去除注釋正則
              const reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g

              compiler.hooks.emit.tap('RemoveComment', (compilation) => {
                // 遍歷構(gòu)建產(chǎn)物,.assets中包含構(gòu)建產(chǎn)物的文件名
                Object.keys(compilation.assets).forEach((item) => {
                  // .source()是獲取構(gòu)建產(chǎn)物的文本
                  let content = compilation.assets[item].source()
                  content = content.replace(reg, function (word{
                    // 去除注釋后的文本
                    return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? '' : word
                  })
                  // 更新構(gòu)建產(chǎn)物對象
                  compilation.assets[item] = {
                    source() => content,
                    size() => content.length,
                  }
                })
              })
            }
          }

          module.exports = RemoveCommentPlugin


          1. JavaScript 重溫系列(22篇全)
          2. ECMAScript 重溫系列(10篇全)
          3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
          4. 正則 / 框架 / 算法等 重溫系列(16篇全)
          5. Webpack4 入門(上)|| Webpack4 入門(下)
          6. MobX 入門(上) ||  MobX 入門(下)
          7. 100+篇原創(chuàng)系列匯總

          回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~

          點(diǎn)擊“閱讀原文”查看 100+ 篇原創(chuàng)文章

          瀏覽 51
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产65一二三区 | 苍井さくら在线一区二区 | 亚洲中文字幕在线观看 | www.yiren99 | 久大香蕉伊人 |