webpack源码分析(九)
回归上节内容,我们讲了一个ResolverFactory的demo,得知,他有一个resolve方法,可以让我们拿到具体的路径。
接下来,我们分析代码,他是怎么运行的。
exports.createResolver = function (options) {
// 解析并规范化用户传入的配置
const normalizedOptions = createOptions(options);
const {
alias,
fallback,
aliasFields,
cachePredicate,
cacheWithContext,
conditionNames,
descriptionFiles,
enforceExtension,
exportsFields,
extensionAlias,
importsFields,
extensions,
fileSystem,
fullySpecified,
mainFields,
mainFiles,
modules,
plugins: userPlugins,
pnpApi,
resolveToContext,
preferRelative,
preferAbsolute,
symlinks,
unsafeCache,
resolver: customResolver,
restrictions,
roots
} = normalizedOptions;
const plugins = userPlugins.slice();
const resolver = customResolver
? customResolver
: new Resolver(fileSystem, normalizedOptions);
//// pipeline ////
// 确保hook 存在
resolver.ensureHook("resolve");
resolver.ensureHook("internalResolve");
resolver.ensureHook("newInternalResolve");
resolver.ensureHook("parsedResolve");
resolver.ensureHook("describedResolve");
resolver.ensureHook("rawResolve");
resolver.ensureHook("normalResolve");
resolver.ensureHook("internal");
resolver.ensureHook("rawModule");
resolver.ensureHook("module");
resolver.ensureHook("resolveAsModule");
resolver.ensureHook("undescribedResolveInPackage");
resolver.ensureHook("resolveInPackage");
resolver.ensureHook("resolveInExistingDirectory");
resolver.ensureHook("relative");
resolver.ensureHook("describedRelative");
resolver.ensureHook("directory");
resolver.ensureHook("undescribedExistingDirectory");
resolver.ensureHook("existingDirectory");
resolver.ensureHook("undescribedRawFile");
resolver.ensureHook("rawFile");
resolver.ensureHook("file");
resolver.ensureHook("finalFile");
resolver.ensureHook("existingFile");
resolver.ensureHook("resolved");
// TODO remove in next major
// cspell:word Interal
// Backward-compat
// @ts-ignore
resolver.hooks.newInteralResolve = resolver.hooks.newInternalResolve;
// resolve
for (const { source, resolveOptions } of [
{ source: "resolve", resolveOptions: { fullySpecified } },
{ source: "internal-resolve", resolveOptions: { fullySpecified: false } }
]) {
if (unsafeCache) {
plugins.push(
new UnsafeCachePlugin(
source,
cachePredicate,
/** @type {import("./UnsafeCachePlugin").Cache} */ (unsafeCache),
cacheWithContext,
`new-${source}`
)
);
plugins.push(
new ParsePlugin(`new-${source}`, resolveOptions, "parsed-resolve")
);
} else {
plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
}
}
// parsed-resolve
plugins.push(
new DescriptionFilePlugin(
"parsed-resolve",
descriptionFiles,
false,
"described-resolve"
)
);
plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
// described-resolve
plugins.push(new NextPlugin("described-resolve", "raw-resolve"));
if (fallback.length > 0) {
plugins.push(
new AliasPlugin("described-resolve", fallback, "internal-resolve")
);
}
// raw-resolve
if (alias.length > 0) {
plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve"));
}
aliasFields.forEach(item => {
plugins.push(new AliasFieldPlugin("raw-resolve", item, "internal-resolve"));
});
extensionAlias.forEach(item =>
plugins.push(
new ExtensionAliasPlugin("raw-resolve", item, "normal-resolve")
)
);
plugins.push(new NextPlugin("raw-resolve", "normal-resolve"));
// normal-resolve
if (preferRelative) {
plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative"));
}
plugins.push(
new ConditionalPlugin(
"after-normal-resolve",
{ module: true },
"resolve as module",
false,
"raw-module"
)
);
plugins.push(
new ConditionalPlugin(
"after-normal-resolve",
{ internal: true },
"resolve as internal import",
false,
"internal"
)
);
if (preferAbsolute) {
plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative"));
}
if (roots.size > 0) {
plugins.push(new RootsPlugin("after-normal-resolve", roots, "relative"));
}
if (!preferRelative && !preferAbsolute) {
plugins.push(new JoinRequestPlugin("after-normal-resolve", "relative"));
}
// internal
importsFields.forEach(importsField => {
plugins.push(
new ImportsFieldPlugin(
"internal",
conditionNames,
importsField,
"relative",
"internal-resolve"
)
);
});
// raw-module
exportsFields.forEach(exportsField => {
plugins.push(
new SelfReferencePlugin("raw-module", exportsField, "resolve-as-module")
);
});
modules.forEach(item => {
if (Array.isArray(item)) {
if (item.includes("node_modules") && pnpApi) {
plugins.push(
new ModulesInHierarchicalDirectoriesPlugin(
"raw-module",
item.filter(i => i !== "node_modules"),
"module"
)
);
plugins.push(
new PnpPlugin("raw-module", pnpApi, "undescribed-resolve-in-package")
);
} else {
plugins.push(
new ModulesInHierarchicalDirectoriesPlugin(
"raw-module",
item,
"module"
)
);
}
} else {
plugins.push(new ModulesInRootPlugin("raw-module", item, "module"));
}
});
// module
plugins.push(new JoinRequestPartPlugin("module", "resolve-as-module"));
// resolve-as-module
if (!resolveToContext) {
plugins.push(
new ConditionalPlugin(
"resolve-as-module",
{ directory: false, request: "." },
"single file module",
true,
"undescribed-raw-file"
)
);
}
plugins.push(
new DirectoryExistsPlugin(
"resolve-as-module",
"undescribed-resolve-in-package"
)
);
// undescribed-resolve-in-package
plugins.push(
new DescriptionFilePlugin(
"undescribed-resolve-in-package",
descriptionFiles,
false,
"resolve-in-package"
)
);
plugins.push(
new NextPlugin("after-undescribed-resolve-in-package", "resolve-in-package")
);
// resolve-in-package
exportsFields.forEach(exportsField => {
plugins.push(
new ExportsFieldPlugin(
"resolve-in-package",
conditionNames,
exportsField,
"relative"
)
);
});
plugins.push(
new NextPlugin("resolve-in-package", "resolve-in-existing-directory")
);
// resolve-in-existing-directory
plugins.push(
new JoinRequestPlugin("resolve-in-existing-directory", "relative")
);
// relative
plugins.push(
new DescriptionFilePlugin(
"relative",
descriptionFiles,
true,
"described-relative"
)
);
plugins.push(new NextPlugin("after-relative", "described-relative"));
// described-relative
if (resolveToContext) {
plugins.push(new NextPlugin("described-relative", "directory"));
} else {
plugins.push(
new ConditionalPlugin(
"described-relative",
{ directory: false },
null,
true,
"raw-file"
)
);
plugins.push(
new ConditionalPlugin(
"described-relative",
{ fullySpecified: false },
"as directory",
true,
"directory"
)
);
}
// directory
plugins.push(
new DirectoryExistsPlugin("directory", "undescribed-existing-directory")
);
if (resolveToContext) {
// undescribed-existing-directory
plugins.push(new NextPlugin("undescribed-existing-directory", "resolved"));
} else {
// undescribed-existing-directory
plugins.push(
new DescriptionFilePlugin(
"undescribed-existing-directory",
descriptionFiles,
false,
"existing-directory"
)
);
mainFiles.forEach(item => {
plugins.push(
new UseFilePlugin(
"undescribed-existing-directory",
item,
"undescribed-raw-file"
)
);
});
// described-existing-directory
mainFields.forEach(item => {
plugins.push(
new MainFieldPlugin(
"existing-directory",
item,
"resolve-in-existing-directory"
)
);
});
mainFiles.forEach(item => {
plugins.push(
new UseFilePlugin("existing-directory", item, "undescribed-raw-file")
);
});
// undescribed-raw-file
plugins.push(
new DescriptionFilePlugin(
"undescribed-raw-file",
descriptionFiles,
true,
"raw-file"
)
);
plugins.push(new NextPlugin("after-undescribed-raw-file", "raw-file"));
// raw-file
plugins.push(
new ConditionalPlugin(
"raw-file",
{ fullySpecified: true },
null,
false,
"file"
)
);
if (!enforceExtension) {
plugins.push(new TryNextPlugin("raw-file", "no extension", "file"));
}
extensions.forEach(item => {
plugins.push(new AppendPlugin("raw-file", item, "file"));
});
// file
if (alias.length > 0)
plugins.push(new AliasPlugin("file", alias, "internal-resolve"));
aliasFields.forEach(item => {
plugins.push(new AliasFieldPlugin("file", item, "internal-resolve"));
});
plugins.push(new NextPlugin("file", "final-file"));
// final-file
plugins.push(new FileExistsPlugin("final-file", "existing-file"));
// existing-file
if (symlinks)
plugins.push(new SymlinkPlugin("existing-file", "existing-file"));
plugins.push(new NextPlugin("existing-file", "resolved"));
}
const resolved =
/** @type {KnownHooks & EnsuredHooks} */
(resolver.hooks).resolved;
// resolved
if (restrictions.size > 0) {
plugins.push(new RestrictionsPlugin(resolved, restrictions));
}
plugins.push(new ResultPlugin(resolved));
//// RESOLVER ////
for (const plugin of plugins) {
if (typeof plugin === "function") {
/** @type {function(this: Resolver, Resolver): void} */
(plugin).call(resolver, resolver);
} else if (plugin) {
plugin.apply(resolver);
}
}
return resolver;
};
我先把代码放上来。
一眼望上去400多行代码真多,实际上,他做了3件事情,非常简单
- createOptions(options); normalize options 参数
- 注册tapable钩子resolver.ensureHook("xxx");
- 注册插件
第一步我们直接跳过,不想讲normalize,因为实在没啥意思,无非就是字符串变成字符串数组,来回的变而已。
const resolver = customResolver? customResolver : new Resolver(fileSystem, normalizedOptions);
这行代码的关键是new Resolver(fileSystem, normalizedOptions);,因为我们不会传入自定义的customResolver
constructor(fileSystem, options) {
this.fileSystem = fileSystem;
this.options = options;
/** @type {KnownHooks} */
this.hooks = {
// 每执行一个插件都会调用
resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
// 没有找到具体文件或目录
noResolve: new SyncHook(["request", "error"], "noResolve"),
// 开始解析
resolve: new AsyncSeriesBailHook(
["request", "resolveContext"],
"resolve"
),
// 解析完成
result: new AsyncSeriesHook(["result", "resolveContext"], "result")
};
}
Resolver的构造函数就是这样,其实和compiler很像,都是有tapable的事件。
我们继续往下看 resolver.ensureHook("xxx");
ensureHook(name) {
if (typeof name !== "string") {
return name;
}
name = toCamelCase(name);
if (/^before/.test(name)) {
return /** @type {ResolveStepHook} */ (
this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({
stage: -10
})
);
}
if (/^after/.test(name)) {
return /** @type {ResolveStepHook} */ (
this.ensureHook(name[5].toLowerCase() + name.slice(6)).withOptions({
stage: 10
})
);
}
const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
if (!hook) {
/** @type {KnownHooks & EnsuredHooks} */
(this.hooks)[name] = new AsyncSeriesBailHook(
["request", "resolveContext"],
name
);
return /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
}
return hook;
}
其实就是注册new AsyncSeriesBailHook(),如果带before前缀优先级会高,带after优先级会低,已经注册过,就走已经注册的。
我们接着往下看,下面是注册插件,我们直接走到最后一步,看起来和webpack没区别啊,没错,enhanced-resolve的设计形式和webpack就是非常像。
for (const plugin of plugins) {
if (typeof plugin === "function") {
(plugin).call(resolver, resolver);
} else if (plugin) {
plugin.apply(resolver);
}
}
ResolverFactory()我们就说完了,我们再看他上面的resolve方法为啥可以解析到具体的路径。
resolve(context, path, request, resolveContext, callback) {
const obj = {
context: context, //{}
path: path, //路径
request: request //.a
};
let yield_;
let yieldCalled = false;
let finishYield;
if (typeof resolveContext.yield === "function") {
const old = resolveContext.yield;
yield_ = obj => {
old(obj);
yieldCalled = true;
};
finishYield = result => {
if (result) {
(yield_)(result);
}
callback(null);
};
}
const message = `resolve '
${request}' in '
${path}'`;
/**
*
@param {ResolveRequest} result result
*
@returns {void}
*/
const finishResolved = result => {
return callback(
null,
result.path === false
? false
: `
${result.path.replace(/#/g, "\0#")}${
result.query ? result.query.replace(/#/g, "\0#") : ""
}${result.fragment || ""}`,
result
);
};
/**
*
@param {string[]} log logs
*
@returns {void}
*/
const finishWithoutResolve = log => {
/**
*
@type {ErrorWithDetail}
*/
const error = new Error("Can't " + message);
error.details = log.join("\n");
this.hooks.noResolve.call(obj, error);
return callback(error);
};
if (resolveContext.log) {
// We need log anyway to capture it in case of an error
const parentLog = resolveContext.log;
/**
@type {string[]} */
const log = [];
return this.doResolve(
this.hooks.resolve,
obj,
message,
{
log: msg => {
parentLog(msg);
log.push(msg);
},
yield: yield_,
fileDependencies: resolveContext.fileDependencies,
contextDependencies: resolveContext.contextDependencies,
missingDependencies: resolveContext.missingDependencies,
stack: resolveContext.stack
},
(err, result) => {
if (err) return callback(err);
if (yieldCalled || (result && yield_)) {
return /**
@type {ResolveContextYield} */ (finishYield)(
/**
@type {ResolveRequest} */ (result)
);
}
if (result) return finishResolved(result);
return finishWithoutResolve(log);
}
);
} else {
// Try to resolve assuming there is no error
// We don't log stuff in this case
return this.doResolve(
this.hooks.resolve,
obj,
message,
{
log: undefined,
yield: yield_,
fileDependencies: resolveContext.fileDependencies,
contextDependencies: resolveContext.contextDependencies,
missingDependencies: resolveContext.missingDependencies,
stack: resolveContext.stack
},
(err, result) => {
if (err) return callback(err);
if (yieldCalled || (result && yield_)) {
return /**
@type {ResolveContextYield} */ (finishYield)(
/**
@type {ResolveRequest} */ (result)
);
}
if (result) return finishResolved(result);
// log is missing for the error details
// so we redo the resolving for the log info
// this is more expensive to the success case
// is assumed by default
/**
@type {string[]} */
const log = [];
return this.doResolve(
this.hooks.resolve,
obj,
message,
{
log: msg => log.push(msg),
yield: yield_,
stack: resolveContext.stack
},
(err, result) => {
if (err) return callback(err);
// In a case that there is a race condition and yield will be called
if (yieldCalled || (result && yield_)) {
return /**
@type {ResolveContextYield} */ (finishYield)(
/**
@type {ResolveRequest} */ (result)
);
}
return finishWithoutResolve(log);
}
);
}
);
}
}
看着resolve好像很多的样子,实际上,this.doResolve()就调用了这个方法而已。
doResolve(hook, request, message, resolveContext, callback) {
const stackEntry = Resolver.createStackEntry(hook, request);
/** @type {Set<string> | undefined} */
let newStack;
if (resolveContext.stack) {
newStack = new Set(resolveContext.stack);
if (resolveContext.stack.has(stackEntry)) {
/**
* Prevent recursion
* @type {Error & {recursion?: boolean}}
*/
const recursionError = new Error(
"Recursion in resolving\nStack:\n " +
Array.from(newStack).join("\n ")
);
recursionError.recursion = true;
if (resolveContext.log)
resolveContext.log("abort resolving because of recursion");
return callback(recursionError);
}
newStack.add(stackEntry);
} else {
newStack = new Set([stackEntry]);
}
//默认没有传事件,所以不走
this.hooks.resolveStep.call(hook, request);
if (hook.isUsed()) {
const innerContext = createInnerContext(
{
log: resolveContext.log,
yield: resolveContext.yield,
fileDependencies: resolveContext.fileDependencies,
contextDependencies: resolveContext.contextDependencies,
missingDependencies: resolveContext.missingDependencies,
stack: newStack
},
message
);
return hook.callAsync(request, innerContext, (err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
callback();
});
} else {
callback();
}
}
doResolve其实就是调用了hook.callAsync方法。
我们第一次调用的是 doResolve(this.hooks.resolve)
hooks.resolve
resolve上面挂载了一个方法。
plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("ParsePlugin", (request, resolveContext, callback) => {
debugger
// 调用 resolver 中的 parse 方法初步解析
const parsed = resolver.parse(/** @type {string} */ (request.request));
/** @type {ResolveRequest} */
// 合并成新的 obj 对象
const obj = { ...request, ...parsed, ...this.requestOptions };
if (request.query && !parsed.query) {
obj.query = request.query;
}
if (request.fragment && !parsed.fragment) {
obj.fragment = request.fragment;
}
if (parsed && resolveContext.log) {
if (parsed.module) resolveContext.log("Parsed request is a module");
if (parsed.directory)
resolveContext.log("Parsed request is a directory");
}
// There is an edge-case where a request with # can be a path or a fragment -> try both
if (obj.request && !obj.query && obj.fragment) {
const directory = obj.fragment.endsWith("/");
/** @type {ResolveRequest} */
const alternative = {
...obj,
directory,
request:
obj.request +
(obj.directory ? "/" : "") +
(directory ? obj.fragment.slice(0, -1) : obj.fragment),
fragment: ""
};
resolver.doResolve(
target,
alternative,
null,
resolveContext,
(err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
resolver.doResolve(target, obj, null, resolveContext, callback);
}
);
return;
}
resolver.doResolve(target, obj, null, resolveContext, callback);
});
}
我直接说他做了什么吧,代码也很简单,它先解析parse request,然后 resolver.doResolve(target, obj, null, resolveContext, callback);,继续调用doResolve方法。
parse request 其实就是对request进行解析,得到query,fragment,internal,module,directory.(./src?query=122#id) fragement=#的内容,query=?的内容,有点像浏览器的url解析)
doResolve继续调用,那我们就知道是这段代码是怎么实现的了。
原来就是钩子里面继续调用钩子,先走resolve,走完parsed-resolve,然后依次,知道执行完毕。。。
hooks.parsed-resolve
parsed-resolve 有两个钩子
- plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
- plugins.push(new DescriptionFilePlugin("parsed-resolve",descriptionFiles,false,"described-resolve"));
NextPlugin 非常简单,就是啥也不执行,直接跳到下一个钩子,比如这里的described-resolve
DescriptionFilePlugin 做了哪些处理呢,其实也非常的简单,他会根据当前的路径去找package.json文件,如果这一级没有,就跳到上一级,代码就不列出来了,知道功能就好。
然后就走到described-resolve
hooks.described-resolve
只有NextPlugin这个,直接就跳到raw-resolve。
hooks.raw-resolve
我们的demo没有绑定插件,只有nextPlugin,所以跳到normal-resolve
hooks.normal-resolve
ConditionalPlugin
JoinRequestPlugin
ConditionalPlguin
一种条件判断的Plugin,全部代码如下所示,初始化会传入一个条件,判断是否满足这个条件,则可以继续下一个Plugin
for (const prop of keys) {
if (request[prop] !== test[prop]) return callback();
}
resolver.doResolve(target,request,...)
所以ConditionalPlugin的作用就是根据处理类型不同从而跳转到不同Plugin
很遗憾这里不匹配。
所以走到了JoinRequestPlugin
JoinRequestPlugin
改变path、relativePath、request
- path=path+request
- relativePath=relativePath+request
- request=undefined
本质是将目前请求的路径加上请求文件的名称,形成请求文件的绝对路径
相对路径变成了绝对路径。path="/usr/project/xxx/xxx/index.js" 举个这样的例子
const obj = {
...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);
hooks.relative
执行到这里,其实一共就做了 1. parse requset 解析? # 等信息 2. 找到package.json 3. 把相对路径的requset 和path合并拿到绝对路径。
relative有两个 1.DescriptionFilePlugin 2. nextplugin
这俩哥们真眼熟啊,一个是处理package.json的,一个是跳转到下一个钩子的。
因为我们上边已经处理过了,所以大概率这里会直接callback,走了相当于没走。
没错,debugger代码确实直接callback。
所以直接下一个吧。
hooks.described-relative
有两个 ConditionalPlguin钩子 又是这位老哥
所以二者必走一个
plugins.push(
new ConditionalPlugin(
"described-relative",
{ directory: false },
null,
true,
"raw-file"
)
);
plugins.push(
new ConditionalPlugin(
"described-relative",
{ fullySpecified: false },
"as directory",
true,
"directory"
)
);
一看参数,我们一眼就知道走哪个了,一个是文件夹,一个是文件,我们这次处理的是文件,所以直接跳到raw-file。
hooks.raw-file
raw-file有5个钩子,一个TryNextPlugin,一个ConditionalPlugin,三个AppendPlugin
new ConditionalPlugin( "raw-file", { fullySpecified: true }, null, false,"file")
先走ConditionalPlugin,然后发现不匹配。
再走 plugins.push(new TryNextPlugin("raw-file", "no extension", "file"));
会直接去尝试下一个钩子,如果能走通就走下去,走不通就回来。
看名字 no extension 没有扩展名 进入file钩子
hooks.file
进入file钩子,发现只有nextPlugin,进入finalFile
hooks.finalFile
它内部只有FileExistsPlugin这个钩子
apply(resolver) {
const target = resolver.ensureHook(this.target);
const fs = resolver.fileSystem;
resolver
.getHook(this.source)
.tapAsync("FileExistsPlugin", (request, resolveContext, callback) => {
const file = request.path;
if (!file) return callback();
fs.stat(file, (err, stat) => {
if (err || !stat) {
if (resolveContext.missingDependencies)
resolveContext.missingDependencies.add(file);
if (resolveContext.log) resolveContext.log(file + " doesn't exist");
return callback();
}
if (!stat.isFile()) {
if (resolveContext.missingDependencies)
resolveContext.missingDependencies.add(file);
if (resolveContext.log) resolveContext.log(file + " is not a file");
return callback();
}
if (resolveContext.fileDependencies)
resolveContext.fileDependencies.add(file);
resolver.doResolve(
target,
request,
"existing file: " + file,
resolveContext,
callback
);
});
});
}
核心就是fs.stat(path),很明显,如果我们输入的是具体的./a.js,而不是./a,那就可以走下去,因为我们现在这条路径走下去的是无扩展名的,所以fs.stat返回error的信息。
所以只能原路返回,返回到hooks.raw-file
回到hooks.raw-file
剩下的就是AppendPlugin这个钩子,我们说有三个钩子,这是为什么呢。
extensions.forEach(item => {
plugins.push(new AppendPlugin("raw-file", item, "file"));
});
看名字就知道为啥了,因为一共有三个扩展。
extensions: [".json", ".js", ".ts"]
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("AppendPlugin", (request, resolveContext, callback) => {
debugger
/** @type {ResolveRequest} */
const obj = {
...request,
path: request.path + this.appending,
relativePath:
request.relativePath && request.relativePath + this.appending
};
resolver.doResolve(
target,
obj,
this.appending,
resolveContext,
callback
);
});
}
看一下这个函数,其实就是加别名而已。
还记得上面的流程吗,进入file,走到finalFile,但是这三个钟./a.js能找到文件,所以fs.stat是通过的,我们就可以往下走了。
new FileExistsPlugin("final-file", "existing-file")
hooks.existing-file
plugins.push(new SymlinkPlugin("existing-file", "existing-file"));
plugins.push(new NextPlugin("existing-file", "resolved"));
一个是 SymlinkPlugin , 一个老朋友NextPlugin
SymlinkPlugin软连接
代码不写了,就是利用fs.readlink拿到软连接地址,我们这里没有软连接。
所以走到 NextPlugin
hooks.resolved
plugins.push(new ResultPlugin(resolved)); 只有她了
apply(resolver) {
this.source.tapAsync(
"ResultPlugin",
(request, resolverContext, callback) => {
const obj = { ...request };
if (resolverContext.log)
resolverContext.log("reporting result " + obj.path);
resolver.hooks.result.callAsync(obj, resolverContext, err => {
if (err) return callback(err);
if (typeof resolverContext.yield === "function") {
resolverContext.yield(obj);
callback(null, null);
} else {
callback(null, obj);
}
});
}
);
}
我们的逻辑直接就走到callback(null,obj)
而这个callback就是,我们自己写的callback
(err, path, result) => {
console.log("createResolve path: ", path, result);
}
这就是ResolverFactory.
1. 包含常规前端面试题解析,和源码分析 2. 从0-1深入浅出理解前端相关技术栈
