Webpack源碼分析 - 路徑解析庫(enhanced-resolve)

路徑解析

Webpack封裝了一套解析庫enhanced-resolve專門用於解析路徑,例如咱們寫了require('./index'),Webpack在打包時就會用它來解析出./index的完整路徑。node

咱們能夠看到他的官方介紹:webpack

Offers an async require.resolve function. It's highly configurable. Features:
- plug in system
- provide a custom filesystem
- sync and async node.js filesystems includedgit

能夠看到官方定義他是一個可配置化的異步require.resolve。若是不瞭解reqire.resolve的同窗能夠先看看require.resolve是什麼github

reqire.resolve的不足

能夠看到本質上他們作的事情都是同樣的,只是在解析路徑的規則上enhanced-resolve提供了更強的擴展性,以知足Webpack對解析文件的需求。web

因爲require.resolve只能用在node的環境下,因此在設計時require.resolve只配置了node相關的依賴規則,而Webpack面對的環境多種多樣,如下列舉一些Webpack作的加強內容:json

擴展名查詢配置

例如解析./index時因爲沒有提供擴展名,因此它們都會去嘗試遍歷可能會有的文件,node會去嘗試路徑下是否有.js .json .node文件,而Webpack須要面對的文件擴展不止這三種,可經過配置擴展。segmentfault

文件夾查詢

require.resolve只會去解析文件的完整路徑,可是enhanced-resolve既能夠查詢文件也能夠查詢文件夾。這個功能在Webpack中很是有用,能夠經過它導入一個文件夾下的多個文件,只須要配置resolveToContext: true,就會嘗試解析目錄的完整路徑。緩存

返回結果

在解析成功時,require.resolve的返回值只有一個完整路徑,enhanced-resolve的返回值還包含了描述文件等較爲豐富的數據。bash

別名配置

node下使用別名是挺麻煩的事,可是enhanced-resolve很是好地支持了這個功能,即能讓咱們代碼看上去更整潔,配合緩存還能提升解析效率。app

使用方式

安裝環境

能夠到github: enhanced-resolve下載一份源碼,本次咱們使用4.1.1版本解析,下載完成後切換到對應分支,yarn add下載依賴模塊。

在項目的package.json裏,咱們能夠找到項目的導出文件是"main": "lib/node.js",能夠從這個文件入手開始探究源碼。

使用

在主目錄新建index.js,這裏演示了經常使用的操做:

var resolve = require("./lib/node");
// 解析相對目錄下index.js文件
resolve(__dirname, './index.js', (err, p, result) => {
    // p = /Users/enhanced-resolve/index.js
    console.log(err, p, result)
})
// 解析模塊diff導出文件
resolve(__dirname, 'diff', (err, p, result) => {
    // p = /Users/enhanced-resolve/node_modules/diff/diff.js
    console.log(err, p, result)
})
// 解析絕對目錄下的index.js文件
resolve(__dirname, '/Users/enhanced-resolve/index.js', (err, p, result) => {
    // p = /Users/enhanced-resolve/index.js
    console.log(err, p, result)
})
// 解析相對路徑下的目錄
resolve.context(__dirname, './', (err, p, result) => {
    // p = /Users/enhanced-resolve
    console.log(err, p, result)
})
複製代碼

打印調試信息

因爲項目使用Tapable來組織流程,調試起來比較累,好在它提供的調試打印信息還算豐富,咱們在Resolver.js第176行加上配置log: console.log,能夠打印調試信息到控制檯:

