Webpack源碼分析 - loader及優化

Loader

loader解析文件是Webpack中重要的一環,之因此能一切皆模塊就是由於有許多強大的loader提供的支持。瞭解它的工做原理可讓咱們從容地爲項目選擇合適的配置,還能夠更有目的性的針對性能瓶頸分析優化,更好地作一個合格地Webpack配置工程師。css

若是要了解loader內部執行原理,能夠看這篇文章loader-runnerhtml

loader基礎

loader的配置很是靈活,以致於關於loader的代碼有一大半是在解析參數,因此要看懂代碼就要先清楚經常使用的配置,若是在翻閱代碼時看着比較奇怪,能夠先回來看看這段代碼究竟是處理哪一類配置,下面列舉了一些經常使用的選項:node

loader執行順序

在執行loader前,Webpack已經將其根據配置排好序,指定順序會在建立loader階段執行:webpack

  • 正常狀況loader執行順序: pre -> normal -> inline -> post
  • 資源路徑前使用'xxx!=!'裝飾: pre -> inline -> normal -> post
  • 資源路徑前使用'-!'裝飾: inline -> post
  • 資源路徑前使用'!'裝飾: pre -> inline -> post
  • 資源路徑前使用'!!'裝飾: inline

全局配置

rules: [
    // 前置
    { enforce: 'pre', test: /\.js$/, use: 'babel-loader' },
    // 正則匹配
    { test: /\.js$/, use: 'babel-loader' },
    // 後置
    { enforce: 'post', test: /\.js$/, use: 'babel-loader' },
]
複製代碼

inline配置

// 普通
require('babel-loader!./increment.js')
// 多個loader,從右到左執行
require('style-loader!css-loader!less-loader!./increment.less')
// 帶重命名!=!,交換normalLoader和inlineLoader執行順序
require('aa.js!=!babel-loader!./increment.js')
// 帶!!前綴只執行inline-loader
require('!!babel-loader!./increment.js')
複製代碼

loader匹配條件

每一個模塊都會篩選出它須要的loader和相應的配置:git

inline匹配

// 普通
require('babel-loader!./increment.js')
// 帶參數
require('./myLoader?a=11&b=22!./increment.js')
/** * 指定使用全局options配置 * { * test: () => false, * use: { loader: 'babel-loader', ident: 'babelLoaderOptions', options: { presets: ['@babel/preset-env'] } } * } */
require('babel-loader??babelLoaderOptions!./increment.js')
複製代碼

全局condition條件

條件condition能夠是這些之一,用於匹配資源絕對路徑:github

rules: [
    // 字符串匹配,前綴匹配
    { test: path.resolve('./src/index.js'), use: 'babel-loader' },
    // 正則匹配
    { test: /\.js$/, use: 'babel-loader' },
    // 函數匹配,返回true表示匹配成功
    { test: (resourcePath) => { return true }, use: 'babel-loader' },
    // 數組匹配,只要一個匹配條件算匹配成功
    { test: [/\.js$/, /\.ts$/], use: 'babel-loader' },
    // 對象匹配,匹配上全部條件算匹配成功
    { test: { or: [/\.js$/, /.ts$/], exclude: /node_modules/ }, use: 'babel-loader' },
]
複製代碼

resourceQuery

用於匹配路徑參數,路徑參數是引用資源時後面問號的內容,如require('./a.js?matchMe)將能匹配下面的loader:web

{
  test: /.js$/,
  resourceQuery: /matchMe$/,
  use: 'babel-loader'
}
複製代碼

oneOf

使用第一個成功匹配的規則:數組

{
  test: /.css$/,
  oneOf: [
    // 匹配require('foo.css?inline')
    { resourceQuery: /inline/,  use: 'url-loader' },
    // 匹配require('foo.css?external')
    { resourceQuery: /external/, use: 'file-loader' }
  ]
}
複製代碼

其餘

  • issure: 匹配引用這個資源的模塊路徑,如foo.jsbar.js同時引用了inc.js,能夠指定只有foo.js才使用該loader。
  • compiler: 匹配編譯器名,通常沒啥用。

use配置

use用於指定匹配成功後須要用到哪些loader解析:緩存

// 字符串指定
use: 'babel-loader'
// 數組指定: 右到左執行
use: ['style-loader', 'css-loader']
// 對象指定,帶參數
use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } }
// 數組混合
use: ['style-loader', { loader: 'css-loader', options: { modules: true } }, 'postcss-loader' ]
複製代碼

