Webpack: Loader開發 (2)

概述

  • 在上一篇文章中,我們已經詳細了解了開發 Webpack Loader 需要用到的基本技能,包括:Loader 基本形態、如何構建測試環境、如何使用 Loader Context 接口等。接下來我們繼續拓展學習一些 Loader 輔助工具,包括:
    • 了解 loader-utils,并使用 loader-utils 拼接文件名;
    • 了解 schema-tiles,以及其背后的 ajv 庫與 JSON-Schema 協議,學習使用 schema-utils 實現參數校驗
  • 文章最后還會深入剖析 vue-loader 組件源碼,通過實戰方式幫助大家更深入理解:如何開發一個成熟 Loader

使用 schema-utils

Webpack,以及 Webpack 生態下的諸多 Loader、Plugin 基本上都會提供若干“配置項”,供用戶調整組件的運行邏輯,這些組件內部通常都會使用 schema-utils 工具庫校驗用戶傳入的配置是否滿足要求。

因此,若我們開發的 Loader 需要對外暴露配置項,建議也盡量使用這一工具,基本用法:

  1. 安裝依賴:

    yarn add -D schema-utils
    
  2. 編寫配置對象的 Schema 描述,例如:

    // options.json
    {"type": "object","properties": {"name": {"type": "boolean"}},"required": ["name"],"additionalProperties": false
    }
    
  3. 在 Loader 中調用 schema-utils 校驗配置對象:

    import { validate } from "schema-utils";
    import schema from "./options.json";// 調用 schema-utils 完成校驗
    export default function loader(source) {const options = this.getOptions();validate(schema, options);return source;
    }// Webpack5 之后還可以借助 Loader Context 的 `getOptions` 接口完成校驗
    export default function loader(source) {const options = this.getOptions(schema);return source;
    }
    

之后,若用戶傳入不符合 Schema 描述的參數對象,會報類似下面這種錯誤提示:

在這里插入圖片描述
schema-utils 的校驗能力很強,能夠完美支撐起 Webpack 生態下非常復雜的參數校驗需求,但官方文檔非常語焉不詳,翻閱源碼后發現,它底層主要依賴于 ajv ,這是一個應用廣泛、功能強大且性能優異的校驗工具:

  • 提示:ajv 在對象校驗、JSON 序列化/反序列化方面的性能表現非常突出,許多知名開源框架 如:ESLint、fast-json-stringify、middy、swagger、tailwind 等底層都依賴于 ajv,值得我們學習、復用到業務項目中。

ajv 功能非常完備,基本上已經覆蓋了“使用 JSON 描述對象約束”的所有場景,我們不可能在一篇文章里介紹所有細節,所以我下面只摘要介紹一些比較重要的能力與實例,更多信息建議參考 官網。

  • ajv 數據描述格式基礎知識:

  • schema-utils 內部使用 ajv 的 JSON-Schema 模式實現參數校驗,而 JSON-Schema 是一種以 JSON 格式描述數據結構的 公共規范,使用時至少需要提供 type 參數,如:

    {"type": "number"
    }
    

