webpack系列之三resolve

做者:崔静css

介绍

webpack 的特色之一是处理一切模块,咱们能够将逻辑拆分到不一样的文件中,而后经过模块化方案进行导出和引入。如今 ES6 的 Module 则是你们最经常使用的模块化方案,因此你必定写过 import './xxx' 或者 import 'something-in-nodemodules' 再或者 import '@/xxx'(@ 符号经过 webpack 配置中 alias 设置)。webpack 处理这些模块引入 import 的时候,有一个重要的步骤,就是如何正确的找到 './xxx''something-in-nodemodules' 或者 '@/xxx' 等等对应的是哪一个文件。这个步骤就是 resolve 的部分须要处理的逻辑。vue

其实不只是针对源码中的模块须要 resolve,包括 loader 在内,webpack 的总体处理过程当中,涉及到文件路径的,都离不开 resolve 的过程。node

同时 webpack 在配置文件中有一个 resolve 的配置,能够对 resolve 的过程进行适当的配置,好比设置文件扩展名,查找搜索的目录等(更多的参考官方介绍)。webpack

下面,将主要介绍针对普通文件的 resolve 流程 和 loader 的 resolve 主流程。git

resolve 主流程介绍

首先先准备一个简单的 demogithub

import { A } from './a.js'
复制代码

而后针对这个 demo 来看主流程。在 webpack 系列之一总览 文章中有一个 webpack 编译总流程图,图中能够看到在 webpack 处理每个文件开始以前都会有一个 resolve 的过程,找到完整的文件路径信息。web

webpack编译流程

webpack 源码中 resolve 流程开始的入口在 factory 阶段, factory 事件会触发 NormalModuleFactory 中的函数。先放一张粗略的整体流程图,在深刻源码前现有一个大概的框架图json

resolve总览

接下来咱们就从 NormalModuleFactory.js 文件中开始看起缓存

this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
    // 首先获得 resolver
	let resolver = this.hooks.resolver.call(null);

	// Ignored
	if (!resolver) return callback();

   // 执行
	resolver(result, (err, data) => {
		if (err) return callback(err);

		// Ignored
		if (!data) return callback();

		// direct module
		if (typeof data.source === "function") return callback(null, data);

		this.hooks.afterResolve.callAsync(data, (err, result) => {
			//... resolve结束后流程,此处省略
		});
	});
});
复制代码

第一步得到 resolver 逻辑比较简单,触发 resolver 事件(SyncWaterfallHook类型的Hook,关于Hook的类型,能够参考上一篇文章),同时 NormalModuleFactory 中注册了 resolver 事件。下面是 resolver 事件的代码,能够看到返回了一个函数。bash

this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
  //...先展现省略具体内容,后面会详细解释。
})
复制代码

所以 this.hooks.resolver.call(null); 结束后,将获得一个函数。而后接下来就是执行该函数得到 resolver 结果。 resolver 函数中,从总体看分为两大主要流程 loader 和 文件。

loader流程

  1. 获取到 inline loader 的 request 部分。例如,针对以下写法
import Styles from 'style-loader!css-loader?modules!./styles.css';
复制代码

会从中解析出 style-loadercss-loader。因为此步骤只是为了解析出路径,因此对于 loader 的配置部分并不关心。

  1. 获得 loader 类型的 resolver 处理实例,即 const loaderResolver = this.getResolver("loader");

  2. 对每个 loader 用 loaderResolver 依次处理,获得执行文件的路径。

文件流程

  1. 获得普通文件的 resolver 处理实例,即代码 const normalResolver = this.getResolver("normal", data.resolveOptions);

  2. 用 normalResolver 处理文件,获得最终文件绝对路径

下面是具体的 resolver 代码:

this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
	const contextInfo = data.contextInfo;
	const context = data.context;
	const request = data.request;
   
   // ... 省略部分和 loader 处理相关的代码
   // 处理 inline loaders,拿到 loader request 部分(loader 的名称或者 loader 的路径,因为这里不关系 loader 的配置等其余细节,因此直接将开头的 -!, 和 ! 直接替换掉,将多个 ! 替换成一个,方便后面处理)
	let elements = request
		.replace(/^-?!+/, "")
		.replace(/!!+/g, "!")
		.split("!");
	let resource = elements.pop();
	// 提取出具体的 loader
	elements = elements.map(identToLoaderRequest);

	const loaderResolver = this.getResolver("loader");
	const normalResolver = this.getResolver("normal", data.resolveOptions);

	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) => {
		  // ... reslover callback
		})
	)
})
		
