前端工程師都得掌握的 webpack Loader

0. 前言

本文將 webpack 的 Loader 相關的知識點整理了一下,部分文字是從官方文檔中直接摘錄過來的,並附上自己的理解。如果覺得看起來和官方文檔差不多,直接看官方文檔最好啦~

1. 簡述 webpack 工作流程

本文不過多描述 webpack 的作用和使用方法,如果還不是太熟悉,可以打開 https://webpack.js.org/ 先熟悉一下。

關於 webpack 的工作流程,簡單來說可以概括爲以下幾步:

  1. 參數解析
  2. 找到入口文件
  3. 調用 Loader 編譯文件
  4. 遍歷 AST,收集依賴
  5. 生成 Chunk
  6. 輸出文件

其中,真正起編譯作用的便是 Loader,本文也就 Loader 進行詳細的闡述,其餘部分暫且不談。

2. 關於 Loader

Loader allow webpack to process other types of files and convert them into valid modules.

Loader 的作用很簡單,就是處理任意類型的文件,並且將它們轉換成一個讓 webpack 可以處理的有效模塊。

2.1 Loader 的配置和使用

2.1.1 在 config 裏配置

Loader 可以在 webpack.config.js裏配置,這也是推薦的做法,定義在 module.rules 裏:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' },
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          { loader: 'css-loader' },
          { loader: 'postcss-loader' },
        ]
      }
    ]
  }
};

每一條 rule 會包含兩個屬性:test 和 use,比如 { test: /\.js$/, use: 'babel-loader' } 意思就是:當 webpack 遇到擴展名爲 js 的文件時,先用 babel-loader 處理一下,然後再打包它。

use 的類型:string|array|object|function

  • string: 只有一個 Loader 時,直接聲明 Loader,比如 babel-loader
  • array: 聲明多個 Loader 時,使用數組形式聲明,比如上文聲明 .css 的 Loader
  • object: 只有一個 Loader 時,需要有額外的配置項時。
  • functionuse 也支持回調函數的形式。

關於 use 的多種配置方式,這裏就不多說了,可以點擊 更多關於 use

注意: 當 use 是通過數組形式聲明 Loader 時,Loader 的執行順序是從右到左,從下到上。比如暫且認爲上方聲明是這樣執行的:

postcss-loader -> css-loader -> style-loader

其實就是:

styleLoader(cssLoader(postcssLoader(content)))

爲什麼說是暫且呢,因爲 style-loader 有點特殊,有興趣的看看這個 webpack loader 從上手到理解系列:style-loader

webpack 提供了多種配置 Loader 的方法,不過一般來說,use 就已經足夠用了,如果想了解更多,可以點擊 更多關於 rule 的配置

2.1.2 內聯

可以在 import 等語句裏指定 Loader,使用 ! 來將 Loader分開:

import style from 'style-loader!css-loader?modules!./styles.css';

內聯時,通過 query 來傳遞參數,例如 ?key=value

一般來說,推薦使用統一 config 的形式來配置 Loader,內聯形式多出現於 Loader 內部,比如 style-loader 會在自身代碼裏引入 css-loader

require("!!../../node_modules/css-loader/dist/cjs.js!./styles.css");

2.2 Loader 類型

2.2.1 同步 Loader

module.exports = function(source) {
  const result = someSyncOperation(source); // 同步邏輯
  return result;
}

一般來說,Loader 都是同步的,通過 return 或者 this.callback 來同步地返回 source轉換後的結果。

2.2.2 異步 Loader

有的時候,我們需要在 Loader 裏做一些異步的事情,比如說需要發送網絡請求。如果同步地等着,網絡請求就會阻塞整個構建過程,這個時候我們就需要進行異步 Loader,可以這樣做:

module.exports = function(source) {
  // 告訴 webpack 這次轉換是異步的
  const callback = this.async();
  // 異步邏輯
  someAsyncOperation(content, function(err, result) {
    if (err) return callback(err);
    // 通過 callback 來返回異步處理的結果
    callback(null, result, map, meta);
  });
};

2.2.3 Pitching Loader

Pitching Loader 是一個比較重要的概念,之前在 style-loader 裏有提到過。

{
  test: /\.js$/,
  use: [
    { loader: 'aa-loader' },
    { loader: 'bb-loader' },
    { loader: 'cc-loader' },
  ]
}

我們知道,Loader 總是從右到左被調用。上面配置的 Loader,就會按照以下順序執行:

cc-loader -> bb-loader -> aa-loader

每個 Loader 都支持一個 pitch 屬性,通過 module.exports.pitch 聲明。如果該 Loader 聲明瞭 pitch,則該方法會優先於 Loader 的實際方法先執行,官方也給出了執行順序:

|- aa-loader `pitch`
  |- bb-loader `pitch`
    |- cc-loader `pitch`
      |- requested module is picked up as a dependency
    |- cc-loader normal execution
  |- bb-loader normal execution
|- aa-loader normal execution

也就是會先從左向右執行一次每個 Loader 的 pitch 方法,再按照從右向左的順序執行其實際方法。

2.2.4 Raw Loader

我們在 url-loader 裏和 file-loader 最後都見過這樣一句代碼:

export const raw = true;

默認情況下,webpack 會把文件進行 UTF-8 編碼,然後傳給 Loader。通過設置 rawLoader 就可以接受到原始的 Buffer 數據。

2.3 Loader 幾個重要的 api

