揭秘webpack loader

来源:https://www.cnblogs.com/champyin/archive/2020/01/28/12238528.html
-Advertisement-
Play Games

Loader(載入器) 是 webpack 的核心之一。它用於將不同類型的文件轉換為 webpack 可識別的模塊。本文將深入探索 webpack 中的 loader,揭秘它的工作原理,以及如何開發一個 loader。 ...


前言

Loader(載入器) 是 webpack 的核心之一。它用於將不同類型的文件轉換為 webpack 可識別的模塊。本文將嘗試深入探索 webpack 中的 loader,揭秘它的工作原理,以及如何開發一個 loader。

一、Loader 工作原理

webpack 只能直接處理 javascript 格式的代碼。任何非 js 文件都必須被預先處理轉換為 js 代碼,才可以參與打包。loader(載入器)就是這樣一個代碼轉換器。它由 webpack 的 loader runner 執行調用,接收原始資源數據作為參數(當多個載入器聯合使用時,上一個loader的結果會傳入下一個loader),最終輸出 javascript 代碼(和可選的 source map)給 webpack 做進一步編譯。

二、 Loader 執行順序

1. 分類

  • pre: 前置loader
  • normal: 普通loader
  • inline: 內聯loader
  • post: 後置loader

2. 執行優先順序

  • 4類 loader 的執行優級為:pre > normal > inline > post
  • 相同優先順序的 loader 執行順序為:從右到左,從下到上

3. 首碼的作用

內聯 loader 可以通過添加不同首碼,跳過其他類型 loader。

  • ! 跳過 normal loader。
  • -! 跳過 pre 和 normal loader。
  • !! 跳過 pre、 normal 和 post loader。

這些首碼在很多場景下非常有用。

三、如何開發一個loader

loader 是一個導出一個函數的 node 模塊。

1. 最簡單的 loader

當只有一個 loader 應用於資源文件時,它接收源碼作為參數,輸出轉換後的 js 代碼。

// loaders/simple-loader.js

module.exports = function loader (source) {
    console.log('simple-loader is working');
    return source;
}

這就是一個最簡單的 loader 了,這個 loader 啥也沒乾,就是接收源碼,然後原樣返回,為了證明這個loader被調用了,我在裡面列印了一句話‘simple-loader is working’。

測試這個 loader:
需要先配置 loader 路徑
若是使用 npm 安裝的第三方 loader,直接寫 loader 的名字就可以了。但是現在用的是自己開發的本地 loader,需要我們手動配置路徑,告訴 webpack 這些 loader 在哪裡。

// webpack.config.js

const path = require('path');
module.exports = {
  entry: {...},
  output: {...},
  module: {
    rules: [
      {
        test: /\.js$/,
        // 直接指明 loader 的絕對路徑
        use: path.resolve(__dirname, 'loaders/simple-loader')
      }
    ]
  }
}

如果覺得這樣配置本地 loader 並不優雅,可以在 webpack配置本地loader的四種方法 中挑一個你喜歡的。

執行webpack編譯
可以看到,控制台輸出 ‘simple-loader is working’。說明 loader 成功被調用。

webpack-loader1.jpg

2. 帶 pitch 的 loader

pitch 是 loader 上的一個方法,它的作用是阻斷 loader 鏈。

// loaders/simple-loader-with-pitch.js

module.exports = function (source) {  
    console.log('normal excution');   
    return source;
}

// loader上的pitch方法,非必須
module.exports.pitch =  function() { 
    console.log('pitching graph');
    // todo
}

pitch 方法不是必須的。如果有 pitch,loader 的執行則會分為兩個階段:pitch 階段 和 normal execution 階段。webpack 會先從左到右執行 loader 鏈中的每個 loader 上的 pitch 方法(如果有),然後再從右到左執行 loader 鏈中的每個 loader 上的普通 loader 方法。

假如配置瞭如下 loader 鏈:

use: ['loader1', 'loader2', 'loader3']

真實的 loader 執行過程是:

webpack-loader-flow-with-pitch.png