Loader處理流程

Webpack對loader的處理主要在模塊構建期間,另外作了緩存和監聽操做,主要有下面三個流程:babel

  • RuleSet:初始化時解析loader相關配置;
  • NormalModuleFactory.create:根據配置匹配當前模塊用到的loader,並將初始化好的loader數組經過構造函數傳入;
  • NormalModule:構建模塊時使用loader-runner解析loader,獲取處理後的文件內容;
class NormalModuleFactory extends Tapable {
	constructor(context, resolverFactory, options) {
        super();
        // 參數解析
		this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
    }

	create(data, callback) {
        // 解析inline-loader/normal-loader
        resolver(data, (result) => {
            // ...使用解析好的loaders建立NormalModule
            createdModule = new NormalModule({
                loaders: result.loaders,
                resource: result.resource,
            });
            // 返回建立好的NormalModule
            callback(null, createdModule)
        })
    }
}

class NormalModule extends Module {
	constructor({ loaders, resource }) {
        this.loaders = loaders
        // 文件路徑
        this.resource = resource
    }

    // 構建module
	doBuild(options, compilation, resolver, fs, callback) {
        // 上下文,loader裏的this指向這個對象
		const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);
        // 調用loader-runner解析loader
		runLoaders({
            resource: this.resource,
            loaders: this.loaders,
            context: loaderContext,
            readResource: fs.readFile.bind(fs)
        }, (err, result) => {
            if (result) {
                // 緩存
                this.buildInfo.cacheable = result.cacheable;
                // 文件監聽依賴
                this.buildInfo.fileDependencies = new Set(result.fileDependencies);
                // 文件夾監聽依賴
                this.buildInfo.contextDependencies = new Set(result.contextDependencies);
            }
            // 最原始的文件數據buffer
            const resourceBuffer = result.resourceBuffer;
            // loader轉換後的文件內容
            const source = result.result[0];
            // 有傳出sourceMap就在這裏取
            const sourceMap = result.result.length >= 1 ? result.result[1] : null;
            // 其餘的額外內容,如解析出的AST
            const extraInfo = result.result.length >= 2 ? result.result[2] : null;
            this._ast = extraInfo.webpackAST
            return callback();
        });
    }
}
複製代碼

RuleSet配置解析

參數解析應該是這裏面最繁瑣的階段,由於支持的配置多因此解析起來麻煩,基本上都是規範化配置,使最終獲得統一格式,下面分別看看這裏面的關鍵函數:

normalizeRule

用於規範化單個規則,有一大半代碼是在作兼容處理:

normalizeRule(rule, refs, ident) {
    const newRule = {}
    // ...
    if (rule.test || rule.include || rule.exclude) {
        // 檢測同級互斥關係:(rule.test || rule.include || rule.exclude) 和 resource 只能存在一項
        checkResourceSource("test + include + exclude");
        condition = {
            test: rule.test,
            include: rule.include,
            exclude: rule.exclude
        };
        newRule.resource = RuleSet.normalizeCondition(condition);
    }
    // 和(rule.test || rule.include || rule.exclude)作的事情同樣,兼容寫法
    if (rule.resource) {
        checkResourceSource("resource");
        newRule.resource = RuleSet.normalizeCondition(rule.resource);
    }

    if (rule.use) {
        // 檢測同級互斥關係: use, loaders, loader, loader + options/query 配置只能存在一項
        checkUseSource("use");
        // 規範化use配置
        newRule.use = RuleSet.normalizeUse(rule.use, ident);
    }

    // 將有自定義id的規則記錄下來,可用於替換inline-loader的options
    if (Array.isArray(newRule.use)) {
        for (const item of newRule.use) {
            if (item.ident) {
                refs[item.ident] = item.options;
            }
        }
    }
    return newRule;
}
複製代碼

normalizeCondition

用於輸出匹配條件,咱們在規則上配置的test, include, exclude, and, or not將在這裏轉換成匹配函數,用於解析某個資源時匹配是否須要這個loader:

normalizeCondition(condition) {
    // 字符條件串兼容
    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)) return orMatcher(condition.map(c => normalizeCondition(c)));
    // 對象條件兼容
    const matchers = [];
    Object.keys(condition).forEach(key => {
        const value = condition[key];
        switch (key) {
            case "or": case "include": case "test":
                if (value) matchers.push(normalizeCondition(value));
                break;
            case "and":
                if (value) matchers.push(andMatcher(value.map(c => normalizeCondition(c))));
                break;
            case "not": case "exclude":
                if (value) matchers.push(notMatcher(normalizeCondition(value)));
                break;
        }
    });
    return andMatcher(matchers);
}
notMatcher = matcher => str => !matcher(str);
orMatcher = items => str => {
    for (let i = 0; i < items.length; i++) {
        if (items[i](str)) return true;
    }
    return false;
};
andMatcher = items => str => {
    for (let i = 0; i < items.length; i++) {
        if (!items[i](str)) return false;
    }
    return true;
};
複製代碼

inline-loader解析

行內loader指直接在路徑名前添加loader的狀況,如require('!babel-loader!./increment.js')

resolver(data, callback) {
    // ...
    // 將以!=!開頭的資源名去除,獲得帶有inline-loader信息的資源路徑
    // eg: 'haha!=!babel-loader!./increment.js' => 'babel-loader!./increment.js'
    requestWithoutMatchResource = handleMatchResourcde(data.request)

    // 分割出來全部的inline-loader
    let elements = requestWithoutMatchResource
        .replace(/^-?!+/, "")
        .replace(/!!+/g, "!")
        .split("!");
    let resource = elements.pop();
    // 分離inline-loader帶的query參數
    // eg: babel-loader?a=1!./increment.js => [ { loader: 'babel-loader', options: 'a=1' } ]
    elements = elements.map(identToLoaderRequest);

    // 獲取loader的路徑
    this.resolveRequestArray(contextInfo, context, elements, loaderResolver, (err, loaders) => {
        // 若是指定了使用某個定義的options,替換爲自定義選項
        // eg: babel-loader??myOptions./increment.js 將會使用配置中 rules.use.ident=myOptions 的選項
        for (const item of loaders) {
            if (typeof item.options === "string" && item.options[0] === "?") {
                const ident = item.options.substr(1);
                item.options = this.ruleSet.findOptionsByIdent(ident);
                item.ident = ident;
            }
        }
    })
    // ... 解析RuleSet裏的loader
}
複製代碼

全局loader解析

resolver(data, callback) {
    // ...
    // 忽略normalLoader和preLoader
    const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
    // 忽略normalLoader
    const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
    // 忽略normalLoader和preLoader和postLoader
    const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
    // RuleSet匹配loader
    const result = this.ruleSet.exec({
        resource: resource,
        realResource: resource.replace(/\?.*/, ""),
        resourceQuery,
        issuer: contextInfo.issuer,
        compiler: contextInfo.compiler
    });
    // 三種優先級loader在這裏匹配
    const useLoadersPost = [];
    const useLoaders = [];
    const useLoadersPre = [];
    for (const r of result) {
        if (r.type === "use") {
            if (r.enforce === "post" && !noPrePostAutoLoaders) {
                useLoadersPost.push(r.value);
            } else if (r.enforce === "pre" && !noPreAutoLoaders && !noPrePostAutoLoaders) {
                useLoadersPre.push(r.value);
            } else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) {
                useLoaders.push(r.value);
            }
        }
    }
    // 解析三種loader完整路徑
    const postLoaders = this.resolveRequestArray(contextInfo, this.context, useLoadersPost, loaderResolver)
    const defaultLoaders = this.resolveRequestArray(contextInfo, this.context, useLoaders, loaderResolver)
    const preLoaders = this.resolveRequestArray(contextInfo, this.context, useLoadersPre, loaderResolver)
    if (matchResource === undefined) {
        // post -> inline -> normal -> pre
        loaders = postLoaders.concat(inlineLoaders, defaultLoaders, preLoaders);
    } else {
        // post -> normal -> inline -> pre
        loaders = postLoaders.concat(defaultLoaders, inlineLoaders, preLoaders);
    }
    callback(null, {
        loaders,
        resource,
    })
}
複製代碼

RuleSet匹配loader

