webpack系列之四loader詳解1

滴滴WebApp架構組發表於2019-02-21

系列作者:肖磊

GitHub: github.com/CommanderXL

本篇來分析下 webpack loader 詳細的分析部分,由於涉及內容比較多,所以總共分成三篇文章來分析:

  1. loader 的基本配置以及匹配規則
  2. loader 的解析執行詳解
  3. loader 的實踐

loader 的配置

webpack 對於一個 module 所使用的 loader 對開發者提供了2種使用方式:

  1. webpack config 配置形式,形如:
// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [{
      test: /.vue$/,
      loader: 'vue-loader'
    }, {
      test: /.scss$/,
      use: [
        'vue-style-loader',
        'css-loader',
        {
          loader: 'sass-loader',
          options: {
            data: '$color: red;'
          }
        }
      ]
    }]
  }
  ...
}
複製程式碼
  1. inline 內聯形式
// module

import a from 'raw-loader!../../utils.js'
複製程式碼

2 種不同的配置形式,在 webpack 內部有著不同的解析方式。此外,不同的配置方式也決定了最終在實際載入 module 過程中不同 loader 之間相互的執行順序等。

loader 的匹配

在講 loader 的匹配過程之前,首先從整體上了解下 loader 在整個 webpack 的 workflow 過程中出現的時機。

webpack loader

在一個 module 構建過程中,首先根據 module 的依賴型別(例如 NormalModuleFactory)呼叫對應的建構函式來建立對應的模組。在建立模組的過程中(new NormalModuleFactory()),會根據開發者的 webpack.config 當中的 rules 以及 webpack 內建的 rules 規則例項化 RuleSet 匹配例項,這個 RuleSet 例項在 loader 的匹配過濾過程中非常的關鍵,具體的原始碼解析可參見Webpack Loader Ruleset 匹配規則解析。例項化 RuleSet 後,還會註冊2個鉤子函式:

class NormalModuleFactory {
  ...
  // 內部巢狀 resolver 的鉤子,完成相關的解析後,建立這個 normalModule
  this.hooks.factory.tap('NormalModuleFactory', () => (result, callback) => { ... })

  // 在 hooks.factory 的鉤子內部進行呼叫,實際的作用為解析構建一共 module 所需要的 loaders 及這個 module 的相關構建資訊(例如獲取 module 的 packge.json等)
  this.hooks.resolver.tap('NormalModuleFactory', () => (result, callback) => { ... })
  ...
}
複製程式碼

當 NormalModuleFactory 例項化完成後,並在 compilation 內部呼叫這個例項的 create 方法開始真實開始建立這個 normalModule。首先呼叫hooks.factory獲取對應的鉤子函式,接下來就呼叫 resolver 鉤子(hooks.resolver)進入到了 resolve 的階段,在真正開始 resolve loader 之前,首先就是需要匹配過濾找到構建這個 module 所需要使用的所有的 loaders。首先進行的是對於 inline loaders 的處理:

// NormalModuleFactory.js

// 是否忽略 preLoader 以及 normalLoader
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
// 是否忽略 normalLoader
const noAutoLoaders =
  noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
// 忽略所有的 preLoader / normalLoader / postLoader
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");

// 首先解析出所需要的 loader,這種 loader 為內聯的 loader
let elements = requestWithoutMatchResource
  .replace(/^-?!+/, "")
  .replace(/!!+/g, "!")
  .split("!");
let resource = elements.pop(); // 獲取資源的路徑
elements = elements.map(identToLoaderRequest); // 獲取每個loader及對應的options配置(將inline loader的寫法變更為module.rule的寫法)
複製程式碼

首先是根據模組的路徑規則,例如模組的路徑是以這些符號開頭的 ! / -! / !! 來判斷這個模組是否只是使用 inline loader,或者剔除掉 preLoader, postLoader 等規則:

  • ! 忽略 webpack.config 配置當中符合規則的 normalLoader
  • -! 忽略 webpack.config 配置當中符合規則的 preLoader/normalLoader
  • !! 忽略 webpack.config 配置當中符合規則的 postLoader/preLoader/normalLoader

這幾個匹配規則主要適用於在 webpack.config 已經配置了對應模組使用的 loader,但是針對一些特殊的 module,你可能需要單獨的定製化的 loader 去處理,而不是走常規的配置,因此可以使用這些規則來進行處理。

接下來將所有的 inline loader 轉化為陣列的形式,例如:

import 'style-loader!css-loader!stylus-loader?a=b!../../common.styl'
複製程式碼

最終 inline loader 統一格式輸出為:

[{
  loader: 'style-loader',
  options: undefined
}, {
  loader: 'css-lodaer',
  options: undefined
}, {
  loader: 'stylus-loader',
  options: '?a=b'
}]

複製程式碼

對於 inline loader 的處理便是直接對其進行 resolve,獲取對應 loader 的相關資訊:

asyncLib.parallel([
  callback => 
    this.resolveRequestArray(
      contextInfo,
      context,
      elements,
      loaderResolver,
      callback
    ),
  callback => {
    // 對這個 module 進行 resolve
    ...
    callack(null, {
      resouceResolveData, // 模組的基礎資訊,包含 descriptionFilePath / descriptionFileData 等(即 package.json 等資訊)
      resource // 模組的絕對路徑
    })
  }
], (err, results) => {
  const loaders = results[0] // 所有內聯的 loaders
  const resourceResolveData = results[1].resourceResolveData; // 獲取模組的基本資訊
  resource = results[1].resource; // 模組的絕對路徑
  ...
  
  // 接下來就要開始根據引入模組的路徑開始匹配對應的 loaders
  let resourcePath =
    matchResource !== undefined ? matchResource : resource;
  let resourceQuery = "";
  const queryIndex = resourcePath.indexOf("?");
  if (queryIndex >= 0) {
    resourceQuery = resourcePath.substr(queryIndex);
    resourcePath = resourcePath.substr(0, queryIndex);
  }
  // 獲取符合條件配置的 loader,具體的 ruleset 是如何匹配的請參見 ruleset 解析(https://github.com/CommanderXL/Biu-blog/issues/30)
  const result = this.ruleSet.exec({
    resource: resourcePath, // module 的絕對路徑
    realResource:
      matchResource !== undefined
        ? resource.replace(/\?.*/, "")
        : resourcePath,
    resourceQuery, // module 路徑上所帶的 query 引數
    issuer: contextInfo.issuer, // 所解析的 module 的釋出者
    compiler: contextInfo.compiler 
  });

  // result 為最終根據 module 的路徑及相關匹配規則過濾後得到的 loaders,為 webpack.config 進行配置的
  // 輸出的資料格式為:

  /* [{
    type: 'use',
    value: {
      loader: 'vue-style-loader',
      options: {}
    },
    enforce: undefined // 可選值還有 pre/post  分別為 pre-loader 和 post-loader
  }, {
    type: 'use',
    value: {
      loader: 'css-loader',
      options: {}
    },
    enforce: undefined
  }, {
    type: 'use',
    value: {
      loader: 'stylus-loader',
      options: {
        data: '$color red'
      }
    },
    enforce: undefined 
  }] */

  const settings = {};
  const useLoadersPost = []; // post loader
  const useLoaders = []; // normal loader
  const useLoadersPre = []; // pre loader
  for (const r of result) {
    if (r.type === "use") {
      // postLoader
      if (r.enforce === "post" && !noPrePostAutoLoaders) {
        useLoadersPost.push(r.value);
      } else if (
        r.enforce === "pre" &&
        !noPreAutoLoaders &&
        !noPrePostAutoLoaders
      ) {
        // preLoader
        useLoadersPre.push(r.value);
      } else if (
        !r.enforce &&
        !noAutoLoaders &&
        !noPrePostAutoLoaders
      ) {
        // normal loader
        useLoaders.push(r.value);
      }
    } else if (
      typeof r.value === "object" &&
      r.value !== null &&
      typeof settings[r.type] === "object" &&
      settings[r.type] !== null
    ) {
      settings[r.type] = cachedMerge(settings[r.type], r.value);
    } else {
      settings[r.type] = r.value;
    }

    // 當獲取到 webpack.config 當中配置的 loader 後,再根據 loader 的型別進行分組(enforce 配置型別)
    // postLoader 儲存到 useLoaders 內部
    // preLoader 儲存到 usePreLoaders 內部
    // normalLoader 儲存到 useLoaders 內部
    // 這些分組最終會決定載入一個 module 時不同 loader 之間的呼叫順序

    // 當分組過程進行完之後,即開始 loader 模組的 resolve 過程
    asyncLib.parallel([
      [
        // resolve postLoader
        this.resolveRequestArray.bind(
          this,
          contextInfo,
          this.context,
          useLoadersPost,
          loaderResolver
        ),
        // resove normal loaders
        this.resolveRequestArray.bind(
          this,
          contextInfo,
          this.context,
          useLoaders,
          loaderResolver
        ),
        // resolve preLoader
        this.resolveRequestArray.bind(
          this,
          contextInfo,
          this.context,
          useLoadersPre,
          loaderResolver
        )
      ],
      (err, results) => {
        ...
        // results[0]  ->  postLoader
        // results[1]  ->  normalLoader
        // results[2]  ->  preLoader
        // 這裡將構建 module 需要的所有型別的 loaders 按照一定順序組合起來,對應於:
        // [postLoader, inlineLoader, normalLoader, preLoader]
        // 最終 loader 所執行的順序對應為: preLoader -> normalLoader -> inlineLoader -> postLoader
        // 不同型別 loader 上的 pitch 方法執行的順序為: postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch (具體loader內部執行的機制後文會單獨講解)
        loaders = results[0].concat(loaders, results[1], results[2]);

        process.nextTick(() => {
          ...
          // 執行回撥,建立 module
        })
      }
    ])
  }
})
複製程式碼

簡單總結下匹配的流程就是:

首先處理 inlineLoaders,對其進行解析,獲取對應的 loader 模組的資訊,接下來利用 ruleset 例項上的匹配過濾方法對 webpack.config 中配置的相關 loaders 進行匹配過濾,獲取構建這個 module 所需要的配置的的 loaders,並進行解析,這個過程完成後,便進行所有 loaders 的拼裝工作,並傳入建立 module 的回撥中。

相關文章