复制代码

结合上面的步骤和代码看,其实 loader 类和普通文件类型(后面称为 normal 类),大体流程是类似的。咱们先看获取不一样类型的 resolver 实例部分。

获取不一样类型 resolver 处理实例

getResolver 函数,会调用到 webpack/lib/ResolverFactory.js 中的 get 方法。该方法中获取 resolver 实例的具体流程以下图。

获取resolver实例

上图中,首先根据不一样 type 获取 options 。那么这些 options 配置都存在哪里呢?

webpack中options配置

webpack 直接对外暴露的 resolve 的配置,在配置文件中 resolve 和 resolveLoader 部分,详细的字段见官网。可是其内部会有一个默认的配置,在 webpack.js 入口处理函数中,初始化了全部的默认配置

// ...
if (Array.isArray(options)) {
	compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if (typeof options === "object") {
	options = new WebpackOptionsDefaulter().process(options);
	compiler = new Compiler(options.context);
	compiler.options = options;
// ...
复制代码

WebpackOptionsDefaulter() 中,配置了不少关于 resolve 和 resolveLoader 的配置。process 方法将咱们写的 webpack 的配置 和默认的配置合并。

// WebpackOptionsDefaulter.js 文件
//...
this.set("resolve", "call", value => Object.assign({}, value));
this.set("resolve.unsafeCache", true); // 默认开启缓存
this.set("resolve.modules", ["node_modules"]); // 默认从 node_modules 中查找
// ...
复制代码

webpack.js 中,接下来有一句

new WebpackOptionsApply().process(options, compiler);
复制代码

其中 process 过程里会注入关于 normal/context/loader 的默认配置的获取函数。

compiler.resolverFactory.hooks.resolveOptions
  	.for("normal")
  	.tap("WebpackOptionsApply", resolveOptions => {
  		return Object.assign(
  			{
  				fileSystem: compiler.inputFileSystem
  			},
  			options.resolve,
  			resolveOptions
  		);
  	});
  compiler.resolverFactory.hooks.resolveOptions
  	.for("context")
  	.tap("WebpackOptionsApply", resolveOptions => {
  		return Object.assign(
  			{
  				fileSystem: compiler.inputFileSystem,
  				resolveToContext: true
  			},
  			options.resolve,
  			resolveOptions
  		);
  	});
  compiler.resolverFactory.hooks.resolveOptions
  	.for("loader")
  	.tap("WebpackOptionsApply", resolveOptions => {
  		return Object.assign(
  			{
  				fileSystem: compiler.inputFileSystem
  			},
  			options.resolveLoader,
  			resolveOptions
  		);
  	});
复制代码

options 介绍到此先结束,咱们继续沿着上面流程图往下看。当获取到 resolver 实例后,就开始 resolver 的过程:根据类型的不一样,会有 normalResolver 和 loaderResolver,同时在 normalResolver 中会区分文件和 module。

webpack 中有不少针对路径的配置,例如 alias, extensions, modules 等等,node.js 中的 require 已经没法知足 webpack 对路径的解析的要求。所以,webpack 封装出一个单独的库 enhanced-resolve,专门用来处理各类路径的解析,仍然采用了 webpack 的插件模式来组织代码。 接下来会深刻到这个库中,依次介绍普通文件、module 和 loader 的处理过程(webpack 中还有一个 context 的 resolve 过程,因为其过程没太多特别之处,放在 module 过程当中一块儿介绍)。先看普通文件的处理过程。

普通文件的 resolve 过程

普通文件 resolver 处理入口为 webpack 中 normalResolver.resolve 方法,而整个 resolve 过程能够当作事件的串联,当全部串联在一块儿的事件执行完以后,resolve 就结束了。

resolve事件环

将这些事件一个一个串联起来的关键部分在 doResolve 和每一个事件的处理函数中。这里以 doResolve 和调用的 UnsafePlugin 为例,看一下衔接的过程。

// 第一个参数 hook,函数中用到的 hook 是经过参数传进来的。
doResolve(hook, request, message, resolveContext, callback) {
	// ...
	// 生成 context 栈。
	const stackLine = hook.name + ": (" + request.path + ") " +
		(request.request || "") + (request.query || "") +
		(request.directory ? " directory" : "") +
		(request.module ? " module" : "");

	let newStack;
	if(resolveContext.stack) {
		newStack = new Set(resolveContext.stack);
		if(resolveContext.stack.has(stackLine)) {
			// Prevent recursion
			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(stackLine);
	} else {
		newStack = new Set([stackLine]);
	}
	// 简单的demo中这里没有事件注册,先忽略
	this.hooks.resolveStep.call(hook, request);

    // 若是该hook有注册过事件,则调触发该 hook
	if(hook.isUsed()) {
		const innerContext = createInnerContext({
			log: resolveContext.log,
			missing: resolveContext.missing,
			stack: newStack
		}, message);
		return hook.callAsync(request, innerContext, (err, result) => {
			if(err) return callback(err);
			if(result) return callback(null, result);
			callback();
		});
	} else {
		callback();
	}
}
复制代码

调用到 hook.callAsync 时,进入 UnsafeCachePlugin,而后看 UnsafeCachePlugin 中部分实现:

class UnsafeCachePlugin {
	constructor(source, filterPredicate, cache, withContext, target) {
		this.source = source;
       // ... 省略部分
		this.target = target;
	}
	apply(resolver) {
	   // ensureHook 主要逻辑:若是 resolver 已经有对应的 hook 则返回;若是没有,则会给 resolver 增长一个 this.target 类型的 hook
		const target = resolver.ensureHook(this.target);
		// getHook 会根据 this.source 字符串获取对应的 hook
		resolver.getHook(this.source).tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {
		   //... 先省略 UnsafeCache 中其余逻辑,只看衔接部分
			// 继续调用 doResolve,可是注意这里的 target 
			resolver.doResolve(target, request, null, resolveContext, (err, result) => {
				if(err) return callback(err);
				if(result) return callback(null, this.cache[cacheId] = result);
				callback();
			});
		});
	}
}
复制代码

UnsafeCachePlugin 分为两部分:事件注册(new 和 执行apply) 和事件执行(resolver.getHook(this.source).tapAsync 的回调部分)。事件注册阶段发在 webpack 获取不一样类型 resolve 处理实例时(前面获取不一样类型 resolver 处理实例小节中,getResolver 的时候),这时会传入一个 source 值(字符串类型)和一个 target 值(字符串类型),代码以下

// source 值为 resolve,target 值为 new-resolve
new UnsafeCachePlugin("resolve", cachePredicate, unsafeCache, cacheWithContext, "new-resolve")` //...而后会调用 apply 方法 复制代码

apply 中,将 UnsafeCachePlugin 的处理逻辑注册为 source 事件的回调,同时确保 target 事件的存在(若是没有则注册一个)。

事件执行阶段,完成 UnsafeCachePlugin 自己的逻辑以后,递归调用 resolver.doResolve(target, ...),这时第一个参数为 UnsafeCachePlugin 中的 target 事件。如此,再进入到 doResolve 以后,再触发 target 的事件,这样就造成了事件流。而总体的调用过程,简化来看总体逻辑就是:

doResolve(target1) 
  -> target1 事件(srouce:target1, target: target2) 
  -> 递归调用doResolve(target2) 
     -> target2 事件(srouce:target2, target: target3) 
     -> 递归调用doResolve(target3) 
        -> target3 事件(srouce:target3, target: target4) 
        ...
        ->遇到递归结束标识,结束递归
     
复制代码

经过对 doResolve 的递归调用,事件之间就衔接了起来,造成完整的处事件流,最终获得 resolve 结果。在 ResolverFactory.js 文件的 createResolver 方法中各个 plugin 的注册方法,决定了整个 resolve 的事件流。

exports.createResolver = function(options) {
    // ...
	// 根据 options 中条件的不一样,加入各类 plugin
	if(unsafeCache) {
		plugins.push(new UnsafeCachePlugin("resolve", cachePredicate, unsafeCache, cacheWithContext, "new-resolve"));
		plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));
	} else {
		plugins.push(new ParsePlugin("resolve", "parsed-resolve"));
	}
    // ... plugin 加入的代码
	plugins.forEach(plugin => {
		plugin.apply(resolver);
	});
	// ...
复制代码

上面代码整理一下,能够获得完整的事件流图(下图为简化版本,完成版本附图)

resolve事件流简版

结合上面的图和 demo,咱们来一步一步看这个事件流中每一环都作了什么。(ps:下面步骤中,会涉及到 request 参数,这个参数贯穿全部事件处理逻辑,保存了整个 resolve 的信息)

  1. UnsafeCachePlugin

增长一层缓存,因为 webpack 处理打包的过程当中,涉及到大量的 resolve 过程。因此须要增长一层缓存,提升效率。webpack 默认会启用 UnsafeCache。

  1. ParsePlugin

    初步解析路径,判断是否为 module/directory/file,结果保存到 request 参数中。

  2. DescriptionFilePlugin 和 NextPlugin

    DescriptionFilePlugin 中会寻找描述文件,默认会寻找 package.json。首先会在 request.path 这个目录下寻找,若是没有则按照路径一层一层往上寻找。最后读取到 package.json 的信息和其所在的目录/路径信息,存入 request 中。咱们在 demo 的根目录有 package.json 文件,因此这里会获取到根目录的文件。

    NextPlugin 起一个衔接的做用,内部逻辑就是直接调用 doResolve,而后触发下一个事件。当 DescriptionFilePlugin 中未找到 package.json 文件时,会进入 NextPlugin,而后让事件流继续。

  3. AliasPlugin/AliasFieldPlugin

这一步开始处理别名,因为 AliasFieldPlugin 中依赖于 package.json 的配置,因此这一步放在了 DescriptionFilePlugin 以后。 除了咱们在配置文件中写一些别名外,webpack 还会有一些自带的 alias;每个 alias 配置,都会注册一个函数。这一步将执行全部的函数,一一对比。 若命中某一 alias 的配置或者 aliasField,那么就会进入上图红色虚线的分支。用新的别名替换 request 参数内容,而后再次开始 resolve 过程。 没有命中,则进入下一个处理函数 ModuleKindPlugin

  1. ModuleKindPlugin

根据 request.module 的值走不一样的分支。若是是 module,则后续进入 rawModule 的逻辑。前面 ParsePlugin 中获得的结果中 request.modulefalse,因此这里返回 undefined,继续进入下一个处理函数。

  1. JoinRequestPlugin

将 request 中 path 和 request 合并起来,将 request 中 relativePath 和 request 合并起来,获得两个完整的路径。在这个 demo 中会获得 /Users/didi/dist/webpackdemo/webpack-demos/demo01/a.js./demo01/a.js

  1. DescriptionFilePlugin

这时会再次进入 DescriptionFilePlugin 。不过与第一次进入时不一样之处在于,此时的 request.path 变成了 /dir/demo/a.js`。因为 path 改变了,因此须要再次查找一下 package.json

随后触发 describedRelative 事件,进入下一个流程

  1. FileKindPlugin

判断是否为一个 directory,若是是则返回 undefined, 进入下一个 tryNextPlugin,这时会进入 directory 的分支。不然,则代表是一个文件,进入 rawFile 事件。咱们的 demo 中,这里将走向 rawFile 分支。

  1. TryNextPlugin/ConcordExtensionsPlugin/AppendPlugin

因为 webpack 中默认的 enforceExtension 值为 true,因此这里会进入 TryNextPlugin,同时 enableConcord 为 false,不会有 ConcordExtensionsPlugin。

TryNextPlugin 和 NextPlugin 相似,起一个衔接的做用,内部逻辑就是直接调用 doResolve,而后触发下一个事件。因此在这个阶段会直接走到触发 file 事件的分支。 当 TryNextPlugin 有返回,且返回为 undefined 。这时意味着没有找到 request.path 所对应的文件,那么会继续执行后续的 AppendPlugin。

AppendPlugin 主要逻辑:webpack 会设置 resolve.extensions 参数(配置中设置或者使用 webpack 默认的),AppendPlugin 会给 request.path 和 request.relativePath 逐一添加这些后缀,而后进入 file 分支,继续事件流程。

  1. AliasPlugin/AliasFields/ConcorModulesPlugin/SymlinkPlugin

这时会再次进入到 Alias 的处理逻辑,注意在此步中 webpack 内部自带的不少 Alias 不会再有。 与前面相同,这里依然没有 ConcorModulesPlugin SymlinkPlugin 用来处理路径中存在 link 的状况。因为 webpack 默认是按照真实的路径来解析的,因此这里会检查路径中每一段,若是遇到 link,则替换为真实路径。因为 path 改变了,因此会再回到 relative 阶段。 若路径中没有 link,则进入 FileExistsPlugin

  1. FileExistsPlugin

读取 request.path 所在的文件,看文件是否存在。文件存在则进入到 existingFile 事件。

  1. NextPlugin/ResultPlugin

经过 NextPlugin 衔接,再进入 Resolved 事件。而后执行 ResultPlugin,到此 resolve 整个流程就结束了,request 保存了 resolve 的结果。

module 的 resolve 过程

在 webpack 中,咱们除了会 import 一个文件之外,还会 import 一个模块,好比 import Vue from 'vue'。那么这时候,webpack 就须要正确找到 vue 所对应的入口文件在哪里。针对 vue,ParsePlugin 结果中 request.module = true,随后在 ModuleKindPlugin 就会进入上面图中 rawModule 的分支。咱们就以 import Vue from 'vue' 为 demo,看一下 rawModule 分支流程。

  1. ModuleAppendPlugin/TryNextPlugin

ModuleAppendPlugin 和上面的 AppendPlugin 相似,添加后缀。 TryNextPlugin 进入 module 事件

  1. ModulesInHierachicDirectoriesPlugin/ModulesInRootPlugin

ModulesInHierachicDirectoriesPlugin 中会依次在 request.path 的每一层目录中寻找 node_modules。例如 request.path = 'dir/demo' 那么寻找 node_modules 的过程为:

dir/demo/node_modules
dir/node_modules
/node_modules
复制代码

若是 dir/demo/node_modules 存在,则修改 request.path 和 request.request

const obj = Object.assign({}, request, {
  	path: addr, // node_module 所在的路径
  	request: "./" + request.request
  });
复制代码

对于 ModulesInRootPlugin,则默认为在根目录下寻找,直接进行替换

const obj = Object.assign({}, request, {
  	path: this.path,
  	request: "./" + request.request
  });
复制代码

随后,因为改变了 request.path 和 request.request,因此从新回到 resolve 开始的阶段。可是这时 request.request 从一个 module 变成了一个普通文件类型./vue

  1. 与普通文件 resolve 过程分叉点

按照普通文件的方式查找 dir/demo/node_module/vue 的过程与前文中普通文件 resolve 过程相似,经历上一节中 1-7 的步骤,而后触发 describedRelative 事件(这个事件下注册了两个函数 FileKindPlugin 和 TryNextPlugin)。 首先进入 FileKindPlugin 的逻辑,因为 dir/demo/node_module/vue 不是一个文件地址,因此在第 8 步 FileKindPlugin 中最终会返回 undefined。 这时候会进入下一个处理事件 TryNextPlugin,而后触发 directory 事件,把 dir/demo/node_module/vue 按照文件夹的方式来解析。

  1. DirectoryExisitsPlugin

确认 dir/demo/node_module/vue 是否存在。(ps: 针对 context 的 resolve 过程,到这里若是文件夹存在,则就结束了。)

  1. MainFieldPlugin

webpack 默认的 mainField 为 ['browser', 'module', 'main']。这里会按照顺序,在 dir/demo/node_module/vue/package.json 中找对应字段。 vue 的 package.json 中定义了

{
  "module": "dist/vue.runtime.esm.js"
}
复制代码

因此找到该字段后,会将 request.request 的值替换为 ./dist/vue.runtime.esm.js。以后又回到 resolve 节点,开始新一轮,寻找一个普通文件 ./dist/vue.runtime.esm.js 的过程。 当 MainFieldPlugin 执行完,都没有结果时,会进入 UseFilePlugin

  1. UseFilePlugin

当咱们 package.json 中没有写 browser、module、main 时,webpack 会自动去找目录下的 index 文件,request 变成以下

{
  //...省略其余部分
  relativePath: "./index",
  path: 'dir/demo/node_modules/vue/index'
}
复制代码

而后触发 undescribedRawFile 事件

  1. DescriptionFilePlugin/TryNextPlugin

针对新的 request.path ,从新寻找描述文件,即 package.json

  1. AppendPlugin

依次为 'dir/demo/node_modules/vue/index' 添加后缀名,而后寻找该文件是否存在。与前文中 file 以后的流程相同。直到最后找到存在的文件,整个针对 module 的 resolve 过程就结束了。

loader 的 resolve 过程

loader 的 resolve 过程和 module 的过程相似,咱们以 url-loader 为例,入口在 NormalModuleFactory.js 中 resolveRequestArray 函数。这里会执行 resolver.resolve,这里的 resolver 为以前获得的 loaderResolver,resolve 过程开始时 request 参数以下:

{
  context: {
    compiler: undefined,
    issuer: "/dir/demos/main.js"
  },
  path: "/dir/demos"
  request: "url-loader"
}
复制代码

在 ParsePlugin 中,request: "url-loader" 会被解析为 module。随后过程当中整个和 module 执行流程相同。

到此 webpack 中关于 resolve 流程就结束了。除此以外 webpack 还有很多的细节处理,鉴于篇幅有限这里就不展开细细讨论了,你们能够结合文章看 webpack 代码时去细细品味。

从原理到优化

webpack 中每涉及到一个文件,就会通过 resolve 的过程。而 resolve 过程当中其中针对一些不肯定的因素,好比后缀名,node_modules 路径等,会存在探索的过程,从而使得整个 resolve 的链条很长。不少针对 webpack 的优化,都会提到利用 resolve 配置来减小文件搜索范围:

  1. 使用 resolve.alias

咱们平常开发项目中,经常会存在相似 common 这样的目录,common 目录下的文件,会被常常引用。好比 'common/index.js'。若是咱们针对 common 目录创建一个 alias 的话,在全部用到 'common/index.js' 的文件中,能够写 import xx from 'common/index.js'。 因为 UnsafeCachePlugin 的存在,当 webpack 再次解析到 'common/index.js' 时,就能够直接使用缓存。

不止如此,重点是解析链条变短,缓存只是一部分吧

  1. 设置 resolve.modules

resolve.modules 的默认值为 ['node_modules'],因此在对 module 的 resolve 过程当中,会依次查找 ./node_modules、../node_modules、../../node_modules 等,即沿着路径一层一层往上找,直到找到 node_modules。能够直接设置

resolve.modules:[path.resolve(__dirname, 'node_modules')]
复制代码

如此会进入 ModulesInRootPlugin 而不是 ModulesInHierachicDirectoriesPlugin,避免了层层寻找 node_modules 的开销。

  1. 对第三方模块设置 resolve.alias

对第三方的 module 进行 resolve 过程当中,除了上面提到的 node_modules 目录查找过程,还会涉及到对 package.json 中配置的解析等。能够直接为其设置 alias 为执行文件,来简化整个 resolve 过程,以下:

resolve.alias: {
    'vue': path.resolve(__dirname, './node_modules/vue/dist/vue.common.js')
}
复制代码
  1. 合理设置 resolve.extensions,减小文件查找

当咱们的文件没有后缀时,AppendPlugin 会根据 resolve.extensions 中的值,依次添加后缀而后查找文件。为了减小文件查找,咱们能够直接将文件后缀写上,或者设置 resolve.extensions 中的值,列表值尽可能少频率高的文件类型的后缀写在前面

明白了 resolve 的细节以后,再来看这些优化策略,即可以更好的了解其缘由,作到“知其然知其因此然”。

附图(resolve事件流完整版):

resolve事件流完整版
相关文章
相关标签/搜索