_run(data, rule, result) {
    // 因爲規則已經在解析配置時轉換成了函數,因此這裏使用函數調用方式判斷是否須要該loader
    if (rule.resource && !rule.resource(data.resource)) return false;
    if (rule.realResource && !rule.realResource(data.realResource)) return false;
    if (data.issuer && rule.issuer && !rule.issuer(data.issuer)) return false;
    if (data.resourceQuery && rule.resourceQuery && !rule.resourceQuery(data.resourceQuery)) return false;
    if (data.compiler && rule.compiler && !rule.compiler(data.compiler)) return false;
    // use的值能夠是對象,數組或函數就是在這裏作的兼容
    // 若是資源匹配上,加到結果集裏
    if (rule.use) {
        const process = use => {
            if (typeof use === "function") {
                process(use(data));
            } else if (Array.isArray(use)) {
                use.forEach(process);
            } else {
                result.push({ type: "use", value: use, enforce: rule.enforce });
            }
        };
        process(rule.use);
    }
    // 循環匹配
    if (rule.rules) {
        for (let i = 0; i < rule.rules.length; i++) {
            this._run(data, rule.rules[i], result);
        }
    }
    // 只要有一個rule匹配上就使用該loader
    if (rule.oneOf) {
        for (let i = 0; i < rule.oneOf.length; i++) {
            if (this._run(data, rule.oneOf[i], result)) break;
        }
    }
    return true;
}
複製代碼

resolveRequestArray

NormalModuleFactory.resolveRequestArray主要處理如下功能:

  • 獲取loader的完整路徑地址
  • 兼容處理不規範的配置寫法
resolveRequestArray(contextInfo, context, array, resolver, callback) {
    // 循環全部loader配置
    asyncLib.map(array, (item, callback) => {
        // 解析loader完整路徑地址
        resolver.resolve(contextInfo, context, item.loader, {}, (err, result) => {
            // 若是沒找到loader是由於省略了-loader,拋出異常提示
            if (err && /^[^/]*$/.test(item.loader) && !/-loader$/.test(item.loader)) {
                return resolver.resolve(contextInfo, context, item.loader + "-loader", {}, err2 => {
                    if (!err2) {
                        err.message = err.message + "loader不支持省略 '-loader' 後綴 \n" +
                            `You need to specify '${item.loader}-loader' instead of '${item.loader}',\n`
                    }
                    callback(err);
                });
            }
            if (err) return callback(err);
            // 格式化並輸出結果
            const optionsOnly = item.options ? { options: item.options } : undefined;
            callback(null, Object.assign({}, item, identToLoaderRequest(result), optionsOnly))
        });
    }, callback)
}
複製代碼

輸出結果

result[source, sourceMap, extraInfo]

  • source: 是通過loader處理後的內容;
  • sourceMap: 若是有sourceMap,能夠在這裏獲取;
  • extraInfo: 這裏放額外的輸出內容,好比AST等;

resourceBuffer

這是最原始文件的資源,沒有通過loader

fileDependencies / contextDependencies

若是文件須要foo-loader處理,那麼foo-loader會默認將該文件添加到本身的依賴上,開啓文件監聽後若是文件改變,那麼會從新執行foo-loader

默認狀況下loader只會添加匹配到的文件做爲依賴,若是在loader執行過程當中,須要用到其餘文件如data.txt,且更新後要從新輸出結果,那麼可使用this.addDependency來將其添加爲依賴項,這樣data.txt更新後會從新執行loader輸出。

cacheable

若是一個loader的輸入和相關依賴沒變化時輸出結果不變,那麼這個loader應該設置爲能夠緩存,在解析完全部loader後,會將結果緩存下來。

在監聽文件狀況下若是輸出結果沒變,文件不會從新輸出。

默認狀況下loader會開啓緩存,在loader中可使用this.cacheable(false)關閉緩存。

優化

loader是打包耗時的大塊頭,好比babel-loader在執行時能明顯感受到很是慢,因此瞭解了loader的基本原理,咱們就能夠針對性的對咱們的項目作些優化:

縮小loader做用範圍

最主要的優化仍是在排除掉沒必要要的解析,用好exclude等選項基本上能知足大多數場景。

loader緩存

有些loader自己提供了緩存的功能,好比babel-loadercacheDirectory等,使用loader時須要咱們熟悉它們的配置。

預編譯

DllPlugin用於將代碼預先打包抽離出來,使用時Webpack將不會從新編譯直接引用。對於咱們項目中的代碼,不能用排除方法去除loader時,能夠考慮先將比較穩定的代碼預先編譯一次,下次使用就能夠不須要通過loader直接使用啦。

多進程編譯

thread-loaderHappyPack可讓Webpack使用多個進程同時對文件執行loader。咱們知道node是單線程模型,用一個線程處理固然很慢,若是能發揮多核CPU並行優點同時編譯的話,編譯速度能快很多。使用時要注意它們和loader的兼容性。

參考資料

loader-runner

webpack rule 文檔

issure的解析

DllPlugin

thread-loader

HappyPack

相關文章
相關標籤/搜索