看完這篇webpack-loader,再也不怕面試官問了

對於webpack,一切皆模塊。所以,不管什麼文件,都須要轉換成js可識別模塊。你能夠理解爲,不管什麼後綴的文件,都看成js來使用(即便是img、ppt、txt文件等等)。可是直接看成js使用確定是不行的,需轉換爲一種能被js理解的方式才能看成js模塊來使用——這個轉換的過程由webpack的loader來處理。一個webpack loader 是一個導出爲函數的 js 模塊。webpack內部的loader runner會調用這個函數,而後把上一個 loader 產生的結果或者資源文件傳入進去,而後返回處理後的結果前端

下面會從基本使用開始出發,探究一個loader怎麼寫,並實現raw-loaderjson-loaderurl-loaderbundle-loadernode

準備工做: 先安裝webpackwebpack-cliwebpack-dev-server,後面的實踐用到什麼再裝什麼react

loader使用

  1. 常規方法:webpack.config裏面配置rules
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/, // 匹配規則
        use: ['babel-loader'] // require的loader路徑數組
      }
    ]
  }
}
複製代碼

寫了這個規則,只要匹配的文件名以.js爲結尾的,那就會通過use裏面全部的loader處理webpack

  1. loadername! 前綴方式 好比有一個txt文件,咱們想經過raw-loader來獲取整個txt文件裏面的字符串內容。除了使用統一webpack config配置的方式以外,咱們還能夠在引入的時候,用這樣的語法來引入:
import txt from "raw-loader!./1.txt";
// txt就是這個文件裏面全部的內容
複製代碼

其實使用webpack.config文件統一配置loader後,最終也是會轉成這種方式使用loader再引入的。支持多個loader,語法: loader1!loader2!yourfilenamegit

query替代optionsgithub

使用loadername! 前綴語法:raw-loader?a=1&b=2!./1.txt,等價於webpack配置:web

{
        test: /^1\.txt$/,
        exclude: /node_modules/,
        use: [
          { loader: "raw-loader", options: { a: '1', b: '2' } },
        ]
      },
複製代碼

在寫本身的loader的時候,常常會使用loader-utils(不須要特意安裝,裝了webpack一套就自帶)來獲取傳入參數json

const { getOptions } = require("loader-utils");
module.exports = function(content) {
  const options = getOptions(this) || {};
  // 若是是配置,返回的是options;若是是loadername!語法,返回根據query字符串生成的對象
 // ...
};
複製代碼

下文爲了方便演示,會屢次使用此方法配置loader。若是沒用過這種方法的,就看成入門學習吧😊。搞起~api

一個loader通常是怎樣的

一個loader是一個導出爲函數的 js 模塊,這個函數有三個參數:content, map, meta數組

  • content: 表示源文件字符串或者buffer
  • map: 表示sourcemap對象
  • meta: 表示元數據,輔助對象

咱們實現一個最最最簡單的,給代碼加上一句console的loader:

// console.js
module.exports = function(content, map, meta) {
  return `${content}; console.log('loader exec')`;
};
複製代碼

webpack配置

module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          { loader: "./loaders/console" }, // 加上本身寫的loader
        ]
      }
    ]
  },
複製代碼

咱們發現,從新跑構建後,每個js都打印一下 'loader exec'

最簡單的loader——raw-loader和json-loader

這兩個loader就是讀取文件內容,而後可使用import或者require導入原始文件全部的內容。很明顯,原文件被看成js使用的時候,缺乏了一個導出語句,loader作的事情就是加上導出語句。

好比有一個這樣的txt

this is a txt file
複製代碼

假如你把它看成js來用,import或者require進來的時候,執行this is a txt file這句js,確定會報錯。若是想正常使用,那麼這個txt文件須要改爲:

export default 'this is a txt file'
複製代碼

最終的效果就是,不管是什麼文件,txt、md、json等等,都看成一個js文件來用,原文件內容至關於一個字符串,被導出了:

// 本身寫的raw-loader
const { getOptions } = require("loader-utils");
// 獲取webpack配置的options,寫loader的固定套路第一步

module.exports = function(content, map, meta) {
  const opts = getOptions(this) || {};

  const code = JSON.stringify(content);
  const isESM = typeof opts.esModule !== "undefined" ? options.esModule : true;
// 直接返回原文件內容
  return `${isESM ? "export default" : "module.exports ="} ${code}`;
};
複製代碼

raw-loaderjson-loader幾乎都是同樣的,他們的目的就是把原文件全部的內容做爲一個字符串導出,而json-loader多了一個json.parse的過程

注意:看了一下官方的loader源碼,發現它們還會多一個步驟

JSON.stringify(content)
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');
複製代碼

