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);
}
复制代码