在這個過程中如果任何 pitch 有返回值,則 loader 鏈被阻斷。webpack 會跳過後面所有的的 pitch 和 loader,直接進入上一個 loader 的 normal execution

假設在 loader2 的 pitch 中返回了一個字元串,此時 loader 鏈發生阻斷:

webpack-loader-flow-with-pitch2.png

3. 寫一個簡版的 style-loader

style-loader 通常不會獨自使用,而是跟 css-loader 連用。css-loader 的返回值是一個 js 模塊,大致長這樣:

// 列印 css-loader 的返回值

// Imports
var ___CSS_LOADER_API_IMPORT___ = require("../node_modules/css-loader/dist/runtime/api.js");
exports = ___CSS_LOADER_API_IMPORT___(false);
// Module
exports.push([module.id, "\nbody {\n    background: yellow;\n}\n", ""]);
// Exports
module.exports = exports;

這個模塊在運行時上下文中執行後返回 css 代碼 "\nbody {\n background: yellow;\n}\n"

style-loader 的作用就是將這段 css 代碼轉成 style 標簽插入到 htmlhead 中。

設計思路

  1. style-loader 最終需返回一個 js 腳本:在腳本中創建一個 style 標簽,將 css 代碼賦給 style 標簽,再將這個 style 標簽插入 htmlhead 中。
  2. 難點是獲取 css 代碼,因為 css-loader 的返回值只能在運行時的上下文中執行,而執行 loader 是在編譯階段。換句話說,css-loader 的返回值在 style-loader 里派不上用場。
  3. 曲線救國方案:使用獲取 css 代碼的表達式,在運行時再獲取 css (類似 require('css-loader!index.css'))。
  4. 在處理 css 的 loader 中又去調用 inline loader require css 文件,會產生迴圈執行 loader 的問題,所以我們需要利用 pitch 方法,讓 style-loader 在 pitch 階段返回腳本,跳過剩下的 loader,同時還需要內聯首碼 !! 的加持。

註:pitch 方法有3個參數:

  • remainingRequest:loader鏈中排在自己後面的 loader 以及資源文件的絕對路徑以!作為連接符組成的字元串。
  • precedingRequest:loader鏈中排在自己前面的 loader 的絕對路徑以!作為連接符組成的字元串。
  • data:每個 loader 中存放在上下文中的固定欄位,可用於 pitch 給 loader 傳遞數據。

可以利用 remainingRequest 參數獲取 loader 鏈的剩餘部分。

實現

// loaders/simple-style-loader.js

const loaderUtils = require('loader-utils');
module.exports = function(source) {
    // do nothing
}

module.exports.pitch = function(remainingRequest) {
  console.log('simple-style-loader is working');
    // 在 pitch 階段返回腳本
    return (
      `
      // 創建 style 標簽
      let style = document.createElement('style');
      
      /**
      * 利用 remainingRequest 參數獲取 loader 鏈的剩餘部分
      * 利用 ‘!!’ 首碼跳過其他 loader 
      * 利用 loaderUtils 的 stringifyRequest 方法將模塊的絕對路徑轉為相對路徑
      * 將獲取 css 的 require 表達式賦給 style 標簽
      */
      style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});
      
      // 將 style 標簽插入 head
      document.head.appendChild(style);
      `
    )
}

一個簡易的 style-loader 就完成了。

試用

webpack 配置

// webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry: {...},
  output: {...},
  // 手動配置 loader 路徑
  resolveLoader: {
    modules: [path.resolve(__dirname, 'loaders'), 'node_modules']
  },
  module: {
    rules: [
      {
        // 配置處理 css 的 loader
        test: /\.css$/,
        use: ['simple-style-loader', 'css-loader']
      }
    ]
  },
  plugins: [
    // 渲染首頁
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ]
}

在 index.js 中引入一個 css 樣式文件

// src/index.js

require('./index.css');
console.log('Brovo!');

樣式文件中將 body 的背景色設置為黃色

// src/index.css

body {
  background-color: yellow;
}

執行webpack

npm run build

可以看到命令行控制台列印了 'simple-style-loader is working',說明 webpack 成功調用了我們編寫的 loader。

webpack-loader3.jpg