\u2028\u2029是特殊字符,和\n\b之類的相似,但它們特殊之處在於——轉義後直觀上看仍是一個空字符串。能夠看見它特殊之處:

即便你看得見中間有一個奇怪的字符,可是你再按下enter,仍是'ab'\u2028字符串在直觀上來看至關於空字符串(實際上字符是存在的,卻沒有它的帶來的效果)。而對於除了2028和2029,好比\u000A\n,是有換行的效果的(字符存在,也有它帶來的效果)。所以,對於低機率出現的字符值爲2028和2029的轉義是有必要的

Unicode 字符值 轉義序列 含義 類別
\u0008 \b Backspace
\u0009 \t Tab 空白
\u000A \n 換行符(換行) 行結束符
\u000B \v 垂直製表符 空白
\u000C \f 換頁 空白
\u000D \r 回車 行結束符
\u0022 " 雙引號 (")
\u0027 \‘ 單引號 (‘)
\u005C \ 反斜槓 ()
\u00A0 不間斷空格 空白
\u2028 行分隔符 行結束符
\u2029 段落分隔符 行結束符
\uFEFF 字節順序標記 空白

raw模式與url-loader

咱們前面已經實現了raw-loader,這個loader是把原文件裏面的內容以字符串形式返回。可是問題來了,有的文件並非一個字符串就能夠解決的了的,好比圖片、視頻、音頻。此時,咱們須要直接利用原文件的buffer。剛好,loader函數的第一個參數content,支持string/buffer

如何開啓buffer類型的content?

// 只須要導出raw爲true
module.exports.raw = true
複製代碼

url-loader的流程就是,讀取配置,是否能夠轉、怎麼轉=>讀取原文件buffer=>buffer轉base64輸出 => 沒法轉換的走fallback流程。咱們下面實現一個簡易版本的url-loader,僅僅實現核心功能

const { getOptions } = require("loader-utils");

module.exports = function(content) {
  const options = getOptions(this) || {};
  const mimetype = options.mimetype;

  const esModule =
    typeof options.esModule !== "undefined" ? options.esModule : true;

// base編碼組成:data:[mime類型];base64,[文件編碼後內容]
  return `${esModule ? "export default" : "module.exports ="} ${JSON.stringify( `data:${mimetype || ""};base64,${content.toString("base64")}` )}`;
};

module.exports.raw = true;
複製代碼

而後,咱們隨便弄一張圖片,import進來試一下:

// loader路徑自行修改
// img就是一個base64的圖片路徑,能夠直接放img標籤使用
import img from "../../loaders/my-url-loader?mimetype=image!./1.png";
複製代碼

至於file-loader,相信你們也有思路了吧,流程就是:讀取配置裏面的publicpath=>肯定最終輸出路徑=>文件名稱加上MD5 哈希值=>搬運一份文件,文件名改新的名=>新文件名拼接前面的path=>輸出最終文件路徑

pitch與bundle-loader

官網對pitching loader介紹是: loader 老是從右到左地被調用。有些狀況下,loader 只關心 request 後面的元數據(metadata),而且忽略前一個 loader 的結果。在實際(從右到左)執行 loader 以前,會先從左到右調用 loader 上的 pitch 方法。其次,若是某個 loader 在 pitch 方法中返回一個結果,那麼這個過程會跳過剩下的 loader

pitch方法的三個參數:

  • remainingRequest: 後面的loader+資源路徑,loadername!的語法
  • precedingRequest: 資源路徑
  • metadata: 和普通的loader函數的第三個參數同樣,輔助對象,並且loader執行的全程用的是同一個對象哦

loader從後往前執行這個過程,你能夠視爲順序入棧倒序出棧。好比命中某種規則A的文件,會經歷3個loader: ['a-loader', 'b-loader', 'c-loader']

會經歷這樣的過程:

  • 執行a-loader的pitch方法
  • 執行b-loader pitch方法
  • 執行c-loader pitch方法
  • 根據import/require路徑獲取資源內容
  • c-loader 執行
  • b-loader 執行
  • a-loader 執行

若是b-loader裏面有一個pitch方法,並且這個pitch方法有返回結果,那麼上面這個過程自從通過了b-loader後,就不會再將c-loader入棧

// b-loader
module.exports = function(content) {
  return content;
};

// 沒作什麼,就透傳import進來再export出去
module.exports.pitch = function(remainingRequest) {
// remainingRequest路徑要加-! 前綴
  return `import s from ${JSON.stringify( `-!${remainingRequest}` )}; export default s`;
};
複製代碼

b-loader的pitch方法有返回結果,會經歷這樣的過程:

  • 執行a-loader的pitch方法
  • 執行b-loader pitch方法(有返回結果,跳過c-loader)
  • 根據import/require路徑獲取資源內容
  • b-loader 執行
  • a-loader 執行

