webpack loader配置全流程詳解

a1322674015發表於2019-09-25

前言


1.主要目的為稍微梳理從配置到裝載的流程。另外詳解當然要加點原始碼提升格調(本人菜鳥,有錯還請友善指正)

2.被的WebPack打包的檔案,都被轉化為一個模組,比如import './xxx/x.jpg'或require('./xxx/x.js')。至於具體實際怎麼轉化,交由裝載機處理

3.下文會使用打字稿(勸退警告?)以方便說明有哪些選項和各個選項的值型別


配置語法解析


模組屬性

module.exports = {
    ...
    module: {
        noParse: /jquery/,
        rules: [
            {
                test: /\.js/,
                exclude: /node_modules/,
                use:[
                    {
                        loader: './loader1.js?num=1',
                        options: {myoptions:false},
                    },
                    "./loader2.js?num=2",
                ]
            },
            {
                test: /\.js/,
                include: /src/,
                loader: './loader1.js!./loader2.js',
            },
        ]
    }
}



上述是展示常見的配置寫法.webpack為其選項都編寫了打字稿宣告,這個模組屬性的宣告在的WebPack /宣告中可見:



export interface ModuleOptions {
    // 一般下面這兩個
    noParse?: RegExp[] | RegExp | Function | string[] | string;
    rules?: RuleSetRules;
    
    // 這些...已被廢棄,即將被刪除,不用看
    defaultRules?: RuleSetRules;
    exprContextCritical?: boolean;
    exprContextRecursive?: boolean;
    exprContextRegExp?: boolean | RegExp;
    exprContextRequest?: string;
    strictExportPresence?: boolean;
    strictThisContextOnImports?: boolean;
    unknownContextCritical?: boolean;
    unknownContextRecursive?: boolean;
    unknownContextRegExp?: boolean | RegExp;
    unknownContextRequest?: string;
    unsafeCache?: boolean | Function;
    wrappedContextCritical?: boolean;
    wrappedContextRecursive?: boolean;
    wrappedContextRegExp?: RegExp;
}



noParse 用於讓的WebPack跳過對這些檔案的轉化,也就是他們不會被載入程式所處理(但還是會被打包並輸出到DIST目錄)

rules 核心配置,見下文

module.rules屬性

module.rules型別是RuleSetRule[],請繼續的WebPack /宣告檢視其打字稿,有哪些屬性,屬性型別一目瞭然。

注意RuleSetConditionsRecursive這個東西在另外一個檔案宣告,是interface RuleSetConditionsRecursive extends Array<import("./declarations/WebpackOptions").RuleSetCondition> {},其實就是export type RuleSetConditionsRecursive = RuleSetCondition[];,代表一個RuleSetCondition陣列

意義直接貼中文文件:模組。


好了,上面基本是搬運打字稿宣告,結合文件基本能知道有哪些屬性,屬性的型別和含義。下面結合原始碼對文件一些難以理解的地方補充說明。


 正文


規則集

規則的規範化(型別收斂)

由上可知一個規則物件,其屬性型別有多種可能,所以應該對其規範化,底層減少程式碼的大量typeof等判斷。這是由RuleSet.js進行規範化的。下面是經過規則集處理後的一個規則物件大致形式:


// rule 物件規範化後的形狀應該是:
{
resource: function(),
resourceQuery: function(),
compiler: function(),
issuer: function(),
use: [
{
loader: string,
options: string | object, // 原始碼的註釋可能是歷史遺留原因,options也可為object型別
<any>: <any>
} // 下文稱呼這個為use陣列的單個元素為 loader物件,規範化後它一般只有loader和options屬性
],
rules: [<rule>],
oneOf: [<rule>],
<any>: <any>,
}


rules狀語從句:oneOf的英文用來巢狀的,裡面的也是規範過的規則物件。

它這裡的四個函式是的WebPack用來判斷是否需要把檔案內容交給裝載器處理的。如的WebPack遇到了import './a.js',那麼rule.resource('f:/a.js')===true時會才把檔案交由規則中指定的裝載機去處理,resourceQuery等同理。

的這裡的傳入引數'f:/a.js'就是官網所說的


 條件已經兩個輸入值:

 資源:請求檔案的絕對路徑。它已經根據resolve規則解析。issuer :被請求資源(請求的資源)的模組檔案的絕對路徑。是匯入時的位置。


首先要做的是把Rule.loader, ,Rule.options(Rule.query已廢棄,但尚未刪除),移動全部到Rule.use陣列元素的物件裡。主要這由static normalizeRule(rule, refs, ident)函式處理,程式碼主要是處理各種“簡寫”,把值搬運到裝載器物件,做一些報錯處理,難度不大看一下即可,下面挑它裡面的“條件函式”規範化來說一說。


Rule.resource規範化

由上可知這是一個“條件函式”,它是根據我們的配置中的test,include,exclude,resource規範化而生成的原始碼180多行中:


if (rule.test || rule.include || rule.exclude) {
    checkResourceSource("test + include + exclude");
    condition = {
        test: rule.test,
        include: rule.include,
        exclude: rule.exclude
    };
    try {
        newRule.resource = RuleSet.normalizeCondition(condition);
    } catch (error) {
        throw new Error(RuleSet.buildErrorMessage(condition, error));
    }
}
if (rule.resource) {
    checkResourceSource("resource");
    try {
        newRule.resource = RuleSet.normalizeCondition(rule.resource);
    } catch (error) {
        throw new Error(RuleSet.buildErrorMessage(rule.resource, error));
    }
}

中文件說Rule.test的英文Rule.resource.test的簡寫,實際就是這串程式碼。

checkResourceSource用來檢查是否重複配置,即文件中提到的:你如果提供了一個Rule.test選項對話,就不能再提供Rule.resource


求最後RuleSet.normalizeCondition生成一個“條件函式”,如下:


static normalizeCondition(condition) {
    if (!condition) throw new Error("Expected condition but got falsy value");
    if (typeof condition === "string") {
        return str => str.indexOf(condition) === 0;
    }
    if (typeof condition === "function") {
        return condition;
    }
    if (condition instanceof RegExp) {
        return condition.test.bind(condition);
    }
    if (Array.isArray(condition)) {
        const items = condition.map(c => RuleSet.normalizeCondition(c));
        return orMatcher(items);
    }
    if (typeof condition !== "object") {
        throw Error(
            "Unexcepted " +
                typeof condition +
                " when condition was expected (" +
                condition +
                ")"
        );
    }
    const matchers = [];
    Object.keys(condition).forEach(key => {
        const value = condition[key];
        switch (key) {
            case "or":
            case "include":
            case "test":
                if (value) matchers.push(RuleSet.normalizeCondition(value));
                break;
            case "and":
                if (value) {
                    const items = value.map(c => RuleSet.normalizeCondition(c));
                    matchers.push(andMatcher(items));
                }
                break;
            case "not":
            case "exclude":
                if (value) {
                    const matcher = RuleSet.normalizeCondition(value);
                    matchers.push(notMatcher(matcher));
                }
                break;
            default:
                throw new Error("Unexcepted property " + key + " in condition");
        }
    });
    if (matchers.length === 0) {
        throw new Error("Excepted condition but got " + condition);
    }
    if (matchers.length === 1) {
        return matchers[0];
    }
    return andMatcher(matchers);
}

這串程式碼主要就是根據字串,正規表示式,物件,功能型別來生成不同的“條件函式”,難度不大。

notMatcher,orMatcher,andMatcher這三個是輔助函式,看名字就知道了,實現上非常簡單,不貼原始碼了。有什麼不明白的邏輯,代入進去跑一跑就知道了

規則使用規範化

我們接下來要把Rule.use給規範分類中翻譯上面提到的那種形式,即讓裝載機只物件保留loader狀語從句:options這兩個屬性(當然,並不是它一定只有這兩個屬性)原始碼如下:

static normalizeUse(use, ident) {
    if (typeof use === "function") {
        return data => RuleSet.normalizeUse(use(data), ident);
    }
    if (Array.isArray(use)) {
        return use
            .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`))
            .reduce((arr, items) => arr.concat(items), []);
    }
    return [RuleSet.normalizeUseItem(use, ident)];
}
static normalizeUseItemString(useItemString) {
    const idx = useItemString.indexOf("?");
    if (idx >= 0) {
        return {
            loader: useItemString.substr(0, idx),
            options: useItemString.substr(idx + 1)
        };
    }
    return {
        loader: useItemString,
        options: undefined
    };
}
static normalizeUseItem(item, ident) {
    if (typeof item === "string") {
        return RuleSet.normalizeUseItemString(item);
    }
    const newItem = {};
    if (item.options && item.query) {
        throw new Error("Provided options and query in use");
    }
    if (!item.loader) {
        throw new Error("No loader specified");
    }
    newItem.options = item.options || item.query;
    if (typeof newItem.options === "object" && newItem.options) {
        if (newItem.options.ident) {
            newItem.ident = newItem.options.ident;
        } else {
            newItem.ident = ident;
        }
    }
    const keys = Object.keys(item).filter(function(key) {
        return !["options", "query"].includes(key);
    });
    for (const key of keys) {
        newItem[key] = item[key];
    }
    return newItem;
}

這幾個函式比較繞,但總體來說難度不大。

這裡再稍微總結幾點現象:


1.loader: './loader1!./loader2',如果在Rule.loader指明瞭兩個以以上裝載機,那麼不可設定Rule.options,因為不知道該把這個選項傳給哪個裝載機,直接報錯

2.-loader不可省略,如babel!./loader的英文非法的,因為在webpack/lib/NormalModuleFactory.js440行左右,已經不再支援這種寫法,直接報錯叫你寫成babel-loader

3.loader: './loader1?num1=1&num2=2'將被處理成{loader: './loader', options: 'num=1&num=2'},以?進行了字串分割,最終處理成規範化裝載機物件


規則集規範化到此結束,有興趣的可以繼續圍觀原始碼的高管方法和建構函式


裝載機


接下來算是番外,討論各種裝載機如何讀取我們配置的物件。

**屬性在的WebPack的傳遞與處理選項**

首先一個裝載機就是簡單的匯出一個函式即可,比如上面舉例用到的



loader1.js:
module.exports = function (content){
    console.log(this)
    console.log(content)
    return content
}


這個函式里面的這個被繫結到一個loaderContext(loader上下文)中,官方api:loader API。


直接把這個loader1.js加入到配置檔案webpack.config.js裡面即可,在編譯時他就會列印出一些東西。


簡單而言,就是在裝載機中,可以我們透過this.query來訪問到規範化裝載機物件options屬性。比如{loader: './loader1.js', options: 'num1=1&num=2'},那麼this.query === '?num1=1&num=2'。

問題來了,這個問號哪裡來的如果它是一個物件?

的WebPack透過裝載機的領先者來執行裝載機,這個問題可以去loader-runner/lib/LoaderRunner.js,在createLoaderObject函式中有這麼一段:


if (obj.options === null)
    obj.query = "";
else if (obj.options === undefined)
    obj.query = "";
else if (typeof obj.options === "string")
    obj.query = "?" + obj.options;
else if (obj.ident) {
    obj.query = "??" + obj.ident;
}
else if (typeof obj.options === "object" && obj.options.ident)
    obj.query = "??" + obj.options.ident;
else
    obj.query = "?" + JSON.stringify(obj.options);


在以及runLoaders函式里面的這段:


Object.defineProperty(loaderContext, "query", {
    enumerable: true,
    get: function() {
        var entry = loaderContext.loaders[loaderContext.loaderIndex];
        return entry.options && typeof entry.options === "object" ? entry.options : entry.query;
    }
});



總結來說,當選項存在且是一個物件時,那麼this.query就是這個物件;如果選項是一個字串,那麼this.query等於一個問號+這個字串


多數裝載機讀取選項的方法


const loaderUtils=require('loader-utils')
module.exports = function (content){
    console.log(loaderUtils.getOptions(this))
    return content
}

藉助架utils的讀取那麼接下來走進loaderUtils.getOptions看看:


const query = loaderContext.query;
if (typeof query === 'string' && query !== '') {
  return parseQuery(loaderContext.query);
}
if (!query || typeof query !== 'object') {
  return null;
}
return query;

這裡只複製了關鍵程式碼,它主要是做一些簡單判斷,對字串的核心轉換在parseQuery上,接著看:

const JSON5 = require('json5');
function parseQuery(query) {
  if (query.substr(0, 1) !== '?') {
    throw new Error(
      "A valid query string passed to parseQuery should begin with '?'"
    );
  }
  query = query.substr(1);
  if (!query) {
    return {};
  }
  if (query.substr(0, 1) === '{' && query.substr(-1) === '}') {
    return JSON5.parse(query);
  }
  const queryArgs = query.split(/[,&]/g);
  const result = {};
  queryArgs.forEach((arg) => {
    const idx = arg.indexOf('=');
    if (idx >= 0) {
      let name = arg.substr(0, idx);
      let value = decodeURIComponent(arg.substr(idx + 1));
      if (specialValues.hasOwnProperty(value)) {
        value = specialValues[value];
      }
      if (name.substr(-2) === '[]') {
        name = decodeURIComponent(name.substr(0, name.length - 2));
        if (!Array.isArray(result[name])) {
          result[name] = [];
        }
        result[name].push(value);
      } else {
        name = decodeURIComponent(name);
        result[name] = value;
      }
    } else {
      if (arg.substr(0, 1) === '-') {
        result[decodeURIComponent(arg.substr(1))] = false;
      } else if (arg.substr(0, 1) === '+') {
        result[decodeURIComponent(arg.substr(1))] = true;
      } else {
        result[decodeURIComponent(arg)] = true;
      }
    }
  });
  return result;
}


使用了json5庫,以及自己的一套引數的轉換。

總結來說,只要你能確保自己使用的裝載器是透過loader-utils來獲取選項物件的,那麼你可以直接給選項寫成如下字串(inline loader中常用,如import 'loader1?a=1&b=2!./a.js'):


options: "{a: '1', b: '2'}" // 非json,是json5格式字串,略有出入,請右轉百度
options: "list[]=1&list=2[]&a=1&b=2" // http請求中常見的url引數部分

更多示例可在的WebPack /架utils的中檢視


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946034/viewspace-2658246/,如需轉載,請註明出處,否則將追究法律責任。

相關文章