在瀏覽器打開 dist 下的 index.html 頁面,可以看到樣式生效,而且成功插入到了頁面頭部!

webpack-loader2.jpg

說明我們編寫的 loader 發揮作用了。

成功!

三、一些 tips

推薦2個工具包

開發 loader 必備:

1. loader-utils
這個模塊中常用的幾個方法:

  • getOptions 獲取 loader 的配置項。
  • interpolateName 處理生成文件的名字。
  • stringifyRequest 把絕對路徑處理成相對根目錄的相對路徑。

2. schema-utils
這個模塊可以幫你驗證 loader option 配置的合法性。
用法:

// loaders/simple-loader-with-validate.js

const loaderUtils = require('loader-utils');
const validate = require('schema-utils');
module.exports = function(source) {
  // 獲取 loader 配置項
  let options = loaderUtils.getOptions(this) || {};
  // 定義配置項結構和類型
  let schema = {
    type: 'object',
    properties: {
      name: {
        type: 'string'
      }
    }
  }
  // 驗證配置項是否符合要求
  validate(schema, options);
  return source;
}

當配置項不符合要求,編譯就會中斷併在控制台列印錯誤信息:

webpack-loader4.jpg

開發非同步 loader

非同步 loader 的開發(例如裡面有一些需要讀取文件的操作的時候),需要通過 this.async() 獲取非同步回調,然後手動調用它。
用法:

// loaders/simple-async-loader.js

module.exports = function(source) {
    console.log('async loader');
    let cb = this.async();
    setTimeout(() => {
      console.log('ok');
      // 在非同步回調中手動調用 cb 返回處理結果
      cb(null, source);
    }, 3000);
}

註: 非同步回調 cb() 的第一個參數是 error,要返回的結果放在第二個參數。

raw loader

如果是處理圖片、字體等資源的 loader,需要將 loader 上的 raw 屬性設置為 true,讓 loader 支持二進位格式資源(webpack預設是以 utf-8 的格式讀取文件內容給 loader)。
用法:

// loaders/simple-raw-loader.js

module.exports = function(source) {
  // 將輸出 buffer 類型的二進位數據
  console.log(source);
  // todo handle source
  let result = 'results of processing source'
  return `
    module.exports = '${result}'
  `;
}
// 告訴 wepack 這個 loader 需要接收的是二進位格式的數據
module.exports.raw = true;

webpack-loader5.jpg

註:通常 raw 屬性會在有文件輸出需求的 loader 中使用。

輸出文件

在開發一些處理資源文件(比如圖片、字體等)的 loader 中,需要拷貝或者生成新的文件,可以使用內部的 this.emitFile() 方法.
用法:

// loaders/simple-file-loader.js

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 獲取 loader 的配置項
  let options = loaderUtils.getOptions(this) || {};
  // 獲取用戶設置的文件名或者製作新的文件名
  // 註意第三個參數,是計算 contenthash 的依據
  let url = loaderUtils.interpolateName(this, options.filename || '[contenthash].[ext]', {content: source});
  // 輸出文件
  this.emitFile(url, source);
  // 返回導出文件地址的模塊腳本
  return `module.exports = '${JSON.stringify(url)}'`;
}
module.exports.raw = true;

webpack-loader6.jpg

在這個例子中,loader 讀取圖片內容(buffer),將其重命名,然後調用 this.emitFile() 輸出到指定目錄,最後返回一個模塊,這個模塊導出重命名後的圖片地址。於是當 require 圖片的時候,就相當於 require 了一個模塊,從而得到最終的圖片路徑。(這就是 file-loader 的基本原理)

開發約定

為了讓我們的 loader 具有更高的質量和復用性,記得保持簡單。也就是儘量保持讓一個 loader 專註一件事情,如果發現你寫的 loader 比較龐大,可以試著將其拆成幾個 loader 。

在 webpack 社區,有一份 loader 開發準則,我們可以去參考它來指導我們的 loader 設計:

  • 保持簡單。
  • 利用多個loader鏈。
  • 模塊化輸出。
  • 確保loader是無狀態的。
  • 使用 loader-utils 包。
  • 標記載入程式依賴項。
  • 解析模塊依賴關係。
  • 提取公共代碼。
  • 避免絕對路徑。
  • 使用 peerDependency 對等依賴項。