所謂 Loader,也只是一個符合 commonjs 規範的 node 模塊,它會導出一個可執行函數。loader runner 會調用這個函數,將文件的內容或者上一個 Loader 處理的結果傳遞進去。同時,webpack 還爲 Loader 提供了一個上下文 this,其中有很多有用的 api,我們找幾個典型的來看看。

2.3.1 this.callback()

在 Loader 中,通常使用 return 來返回一個字符串或者 Buffer。如果需要返回多個結果值時,就需要使用 this.callback,定義如下:

this.callback(
  // 無法轉換時返回 Error,其餘情況都返回 null
  err: Error | null,
  // 轉換結果
  content: string | Buffer,
  // source map,方便調試用的
  sourceMap?: SourceMap,
  // 可以是任何東西。比如 ast
  meta?: any
);

一般來說如果調用該函數的話,應該手動 return,告訴 webpack 返回的結果在 this.callback 中,以避免含糊不清的結果:

module.exports = function(source) {
  this.callback(null, source, sourceMaps);
  return;
};

2.3.2 this.async()

同上,異步 Loader

2.3.3 this.cacheable()

有些情況下,有些操作需要耗費大量時間,每一次調用 Loader 轉換時都會執行這些費時的操作。

在處理這類費時的操作時, webapck 會默認緩存所有 Loader 的處理結果,只有當被處理的文件發生變化時,纔會重新調用 Loader 去執行轉換操作。

webpack 是默認可緩存的,可以執行 this.cacheable(false) 手動關閉緩存。

2.3.4 this.resource

當前處理文件的完整請求路徑,包括 query,比如 /src/App.vue?type=templpate

2.3.5 this.resourcePath

當前處理文件的路徑,不包括 query,比如 /src/App.vue

2.3.6 this.resourceQuery

當前處理文件的 query 字符串,比如 ?type=template。我們在 vue-loader 裏有見過如何使用它:

const qs = require('querystring');

const { resourceQuery } = this;
const rawQuery = resourceQuery.slice(1); // 刪除前面的 ?
const incomingQuery = qs.parse(rawQuery); // 解析字符串成對象
// 取 query
if (incomingQuery.type) {}

2.3.7 this.emitFile

讓 webpack 在輸出目錄新建一個文件,我們在 file-loader 裏有見過:

if (typeof options.emitFile === 'undefined' || options.emitFile) {
  this.emitFile(outputPath, content);
}

更多的 api 可在官方文檔中查看:Loader Interface

3. Loader 工作流程簡述

我們來回顧一下 Loader 的一些特點:

  • Loader 是一個 node 模塊;
  • Loader 可以處理任意類型的文件,轉換成 webpack 可以處理的模塊;
  • Loader 可以在 webpack.config.js 裏配置,也可以在 require 語句裏內聯;
  • Loader 可以根據配置從右向左鏈式執行;
  • Loader 接受源文件內容字符串或者 Buffer
  • Loader 分爲多種類型:同步、異步和 pitching,他們的執行流程不一樣;
  • webpack 爲 Loader 提供了一個上下文,有一些 api 可以使用;
  • ...

我們根據以上暫時知道的特點,可以對 Loader 的工作流程有個猜測,假設有一個 js-loader,它的工作流程簡單來說是這樣的:

  1. webpack.config.js 裏配置了一個 js 的 Loader
  2. 遇到 js 文件時,觸發了 js-loader;
  3. js-loader 接受了一個表示該 js 文件內容的 source;
  4. js-loader 使用 webapck 提供的一系列 api 對 source 進行轉換,得到一個 result;
  5. 將 result 返回或者傳遞給下一個 Loader,直到處理完畢。

webpack 的編譯流程非常複雜,暫時還不能看明白並且梳理清楚,在這裏就不誤導大家了。

關於 Loader 的工作流程以及源碼分析可以看 【webpack進階】你真的掌握了loader麼?- loader十問

4. 如何編寫一個 Loader

雖然我們對於 webpack 的編譯流程不是很熟悉,但是我們可以試着編寫一個簡單功能的 Loader,從而加深對 Loader 的理解。

4.1 Loader 用法準則

編寫 Loader 時需要遵循一些準則,官方有很詳細的文檔,就不重複闡述了。點擊 Loaders 用法準則 查看。

這裏說一下單一任務和鏈式調用

一個 Loader 應該只完成一個功能,如果需要多步的轉換工作,則應該編寫多個 Loader 來進行鏈式調用完成轉換。比如 vue-loader 只是處理了 vue 文件,起到一個分發的作用,將其中的 template/style/script 分別交給不同的處理器來處理。

這樣會讓維護 Loader 變得更簡單,也能讓不同的 Loader 更容易地串聯在一起,而不是重複造輪子。

4.2 Loader 工具庫

編寫 Loader 的過程中,最常用的兩個工具庫是 loader-utils 和 schema-utils,在現在常見的 Loader 中都能看到它們的身影。

4.2.1 loader-utils

它提供了許多有用的工具,但最常用的一種工具是獲取傳遞給 Loader 的選項:

import { getOptions } from 'loader-utils';

export default function loader(src) {
  // 加載 options
  const options = getOptions(this) || {};
}

loader-utils

4.2.2 schema-utils

配合 loader-utils,用於保證 Loader 選項,進行與 JSON Schema 結構一致的校驗。

import validateOptions from 'schema-utils';
import schema from './options.json';

export default function loader(src) {
  // 校驗 options
  validateOptions(schema, options, {
    name: 'URL Loader',
    baseDataPath: 'options',
  });
}

schema-utils

更多關於如何編寫一個 Loader傳送門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章