ajv 默認支持七種基本數據類型。

  • number :數值型,支持整數、浮點數,支持如下校驗規則:

    • maximumminimum:屬性值必須大于等于 minimum ,且小于等于 maximum
    • exclusiveMaximumexclusiveMinimum:屬性值必須大于 exclusiveMinimum ,且小于 exclusiveMinimum
    • multipleOf:屬性值必須為 multipleOf 的整數倍,例如對于 multipleOf = 5,則 10/20/5 均符合預期,但 8/9/1 等不符合預期。
  • interger:整數型,與 number 類似,也支持上面介紹的 maximum 等校驗規則;

  • string :字符串型,支持如下校驗規則:

    • maxLengthminLength:限定字符串的最大長度、最小長度;
    • pattern:以正則表達式方式限定字符串內容;
    • format:聲明字符串內容格式,schema-utils 底層調用了 [ajv-formats](https://github.com/ajv-validator/ajv-formats) 插件,開箱支持 date/ipv4/regex/uuid 等格式。
  • boolean:bool 值;

  • array :數組型,支持如下校驗屬性:

    • maxItemsminItems:限定數組的最多、最少的元素數量;
    • uniqueItems:限定數組元素是否必須唯一,不可重復;
    • items:聲明數組項的 Schema 描述,數組項內可復用 JSON-Schema 的任意規則,從而形成嵌套定義結構;
  • null:空值,常用于復合 type 類型,如 type = ['object', 'null'] 支持傳入對象結構或 null 值;

  • object :對象結構,這是一個比較負責的結構,支持如下校驗屬性:

    • maxProperties / minProperties:限定對象支持的最多、最少屬性數量;

    • required:聲明哪些屬性不可為空,例如 required = ['name', 'age'] 時,傳入的值必須至少提供 name/age 屬性;

    • properties:定義特定屬性的 Schema 描述,與 arrayitems 屬性類似,支持嵌套規則,例如:

      {type: "object",properties: {foo: {type: "string"},bar: {type: "number",minimum: 2}}
      }
      
  • patternProperties:同樣用于定義對象屬性的 Schema,但屬性名支持正則表達式形式,例如:

    {type: "object",patternProperties: {"^fo.*$": {type: "string"},"^ba.*$": {type: "number"}}
    }
    
  • additionalProperties:限定對象是否可以提供除 propertiespatternProperties 之外的屬性;

除此之外,Schema 節點還支持一些通用的規則字段,包括:

  • enum:枚舉數組,屬性值必須完全等于(Deep equal)這些值之一,例如:

    // JSON-Schema
    {"type": "string","enum": ["fanwenjie","tecvan"]
    }// 有效值:
    "fanwenjie"/"tecvan"
    // 無效值,如:
    "foo bar"
  • const:靜態數值,屬性值必須完全等于 const 定義,單獨看 const 似乎作用不大,但配合 $data 指令的 JSON-Pointer 能力,可以實現關聯相等的效果,例如:

    // JSON-Schema
    {type: "object",properties: {foo: {type: "string"},bar: {const: {$data: "1/foo"}}}
    }// bar 必須等于 foo,如:
    {"foo": "fanwenjie","bar": "fanwenjie"
    }
    // 否則無效:
    {"foo": "fanwenjie","bar": "tecvan"
    }
    
  • 這些基礎數據類型與校驗規則奠定了 ajv 的基礎校驗能力,我們使用 schema-utils 時大部分時間都需要與之打交道,建議同學們多加學習掌握。

  • 使用 ajv 復合條件指令

除上述介紹的基本類型與基礎校驗規則外,ajv 還提供了若干復合校驗指令:

  • not:數值必須不符合該條件,例如:{type: "number", not: {minimum: 3}} 時,傳入數值必須小于 3;

  • anyof:數值必須滿足 anyof 條件之一,這是一個非常實用的指令,例如在 css-loader 中:

    // css-loader/src/options.json
    {"additionalProperties": false,"properties": {"url": {"description": "Enables/Disables 'url'/'image-set' functions handling (https://github.com/webpack-contrib/css-loader#url).","anyOf": [{"type": "boolean"},{"instanceof": "Function"}]},// more properties},"type": "object"
    }
    

這意味著 css-loaderurl 配置項只接受 Bool 或函數值。

  • oneof:數值必須滿足且只能滿足 oneof 條件之一,例如:

    {type: "number",oneOf: [{maximum: 3}, {type: "integer"}]
    }
    // 下述數值符合要求:
    1.12.145// 下述數值不符合要求:
    3.521

數值要么是小于等于3的浮點數,要么是大于3的整數,不在此區間的數值如“3.5/2” 等均不符合要求。

  • allof:數值必須滿足 allof 指定的所有條件,例如:

    {type: "number",allOf: [{maximum: 3}, {type: "integer"}]
    }
    // 下述數值符合要求:
    123// 下述數值不符合要求:
    1.145

這要求傳入的數值必須小于 3,且必須為整型。

  • if/then/else:這是一個稍顯復雜的三元組復合條件,大致邏輯為:若傳入的數值滿足 if 條件,則必須同時滿足 then 條件;若不滿足 if 則必須同時滿足 else,其中 else 可選。例如:

    {type: "object",if: {properties: {foo: {minimum: 10}}},then: {required: ["bar"]},else: {required: ["baz"]}
    }
    

這意味著,若傳入的 foo 屬性值大于等于 10 時,則必須同時提供 then 所要求的 bar 屬性;否則必須同時提供 else 所要求的 baz 屬性。

總結一下,Webpack 官方選擇 ajv 作用配置參數的校驗工具,并將其二次封裝為 schema-utils 庫,供 Webpack 生態下的諸多 Loader、Plugin 使用。

而上面介紹的基礎類型、類型校驗、復合校驗規則等內容是 ajv 非常基礎且重要的知識點,三者協作組成 ajv 校驗 schema 的框架結構,除此之外還有許多增強 Schema 表述能力的增強指令,包括:$data$refdefinitions 等,篇幅關系這里不一一列舉。同學們也可以參考 Webpack 官方編寫的 Schema 文件,學習各種校驗規則的寫法。

使用 loader-utils

在 Webpack5 之前,loader-utils 是一個非常重要的 Loader 開發輔助工具,為開發者提供了諸如 getOptions/getCurrentRequest/parseQuery 等核心接口,這些接口被諸多 Loader 廣泛使用,到 Webpack5 之后干脆將這部分能力遷移到 Loader Context,致使 loader-utils 被大幅裁減簡化。

被裁減后的 loader-utils 僅保留了四個接口:

  • urlToRequest:用于將模塊路徑轉換為文件路徑的工具函數;
  • isUrlRequest:用于判定字符串是否為模塊請求路徑;
  • getHashDigest:用于計算內容 Hash 值;
  • interpolateName:用于拼接文件名的模板工具;

翻閱大量 Loader 源碼后發現,前三個接口使用率極低,實用性不大,因此本文直接跳過,僅側重介紹 interpolateName 接口。

使用 interpolateName 拼接文件名

Webpack 支持以類似 [path]/[name]-[hash].js 方式設定 output.filename 即輸出文件的命名,這一層規則通常不需要關注,但在編寫類似 webpack-contrib/file-loader 這種自行輸出產物文件的 Loader 時,需要由開發者自行處理產物路徑邏輯。

此時可以使用 loader-utils 提供的 interpolateName 方法在 Loader 中以類似 Webpack 的 output.filename 規則拼接資源路徑及名稱,例如:

// file-loader/src/index.js
import { interpolateName } from 'loader-utils';export default function loader(content) {const context = options.context || this.rootContext;const name = options.name || '[contenthash].[ext]';// 拼接最終輸出的名稱const url = interpolateName(this, name, {context,content,regExp: options.regExp,});let outputPath = url;// ...let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;// ...if (typeof options.emitFile === 'undefined' || options.emitFile) {// ...// 提交、寫出文件this.emitFile(outputPath, content, null, assetInfo);}// ...const esModule =typeof options.esModule !== 'undefined' ? options.esModule : true;// 返回模塊化內容return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`;
}export const raw = true;

代碼的核心邏輯:

  1. 根據 Loader 配置,調用 interpolateName 方法拼接目標文件的完整路徑;
  2. 調用上下文 this.emitFile 接口,寫出文件;
  3. 返回 module.exports = ${publicPath} ,其它模塊可以引用到該文件路徑。
  • 提示:除 file-loader 外,css-loadereslint-loader 都有用到該接口,感興趣的同學請自行前往查閱源碼。

interpolateName 功能稍弱于 Webpack 的 Template String 規則,僅支持如下占位符:

  • [ext]:原始資源文件的擴展名,如 .js
  • [name]:原始文件名;
  • [path]:原始文件相對 context 參數的路徑;
  • [hash]:原始文件的內容 Hash 值,與 output.file 類似同樣支持 [hash:length] 指定 Hash 字符串的長度;
  • [contenthash]:作用、用法都與上述 [hash] 一模一樣。

綜合示例:Vue-loader

接下來,我們再結合 vue-loader 源碼進一步學習 Loader 開發的進階技巧。vue-loader 是一個綜合性很強的示例,它借助 Webpack 與組件的一系列特性巧妙地解決了:如何區分 Vue SFC 不同代碼塊,并復用其它 Loader 處理不同區塊的內容?

先從結構說起,vue-loader 內部實際上包含了三個組件:

  • lib/index.js 定義的 Normal Loader,負責將 Vue SFC 不同區塊轉化為 JavaScript import 語句,具體邏輯下面細講;
  • lib/loaders/pitcher.js 定義的 Pitch Loader,負責遍歷的 rules 數組,拼接出完整的行內引用路徑;
  • lib/plugin.js 定義的插件,負責初始化編譯環境,如復制原始 rules 配置等;

三者協作共同完成對 SFC 的處理,使用時需要用戶同時注冊 Normal Loader 和 Plugin,如:

const VueLoaderPlugin = require("vue-loader/lib/plugin");module.exports = {module: {rules: [{test: /.vue$/,use: [{ loader: "vue-loader" }],}],},plugins: [new VueLoaderPlugin()],
};

vue-loader 運行過程大致上可以劃分為兩個階段:

  1. 預處理階段:動態修改 Webpack 配置,注入 vue-loader 專用的一系列 module.rules
  2. 內容處理階段:Normal Loader 配合 Pitch Loader 完成文件內容轉譯。

預處理階段

vue-loader 插件會在 apply 函數中動態修改 Webpack 配置,核心代碼如下:

class VueLoaderPlugin {apply (compiler) {// ...const rules = compiler.options.module.rules// ...const clonedRules = rules.filter(r => r !== rawVueRules).map((rawRule) => cloneRule(rawRule, refs))// ...// global pitcher (responsible for injecting template compiler loader & CSS// post loader)const pitcher = {loader: require.resolve('./loaders/pitcher'),resourceQuery: query => {if (!query) { return false }const parsed = qs.parse(query.slice(1))return parsed.vue != null}// ...}// replace original rulescompiler.options.module.rules = [pitcher,...clonedRules,...rules]}
}function cloneRule (rawRule, refs) {// ...
}module.exports = VueLoaderPlugin

拆開來看,插件主要完成兩個任務:

  • 初始化并注冊 Pitch Loader:代碼第16行,定義pitcher對象,指定loader路徑為 require.resolve('./loaders/pitcher') ,并將pitcher注入到 rules 數組首位。

這種動態注入的好處是用戶不用關注 —— 不去看源碼根本不知道還有一個pitcher loader,而且能保證pitcher能在其他rule之前執行,確保運行順序。

  • 復制 rules 配置:代碼第8行遍歷 compiler.options.module.rules 數組,也就是用戶提供的 Webpack 配置中的 module.rules 項,對每個rule執行 cloneRule 方法復制規則對象。

之后,將 Webpack 配置修改為 [pitcher, ...clonedRules, ...rules] 。感受一下實際效果,例如:

module.exports = {module: {rules: [{test: /.vue$/i,use: [{ loader: "vue-loader" }],},{test: /\.css$/i,use: [MiniCssExtractPlugin.loader, "css-loader"],},{test: /\.js$/i,exclude: /node_modules/,use: {loader: "babel-loader",options: {presets: [["@babel/preset-env", { targets: "defaults" }]],},},},],},plugins: [new VueLoaderPlugin(),new MiniCssExtractPlugin({ filename: "[name].css" }),],
};

這里定義了三個 rule,分別對應 vue、js、css 文件。經過 plugin 轉換之后的結果大概為:

module.exports = {module: {rules: [{loader: "/node_modules/vue-loader/lib/loaders/pitcher.js",resourceQuery: () => {},options: {},},{resource: () => {},resourceQuery: () => {},use: [{loader: "/node_modules/mini-css-extract-plugin/dist/loader.js",},{ loader: "css-loader" },],},{resource: () => {},resourceQuery: () => {},exclude: /node_modules/,use: [{loader: "babel-loader",options: {presets: [["@babel/preset-env", { targets: "defaults" }]],},ident: "clonedRuleSet-2[0].rules[0].use",},],},{test: /\.vue$/i,use: [{ loader: "vue-loader", options: {}, ident: "vue-loader-options" },],},{test: /\.css$/i,use: [{loader: "/node_modules/mini-css-extract-plugin/dist/loader.js",},{ loader: "css-loader" },],},{test: /\.vue$/i,exclude: /node_modules/,use: [{loader: "babel-loader",options: {presets: [["@babel/preset-env", { targets: "defaults" }]],},ident: "clonedRuleSet-2[0].rules[0].use",},],},],},
};

轉換之后生成6個rule,按定義的順序分別為:

  1. 針對 xx.vue&vue 格式路徑生效的規則,只用了 vue-loader 的 Pitch 作為 Loader;
  2. 被復制的 CSS 處理規則,use 數組與開發者定義的規則相同;
  3. 被復制的 JS 處理規則,use 數組也跟開發者定義的規則相同;
  4. 開發者定義的 vue-loader 規則,內容及配置都不變;
  5. 開發者定義的css規則,用到 css-loadermini-css-extract-plugin loader
  6. 開發者定義的js規則,用到 babel-loader

可以看到,第2、3項是從開發者提供的配置中復制過來的,內容相似,只是 cloneRule 在復制過程會給這些規則重新定義 resourceQuery 函數:

function cloneRule (rawRule, refs) {const rules = ruleSetCompiler.compileRules(`clonedRuleSet-${++uid}`, [{rules: [rawRule]}], refs)const conditions = rules[0].rules.map(rule => rule.conditions)// shallow flat.reduce((prev, next) => prev.concat(next), [])// ...const res = Object.assign({}, rawRule, {resource: resources => {currentResource = resourcesreturn true},resourceQuery: query => {if (!query) { return false }const parsed = qs.parse(query.slice(1))if (parsed.vue == null) {return false}if (!conditions) {return false}// 用import路徑的lang參數測試是否適用于當前ruleconst fakeResourcePath = `${currentResource}.${parsed.lang}`for (const condition of conditions) {// add support for resourceQueryconst request = condition.property === 'resourceQuery' ? query : fakeResourcePathif (condition && !condition.fn(request)) {return false}}return true}})// ...return res}

cloneRule 內部定義的 resourceQuery 函數對應 module.rules.resourceQuery 配置項,與我們經常用的 test 差不多,都用于判斷資源路徑是否適用這個rule。這里 resourceQuery 核心邏輯就是取出路徑中的lang參數,偽造一個以 lang 結尾的路徑,傳入rule的condition中測試路徑名對該rule是否生效,例如下面這種會命中 /\.js$/i 規則:

import script from "./index.vue?vue&type=script&lang=js&"

vue-loader 正是基于這一規則,為不同內容塊 (css/js/template) 匹配、復用用戶所提供的 rule 設置。

內容處理階段

插件處理完配置,webpack 運行起來之后,Vue SFC 文件會被多次傳入不同的 Loader,經歷多次中間形態變換之后才產出最終的 js 結果,大致上可以分為如下步驟:

  1. 路徑命中 /\.vue$/i 規則,調用 vue-loader 生成中間結果 A;
  2. 結果 A 命中 xx.vue?vue 規則,調用 vue-loader Pitch Loader 生成中間結果 B;
  3. 結果 B 命中具體 Loader,直接調用 Loader 做處理。

過程大致為:
在這里插入圖片描述

舉個轉換過程的例子:

// 原始代碼
import xx from './index.vue';
// 第一步,命中 vue-loader,轉換為:
import { render, staticRenderFns } from "./index.vue?vue&type=template&id=2964abc9&scoped=true&"
import script from "./index.vue?vue&type=script&lang=js&"
export * from "./index.vue?vue&type=script&lang=js&"
import style0 from "./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&"// 第二步,命中 pitcher,轉換為:
export * from "-!../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=template&id=2964abc9&scoped=true&"
import mod from "-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-2[0].rules[0].use!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js&"; 
export default mod; export * from "-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-2[0].rules[0].use!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js&"
export * from "-!../../node_modules/mini-css-extract-plugin/dist/loader.js!../../node_modules/css-loader/dist/cjs.js!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&"// 第三步,根據行內路徑規則按序調用loader

第一次執行 vue-loader

在運行階段,根據配置規則, Webpack 首先將原始的 SFC 內容傳入 vue-loader,例如對于下面的代碼:

// main.js
import xx from 'index.vue';// index.vue 代碼
<template><div class="root">hello world</div>
</template><script>
export default {data() {},mounted() {console.log("hello world");},
};
</script><style scoped>
.root {font-size: 12px;
}
</style>

此時第一次執行 vue-loader ,執行如下邏輯:

  1. 調用 @vue/component-compiler-utils 包的parse函數,將SFC 文本解析為AST對象;
  2. 遍歷 AST 對象屬性,轉換為特殊的引用路徑;
  3. 返回轉換結果。

對于上述 index.vue 內容,轉換結果為:

import { render, staticRenderFns } from "./index.vue?vue&type=template&id=2964abc9&scoped=true&"
import script from "./index.vue?vue&type=script&lang=js&"
export * from "./index.vue?vue&type=script&lang=js&"
import style0 from "./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&"/* normalize component */
import normalizer from "!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js"
var component = normalizer(script,render,staticRenderFns,false,null,"2964abc9",null)...
export default component.exports

注意,這里并沒有真的處理 block 里面的內容,而是簡單地針對不同類型的內容塊生成 import 語句:

  • Script:"./index.vue?vue&type=script&lang=js&"
  • Template: "./index.vue?vue&type=template&id=2964abc9&scoped=true&"
  • Style: "./index.vue?vue&type=style&index=0&id=2964abc9&scoped=true&lang=css&"

這些路徑都對應原始的 .vue 路徑基礎上增加了 vue 標志符及 type、lang 等參數。

執行 Pitch Loader

如前所述,vue-loader 插件會在預處理階段插入帶 resourceQuery 函數的 Pitch Loader:

const pitcher = {loader: require.resolve('./loaders/pitcher'),resourceQuery: query => {if (!query) { return false }const parsed = qs.parse(query.slice(1))return parsed.vue != null}
}

其中, resourceQuery 函數命中 xx.vue?vue 格式的路徑,也就是說上面 vue-loader 轉換后的 import 路徑會被 Pitch Loader 命中,做進一步處理。Pitch Loader 的邏輯比較簡單,做的事情也只是轉換 import 路徑:

const qs = require('querystring')
...const dedupeESLintLoader = loaders => {...}const shouldIgnoreCustomBlock = loaders => {...}// 正常的loader階段,直接返回結果
module.exports = code => codemodule.exports.pitch = function (remainingRequest) {const options = loaderUtils.getOptions(this)const { cacheDirectory, cacheIdentifier } = options// 關注點1: 通過解析 resourceQuery 獲取loader參數const query = qs.parse(this.resourceQuery.slice(1))let loaders = this.loaders// if this is a language block request, eslint-loader may get matched// multiple timesif (query.type) {// if this is an inline block, since the whole file itself is being linted,// remove eslint-loader to avoid duplicate linting.if (/\.vue$/.test(this.resourcePath)) {loaders = loaders.filter(l => !isESLintLoader(l))} else {// This is a src import. Just make sure there's not more than 1 instance// of eslint present.loaders = dedupeESLintLoader(loaders)}}// remove selfloaders = loaders.filter(isPitcher)// do not inject if user uses null-loader to void the type (#1239)if (loaders.some(isNullLoader)) {return}const genRequest = loaders => {... }// Inject style-post-loader before css-loader for scoped CSS and trimmingif (query.type === `style`) {const cssLoaderIndex = loaders.findIndex(isCSSLoader)if (cssLoaderIndex > -1) {...return query.module? `export { default } from  ${request}; export * from ${request}`: `export * from ${request}`}}// for templates: inject the template compiler & optional cacheif (query.type === `template`) {...// console.log(request)// the template compiler uses esm exportsreturn `export * from ${request}`}// if a custom block has no other matching loader other than vue-loader itself// or cache-loader, we should ignore itif (query.type === `custom` && shouldIgnoreCustomBlock(loaders)) {return ``}const request = genRequest(loaders)return `import mod from ${request}; export default mod; export * from ${request}`
}

核心功能是遍歷用戶定義的rule數組,拼接出完整的行內引用路徑,例如:

// 開發代碼:
import xx from 'index.vue'
// 第一步,通過vue-loader轉換成帶參數的路徑
import script from "./index.vue?vue&type=script&lang=js&"
// 第二步,在 pitcher 中解讀loader數組的配置,并將路徑轉換成完整的行內路徑格式
import mod from "-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-2[0].rules[0].use!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js&";

第二次執行vue-loader

通過上面 vue-loader -> Pitch Loader 處理后,會得到一個新的行內路徑,例如:

import mod from "-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-2[0].rules[0].use!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./index.vue?vue&type=script&lang=js&";

以這個 import 語句為例,之后 Webpack 會按照下述邏輯運行:

  • 調用 vue-loader 處理 index.js 文件;
  • 調用 babel-loader 處理上一步返回的內容。

這就給了 vue-loader 第二次執行的機會,再回過頭來看看 vue-loader 的代碼:

module.exports = function (source) {// ...const {target,request,minimize,sourceMap,rootContext,resourcePath,resourceQuery = "",} = loaderContext;// ...const descriptor = parse({source,compiler: options.compiler || loadTemplateCompiler(loaderContext),filename,sourceRoot,needMap: sourceMap,});// if the query has a type field, this is a language block request// e.g. foo.vue?type=template&id=xxxxx// and we will return earlyif (incomingQuery.type) {return selectBlock(descriptor,loaderContext,incomingQuery,!!options.appendExtension);}//...return code;
};module.exports.VueLoaderPlugin = plugin;

第二次運行時由于路徑已經帶上了 type 參數,會命中上面第26行的判斷語句,進入 selectBlock 函數,這個函數的邏輯很簡單:

module.exports = function selectBlock (descriptor,loaderContext,query,appendExtension
) {// templateif (query.type === `template`) {if (appendExtension) {loaderContext.resourcePath += '.' + (descriptor.template.lang || 'html')}loaderContext.callback(null,descriptor.template.content,descriptor.template.map)return}// scriptif (query.type === `script`) {if (appendExtension) {loaderContext.resourcePath += '.' + (descriptor.script.lang || 'js')}loaderContext.callback(null,descriptor.script.content,descriptor.script.map)return}// stylesif (query.type === `style` && query.index != null) {const style = descriptor.styles[query.index]if (appendExtension) {loaderContext.resourcePath += '.' + (style.lang || 'css')}loaderContext.callback(null,style.content,style.map)return}// customif (query.type === 'custom' && query.index != null) {const block = descriptor.customBlocks[query.index]loaderContext.callback(null,block.content,block.map)return}
}

至此,就可以完成從 Vue SFC 文件中抽取特定 Block 內容,并復用用戶定義的其它 Loader 加載這些 Block

綜上,我們可以將 vue-loader 的核心邏輯總結為:

  • 首先給原始文件路徑增加不同的參數,后續配合 resourceQuery 參數就可以分開處理這些內容,這樣的實現相比于一次性處理,邏輯更清晰簡潔,更容易理解;
  • 經過 Normal Loader、Pitch Loader 兩個階段后,SFC 內容會被轉化為 import xxx from '!-babel-loader!vue-loader?xxx' 格式的引用路徑,以此復用用戶配置。

總結

  • 使用 schema-utilsloader-utils 工具實現更多 Loader 進階特性,并進一步剖析 vue-loader 源碼,講解如何構建一個成熟的 Webpack Loader 組件。

  • 我們可以總結一些常用的開發方法論,包括:

    • Loader 主要負責將資源內容轉譯為 Webpack 能夠理解、處理的標準 JavaScript 形式,所以通常需要做 Loader 內通過 return/this.callback 方式返回翻譯結果;
    • Loader Context 提供了許多實用接口,我們可以借助這些接口讀取上下文信息,或改變 Webpack 運行狀態(相當于產生 Side Effect,例如通過 emitFile 接口);
    • 假若我們開發的 Loader 需要對外提供配置選項,建議使用 schema-utils 校驗配置參數是否合法;
    • 假若 Loader 需要生成額外的資源文件,建議使用 loader-utils 拼接產物路徑;
    • 執行時,Webpack 會按照 use 定義的順序從前到后執行 Pitch Loader,從后到前執行 Normal Loader,我們可以將一些預處理邏輯放在 Pitch 中(如 vue-loader) 等等
  • 最后,參考一些知名 Loader 的源碼,如:css-loader/babel-loader/file-loader

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/39506.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/39506.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/39506.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

telegram支付

今天開始接入telegram支付,參考教程這個是telegram的官方說明,詳細介紹了機器人支付API。 文章公開地址 新建機器人 因為支付是一個單獨的系統,所以在做支付的時候單獨創建了一個bot,沒有用之前的bot了,特意這樣將其分開。創建bot的方法和之前不變,這里不過多介紹。 獲…

Linux文件數據寫入

結構體 fd fd也就是文件描述符&#xff0c;用于標識已經打開的文件、管道、socket等。是進程和內核的橋梁&#xff0c;允許進程執行各種文件操作 struct fd {struct file *file;unsigned int flags; };file Linux內核中表示打開文件的結構體&#xff0c;包含了文件操作所需…

什么是自然語言處理(NLP)?詳細解讀文本分類、情感分析和機器翻譯的核心技術

什么是自然語言處理&#xff1f; 自然語言處理&#xff08;Natural Language Processing&#xff0c;簡稱NLP&#xff09;是人工智能的一個重要分支&#xff0c;旨在讓計算機理解、解釋和生成人類的自然語言。打個比方&#xff0c;你和Siri對話&#xff0c;或使用谷歌翻譯翻譯一…

2024廣州國際米粉產業展覽會暨米粉節

2024廣州國際米粉產業展覽會 時間&#xff1a;2024年11月16-18日 地點&#xff1a;廣州中國進出口商品交易會展館 主辦單位&#xff1a;企陽國際會展集團 【展會簡介】 米粉作為一種歷史悠久&#xff0c;人們日常食用的食物&#xff0c;其市場需求穩定&#xff0c;且隨著人…

學習.NET 8 MiniApis入門

介紹篇 什么是MiniApis&#xff1f; MiniApis的特點和優勢 MiniApis的應用場景 環境搭建 系統要求 安裝MiniApis 配置開發環境 基礎概念 MiniApis架構概述 關鍵術語解釋&#xff08;如Endpoint、Handler等&#xff09; MiniApis與其他API框架的對比 第一個MiniApis…

WSL2安裝ContOS7并更新gcc

目錄 WSL2安裝CentOS7下載安裝包安裝啟動CentOS7 CentOS7更換國內源gcc從源碼安裝gcc卸載gcc CMake中使用gcc關于linux配置文件參考 WSL2安裝CentOS7 Windows11官方WSL2已經支持Ubuntu、Open SUSE、Debian。但是沒有centos&#xff0c;所以centos的安裝方式略有不同。 下載安…

【面試題】網絡IP協議(第六篇)

1.簡述IP協議的作用。 IP協議&#xff08;Internet Protocol&#xff09;是TCP/IP協議族中的核心協議之一&#xff0c;主要用于在互聯網上進行數據傳輸。它的主要作用包括&#xff1a; 尋址&#xff1a;IP協議通過IP地址來唯一標識網絡中的每一臺設備&#xff0c;確保數據包能…

家政小程序的開發:打造現代式便捷家庭服務

隨著現代生活節奏的加快&#xff0c;人們越來越注重生活品質與便利性。在這樣的背景下&#xff0c;家政服務市場迅速崛起&#xff0c;成為許多家庭日常生活中不可或缺的一部分。然而&#xff0c;傳統的家政服務往往存在信息不對稱、服務效率低下等問題。為了解決這些問題&#…

mindspore打卡之量子測量

mindspore打卡之量子測量 我們可以看到&#xff0c;采樣1000中&#xff0c;00’出現了503次&#xff0c;11’出現了497次&#xff08;由于測量具有隨機性&#xff0c;每次運行結果會略有不同&#xff09;&#xff0c;采樣結果符合概率分布&#xff0c;細微的誤差是由模擬器噪聲…

【D3.js in Action 3 精譯】1.2.2 可縮放矢量圖形(三)

當前內容所在位置 第一部分 D3.js 基礎知識 第一章 D3.js 簡介 1.1 何為 D3.js&#xff1f;1.2 D3 生態系統——入門須知 1.2.1 HTML 與 DOM1.2.2 SVG - 可縮放矢量圖形 ?? 第一部分第二部分【第三部分】?? 1.2.3 Canvas 與 WebGL&#xff08;精譯中 ?&#xff09;1.2.4 C…

gameui C++的代碼

gameui C的代碼 #include <graphics.h> #include "gameboard.h" const int WIDTH 560; const int HEIGHT 780; const int GRID_SIZE 120; class GameUi { private: public:GameUi(GameBoard& gb) {// 初始化圖形窗口initgraph(WIDTH, HEIGHT);// 設置…

小米內推官

小米硬件提前批開始啦&#xff01;&#xff01;&#xff01;&#xff01; 請使用小米公司內推碼: Q9BN21S 招聘對象&#xff1a;25屆秋招 中國內地:畢業時間2025.01-2025.12 中國港澳臺及海外:畢業時間2025.07-2025.12 工作地點&#xff1a;北京、南京、武漢、深圳、上海等…

C++中Ranges庫的基本用法,如何簡化和增強對集合的操作。

Ranges庫的基本用法 什么是Ranges庫&#xff1f; C20引入的Ranges庫&#xff08;<ranges>&#xff09;是一個強大的工具集&#xff0c;旨在簡化和增強對集合&#xff08;如數組、向量等序列容器&#xff09;的操作。它提供了一套用于操作序列的功能性接口&#xff0c;使…

獨立站新風口:TikTok達人帶貨背后的雙贏合作之道

TikTok以其龐大的用戶基礎、高度互動性和創新的內容形式&#xff0c;為獨立站帶來了前所未有的發展機遇。獨立站與TikTok達人的合作&#xff0c;不僅能夠幫助獨立站快速提升品牌知名度和銷售額&#xff0c;還能為TikTok達人帶來更多商業機會和影響力。本文Nox聚星將和大家探討獨…

Android sdk 安裝已經環境配置

&#x1f34e;個人博客&#xff1a;個人主頁 &#x1f3c6;個人專欄&#xff1a;Android ?? 功不唐捐&#xff0c;玉汝于成 目錄 正文 一、下載 二、安裝 三、環境配置 我的其他博客 正文 一、下載 1、大家可去官網下載 因為需要魔法 所以就不展示了 2、去下面這…

經典shell運維實用腳本~

以下腳本可謂日常工作常用到的經典腳本案例。希望可以幫助大家提升提升自動化能力&#xff01; 1、監控100臺服務器磁盤利用率腳本 #!/bin/bashHOST_INFOhost.infofor IP in $(awk /^[^#]/{print $1} $HOST_INFO); do USER$(awk -v ip$IP ip$1{print $2} $HOST_INFO) P…

【JS】純web端使用ffmpeg實現的視頻編輯器-視頻合并

純前端實現的視頻合并 接上篇ffmpeg文章 【JS】純web端使用ffmpeg實現的視頻編輯器 這次主要添加了一個函數&#xff0c;實現了視頻合并的操作。 static mergeArgs(timelineList) {const cmd []console.log(時間軸數據,timelineList)console.log("文件1",this.readD…

常用目標檢測的格式轉換腳本文件txt,json等

常用目標檢測的格式轉換腳本文件txt,json等 文章目錄 常用目標檢測的格式轉換腳本文件txt,json等前言一、json格式轉yolo的txt格式二、yolov8的關鍵點labelme打的標簽json格式轉可訓練的txt格式三、yolo的目標檢測txt格式轉coco數據集標簽的json格式四、根據yolo的目標檢測訓練…

Vue+ElementUi實現錄音播放上傳及處理getUserMedia報錯問題

1.Vue安裝插件 npm install --registryhttps://registry.npmmirror.com 2.Vue頁面使用 <template><div class"app-container"><!-- header --><el-header class"procedureHeader" style"height: 20px;"><el-divid…

vue2 接口文檔

const assetmanagementIndex (params) > getAction("/asset/assetmanagementsystem/page", params); //資產管理制度表分頁列表 const assetmanagementPost (params) > postAction("/asset/assetmanagementsystem", params); //資產管理制度表新增…