什麼狀況下須要跳過剩下的loader呢?最多見的,就是動態加載和緩存讀取了,要跳事後面loader的計算。bundle-loader是一個典型的例子

bundle-loader實現的是動態按需加載,怎麼使用呢?咱們能夠對react最終ReactDom.render那一步改造一下,換成動態加載react-dom,再體會一下區別

- import ReactDom from "react-dom";
+ import LazyReactDom from "bundle-loader?lazy&name=reactDom!react-dom";

+ LazyReactDom(ReactDom => {
+ console.log(ReactDom, "ReactDom");
ReactDom.render(<S />, document.getElementById("root"));
+});
複製代碼

能夠看見reactdom被隔離開來,動態引入

點開bundle-loader源碼,發現它利用的是require.ensure來動態引入,具體的實現也很簡單,具體看bundle-loader源碼。時代在變化,新時代的動態引入應該是動態import,下面咱們本身基於動態import來實現一個新的bundle-loader。(僅實現lazy引入的核心功能)

// 獲取ChunkName
function getChunkNameFromRemainingRequest(r) {
  const paths = r.split("/");
  let cursor = paths.length - 1;
  if (/^index\./.test(paths[cursor])) {
    cursor--;
  }
  return paths[cursor];
}

// 原loader不須要作什麼了
module.exports = function() {};

module.exports.pitch = function(remainingRequest, r) {
  // 帶loadername!前綴的依賴路徑
  const s = JSON.stringify(`-!${remainingRequest}`);
  // 使用註釋webpackChunkName來定義chunkname的語法
  return `export default function(cb) { return cb(import(/* webpackChunkName: "my-lazy-${getChunkNameFromRemainingRequest( this.resource )}" */${s})); }`;
};

複製代碼

用法和官方的bundle-loader基本差很少,只是動態import返回一個promise,須要改一下使用方法:

import LazyReactDom from "../loaders/my-bundle!react-dom";

setTimeout(() => {
  LazyReactDom(r => {
    r.then(({ default: ReactDom }) => {
      ReactDom.render(<S />, document.getElementById("root")); }); }); }, 1000); 複製代碼

loader上下文

上文咱們看見有在寫loader的時候使用this,這個this就是loader的上下文。具體可見官網

一堆上下文的屬性中,咱們拿其中一個來實踐一下: this.loadModule

loadModule(request: string, callback: function(err, source, sourceMap, module))

loadModule方法做用是,解析給定的 request 到一個模塊,應用全部配置的 loader ,而且在回調函數中傳入生成的 source 、sourceMap和webpack內部的NormalModule實例。若是你須要獲取其餘模塊的源代碼來生成結果的話,你可使用這個函數。

很明顯,這個方法其中一個應用場景就是,在已有代碼上注入其餘依賴

let's coding

背景:已有一個api文件api.js

const api0 = {
  log(...args) {
    console.log("api log>>>", ...args);
  }
};
module.exports = api0;
複製代碼

但願效果:咱們使用下面這個a.jsjs文件的時候,能夠直接使用api,且不報錯

// a.js
export default function a() {
  return 1;
}
// 其餘代碼
// ...

api.log("a", "b");
複製代碼

所以,咱們須要構建的時候loader把api打進去咱們的代碼裏面:

// addapi的loader
module.exports = function(content, map, meta) {
// 涉及到加載模塊,異步loader
  const callback = this.async();
  this.loadModule("../src/api.js", (err, source, sourceMap, module) => {
// source是一個module.exports = require(xxx)的字符串,咱們須要require那部分
    callback(
      null,
      `const api = ${source.split("=")[1]}; ${content};`,
      sourceMap,
      meta
    );
  });
  return;
};
複製代碼

loader寫好了,記得去webpack配置裏面加上,或者使用loadername!的語法引入a.js(./loaders/addapi!./a.js)

最後咱們能夠看見成功運行了api.js的log

平時也有一些熟悉的場景,某某某api、某某某sdk、公共utils方法、每個index頁面的pvuv上報等等,須要先把這些js加載執行完或者導入。若是咱們懶得一個個文件加import/require語句,就能夠用這種方式瞬間完成。這種騷操做的前提是,保證後續同事接手項目難度低、代碼無坑。註釋、文檔、優雅命名都搞起來

最後

loader的做用就是,讓一切文件,轉化爲本身所須要、能使用的js模塊運行起來。babel和loader雙劍合璧更增強大,能夠隨心所欲的修改代碼、偷懶等等。後續還會出webpack插件、babel相關的文章,你們一塊兒來學習交流~

關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索