webpack loader和plugin編寫

1 基礎回顧

首先咱們先回顧一下webpack常見配置,由於後面會用到,因此簡單介紹一下。css

1.1 webpack常見配置

// 入口文件
  entry: {
    app: './src/js/index.js',
  },
  // 輸出文件
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/'     //確保文件資源可以在 http://localhost:3000 下正確訪問
  },
  // 開發者工具 source-map
  devtool: 'inline-source-map',
  // 建立開發者服務器
  devServer: {
    contentBase: './dist',
    hot: true                // 熱更新
  },
  plugins: [
    // 刪除dist目錄
    new CleanWebpackPlugin(['dist']),
    // 從新穿件html文件
    new HtmlWebpackPlugin({
      title: 'Output Management'
    }),
    // 以便更容易查看要修補(patch)的依賴
    new webpack.NamedModulesPlugin(),
    // 熱更新模塊
    new webpack.HotModuleReplacementPlugin()
  ],
  // 環境
  mode: "development",
  // loader配置
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          'file-loader'
        ]
      }
    ]
  }
複製代碼

這裏面咱們重點關注 module和plugins屬性,由於今天的重點是編寫loader和plugin,須要配置這兩個屬性。html

1.2 打包原理

  • 識別入口文件
  • 經過逐層識別模塊依賴。(Commonjs、amd或者es6的import,webpack都會對其進行分析。來獲取代碼的依賴)
  • webpack作的就是分析代碼。轉換代碼,編譯代碼,輸出代碼
  • 最終造成打包後的代碼

這些都是webpack的一些基礎知識,對於理解webpack的工做機制頗有幫助。vue

2 loader

OK今天第一個主角登場node

2.1 什麼是loader?

loader是文件加載器,可以加載資源文件,並對這些文件進行一些處理,諸如編譯、壓縮等,最終一塊兒打包到指定的文件中webpack

  • 處理一個文件可使用多個loader,loader的執行順序是和自己的順序是相反的,即最後一個loader最早執行,第一個loader最後執行。
  • 第一個執行的loader接收源文件內容做爲參數,其餘loader接收前一個執行的loader的返回值做爲參數。最後執行的loader會返回此模塊的JavaScript源碼

2.2 手寫一個loader

需求:es6

  1. 處理.txt文件
  2. 對字符串作反轉操做
  3. 首字母大寫

例如:abcdefg轉換後爲Gfedcbaweb

OK,咱們開始npm

1)首先建立兩個loader(這裏以本地loader爲例)json

爲何要建立兩個laoder?理由後面會介紹小程序

image

reverse-loader.js

module.exports = function (src) {
  if (src) {
    console.log('--- reverse-loader input:', src)
    src = src.split('').reverse().join('')
    console.log('--- reverse-loader output:', src)
  }
  return src;
}
複製代碼

uppercase-loader.js

module.exports = function (src) {
  if (src) {
    console.log('--- uppercase-loader input:', src)
    src = src.charAt(0).toUpperCase() + src.slice(1)
    console.log('--- uppercase-loader output:', src)
  }
  // 這裏爲何要這麼寫?由於直接返回轉換後的字符串會報語法錯誤,
  // 這麼寫import後轉換成可使用的字符串
  return `module.exports = '${src}'`
}
複製代碼

看,loader結構是否是很簡單,接收一個參數,而且return一個內容就ok了。

而後建立一個txt文件

image

2)mytest.txt

abcdefg
複製代碼

3)如今開始配置webpack

module.exports = {
  entry: {
    index: './src/js/index.js'
  },
  plugins: [...],
  optimization: {...},
  output: {...},
  module: {
    rules: [
      ...,
      {
        test: /\.txt$/,
        use: [
          './loader/uppercase-loader.js',
          './loader/reverse-loader.js'
        ]
      }
    ]
  }
}
複製代碼

這樣就配置完成了

4)咱們在入口文件中導入這個腳本

爲何這裏須要導入呢,咱們不是配置了webapck處理全部的.txt文件麼?

