Appearance
resolve 的流程
在 webpack 中一切皆模块,在模块中我们可以通过模块化的方案去导出或者引入其他的模块。其中在 webpack 对于引入其他的模块提供了好几种方式
import "./a.js"
import "./a"
import "@/a.js"(@ 符号通过 webpack 配置中 alias 设置)
import "lodash/merge"
那么 node 如何知道上面 @ 、a、lodash 这三种分别对应什么文件夹、或者什么文件? 这就是 resolve 处理的部分。
当然其不仅仅正对于我们代码中的模块,还有配置中的 loader,只要涉及到文件的路径那么就依赖这个 resolve 去处理
webpack 官方配置
js
let config = {
resolve: {
extensions: ["*", ".js", ".json"],
alias: {
"@": path.resolve(__dirname, "src/"),
vue$: path.resolve(__dirname, "src/"),
},
},
// 这组选项与上面的 resolve 对象的属性集合相同,但仅用于解析 webpack 的 loader 包。
resolveLoader: {
modules: ["node_modules"],
extensions: [".js", ".json"],
mainFields: ["loader", "main"],
},
}
源码分析
当然其第一步肯定也是 webpack 中的第一步,将用户的配置信息与 webpack 的默认配置进行合并
js
// WebpackOptionsDefaulter.js
class WebpackOptionsDefaulter extends OptionsDefaulter {
constructor() {
super()
this.set("resolve", "call", value => Object.assign({}, value))
this.set("resolve.unsafeCache", true)
this.set("resolve.modules", ["node_modules"])
this.set("resolve.extensions", [".wasm", ".mjs", ".js", ".json"])
this.set("resolve.mainFiles", ["index"])
this.set("resolve.aliasFields", "make", options => {
if (options.target === "web" || options.target === "webworker" || options.target === "electron-renderer") {
return ["browser"]
} else {
return []
}
})
this.set("resolve.mainFields", "make", options => {
if (options.target === "web" || options.target === "webworker" || options.target === "electron-renderer") {
return ["browser", "module", "main"]
} else {
return ["module", "main"]
}
})
this.set("resolve.cacheWithContext", "make", options => {
return Array.isArray(options.resolve.plugins) && options.resolve.plugins.length > 0
})
this.set("resolveLoader", "call", value => Object.assign({}, value))
this.set("resolveLoader.unsafeCache", true)
this.set("resolveLoader.mainFields", ["loader", "main"])
this.set("resolveLoader.extensions", [".js", ".json"])
this.set("resolveLoader.mainFiles", ["index"])
this.set("resolveLoader.cacheWithContext", "make", options => {
return Array.isArray(options.resolveLoader.plugins) && options.resolveLoader.plugins.length > 0
})
}
}
可见如果我们不设置 resolve 或者 resolveLoader 其默认的配置信息如下:
js
let config = {
resolve: {
extensions: [".wasm", ".mjs", ".js", ".json"],
unsafeCache: true,
modules: ["node_modules"],
mainFiles: ["index"],
aliasFields: ["browser"],
mainFields: ["browser", "module", "main"],
cacheWithContext: false,
},
// 这组选项与上面的 resolve 对象的属性集合相同,但仅用于解析 webpack 的 loader 包。
resolveLoader: {
unsafeCache: true,
mainFields: ["loader", "main"],
extensions: [".js", ".json"],
mainFiles: ["index"],
cacheWithContext: false,
},
}
然后我们在实例化 compiler 的时候也实例化了一个 ResolverFactory,其代码如下
js
class Compiler extends Tapable {
constructor(context) {
// 实例化 resolve工厂函数
this.resolverFactory = new ResolverFactory()
// TODO remove in webpack 5
this.resolvers = {
normal: {
/**
* 生成 调用 compiler.resolvers.normal.plugin() 调用就显示提示
* 其实就是 不让使用 compiler.resolvers.normal.plugin() 而是应该使用 compiler.resolverFactory.plugin("resolver normal", resolver => {\n resolver.plugin()})去定义,
* 其本质还是在调用 compiler.resolvers.normal.plugin(hook , fn)的时候去执行 compiler.resolverFactory.plugin("resolver normal", resolver => { resolver.plugin(hook, fn)})
* @type {[type]}
*/
plugins: util.deprecate((hook, fn) => {
// 触发我们在 resolverFactory constructor 中定义的 this._pluginCompat.tap("ResolverFactory", options => {})
this.resolverFactory.plugin("resolver normal", resolver => {
// 然后触发 resolverFactory 工厂函数中 resoler实例对象的 this._pluginCompat.tap("Resolver: before/after", options => {})
resolver.plugin(hook, fn)
})
}, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n resolver.plugin(/* … */);\n}); instead.'),
apply: util.deprecate((...args) => {
this.resolverFactory.plugin("resolver normal", resolver => {
resolver.apply(...args)
})
}, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n resolver.apply(/* … */);\n}); instead.'),
},
loader: {
plugins: util.deprecate((hook, fn) => {
this.resolverFactory.plugin("resolver loader", resolver => {
resolver.plugin(hook, fn)
})
}, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n resolver.plugin(/* … */);\n}); instead.'),
apply: util.deprecate((...args) => {
this.resolverFactory.plugin("resolver loader", resolver => {
resolver.apply(...args)
})
}, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n resolver.apply(/* … */);\n}); instead.'),
},
context: {
plugins: util.deprecate((hook, fn) => {
this.resolverFactory.plugin("resolver context", resolver => {
resolver.plugin(hook, fn)
})
}, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n resolver.plugin(/* … */);\n}); instead.'),
apply: util.deprecate((...args) => {
this.resolverFactory.plugin("resolver context", resolver => {
resolver.apply(...args)
})
}, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n resolver.apply(/* … */);\n}); instead.'),
},
}
}
}
对于上面的源码我们需要首先了解 util.deprecate 的作用的作用和Tapable 中 pluginCompat 的使用流程。
那么上面 compiler.resolvers.normal.plugins()的作用是什么?
我们以一个例子作为参考:
js
compiler.resolvers.normal.plugins("resolve", fn)
其触发 this.resolverFactory.plugin('resolver normal', resolver => { resolver.plugin(hook, fn); });,然后我们看 resolverFactory 实例对象中的定义的 resolverFactory._pluginCompat()
。
在resolverFactory._pluginCompat()
通过判断事件流的名称是否以resolve-options、resolver开头的来决定其事件添加在 resolverFactory.hooks`[resolver|resolveOptions]上,并且返回 ture 来阻止 tapable 内置的 Tapable camelCase , "Tapable this.hooks" 两个事件。
然后在其他的地方 **resolverFactory.hooks[resolver|resolveOptions].call()**执行的时候,其也会触发前面在 compiler 上定义的 resolver => { resolver.plugin(hook, fn); }
,这时候又上面类型的流程。
js
module.exports = class ResolverFactory extends Tapable {
constructor() {
super()
this.hooks = {
resolveOptions: new HookMap(() => new SyncWaterfallHook(["resolveOptions"])),
resolver: new HookMap(() => new SyncHook(["resolver", "resolveOptions"])),
}
this._pluginCompat.tap("ResolverFactory", options => {
// 在 compiler 中定义了 this.resolverFactory.plugin("resolver normal", resolver => {}) 的函数,其执行的时候 触发此事件
let match
match = /^resolve-options (.+)$/.exec(options.name)
if (match) {
this.hooks.resolveOptions.for(match[1]).tap(options.fn.name || "unnamed compat plugin", options.fn)
return true
}
// "resolver normal" 匹配成功
match = /^resolver (.+)$/.exec(options.name)
if (match) {
// 那么在 ResolverFactory.hooks.resolver上就会添加一个 key为 normal的事件流 事件的名称为 "unnamed compat plugin"
// options.fn 为上面的 resolver => {}
this.hooks.resolver.for(match[1]).tap(options.fn.name || "unnamed compat plugin", options.fn)
// 不执行 tapable内置的 Tapable camelCase , "Tapable this.hooks" 两个事件
return true
}
})
}
}
执行了 resolver.plugin(hook, fn)
源码如下:
js
class Resolver extends Tapable {
constructor(fileSystem) {
super()
this.fileSystem = fileSystem
this.hooks = {
// 生成 resolver.hooks.resolveStep : new SyncHook(["hook", "request"]) 只是修改 hook.name === 'resolveStep'
resolveStep: withName("resolveStep", new SyncHook(["hook", "request"])),
noResolve: withName("noResolve", new SyncHook(["request", "error"])),
resolve: withName("resolve", new AsyncSeriesBailHook(["request", "resolveContext"])),
result: new AsyncSeriesHook(["result", "resolveContext"]),
}
/*
处理通过 resolver.plugin()的方式添加的事件
1. 通过添加的目标钩子事件流名称前加 before- after- 去决定当前事件在事件流中执行的顺序
*/
this._pluginCompat.tap("Resolver: before/after", options => {
// 处理 resolver.plugin("before-no-resolve" , fn)方式添加到 noResolve钩子事件流上的事件
// 因为其事件流的名称是 resolveStep、noResolve、resolve等,那么 before-no-resolve就找不到noResolve
if (/^before-/.test(options.name)) {
// 修改目标钩子事件流的名称 before-no-resolve -> no-resolve
options.name = options.name.substr(7)
// 将事件流的权重设置为 负数, 较高
options.stage = -10
} else if (/^after-/.test(options.name)) {
// 一样的方式 修改目标钩子事件流的名称 after-no-resolve -> no-resolve
options.name = options.name.substr(6)
// 将事件流的权重设置为 10, 较低
options.stage = 10
}
})
/*
*/
this._pluginCompat.tap("Resolver: step hooks", options => {
// 获取目标钩子事件流的名称,已经经过上面 "Resolver: before/after"事件处理 ,所以当前 options.name 为 no-resolve
const name = options.name
// 处理 resolveStep no-resolve 两种类型的钩子事件流
const stepHook = !/^resolve(-s|S)tep$|^no(-r|R)esolve$/.test(name)
if (stepHook) {
// 事件通过异步的方式添加
options.async = true
// 获取 name 类型的钩子事件流
this.ensureHook(name)
// 获取事件回调函数 fn
const fn = options.fn
// 重写事件的回调函数 TODO: 确定作用和意义
options.fn = (request, resolverContext, callback) => {
const innerCallback = (err, result) => {
if (err) return callback(err)
if (result !== undefined) return callback(null, result)
callback()
}
for (const key in resolverContext) {
innerCallback[key] = resolverContext[key]
}
fn.call(this, request, innerCallback)
}
}
})
}
}
上面我们执行的compiler.resolvers.normal.plugins('resolve', fn);,那么这时候 hook === resolve,然后就会经历上面的 resover 实例对象中的 name 中存在 before 或者 after 设置事件的权重不同、处理 resolveStep no-resolve 两种类型的钩子事件流、Tapable 内置的 name 的驼峰命名转换、Tapable 内置的创建或者返回 resolver.hooks.resolve 的钩子事件流四个流程,然后添加到对应的事件流中。那么我们就可以通过 **resolvers.hooks.resolve.call()**去执行 resolve 事件流。
然后我们进过 compiler -> compilation -> nomalModuleFactory.create -> NormalModuleFactory.resolve 的流程
js
class NormalModuleFactory extends Tapable {
constructor(context, resolverFactory, options) {
super()
this.resolverFactory = resolverFactory
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
let resolver = this.hooks.resolver.call(null)
// Ignored
if (!resolver) return callback()
resolver(result, (err, data) => {})
})
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
const contextInfo = data.contextInfo
const context = data.context
const request = data.request
const loaderResolver = this.getResolver("loader")
const normalResolver = this.getResolver("normal", data.resolveOptions)
let matchResource = undefined
let requestWithoutMatchResource = request
const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request)
if (matchResourceMatch) {
matchResource = matchResourceMatch[1]
if (/^\.\.?\//.test(matchResource)) {
matchResource = path.join(context, matchResource)
}
requestWithoutMatchResource = request.substr(matchResourceMatch[0].length)
}
const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!")
const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!")
const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!")
let elements = requestWithoutMatchResource.replace(/^-?!+/, "").replace(/!!+/g, "!").split("!")
let resource = elements.pop()
elements = elements.map(identToLoaderRequest)
asyncLib.parallel(
[
callback => this.resolveRequestArray(contextInfo, context, elements, loaderResolver, callback),
callback => {
if (resource === "" || resource[0] === "?") {
return callback(null, {
resource,
})
}
normalResolver.resolve(contextInfo, context, resource, {}, (err, resource, resourceResolveData) => {
if (err) return callback(err)
callback(null, {
resourceResolveData,
resource,
})
})
},
],
(err, results) => {
if (err) return callback(err)
let loaders = results[0]
const resourceResolveData = results[1].resourceResolveData
resource = results[1].resource
// translate option idents
try {
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
}
}
} catch (e) {
return callback(e)
}
if (resource === false) {
// ignored
return callback(
null,
new RawModule("/* (ignored) */", `ignored ${context} ${request}`, `${request} (ignored)`)
)
}
const userRequest =
(matchResource !== undefined ? `${matchResource}!=!` : "") +
loaders.map(loaderToIdent).concat([resource]).join("!")
let resourcePath = matchResource !== undefined ? matchResource : resource
let resourceQuery = ""
const queryIndex = resourcePath.indexOf("?")
if (queryIndex >= 0) {
resourceQuery = resourcePath.substr(queryIndex)
resourcePath = resourcePath.substr(0, queryIndex)
}
const result = this.ruleSet.exec({
resource: resourcePath,
realResource: matchResource !== undefined ? resource.replace(/\?.*/, "") : resourcePath,
resourceQuery,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler,
})
const settings = {}
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)
}
} else if (
typeof r.value === "object" &&
r.value !== null &&
typeof settings[r.type] === "object" &&
settings[r.type] !== null
) {
settings[r.type] = cachedCleverMerge(settings[r.type], r.value)
} else {
settings[r.type] = r.value
}
}
asyncLib.parallel(
[
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, loaderResolver),
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, loaderResolver),
this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, loaderResolver),
],
(err, results) => {
if (err) return callback(err)
if (matchResource === undefined) {
loaders = results[0].concat(loaders, results[1], results[2])
} else {
loaders = results[0].concat(results[1], loaders, results[2])
}
process.nextTick(() => {
const type = settings.type
const resolveOptions = settings.resolve
callback(null, {
context: context,
request: loaders.map(loaderToIdent).concat([resource]).join("!"),
dependencies: data.dependencies,
userRequest,
rawRequest: request,
loaders,
resource,
matchResource,
resourceResolveData,
settings,
type,
parser: this.getParser(type, settings.parser),
generator: this.getGenerator(type, settings.generator),
resolveOptions,
})
})
}
)
}
)
})
}
}
在其中有几步比较重要
this.getResolver("loader")
作用
流程
const loaderResolver = this.getResolver("loader");
其访问的是
js
normalModuleFactory.getResolver = (type, resolveOptions) => {
return this.resolverFactory.get(type, resolveOptions || EMPTY_RESOLVE_OPTIONS)
}
此处 type === 'normal', resolveOptions === {},然后其访问 resolverFactory 工厂的 get 方法
我们看过程也是调用 this.resolverFactory.get( 'loader' , resolveOptions || EMPTY_RESOLVE_OPTIONS);
js
/*
resolve(解析)
配置如何解析模块
resolve:{
alias : "" , // 创建 import 或 require 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块
aliasFields : '' // 指定一个字段,例如 browser,根据此规范进行解析。默认:
extensions: [".js", ".json"] , // 配置解析文件的拓展名
enforceExtension :
}
*/
module.exports = class ResolverFactory extends Tapable {
constructor() {
super()
this.cache2 = new Map()
}
/**
* 获取指定类型的的resolver 如果没有就创建这个类型的 resolver
* [get description]
* @param {[type]} type resolver 的type
* @param {[type]} resolveOptions [description]
* @return {[type]} [description]
*/
get(type, resolveOptions) {
// resolve 的options
resolveOptions = resolveOptions || EMTPY_RESOLVE_OPTIONS
// 生成当前type的id 不能只按照 type 去处理
const ident = `${type}|${JSON.stringify(resolveOptions)}`
const resolver = this.cache2.get(ident)
if (resolver) return resolver
const newResolver = this._create(type, resolveOptions)
this.cache2.set(ident, newResolver)
return newResolver
}
/**
* 创建一个 resolve 实例对象
*
* {
* fileSystem:
* withOptions
* hooks
* }
* @param {[type]} type [description]
* @param {[type]} resolveOptions [description]
* @return {[type]} [description]
*/
_create(type, resolveOptions) {
// 生成一个新的 options
const originalResolveOptions = Object.assign({}, resolveOptions)
// 按照 loader normal context 三种类型获取对于的 resolveOptions
resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions)
// 创建 resolver
const resolver = Factory.createResolver(resolveOptions)
if (!resolver) {
throw new Error("No resolver created")
}
/** @type {Map<Object, Resolver>} */
const childCache = new Map()
resolver.withOptions = options => {
const cacheEntry = childCache.get(options)
if (cacheEntry !== undefined) return cacheEntry
const mergedOptions = cachedCleverMerge(originalResolveOptions, options)
const resolver = this.get(type, mergedOptions)
childCache.set(options, resolver)
return resolver
}
//
this.hooks.resolver.for(type).call(resolver, resolveOptions)
return resolver
}
}
从中看出缓存(cache2)中没有 type 为 'loader' , ident : 'loader|{}'的缓存,所以需要去通过this._create(type, resolveOptions)
去创建这一类型的 resolver,具体可以看 enhanced-resolve 如果通过 ResolverFactory 去创建一个 resolve 的过程。
在 resolveFactory._create()
的过程中其存在两个比较特殊的过程
- resolver.withOptions
js
resolver.withOptions = options => {
const cacheEntry = childCache.get(options)
if (cacheEntry !== undefined) return cacheEntry
const mergedOptions = cachedCleverMerge(originalResolveOptions, options)
const resolver = this.get(type, mergedOptions)
childCache.set(options, resolver)
return resolver
}
this.hooks.resolver.for(type).call(resolver, resolveOptions);
在上面的例子中 type 为 normal,那么其在什么时候在 compiler.hooks.resolver 添加了 normal 类型的事件流?
这个还得回到我们的 **WebpackOptionsApply().process()**的时候,其根据 webpack 的 options 添加了很多钩子函数,并且通过 plugins.apply()去应用这些钩子函数,其中就有如同:
发现其虽然有 38 个之多但是基本上都是NodeSourcePlugin的事件,那么我们看一下NodeSourcePlugin是干嘛的?
NodeSourcePlugin
js
/*
我们先看一下 nodeLibsBrowser的内容是什么?
os:"d:\guzhangh\webpack\webpack\node_modules\os-browserify\browser.js"
path:"d:\guzhangh\webpack\webpack\node_modules\path-browserify\index.js"
process:"d:\guzhangh\webpack\webpack\node_modules\process\browser.js"
punycode:"d:\guzhangh\webpack\webpack\node_modules\node-libs-browser\node_modules\punycode\punycode.js"
querystring:"d:\guzhangh\webpack\webpack\node_modules\querystring-es3\index.js"
readline:null
repl:null
stream:"d:\guzhangh\webpack\webpack\node_modules\stream-browserify\index.js"
string_decoder:"d:\guzhangh\webpack\webpack\node_modules\string_decoder\lib\string_decoder.js"
sys:"d:\guzhangh\webpack\webpack\node_modules\node-libs-browser\node_modules\util\util.js"
timers:"d:\guzhangh\webpack\webpack\node_modules\timers-browserify\main.js"
tls:null
tty:"d:\guzhangh\webpack\webpack\node_modules\tty-browserify\index.js"
url:"d:\guzhangh\webpack\webpack\node_modules\url\url.js"
util:"d:\guzhangh\webpack\webpack\node_modules\node-libs-browser\node_modules\util\util.js"
vm:"d:\guzhangh\webpack\webpack\node_modules\vm-browserify\index.js"
其都是一些我们可以在浏览器访问的node提供的属性和其源码地址
*/
compiler.hooks.afterResolvers.tap("NodeSourcePlugin", compiler => {
for (const lib of Object.keys(nodeLibsBrowser)) {
if (options[lib] !== false) {
// 那么这时候我们就可以添加很多在 compilercompiler.resolverFactory.hooks.resolver.mormal上的事件,
// 其内容是支持浏览器访问使用的Node的lib工作方法
// 如上面的 asset
compiler.resolverFactory.hooks.resolver.for("normal").tap("NodeSourcePlugin", resolver => {
new AliasPlugin(
"described-resolve",
{
// 名称 asset
name: lib,
// 这是一个 module 而不是一个路径 就想我们alias 中 "vue$":"" 这种声明的别名一样
onlyModule: true,
// asset的源码路径 "d:\guzhangh\webpack\webpack\node_modules\assert\assert.js"
alias: getPathToModule(lib, options[lib]),
},
// 定义在 resolver 钩子事件流的名称 即这个事件注册到 resolver.resolve钩子事件流上
"resolve"
).apply(resolver)
})
}
}
})
可见其实 NodeSourcePlugin的作用也就是添加了很多 module 类型的 Node 系统支持的插件,所以我们在 webpack 中写代码中可以使用如process.env.xxx
、cluster
等
AMDPlugin
js
/*
添加webpack支持的 AMD 模块加载依赖的插件路径
*/
compiler.hooks.afterResolvers.tap("AMDPlugin", () => {
// 在 normal上注册 amdefine 类型的 resolver事件
compiler.resolverFactory.hooks.resolver.for("normal").tap("AMDPlugin", resolver => {
// 定义 const amd = reuquire("amdefine") -> "d:\guzhangh\webpack\webpack\node_modules\webpack\buildin\amd-define.js"
new AliasPlugin(
"described-resolve",
{
name: "amdefine",
alias: path.join(__dirname, "..", "..", "buildin", "amd-define.js"),
},
"resolve"
).apply(resolver)
// 定义 const amd = reuquire("amdefine") -> "d:\guzhangh\webpack\webpack\node_modules\webpack\buildin\amd-define.js"
new AliasPlugin(
"described-resolve",
{
name: "webpack amd options",
alias: path.join(__dirname, "..", "..", "buildin", "amd-options.js"),
},
"resolve"
).apply(resolver)
// 定义 const amd = reuquire("amdefine") -> "d:\guzhangh\webpack\webpack\node_modules\webpack\buildin\amd-define.js"
new AliasPlugin(
"described-resolve",
{
name: "webpack amd define",
alias: path.join(__dirname, "..", "..", "buildin", "amd-define.js"),
},
"resolve"
).apply(resolver)
})
})
其主要是添加 AMD 模块化加载依赖的插件文件
通过上面的创建 resolver、加载应用相应插件、在WebpackOptionsApply().process()
的时候还有两个插件添加了 Node 支持的浏览器端属性和方法的插件与 AMD 模块化依赖的插件等就形成了 type 为 normal 类型的 resolver 实例化对象
normalResolver.resolve( contextInfo, context, resource, {}, (err, resource, resourceResolveData) => { });
下面我们以 request === './resolve/src/main.js'
为例子讲解 normalResolver.resolve()的过程。
首先在 NormalModuleFactory 中通过 normalResolver 去使用对应类型的 resolver 实例对象去 resolve 加载相应的文件,这是一个非常好玩的过程,在 resolver 、 UnsafeCachePlugin 、ParsePlugin 之间来回纠缠。
当生成对应类型的 resolver 的时候其通过 normalResolver.resolve() 去加载相应文件的时候,经历一下过程
- resolve 钩子事件流
第一次 normalResolver.resolve()触发 resoler.resolve('resolve'),这时候 hooks 为 resolver.hooks.resolve。所以执行 resolver.doResolve('resolve Hooks')
这时候 resolver.hooks.resolve 下有 UnsafeCachePlugin 插件添加的 resolve 事件(根据文件的 request 和 context 生成文件的 ID 进行缓存)。
然后进行 resoler.doResolve('new-resolve')。
- newResolve 钩子事件流
进行 new-resolve 的事件流过程,触发resolver.hooks.newResolve.callAsync(request),这时候 ParsePlugin 插件添加的 newResolve 事件
根据请求的路径生成 parsed 对象,包含 query、module、directory、request 等信息
然后触发 parsedResolve类型的 doResolve
- parsedResolve 钩子事件流
这时候 request 对象内容是
在这个事件流中存在两个事件:
- DescriptionFilePlugin(加载描述信息文件)
- NextPlugin
parsedResolve 是一个异步串行执行,下一个事件依赖于上一个事件的 callback,所以下一步执行的是 DescriptionFilePlugin 中的 resolver.doResolve('described-resolve')
- describedResolve 钩子事件流
可见在 describedResolve 事件流中主要处理 alias,在前面我们处理NodeSourcePlugin和AMDPlugin添加了很多 aliasPlugin,然后我们 AliasPlugin 这时候就有用了
js
module.exports = class AliasPlugin {
constructor(source, options, target) {
// 实例化的时候 source为 described-resolve
this.source = source
this.options = Array.isArray(options) ? options : [options]
this.target = target
}
apply(resolver) {
const target = resolver.ensureHook(this.target)
// this.source === described-resolve 说明其注册在 resolver.hooks.describedResolve 钩子上 不就是我们3.1步骤的事件流
resolver.getHook(this.source).tapAsync("AliasPlugin", (request, resolveContext, callback) => {
const innerRequest = request.request || request.path
if (!innerRequest) return callback()
for (const item of this.options) {
if (innerRequest === item.name || (!item.onlyModule && startsWith(innerRequest, item.name + "/"))) {
if (innerRequest !== item.alias && !startsWith(innerRequest, item.alias + "/")) {
const newRequestStr = item.alias + innerRequest.substr(item.name.length)
const obj = Object.assign({}, request, {
request: newRequestStr,
})
return resolver.doResolve(
target,
obj,
"aliased with mapping '" + item.name + "': '" + item.alias + "' to '" + newRequestStr + "'",
resolveContext,
(err, result) => {
if (err) return callback(err)
// Don't allow other aliasing or raw request
if (result === undefined) return callback(null, null)
callback(null, result)
}
)
}
}
}
return callback()
})
}
}
第一个 Alias
其主要判断当前用户 alias 的配置中是否与此路径相匹配。如果匹配就进入 resolve 事件流,此处 "@"与"./resolve/src/main.js"不匹配,所以使用 return callback(),进入describedResolve 钩子事件流的下一个事件
第二个: AliasFieldPlugin 主要处理 DescriptionFileUtils.brower 中
第三个: AliasPlugin 处理的是我们前面 NodeSourcePlugin 定义的 Node 内置的第三方插件
第四个: AliasPlugin 处理的是我们前面 NodeSourcePlugin 定义的 Node 内置的第三方插件(buffer)
... child_process、cluster
第 41 个:: AliasPlugin 处理的是定义的 alias 别名是否与其相匹配
第 42 个: ModuleKindPlugin
第 43 个:JoinRequestPlugin
生成 module 的全路径
处理文件路径的分隔符
d:\guzhangh\webpack\webpack\resolve\src/modules/module2 -> "d:\guzhangh\webpack\webpack\resolve\src\modules\module2"
js
module.exports = class JoinRequestPlugin {
apply(resolver) {
const target = resolver.ensureHook(this.target)
resolver.getHook(this.source).tapAsync("JoinRequestPlugin", (request, resolveContext, callback) => {
const obj = Object.assign({}, request, {
// 根据项目路径与module的路径生成module的全路径
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)
})
}
}
- relative 事件流
在这个事件流中也存在两个事件:
- DescriptionFilePlugin(加载描述信息文件)
这时候的 DescriptionFilePlugin 插件 doResolve 的事件流不是前面的 describedResolve 而是 describedRelative 事件流了
- NextPlugin
- describedRelative 事件流
在这个事件流中存在两个事件
- FileKindPlugin
这个插件主要的作用就是判断是否是文件,是文件就进入下一个事件流 rawFile
- rawFile 事件流
插件事件很多
- TryNextPlugin
直接进入下一个事件流 file
- AppendPlugin
- file 事件流
从 rawFile 事件流的第一个事件直接进入了 file 事件流
- AliasPlugin
在前面describedResolve 钩子事件流中进行了很多 Alias 的判断去处理 request.request。但是那个时候处理的路径只是用户 引用的文件的路径,但是在describedResolve 钩子事件流最后一个事件JoinRequestPlugin 其一方面通过 request.path 和 request.request 去生成文件的全路径,一方面将 request.request 赋值为 undefined,那么这时候处理 alias 就是处理的文件的全路径而不是相对路径或者别名路径。
- AliasFieldPlugin
跟describedResolve 钩子事件流处理 package.json 中定义的 brower 路径,进入下一个钩子
- SymlinkPlugin
异步的方式去读取链接
- FileExistsPlugin
判断文件是否存在,如果存在就进入existingFile 事件流
- existingFile 事件流
- NextPlugin
就是进入下一个事件流插件(resolved)
- resolved 事件流
- ResultPlugin
作用很简单 触发 result 事件流
js
module.exports = class ResultPlugin {
apply(resolver) {
this.source.tapAsync("ResultPlugin", (request, resolverContext, callback) => {
const obj = Object.assign({}, request)
if (resolverContext.log) resolverContext.log("reporting result " + obj.path)
// 触发 result的事件流
resolver.hooks.result.callAsync(obj, resolverContext, err => {
if (err) return callback(err)
callback(null, obj)
})
})
}
}
下面开始执行回调了,不断的回调前面的 callback 直到 normalResolver.resolve()的 callback
通过上面 10 个事件流的过程 resolve -> newResolve -> parsedResolve -> NextPlugin -> describedResolve -> relative -> describedRelative -> rawFile -> file -> existingFile -> resolved 发现其主要作用就是
解析其相对路径或者别名路径或者解析成全路径 request.path 去匹配定义的和 Node 系统,AMD 模块化定义的别名匹配
加载描述文件(package.json)中相应的属性(brower)是否存在路径别名信息
解析成绝对路径
判断文件和文件夹是否存在
最后附上一个解析完成的图片
下面是通过文件的路径获取相应的 loaders 的过程,详情请看 loader 篇
下面我们看一下 存在别名的例子
如 "@/modules/module2" 其经历了上面的 resolve -> newResolve -> parsedResolve -> NextPlugin -> describedResolve 然后遇到匹配的@别名事件
js
module.exports = class AliasPlugin {
apply(resolver) {
const target = resolver.ensureHook(this.target)
// this.source === described-resolve 说明其注册在 resolver.hooks.describedResolve 钩子上 不就是我们3.1步骤的事件流
resolver.getHook(this.source).tapAsync("AliasPlugin", (request, resolveContext, callback) => {
// innerRequest 为文件的加载路径
const innerRequest = request.request || request.path
// 遍历设置的所有的alias
for (const item of this.options) {
// 如果 路径直接相同 那就是这种 import vue from "vue" 在 alias上 "vue$" : 'vue/xxx.js'
// 或者如果 别名不是一个module 那么就去判断当前文件的路径前缀是不是以这个别名开头的
// 如果不是一个路径 那就是一个文件 如 import { http } from "@/api/index.js" 那么这时候innerRequest不是不是只是module 且 以 @开始
if (innerRequest === item.name || (!item.onlyModule && startsWith(innerRequest, item.name + "/"))) {
if (innerRequest !== item.alias && !startsWith(innerRequest, item.alias + "/")) {
// 如 "@/modules/module2" 其会使用定义的 @的值和别名路径截取后的值生成全路径
// d:\guzhangh\webpack\webpack\resolve\src/modules/module2
const newRequestStr = item.alias + innerRequest.substr(item.name.length)
// 下面事件流中 request.request的值就是上面的处理后的路径了
const obj = Object.assign({}, request, {
request: newRequestStr,
})
return resolver.doResolve(
target, // resolve
obj,
"aliased with mapping '" + item.name + "': '" + item.alias + "' to '" + newRequestStr + "'",
resolveContext,
(err, result) => {
if (err) return callback(err)
// Don't allow other aliasing or raw request
if (result === undefined) return callback(null, null)
callback(null, result)
}
)
}
}
}
})
}
}
这时候的重点是返回的是 resolver.doResolve("resolve") 又是 resolve 事件流,只是其 request.request 修改成了 d:\guzhangh\webpack\webpack\resolve\src/modules/module2
,我们发现了一个什么问题,就是在 window 系统中路径的定义方式是不同的,所以需要去处理文件的路径,这就是我们上面的 describedResolve 钩子事件流的 JoinRequestPlugin 事件,其会通过 resolver.join(request.path, request.request)
将绝对路径的文件分隔符修改成 d:\guzhangh\webpack\webpack\resolve\src\modules\module2
resolver 的创建过程
- 获取对应类型的resolveOptions
在我们根据用户的配置合并 webpack 的默认配置后通过 **WebpackOptionsApply().process()**去根据配置信息加载相应的依赖插件
js
class WebpackOptionsApply extends OptionsApply {
constructor() {
super()
}
process(options, compiler) {
compiler.resolverFactory.hooks.resolveOptions.for("normal").tap("WebpackOptionsApply", resolveOptions => {
return Object.assign(
{
fileSystem: compiler.inputFileSystem,
},
cachedCleverMerge(options.resolve, resolveOptions)
)
})
compiler.resolverFactory.hooks.resolveOptions.for("context").tap("WebpackOptionsApply", resolveOptions => {
return Object.assign(
{
fileSystem: compiler.inputFileSystem,
resolveToContext: true,
},
cachedCleverMerge(options.resolve, resolveOptions)
)
})
compiler.resolverFactory.hooks.resolveOptions.for("loader").tap("WebpackOptionsApply", resolveOptions => {
return Object.assign(
{
fileSystem: compiler.inputFileSystem,
},
cachedCleverMerge(options.resolveLoader, resolveOptions)
)
})
compiler.hooks.afterResolvers.call(compiler)
return options
}
}
module.exports = WebpackOptionsApply
在加载依赖的最后通过 compiler.resolverFactory.hooks.resolveOptions.for("normal|context|loader")
分别为 loader、normal、context 三种类型的 resolve 添加一个一个合并处理后的 options.resolve 和 resolveOptions 的事件
那么在上面我们创建相应的 resolve 的时候,其resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions);
就会执行上面对应类型的钩子事件流,然后返回合并处理后的resolveOptions, 这就是 webpack 中 resolve 如何获取不同 type 的 reolveOptions 的过程。
获取到其resolveOptions,那么就可以根据配置信息去创建 resolve(const resolver = Factory.createResolver(resolveOptions);
)。
- 创建 resolver
js
exports.createResolver = function (options) {
//// OPTIONS ////
let modules = options.modules || ["node_modules"]
// ...
if (!resolver) {
resolver = new Resolver(useSyncFileSystemCalls ? new SyncAsyncFileSystemDecorator(fileSystem) : fileSystem)
}
// ...
// 处理alias属性
if (typeof alias === "object" && !Array.isArray(alias)) {
alias = Object.keys(alias).map(key => {
let onlyModule = false
let obj = alias[key]
if (/\$$/.test(key)) {
onlyModule = true
key = key.substr(0, key.length - 1)
}
if (typeof obj === "string") {
obj = {
alias: obj,
}
}
obj = Object.assign(
{
name: key,
onlyModule: onlyModule,
},
obj
)
return obj
})
}
// 生成一下 类型的钩子事件流
resolver.ensureHook("resolve")
// ...
resolver.ensureHook("resolved")
// 下面是按照配置添加一大堆的插件
aliasFields.forEach(item => {
plugins.push(new AliasFieldPlugin("described-resolve", item, "resolve"))
})
// 将上面添加的所有的钩子 调用其apply方法
plugins.forEach(plugin => {
plugin.apply(resolver)
})
return resolver
}
class Resolver extends Tapable {
constructor(fileSystem) {
super()
this.fileSystem = fileSystem
this.hooks = {
// 生成 resolver.hooks.resolveStep : new SyncHook(["hook", "request"]) 只是修改 hook.name === 'resolveStep'
resolveStep: withName("resolveStep", new SyncHook(["hook", "request"])),
noResolve: withName("noResolve", new SyncHook(["request", "error"])),
resolve: withName("resolve", new AsyncSeriesBailHook(["request", "resolveContext"])),
result: new AsyncSeriesHook(["result", "resolveContext"]),
}
/*
处理通过 resolver.plugin()的方式添加的事件
1. 通过添加的目标钩子事件流名称前加 before- after- 去决定当前事件在事件流中执行的顺序
*/
this._pluginCompat.tap("Resolver: before/after", options => {
// 处理 resolver.plugin("before-no-resolve" , fn)方式添加到 noResolve钩子事件流上的事件
// 因为其事件流的名称是 resolveStep、noResolve、resolve等,那么 before-no-resolve就找不到noResolve
if (/^before-/.test(options.name)) {
// 修改目标钩子事件流的名称 before-no-resolve -> no-resolve
options.name = options.name.substr(7)
// 将事件流的权重设置为 负数, 较高
options.stage = -10
} else if (/^after-/.test(options.name)) {
// 一样的方式 修改目标钩子事件流的名称 after-no-resolve -> no-resolve
options.name = options.name.substr(6)
// 将事件流的权重设置为 10, 较低
options.stage = 10
}
})
/*
*/
this._pluginCompat.tap("Resolver: step hooks", options => {
// 获取目标钩子事件流的名称,已经经过上面 "Resolver: before/after"事件处理 ,所以当前 options.name 为 no-resolve
const name = options.name
// 处理 resolveStep no-resolve 两种类型的钩子事件流
const stepHook = !/^resolve(-s|S)tep$|^no(-r|R)esolve$/.test(name)
if (stepHook) {
// 事件通过异步的方式添加
options.async = true
// 获取 name 类型的钩子事件流
this.ensureHook(name)
// 获取事件回调函数 fn
const fn = options.fn
// 重写事件的回调函数 TODO: 确定作用和意义
options.fn = (request, resolverContext, callback) => {
const innerCallback = (err, result) => {
if (err) return callback(err)
if (result !== undefined) return callback(null, result)
callback()
}
for (const key in resolverContext) {
innerCallback[key] = resolverContext[key]
}
fn.call(this, request, innerCallback)
}
}
})
}
/**
* 1. 通过钩子事件流名称来确定事件的权重
* before- 开头的权重较高 为 -10
* after- 开头的权重较低 为 10
* 2. 返回 this.hooks[name]
* 如果没有这个类型的钩子事件流 那么就创建一个以 ["request", "resolveContext"] 的异步串行事件流
* @param {[type]} name [description]
* @return {[type]} [description]
*/
ensureHook(name) {
if (typeof name !== "string") return name
// 驼峰转换
name = toCamelCase(name)
if (/^before/.test(name)) {
return this.ensureHook(name[6].toLowerCase() + name.substr(7)).withOptions({
stage: -10,
})
}
if (/^after/.test(name)) {
return this.ensureHook(name[5].toLowerCase() + name.substr(6)).withOptions({
stage: 10,
})
}
// 获取 name 类型的钩子事件流
const hook = this.hooks[name]
if (!hook) {
// 如果不存在 那么就创建异步串行事件流
return (this.hooks[name] = withName(name, new AsyncSeriesBailHook(["request", "resolveContext"])))
}
return hook
}
}
根据上面 ResolverFactory.createResolver,Resolver 实例化两个流程,发现其实际上也跟 compiler、NomalModuleFactory 等一样,也是通过一下步骤option 的处理,根据 option 创建 resolver 实例对象,根据 option 给 resolver 添加钩子事件流和相应的钩子函数
resolve 的钩子
名称 | 名称 | source | target | 类型 | 地址 | 调用时间 | 说明 |
---|---|---|---|---|---|---|---|
1 | UnsafeCachePlugin | resolve | new-resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
2 | ParsePlugin | new-resolve | parsed-resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
3 | DescriptionFilePlugin | parsed-resolve | described-resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
4 | NextPlugin | after-parsed-resolve | described-resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
5 | AliasPlugin | described-resolve | resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
6 | AliasFieldPlugin | described-resolve | resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
7 | ModuleKindPlugin | after-described-resolve | raw-module | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
8 | JoinRequestPlugin | after-described-resolve | relative | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
9 | TryNextPlugin | raw-module | module | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
10 | ModulesInHierachicDirectoriesPlugin | module | resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
11 | DescriptionFilePlugin | relative | described-relative | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
12 | NextPlugin | after-relative | described-relative | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
13 | FileKindPlugin | described-relative | raw-file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
14 | TryNextPlugin | described-relative | directory | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
15 | DirectoryExistsPlugin | directory | existing-directory | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
16 | MainFieldPlugin | existing-directory | resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
17 | MainFieldPlugin | existing-directory | resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
18 | MainFieldPlugin | existing-directory | resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
19 | UseFilePlugin | existing-directory | undescribed-raw-file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
20 | DescriptionFilePlugin | undescribed-raw-file | raw-file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
21 | NextPlugin | after-undescribed-raw-file | raw-file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
22 | TryNextPlugin | raw-file | file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
23 | AppendPlugin | raw-file | file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
24 | AppendPlugin | raw-file | file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
25 | AppendPlugin | raw-file | file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
26 | AliasPlugin | file | resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
27 | AliasFieldPlugin | file | resolve | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
28 | SymlinkPlugin | file | relative | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
29 | FileExistsPlugin | file | existing-file | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
30 | NextPlugin | existing-file | resolved | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
31 | ResultPlugin | AsyncSeriesBailHook | "" | AsyncSeriesHook | 中等文本 | 中等文本 | 稍微长一点的文本 |
UnsafeCachePlugin
ParsePlugin
DescriptionFilePlugin
NextPlugin
AliasPlugin
AliasFieldPlugin
ModuleKindPlugin
JoinRequestPlugin
TryNextPlugin
ModulesInHierachicDirectoriesPlugin
DescriptionFilePlugin
上面是在