四、總結

  1. loader 的本質是一個 node 模塊,這個模塊導出一個函數,這個函數上可能還有一個 pitch 方法。

  2. 瞭解了 loader 的本質和 loader 鏈的執行機制,其實就已經具備了 loader 開發基礎了。

  3. 開發 loader 不難上手,但是要開發一款高質量的 loader,仍需不斷實踐。

  4. 嘗試自己開發維護一個小 loader 吧~ 沒準以後可以通過自己編寫 loader 來解決項目中的一些實際問題。

文章源碼獲取:https://github.com/yc111/webpack-loader

歡迎交流~

Happy New Year!

--

參考
https://webpack.js.org/concepts/#loaders
https://webpack.js.org/api/loaders/
https://webpack.js.org/contribute/writing-a-loader/
https://github.com/webpack/webpack/blob/v4.41.5/lib/NormalModuleFactory.js
https://github.com/webpack-contrib/style-loader/blob/master/src/index.js
https://www.npmjs.com/package/loader-utils
https://www.npmjs.com/package/schema-utils

歡迎轉載,轉載請註明出處:
https://champyin.com/2020/01/28/%E6%8F%AD%E7%A7%98webpack-loader/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前兩章學習了WPF事件的工作原理,現在分析一下在代碼中可以處理的各類事件。儘管每個元素都提供了許多事件,但最重要的事件通常包括以下5類: 生命周期事件:在元素被初始化、載入或卸載時發生這些事件。 滑鼠事件:這些事件是滑鼠動作的結果。 鍵盤事件:這些事件是鍵盤動作(如按下鍵盤上的鍵)的結果。 手寫筆事 ...
  • 一、類(Class)是CTS中五種基本類型之一,是一種引用類型,封裝了同屬一個邏輯單元的數據(Data)和行為(Behavior),這些數據和行為通過類中的成員表示;使用class關鍵字定義類: //定義一個公共類MyClass public class MyClass { public int M ...
  • 微信公眾號: "Dotnet9" ,網站: "Dotnet9" ,問題或建議: "請網站留言" , 如果對您有所幫助: "歡迎贊賞" 。 C WPF 一個設計界面 今天正月初三,大家在家呆著挺好,不要忘了自我充電。 武漢人民加油,今早又有噩耗,24號(8號)一路走好。 閱讀導航 1. 本文背景 2. ...
  • Linux起源 操作系統出現時間線: Unix1970年誕生 ,71年用C語言重寫 Apple II 誕生於1976年 window誕生於1985年 Linux誕生於1991年,由大學生Linus Torvalds和後來的眾多愛好者共同開發完成。 想必大家看了這個時間線應該能想明白為啥Linux要出 ...
  • docker search nextcloud docker pull docker.io/nextcloud docker images mkdir /home/nextcloud chmod -R 777 nextcloud/ docker run -d --restart=always --n ...
  • spark 1. Spark的四大特性 1. 速度快 spark比mapreduce快的兩個原因 1. 基於記憶體 2. 進程與線程 2. 易用性 1. 可以用java、scala、python、R等不同的語言來快速編寫spark程式 3. 通用性 4. 相容性 1. spark程式有多種運行模式 s ...
  • Redis是用C語言編寫的開源免費的高性能的分散式記憶體資料庫,基於記憶體運行並支持持久化的NoSQL資料庫。 安裝 1)從官網http://download.redis.io/releases/下載redis壓縮包,如redis-5.0.3.tar.gz 2)上傳Linux伺服器目錄opt,解壓文件包 ...
  • html點擊圓形擴散顯示界面特效 "開場白" "效果" "用到的核心代碼" "思考" "探索" "源碼" "相容性問題" 開場白 經常看到某些app有點擊擴散的特效,有些當做擴散顯示界面,有些擴散改變主題顏色,想在網頁上實現一下,所以就有了這個。 效果 不想聽逼逼的直接去 "源碼" 用到的核心代碼 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...