由於webpack會作過濾,若是不引用該文件的話,webpack是不會對該文件進行打包處理的,那麼你的loader也不會執行

import _ from 'lodash';
import txt from '../txt/mytest.txt'
import '../css/style.css'
function component() {
  var element = document.createElement('div');
  var button = document.createElement('button');
  var br = document.createElement('br');

  button.innerHTML = 'Click me and look at the console!';
  element.innerHTML = _.join('【' + txt + '】');
  element.className = 'hello'
  element.appendChild(br);
  element.appendChild(button);

  // Note that because a network request is involved, some indication
  // of loading would need to be shown in a production-level site/app.
  button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
    var print = module.default;

    print();
  });

  return element;
}
document.body.appendChild(component());
複製代碼

package.json配置

{
  ...,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.prod.js",
    "start": "webpack-dev-server --open --config webpack.dev.js",
    "server": "node server.js"
  },
  ...
}

複製代碼

而後執行命令

npm run build
複製代碼

image

這樣咱們的loader就寫完了。

如今回答爲何要寫兩個loader?

看到執行的順序沒,咱們的配置的是這樣的

use: [
  './loader/uppercase-loader.js',
  './loader/reverse-loader.js'
]
複製代碼

正如前文所說,處理一個文件可使用多個loader,loader的執行順序是和自己的順序是相反的

咱們也能夠本身寫loader解析自定義模板,像vue-loader是很是複雜的,它內部會寫大量的對.vue文件的解析,而後會生成對應的html、js和css。

咱們這裏只是講述了一個最基礎的用法,若是有更多的須要,能夠查看 《loader官方文檔》

3 plugin

3.1 什麼是plugin?

在 Webpack 運行的生命週期中會廣播出許多事件,Plugin 能夠監聽這些事件,在合適的時機經過 Webpack 提供的 API 改變輸出結果。

plugin和loader的區別是什麼?

對於loader,它就是一個轉換器,將A文件進行編譯造成B文件,這裏操做的是文件,好比將A.scss或A.less轉變爲B.css,單純的文件轉換過程

plugin是一個擴展器,它豐富了wepack自己,針對是loader結束後,webpack打包的整個過程,它並不直接操做文件,而是基於事件機制工做,會監聽webpack打包過程當中的某些節點,執行普遍的任務。

3.2 一個最簡的插件

/plugins/MyPlugin.js(本地插件)

class MyPlugin {
  // 構造方法
  constructor (options) {
    console.log('MyPlugin constructor:', options)
  }
  // 應用函數
  apply (compiler) {
    // 綁定鉤子事件
    compiler.plugin('compilation', compilation => {
      console.log('MyPlugin')
    ))
  }
}

module.exports = MyPlugin
複製代碼

webpack配置

const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
  entry: {
    index: './src/js/index.js'
  },
  plugins: [
    ...,
    new MyPlugin({param: 'xxx'})
  ],
  ...
};
複製代碼

這就是一個最簡單的插件(雖然咱們什麼都沒幹)

  • webpack 啓動後,在讀取配置的過程當中會先執行 new MyPlugin(options) 初始化一個 MyPlugin 得到其實例。
  • 在初始化 compiler 對象後,再調用 myPlugin.apply(compiler) 給插件實例傳入 compiler 對象。
  • 插件實例在獲取到 compiler 對象後,就能夠經過 compiler.plugin(事件名稱, 回調函數) 監聽到 Webpack 廣播出來的事件。
  • 而且能夠經過 compiler 對象去操做 webpack。

看到這裏可能會問compiler是啥,compilation又是啥?

  • Compiler 對象包含了 Webpack 環境全部的的配置信息,包含 options,loaders,plugins 這些信息,這個對象在 Webpack 啓動時候被實例化,它是全局惟一的,能夠簡單地把它理解爲 Webpack 實例;

  • Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack 以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation 將被建立。Compilation 對象也提供了不少事件回調供插件作擴展。經過 Compilation 也能讀取到 Compiler 對象。

Compiler 和 Compilation 的區別在於:

Compiler 表明了整個 Webpack 從啓動到關閉的生命週期,而 Compilation 只是表明了一次新的編譯。

3.3 事件流

  • webpack 經過 Tapable 來組織這條複雜的生產線。
  • webpack 的事件流機制保證了插件的有序性,使得整個系統擴展性很好。
  • webpack 的事件流機制應用了觀察者模式,和 Node.js 中的 EventEmitter 很是類似。

綁定事件

compiler.plugin('event-name', params => {
  ...	  
});
複製代碼

觸發事件

compiler.apply('event-name',params)
複製代碼

3.4 須要注意的點

  • 只要能拿到 Compiler 或 Compilation 對象,就能廣播出新的事件,因此在新開發的插件中也能廣播出事件,給其它插件監聽使用。
  • 傳給每一個插件的 Compiler 和 Compilation 對象都是同一個引用。也就是說在一個插件中修改了 Compiler 或 Compilation 對象上的屬性,會影響到後面的插件。
  • 有些事件是異步的,這些異步的事件會附帶兩個參數,第二個參數爲回調函數,在插件處理完任務時須要調用回調函數通知 webpack,纔會進入下一處理流程。例如:
compiler.plugin('emit',function(compilation, callback) {
  ...
    
  // 處理完畢後執行 callback 以通知 Webpack 
  // 若是不執行 callback,運行流程將會一直卡在這不往下執行 
  callback();
});
複製代碼

關於complier和compilation,webpack定義了大量的鉤子事件。開發者能夠根據本身的須要在任何地方進行自定義處理。

《compiler鉤子文檔》

《compilation鉤子文檔》

3.5 手寫一個plugin

場景:

小程序mpvue項目,經過webpack編譯,生成子包(咱們做爲分包引入到主程序中),而後考入主包當中。生成子包後,裏面的公共靜態資源wxss引用地址須要加入分包的前綴:/subPages/enjoy_given。

在未編寫插件前,生成的資源是這樣的,這個路徑若是做爲分包引入主包,是無法正常訪問資源的。

image

因此需求來了:

修改dist/static/css/pages目錄下,全部頁面的樣式文件(wxss文件)引入公共資源的路徑。

由於全部頁面的樣式都會引用通用樣式vender.wxss

那麼就須要把@import "/static/css/vendor.wxss"; 改成:@import "/subPages/enjoy_given/static/css/vendor.wxss";
複製代碼

OK 開始!

1)建立插件文件 CssPathTransfor.js

image

CssPathTransfor.js

class CssPathTransfor {
  apply (compiler) {
    compiler.plugin('emit', (compilation, callback) => {
      console.log('--CssPathTransfor emit')
      // 遍歷全部資源文件
      for (var filePathName in compilation.assets) {
        // 查看對應的文件是否符合指定目錄下的文件
        if (/static\/css\/pages/i.test(filePathName)) {
          // 引入路徑正則
          const reg = /\/static\/css\/vendor\.wxss/i
          // 須要替換的最終字符串
          const finalStr = '/subPages/enjoy_given/static/css/vendor.wxss'
          // 獲取文件內容
          let content = compilation.assets[filePathName].source() || ''
          
          content = content.replace(reg, finalStr)
          // 重寫指定輸出模塊內容
          compilation.assets[filePathName] = {
            source () {
              return content;
            },
            size () {
              return content.length;
            }
          }
        }
      }
      callback()
    })
  }
}
module.exports = CssPathTransfor
複製代碼

看着挺多,實際就是遍歷compilation.assets模塊。對符合要求的文件進行正則替換。

2)修改webpack配置

var baseWebpackConfig = require('./webpack.base.conf')
var CssPathTransfor = require('../plugins/CssPathTransfor.js')

var webpackConfig = merge(baseWebpackConfig, {
  module: {...},
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {...},
  plugins: [
    ...,
    // 配置插件
    new CssPathTransfor(),
  ]
})
複製代碼

插件編寫完成後,執行編譯命令

image

搞定~

若是有更多的需求能夠參考《如何寫一個插件》

相關文章
相關標籤/搜索