return this.doResolve(
        this.hooks.resolve,
        obj,
        message,
        {
            missing: resolveContext.missing,
            stack: resolveContext.stack,
+           log: console.log,
        },
複製代碼

核心源碼分析

node.js

這個文件爲咱們提供了開箱即用的解析函數,根據不一樣場景預約義了默認參數,最終經過ResolverFactory.createResolver建立並執行路徑解析,主要有如下三種場景:

  • 文件路徑解析:默認文件後綴爲[".js", ".json", ".node"]
  • 文件夾路徑解析:只判斷文件夾是否存在
  • Loader路徑解析:專門用於Webpack的Loader文件路徑解析

另外每種場景都提供同步和異步調用方式,且默認文件操做經過CachedInputFileSystem包裝提供緩存功能。

ResolverFactory.js

主要作了兩件事,一是參數解析,二是初始化插件,首先會根據參數來將須要用到的插件建立出來,調用他們的apply方法來初始化插件。

// ...
plugins = []
plugins.push(new ParsePlugin("resolve", "parsed-resolve"));
//...
plugins.push(new ResultPlugin(resolver.hooks.resolved));
plugins.forEach(plugin => {
    plugin.apply(resolver);
});
複製代碼

enhanced-resolve經過Tapable將全部插件串聯起來,每一個插件負責一件事情,經過事件流的方式傳遞每一個插件的解析結果。因此只要看懂了插件之間的流轉過程,就能明白它的工做原理。

Resolver.js

這裏是整個解析流程的核心,它繼承了Tapable類,下面咱們重點分析裏面的方法。

class Resolver extends Tapable {
	constructor(fileSystem) {
		super();
		this.fileSystem = fileSystem;
		this.hooks = {
			resolve: new AsyncSeriesBailHook(["request", "resolveContext"])
		};
    }
}
複製代碼

ensureHook()

這個函數主要用與動態添加鉤子,在構造函數中只定義了3個鉤子,可是實際用到的不止這麼少,因此在使用某個鉤子前,都會調用這個方法保證鉤子已經定義。這裏建立的鉤子都是AsyncSeriesBailHook類型,異步,串行執行,獲取第一個返回值不爲空的結果。

若是name的前綴帶有beforeafter,則會調整調用優先級

ensureHook(name) {
    // ...
    const hook = this.hooks[name];
    if (!hook) {
        return this.hooks[name] =
            new AsyncSeriesBailHook(["request", "resolveContext"])
    }
    return hook;
}
複製代碼

例若有兩個插件掛在一個鉤子上,此時調用鉤子hook.callAsync時,由於優先級高會先進入plugin-a的回調,若是返回值是空繼續執行after-plugin-a的回調,不然直接執行hook.callAsync的回調:

this.ensureHook('myhook').tapAsync('after-plugin-a',() => {})
this.ensureHook('myhook').tapAsync('plugin-a',(request, resolveContext, callback) => {
    callback(null, 'ok from plugin-a')
})
this.hooks.myhook.callAsync(request, resolveContext, (err, result) => {
    console.log(result) // ok from plugin-a
})
複製代碼

doResolve()

這個函數是兩個插件的連接點,核心很簡單就是直接調用鉤子執行下一個流程而已,源碼中還有一大堆代碼主要是用來記錄日誌。

hook.callAsync負責真正的調用,會開始執行掛在這個鉤子上的事件。在鉤子裏又會調用doResolve執行下一個鉤子。

doResolve(hook, request, message, resolveContext, callback) {
    // ...
    return hook.callAsync(request, innerContext, (err, result) => {
        if (err) return callback(err);
        if (result) return callback(null, result);
        callback();
    });
}
複製代碼

XXXPlugin

在建立插件時通常會傳入sourcetarget兩個參數:

  • source:插件拿到Resolver.hooks['source']鉤子,並調tap或tapAsync添加處理函數事件。當解析器接收到了source事件時,會執行註冊的處理函數;
  • target:在處理完畢後,調用doResolve觸發一個target事件,交由下一個監聽target事件的插件處理。
class ParsePlugin {
	constructor(source, target) {
		this.source = source;
		this.target = target;
	}
	apply(resolver) {
		const target = resolver.ensureHook(this.target);
		resolver.getHook(this.source)
			.tapAsync("ParsePlugin", (request, resolveContext, callback) => {
                // ...
				resolver.doResolve(target, obj, null, resolveContext, callback);
			});
	}
};
複製代碼

有了註冊事件tapAsync和觸發事件doResolve,各個插件就能夠像積木同樣連接起來

實戰舉例

咱們經過分析一個解析目錄是否存在的流程,來將上面內容串起來:

// index.js
resolve.context(__dirname, './', (err, p, result) => {
    // p = /Users/enhanced-resolve
    console.log(err, p, result)
})
複製代碼

打印出來的調試結果以下:

resolve './' in '/Users/enhanced-resolve'  
Parsed request is a directory  
using description file: /Users/enhanced-resolve/package.json (relative path: .)  
using description file: /Users/enhanced-resolve/package.json (relative path: .)  
as directory  
existing directory  
reporting result /Users/enhanced-resolve  
複製代碼

根據配置一共註冊瞭如下插件,下面咱們逐個分析這裏用到的插件,插件流轉路徑如圖示:

ParsePlugin

用於預解析查詢參數供後續插件使用:

  • 解析查詢路徑中的query參數,如require('./index?id=1'),這是webpack特有的語法,見文檔__resourceQuery;
  • 判斷查詢路徑是不是模塊
  • 判斷查詢路徑是不是目錄
parse(identifier) {
    const idxQuery = identifier.indexOf("?");
    const part = {
        request: identifier.slice(0, idxQuery),
        query: identifier.slice(idxQuery),
        file: false
    };
    part.module = this.isModule(part.request);
    part.directory = this.isDirectory(part.request);
    return part;
}
複製代碼

DescriptionFilePlugin

用於獲取描述文件路徑,會在directory下搜索是否有package.json文件,若是該目錄沒有,就去上一級目錄下查找,並計算出相對與directory的路徑,下面是簡化版的僞代碼:

function loadDescriptionFile(directory, callback) {
    const descriptionFiles = ['package.json']
    let json
    let descriptionFilePath
    for(let i = 0; i < descriptionFiles.length; i++) {
        descriptionFilePath = path.join(directory, descriptionFiles[i])
        if(json = fileSystem.readJson(descriptionFilePath)) break
    }
    if(json === null) {
        directory = cdUp(directory)
        if (!directory) {
            return callback('err')
        } else {
            loadDescriptionFile(directory, callback);
            return
        }
    }
    const relativePath = "." + request.path.substr(result.directory.length).replace(/\\/g, "/");
    callback(null, {
        content: json,
        directory: directory,
        path: descriptionFilePath,
        relativePath,
    });
}
function cdUp(directory) {
	if (directory === "/") return null;
	const i = directory.lastIndexOf("/"),
		j = directory.lastIndexOf("\\");
	const p = i < 0 ? j : j < 0 ? i : i < j ? j : i;
	if (p < 0) return null;
	return directory.substr(0, p || 1);
}
複製代碼

ModuleKindPlugin

若是ParsePlugin解析出來是模塊路徑,就引導至raw-module鉤子,不然往下繼續執行JoinRequestPlugin

(request, resolveContext, callback) => {
    if (!request.module) return callback();
    const obj = Object.assign({}, request);
    delete obj.module;
    resolver.doResolve(target, obj, "resolve as module", resolveContext, callback);
});
複製代碼

