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
能夠看到本質上他們作的事情都是同樣的,只是在解析路徑的規則上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,
},
複製代碼
這個文件爲咱們提供了開箱即用的解析函數,根據不一樣場景預約義了默認參數,最終經過ResolverFactory.createResolver
建立並執行路徑解析,主要有如下三種場景:
另外每種場景都提供同步和異步調用方式,且默認文件操做經過CachedInputFileSystem
包裝提供緩存功能。
主要作了兩件事,一是參數解析,二是初始化插件,首先會根據參數來將須要用到的插件建立出來,調用他們的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
將全部插件串聯起來,每一個插件負責一件事情,經過事件流
的方式傳遞每一個插件的解析結果。因此只要看懂了插件之間的流轉過程,就能明白它的工做原理。
這裏是整個解析流程的核心,它繼承了Tapable
類,下面咱們重點分析裏面的方法。
class Resolver extends Tapable {
constructor(fileSystem) {
super();
this.fileSystem = fileSystem;
this.hooks = {
resolve: new AsyncSeriesBailHook(["request", "resolveContext"])
};
}
}
複製代碼
這個函數主要用與動態添加鉤子,在構造函數中只定義了3個鉤子,可是實際用到的不止這麼少,因此在使用某個鉤子前,都會調用這個方法保證鉤子已經定義。這裏建立的鉤子都是AsyncSeriesBailHook
類型,異步,串行執行,獲取第一個返回值不爲空的結果。
若是name
的前綴帶有before
或after
,則會調整調用優先級
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
})
複製代碼
這個函數是兩個插件的連接點,核心很簡單就是直接調用鉤子執行下一個流程而已,源碼中還有一大堆代碼主要是用來記錄日誌。
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();
});
}
複製代碼
在建立插件時通常會傳入source
和target
兩個參數:
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
複製代碼
根據配置一共註冊瞭如下插件,下面咱們逐個分析這裏用到的插件,插件流轉路徑如圖示:
用於預解析查詢參數供後續插件使用:
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;
}
複製代碼
用於獲取描述文件路徑,會在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);
}
複製代碼
若是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);
});
複製代碼
這裏將會輸出兩個路徑供後續查找:
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);
});
複製代碼
若是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)
複製代碼
直接引導到執行下一個插件
(request, resolveContext, callback) => {
resolver.doResolve(target, obj, message, resolveContext, callback)
複製代碼
判斷文件夾是否存在
(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)
});
複製代碼
直接引導到執行下一個插件,和TryNextPlugin
區別在於有沒有攜帶message
日誌
(request, resolveContext, callback) => {
resolver.doResolve(target, obj, null, resolveContext, callback)
複製代碼
直接返回解析結果
(request, resolverContext, callback) => {
// ...
callback(null, request);
}
複製代碼