天天看點

詳解webpack loader和plugin編寫

1 基礎回顧

首先我們先回顧一下webpack常見配置,因為後面會用到,是以簡單介紹一下。

1.1 webpack常見配置

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

// 入口檔案

 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)$/,

     'file-loader'

   }

  ]

 }

這裡面我們重點關注 module和plugins屬性,因為今天的重點是編寫loader和plugin,需要配置這兩個屬性。

1.2 打包原理

識别入口檔案

通過逐層識别子產品依賴。(Commonjs、amd或者es6的import,webpack都會對其進行分析。來擷取代碼的依賴)

webpack做的就是分析代碼。轉換代碼,編譯代碼,輸出代碼

最終形成打包後的代碼

這些都是webpack的一些基礎知識,對于了解webpack的工作機制很有幫助。

2 loader

OK今天第一個主角登場

2.1 什麼是loader?

loader是檔案加載器,能夠加載資源檔案,并對這些檔案進行一些處理,諸如編譯、壓縮等,最終一起打包到指定的檔案中

處理一個檔案可以使用多個loader,loader的執行順序是和本身的順序是相反的,即最後一個loader最先執行,第一個loader最後執行。

第一個執行的loader接收源檔案内容作為參數,其他loader接收前一個執行的loader的傳回值作為參數。最後執行的loader會傳回此子產品的JavaScript源碼

2.2 手寫一個loader

需求:

處理.txt檔案

對字元串做反轉操作

首字母大寫

例如:abcdefg轉換後為Gfedcba

OK,我們開始

1)首先建立兩個loader(這裡以本地loader為例)

為什麼要建立兩個laoder?理由後面會介紹

詳解webpack loader和plugin編寫

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

  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檔案

詳解webpack loader和plugin編寫

2)mytest.txt

abcdefg

3)現在開始配置webpack

module.exports = {

  index: './src/js/index.js'

 plugins: [...],

 optimization: {...},

 output: {...},

   ...,

    test: /\.txt$/,

     './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

詳解webpack loader和plugin編寫

這樣我們的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')

  ...,

  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。

在未編寫插件前,生成的資源是這樣的,這個路徑如果作為分包引入主包,是沒法正常通路資源的。

詳解webpack loader和plugin編寫

是以需求來了:

修改dist/static/css/pages目錄下,所有頁面的樣式檔案(wxss檔案)引入公共資源的路徑。

因為所有頁面的樣式都會引用通用樣式vender.wxss

那麼就需要把@import "/static/css/vendor.wxss"; 改為:@import "/subPages/enjoy_given/static/css/vendor.wxss";複制代碼

OK 開始!

1)建立插件檔案 CssPathTransfor.js

詳解webpack loader和plugin編寫

CssPathTransfor.js

class CssPathTransfor {

  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,

  // 配置插件

  new CssPathTransfor(),

 ]

})

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

詳解webpack loader和plugin編寫

繼續閱讀