JoinRequestPlugin

這裏將會輸出兩個路徑供後續查找:

  • path:指要查找的文件完整路徑
  • relativePath:指描述文件路徑相對於待查找文件的路徑

關鍵輸入參數:

  • request.path:在哪一個路徑下查找
  • request.request:查找的文件
  • request.relativePath:描述文件路徑相對於request.path的路徑
(request, resolveContext, callback) => {
    const obj = Object.assign({}, request, {
        path: resolver.join(request.path, request.request),
        relativePath: request.relativePath &&
            resolver.join(request.relativePath, request.request),
        request: undefined
    });
    resolver.doResolve(target, obj, null, resolveContext, callback);
});
複製代碼

FileKindPlugin

若是ParsePlugin解析出來是路徑,就往下繼續執行TryNextPlugin,不然就引導至described-relative鉤子

(request, resolveContext, callback) => {
    if (request.directory) return callback();
    const obj = Object.assign({}, request);
    delete obj.directory;
    resolver.doResolve(target, obj, null, resolveContext, callback)
複製代碼

TryNextPlugin

直接引導到執行下一個插件

(request, resolveContext, callback) => {
    resolver.doResolve(target, obj, message, resolveContext, callback)
複製代碼

DirectoryExistsPlugin

判斷文件夾是否存在

(request, resolveContext, callback) => {
    const fs = resolver.fileSystem;
    const directory = request.path;
    fs.stat(directory, (err, stat) => {
        if (err || !stat || !stat.isDirectory()) {
            return callback();
        }
        resolver.doResolve(target, obj, "existing directory", resolveContext, callback)
    });
複製代碼

NextPlugin

直接引導到執行下一個插件,和TryNextPlugin區別在於有沒有攜帶message日誌

(request, resolveContext, callback) => {
    resolver.doResolve(target, obj, null, resolveContext, callback)
複製代碼

ResultPlugin

直接返回解析結果

(request, resolverContext, callback) => {
    // ...
    callback(null, request);
}
複製代碼

參考資料

require.resolve源碼分析

使用Node.js爲require設置別名

相關文章
相關標籤/搜索