本文内容基于webpack 5.74.0enhanced-resolve 5.12.0版本进行剖析

因为enhanced-resolve兼容了多种简单状况的解析,想要将这些状况进行详细分析太消耗精力,因而本文只是尝试将所有流程进行通俗剖析,通过本文,你能够对webpack的resolve配置有一个整体的理解,如果对这方面想更加深刻地钻研,请联合其它文章进行浏览

本文是「Webpack5源码」make阶段(流程图)剖析的补充文章,如果对webpack流程不相熟,请先看「Webpack5源码」make阶段(流程图)剖析

文章内容

  1. 简要介绍webpack是如何应用enhanced-resolve进行门路解析
  2. 分为三个流程图展现enhanced-resolve的解析流程,并为每一个流程图简略形容整体流程

整体流程剖析


从上图能够晓得,咱们在webpack解析过程中,会初始化this.getResolve("loader")this.getResolve("normal")

resolve一共分为两种配置,一种是文件类型的门路解析配置,一种是 loader 包的解析配置,比方上图中的内联url解析进去的到"babel-loader""./index_item-line""babel-loader"会应用getResolve("loader")进行解析,"./index_item-line"会应用getResolve("normal")进行解析

必须是内联url才会触发resolveRequestArray()defaultResolve()?是的,从源码上看只有解析到"-!""!"!!"才会使得unresolvedResourceelements两个数据不为空,才会触发resolveRequestArray()defaultResolve()

如果是import _ from "loadsh"会不会触发getResolve("loader")进行解析?答案是不会的

typenormalloader
resolveOptions

其中normal的参数获取是合并了webpack.config.js和默认options的后果
入口文件是EntryDependency,它具备默认的category="esm"

class EntryDependency extends ModuleDependency {    /**     * @param {string} request request path for entry     */    constructor(request) {        super(request);    }    get type() {        return "entry";    }    get category() {        return "esm";    }}

而在初始化normal类型的Resolver时,会触发hooks.resolveOptions进行webpack.config.js和一些默认参数的初始化

// node_modules/webpack/lib/ResolverFactory.js_create(type, resolveOptionsWithDepType) {    /** @type {ResolveOptionsWithDependencyType} */    const originalResolveOptions = { ...resolveOptionsWithDepType };    const resolveOptionsTemp = this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType);    const resolveOptions = convertToResolveOptions(        resolveOptionsTemp    );}// node_modules/webpack/lib/WebpackOptionsApply.jscompiler.resolverFactory.hooks.resolveOptions    .for("normal")    .tap("WebpackOptionsApply", resolveOptions => {        resolveOptions = cleverMerge(options.resolve, resolveOptions);        resolveOptions.fileSystem = compiler.inputFileSystem;        return resolveOptions;    });

this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)获取到的数据如下图所示

而后触发convertToResolveOptions()办法,经验几个办法的调用执行后,最终触发resolveByProperty(),如下图所示,会依据下面EntryDependency失去的"esm"进行参数的合并,最终失去残缺的配置参数


而后应用resolver.resolve()进行门路的解析

// node_modules/enhanced-resolve/lib/Resolver.jsresolve(context, path, request, resolveContext, callback) {    return this.doResolve(this.hooks.resolve, ...args);}// node_modules/enhanced-resolve/lib/Resolver.jsdoResolve(hook, request, message, resolveContext, callback) {    const stackEntry = Resolver.createStackEntry(hook, request);    if (resolveContext.stack) {        newStack = new Set(resolveContext.stack);        newStack.add(stackEntry);    } else {        newStack = new Set([stackEntry]);    }    return hook.callAsync(request, innerContext, (err, result) => {        if (err) return callback(err);        if (result) return callback(null, result);        callback();    });}

一开始调用doResolve()时,咱们传入的第一个参数hook=this.hooks.resolve,咱们从下面代码块能够晓得,会间接触发hook.callAsync,也就是this.hooks.resolve.callAsync()

咱们在初始化Resolver时,会调用ResolverFactory.createResolver,因而咱们在调试这个流程时只有看这个办法即可,这个办法注册了很多Plugin插件,每一个Plugin对其(多个)下家Plugin的援用而连接起来造成一条链。 申请在这个链上传递,如果合乎其中一个下家Plugin的条件,则以符合条件下家Plugin为根底,传递该申请(到下下家Plugin),直到某一条链走到底部
ResolverFactory.createResolver = function (options) {    const normalizedOptions = createOptions(options);    // resolve    for (const { source, resolveOptions } of [        { source: "resolve", resolveOptions: { fullySpecified } },        { source: "internal-resolve", resolveOptions: { fullySpecified: false } }    ]) {        if (unsafeCache) {            plugins.push(                new UnsafeCachePlugin(                    source,                    cachePredicate,                    unsafeCache,                    cacheWithContext,                    `new-${source}`                )            );            plugins.push(                new ParsePlugin(`new-${source}`, resolveOptions, "parsed-resolve")            );        } else {            plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));        }    }}

每一个Plugin都合乎繁多职责准则,比方有的Plugin就是专门解决alias问题,有的Plugin解决文件后缀问题(增加.js后缀)

初始流程

ParsePlugin

外围代码就是应用resolver.parse(request.request)办法,解析出目前门路所属的类型以及所携带的参数

其中fragment的定义能够参考URI's fragment,fragment就是hash
// node_modules/enhanced-resolve/lib/Resolver.jsparse(identifier) {    const part = {        request: "",        query: "",        fragment: "",        module: false,        directory: false,        file: false,        internal: false    };    const parsedIdentifier = parseIdentifier(identifier);    if (!parsedIdentifier) return part;    [part.request, part.query, part.fragment] = parsedIdentifier;    if (part.request.length > 0) {        part.internal = this.isPrivate(identifier);        part.module = this.isModule(part.request);        part.directory = this.isDirectory(part.request);        if (part.directory) {            part.request = part.request.substr(0, part.request.length - 1);        }    }    return part;}

其中requestqueryfragment的解析应用的是正则表达式匹配拿到对应的值

这正则表达式也太长了=_=
// node_modules/enhanced-resolve/lib/util/identifier.jsconst PATH_QUERY_FRAGMENT_REGEXP = /^(#?(?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/;function parseIdentifier(identifier) {    const match = PATH_QUERY_FRAGMENT_REGEXP.exec(identifier);    if (!match) return null;    return [        match[1].replace(/\0(.)/g, "$1"),        match[2] ? match[2].replace(/\0(.)/g, "$1") : "",        match[3] || ""    ];}

应用一个简略的示例能够帮忙咱们更好地了解正则表达式匹配出什么货色

至于这个正则表达式是否匹配出更加简单的门路,请参考其它文章进行理解

DescriptionFilePlugin

const directory = this.pathIsFile    ? DescriptionFileUtils.cdUp(path)    : path;DescriptionFileUtils.loadDescriptionFile(    resolver,    directory,    this.filenames,    request.descriptionFilePath        ? {            path: request.descriptionFilePath,            content: request.descriptionFileData,            directory: /** @type {string} */ (request.descriptionFileRoot)        }        : undefined,    resolveContext,    (err, result) => {        const relativePath =            "." + path.substr(result.directory.length).replace(/\\/g, "/");        const obj = {            ...request,            descriptionFilePath: result.path,            descriptionFileData: result.content,            descriptionFileRoot: result.directory,            relativePath: relativePath        };        resolver.doResolve(target, obj, ...);    });

DescriptionFileUtils.cdUp(path): 获取最初一个"/"的地位,而后应用path.substr(0, position),获取directory

比方传入path="/Users/A/B/js-enhanced_resolve",获取到的是directory="/Users/A/B"

DescriptionFileUtils.loadDescriptionFile: 先应用directory拼接this.filenames[i]进行形容文件的查找,如果找不到,则将directory往上一级变更,即从"/Users/A/B"->"/Users/A",而后再拼接this.filenames[i]进行形容文件的查找

this.filenames的内容是什么呢?

初始化DescriptionFilePlugin时会传入this.filenames,具体的值为createOptions()创立默认配置时的descriptionFiles参数,默认为["package.json"]

function createOptions(options) {    return {        descriptionFiles: Array.from(            new Set(options.descriptionFiles || ["package.json"])        )    }}

连续下面示例,咱们通过该Plugin能够失去的数据如下所示,其中descriptionFileDatapackage.json的具体内容

relativePath="." + request.path.substr(descriptionFileRoot.length).replace(/\\/g, "/")

AliasPlugin

次要外围点如下图所示

  • innerRequest = request.request || request.path
  • item.name是咱们在webpack.config.js中配置的别名alias,比方在这个例子中,咱们配置了alias如下所示,因而咱们能拿到item.name="aliasTest"
// webpack.config.jsalias: {    aliasTest: resolve(__dirname, 'src/item'),}// src/entry1.jsimport {getG} from "aliasTest/common_____g";
  • 别名替换的规定是,咱们的门路innerRequest.startsWith("aliasTest"),也就是咱们门路的第一个单词必须是别名,能力失常匹配
  • 而后就是替换逻辑,将"aliasTest"替换为咱们在webpack.config.js配置的门路,造成新的门路newRequestStr
  • 最初替换咱们的obj数据中对应的request参数,而后进入下一个Plugin

AliasFieldPlugin

依据环境的不同,应用不同的配置,比方浏览器环境和node环境
默认type="normal"会初始化aliasFields=['browser']
typenormalloader
resolveOptions

如果webpack.config.js配置了aliasFields,则最终normal类型的resolveOptions会间接应用webpack.config.js配置的aliasFields

// webpack.config.jsresolve: {    alias: {        aliasTest: resolve(__dirname, 'src/item'),    },    aliasFields: ['node222'],}


同时咱们还须要在package.json中配置对应的aliasFields属性,比方上面的代码,当咱们找到文件门路为src/dir时,咱们就会替换为node_src/dir

// package.json{    "node222": {        "src/dir": "node_src/dir"    }}

AliasFieldPluginapply()办法解析如下,咱们能够获取到一个文件门路为"src/dir",而后咱们通过判断fieldData={"src/dir": "node_src/dir"}是否满足fileldData[innerRequest]触发替换规定

最终将替换胜利后的data笼罩obj.request门路,本来的门路如上图所示是"src/dir",当初被替换为"node_src/dir",而后触发下一个Plugin

AliasFieldPlugin这个属性集体了解为能够依据不同环境,比方依据browser/node环境别离进行文件门路替换,比方入口文件:browser环境为browser/index.jsnode环境为node/index.js,具体用法请参考其它文章,本文不会做过多的剖析
const obj = {    ...request,    path: request.descriptionFileRoot,    request: data,    fullySpecified: false};resolver.doResolve(target, obj, ...)

ExtensionAliasPlugin

// webpack.config.jsmodule.exports = {  //...  resolve: {    extensionAlias: {      '.js': ['.ts', '.js'],      '.mjs': ['.mts', '.mjs'],    },  },};// node_modules/enhanced-resolve/lib/ResolverFactory.jsextensionAlias.forEach(item =>  plugins.push(    new ExtensionAliasPlugin("raw-resolve", item, "normal-resolve")  ));

具体的apply()代码如下所示,流程也比较简单

  • 判断目前requestPath.endsWith(extension)是否成立,比方"./entry1.js"满足配置的extensionAlias中的".js",因而会命中持续上面的逻辑
  • 判断拿到的alias类型,比方命中extension='.js',那么alias=['.ts', '.js'],会应用forEachBail()遍历尝试每一种状况替换是否满足提议
  • 最终后果调用stoppingCallback()办法,比方"./entry1.js"->"./entry1.ts""./entry1.js"->"./entry1.js",最终只有"./entry1.js"->"./entry1.js"满足提议,调用stoppingCallback()办法
const target = resolver.ensureHook(this.target);const { extension, alias } = this.options;resolver    .getHook(this.source)    .tapAsync("ExtensionAliasPlugin", (request, resolveContext, callback) => {        const requestPath = request.request;        if (!requestPath || !requestPath.endsWith(extension)) return callback();        const resolve = (alias, callback) => {            resolver.doResolve(                target,                {                    ...request,                    request: `${requestPath.slice(0, -extension.length)}${alias}`,                    fullySpecified: true                },                `aliased from extension alias with mapping '${extension}' to '${alias}'`,                resolveContext,                callback            );        };        const stoppingCallback = (err, result) => {            if (err) return callback(err);            if (result) return callback(null, result);            // Don't allow other aliasing or raw request            return callback(null, null);        };        if (typeof alias === "string") {            resolve(alias, stoppingCallback);        } else if (alias.length > 1) {            forEachBail(alias, resolve, stoppingCallback);        } else {            resolve(alias[0], stoppingCallback);        }    });

模块解决

ConditionalPlugin

一种条件判断的Plugin,全副代码如下所示,初始化会传入一个条件,判断是否满足这个条件,则能够持续下一个Plugin

for (const prop of keys) {    if (request[prop] !== test[prop]) return callback();}resolver.doResolve(target,request,...)

举一个例子,上面初始化时传入{ module: true },因而如果这个文件是module类型,那么遇到这个ConditionalPlugin则符合条件,那么它就会从"after-normal-resolve"->"raw-module"ConditionalPlugin起到一种依据解决类型不同从而跳转到不同Plugin的目标

// node_modules/enhanced-resolve/lib/ResolverFactory.jsplugins.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"    ));

RootsPlugin

// webpack.config.jsconst fixtures = path.resolve(__dirname, 'fixtures');module.exports = {  //...  resolve: {    roots: [__dirname, fixtures],  },};

解决resolve.roots参数,判断目前的申请门路是否以"/"结尾,如果是,则阐明是根目录结尾的文件,会将roots[i]拼到文件门路上,造成新的path=roots[i]+request.request

// RootsPlugin applyif (!req.startsWith("/")) return callback();forEachBail(    this.roots,    (root, callback) => {        const path = resolver.join(root, req.slice(1));        const obj = {            ...request,            path,            relativePath: request.relativePath && path        };        resolver.doResolve(            target,            obj,            `root path ${root}`,            resolveContext,            callback        );    },    callback);

JoinRequestPlugin

扭转pathrelativePathrequest

  • path=path+request
  • relativePath=relativePath+request
  • request=undefined

实质是将目前申请的门路加上申请文件的名称,造成申请文件的绝对路径

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);

举个例子,如下图所示,request.request都并入了request.pathrequest.relativePath


ImportsFieldPlugin(简略介绍)

具体配置能够参考webpack官网文档的resolve.importsFields形容

解决resolve.importsFields字段
aliasFields一样,须要在package.json配置字段(配置字段名称跟webpack.config.js申明统一),该字段用于提供包的外部申请(以# 结尾的申请被视为外部申请)

感兴趣请参考其它文章,比方 Node最新Module导入导出标准,本文不会深入研究
上面的阐明摘录自文章 Node最新Module导入导出标准

imports 字段中的入口必须是以 # 结尾的字符串。
导入映射容许映射内部包。这个字段定义了以后包的子门路导入。

// webpack.config.jsmodule.exports = {  //...  resolve: {    //如果不手动配置上面,默认为['imports']    importsFields: ['browser', 'module', 'main'],  },};// package.json{  "imports": {    "#dep": {      "node": "dep-node-native",      "default": "./dep-polyfill.js"    }  }}

SelfReferencePluginPlugin(简略介绍)

具体配置能够参考https://webpack.js.org/guides/package-exports

解决resolve.exportsFields
aliasFields一样,须要在package.json配置字段(配置字段名称跟webpack.config.js申明统一),用于解析模块申请的字段

感兴趣请参考其它文章,比方 Node最新Module导入导出标准,本文不会深入研究
上面的阐明摘录自文章 Node最新Module导入导出标准

"exports" 字段能够定义包的入口,而包则能够通过 node_modules 查找或自援用导入。Node.js 12+ 开始反对 "exports",作为 "main"的代替,它既反对定义子门路导出和条件导出,又关闭了外部未导出的模块。
条件导出也能够在 "exports" 中应用,以定义每个环境中不同的包入口,包含这个包是通过 require 还是通过 import 引入。
所有在 "exports" 中定义的门路必须是以 ./ 结尾的相对路径URL。

// webpack.config.jsmodule.exports = {  //...  resolve: {    //如果不手动配置上面,默认为['exports']    exportsFields: ['exports', 'myCompanyExports'],  },};// package.json{  "exports": {    "./features/*": "./src/features/*.js"  }}

ModulesInHierarchicalDirectoriesPlugin

sourcetarget是高低Plugin的连贯,这个在每一个Plugin都具备
directories默认为["node_modules"]

class ModulesInHierarchicalDirectoriesPlugin {    constructor(source, directories, target) {        this.source = source;        this.directories = /** @type {Array<string>} */ ([]).concat(directories);        this.target = target;    }}
// apply()办法const fs = resolver.fileSystem;const addrs = getPaths(request.path)    .paths.map(p => {        return this.directories.map(d => resolver.join(p, d));    })    .reduce((array, p) => {        array.push.apply(array, p);        return array;    }, []);console.warn(addrs);forEachBail(    addrs,    (addr, callback) => {        fs.stat(addr, (err, stat) => {            if (!err && stat && stat.isDirectory()) {                const obj = {                    ...request,                    path: addr,                    request: "./" + request.request,                    module: false                };                const message = "looking for modules in " + addr;                return resolver.doResolve(                    target,                    obj,                    message,                    resolveContext,                    callback                );            }            if (resolveContext.log)                resolveContext.log(                    addr + " doesn't exist or is not a directory"                );            if (resolveContext.missingDependencies)                resolveContext.missingDependencies.add(addr);            return callback();        });    },    callback);

如下面代码块所示,咱们会应用getPaths()获取所有node_modules目录的可能性,如下图所示,咱们会从path逐级向上遍历,拼凑出pathssegments

而后咱们利用getPaths()paths数据,拼凑出所有node_modules可能的绝对路径(如下图addrs所示),最终咱们通过某一个绝对路径是否是目录,来获取其中某一个门路addr,而后笼罩目前的obj,造成第三方node_modules库的绝对路径,如下图所示的obj.path+obj.request,就第三方库loadsh的绝对路径

ModulesInRootPlugin

在初始化过程中,如果modulesitem是数组类型,则触发下面剖析的ModulesInHierarchicalDirectoriesPlugin
如果modulesitem不是数组类型,则触发ModulesInRootPlugin

//node_modules/enhanced-resolve/lib/ResolverFactory.jsexports.createResolver = function (options) {    modules.forEach(item => {        if (Array.isArray(item)) {            //...            plugins.push(                new ModulesInHierarchicalDirectoriesPlugin(                    "raw-module",                    item,                    "module"                )            );        } else {            plugins.push(new ModulesInRootPlugin("raw-module", item, "module"));        }    });}

ModulesInRootPluginapply代码也比拟粗犷简略,间接认为你传入的item=modules[i]就是node_modules的绝对路径目录,跟ModulesInHierarchicalDirectoriesPlugin相比拟,缩小了所有可能性目录的寻找以及判断是否是目录stat.isDirectory()的逻辑

const obj = {    ...request,    path: this.path,    request: "./" + request.request,    module: false};resolver.doResolve(    target,    obj,    "looking for modules in " + this.path,    resolveContext,    callback);

JoinRequestPartPlugin

检测import的库是否存在多层目录,比方不是"vue",而是"vue/test",将path拼接为原来的path+前缀的"/vue"relativePath拼接为原来的relativePath+前缀目录名"/vue"request拼接为门路的最初一部分,为"./test"

moduleName就是第三方库的目录名称,remainingRequest就是除去第三方库的目录名称的残余局部


DirectoryExistsPlugin

直接判断request.path是否是目录,是的话就持续下一个Plugin

// DirectoryExistsPlugin applyconst fs = resolver.fileSystem;const directory = request.path;if (!directory) return callback();fs.stat(directory, (err, stat) => {    //...省略很多不是directory的提醒逻辑    if (resolveContext.fileDependencies)        resolveContext.fileDependencies.add(directory);    resolver.doResolve(        target,        request,        `existing directory ${directory}`,        resolveContext,        callback    );});

ExportsFieldPlugin(简略介绍)

SelfReferencePluginPlugin(简略介绍)的作用统一,本文不深入研究,请读者自行抉择其它文章进行钻研
具体配置能够参考https://webpack.js.org/guides/package-exports

解决resolve.exportsFields
aliasFields一样,须要在package.json配置字段(配置字段名称跟webpack.config.js申明统一),用于解析模块申请的字段

目录/文件门路解决

UseFilePlugin

解决resolve.mainFiles数据,如果不在webpack.config.js中设置该参数,默认初始化为["index"]

// webpack.config.jsmodule.exports = {  //...  resolve: {    mainFiles: ['test'],  },};

源码也非常简单,进行obj.path=原来path+mainFile的拼接,进行obj.relativePath=原来relativePath+mainFile的拼接,拼接后造成一个文件申请门路,而后进行文件类型的解析

// UseFilePlugin applyconst filePath = resolver.join(request.path, this.filename);const obj = {    ...request,    path: filePath,    relativePath:        request.relativePath &&        resolver.join(request.relativePath, this.filename)};resolver.doResolve(    target,    obj,    "using path: " + filePath,    resolveContext,    callback);

举一个具体的例子如下图所示,咱们申请的是一个import {dirIndex} from "./dir",这个链接咱们是没有申明xx.js文件的,UseFilePlugin会主动帮咱们补齐最初面的index成为import {dirIndex} from "./dir"

UseFilePlugin是目录类型的解决,也就是说如果具备了xxx.js是不会走到目录这一条链路的,如下图所示,先进行了目录是否存在的判断,而后才进行index.js的增加

MainFieldPlugin

解决resolve.mainFields数据,如果不在webpack.config.js中设置该参数,默认初始化为["main"]

留神!UseFilePlugin解决的是resolve.mainFiles!跟MainFieldPlugin解决的resolve.mainFields是不同名字的!
// webpack.config.jsmodule.exports = {  //...  resolve: {    mainFields: ['browser', 'module', 'main'],  },};

aliasField一样,同样须要在package.json中配置对应的参数
比方bable-loader的形容文件在xxxx/node_modules/babel-loader/package.json
babel-loader/package.json"main"申明为"lib/index.js"

// package.json{    "name": "babel-loader",    "version": "9.1.0",    "description": "babel module loader for webpack",    "files": [      "lib"    ],    "main": "lib/index.js",}


那如果没有在package.json中申明"main"所对应的值,比方上面main: "",那会产生什么?

// package.json{    "name": "babel-loader",    "version": "9.1.0",    "description": "babel module loader for webpack",    "files": [      "lib"    ],    "main": ""}

那就会不走这个MainFieldPlugin,而是间接走下面剖析的UseFilePlugin,间接在前面增加一个index.js

UseFilePlugin的剖析能够晓得,UseFilePluginMainFieldPlugin是并行的两个Plugin

AppendPlugin

下面几个Plugin都是目录类型解决的Plugin,当初开始文件类型解决的Plugin
// webpack.config.jsmodule.exports = {  //...  resolve: {    extensions: ['.ts', '.js', '.wasm'],  },};

解决resolve.extensions参数,如果不在webpack.config.js中设置该参数,默认为[".js", ".json", ".node"],然而convertToResolveOptions()办法因为"esm"进行参数的合并,因而resolve.extensions参数不为空,会取[".js", ".json", ".wasm"]

整体代码逻辑也非常简单,间接用一张示例图就能够明确,就是一直往目前的申请门路加后缀,看看哪一个合乎就进入下一个阶段

FileExistsPlugin

直接判断request.path是否存在该文件,是的话就持续下一个Plugin

const file = request.path;if (!file) return callback();fs.stat(file, (err, stat) => {    //...省略很多不是file的提醒逻辑    if (resolveContext.fileDependencies)        resolveContext.fileDependencies.add(file);    resolver.doResolve(        target,        request,        "existing file: " + file,        resolveContext,        callback    );});

SymlinkPlugin

resolve.symlinks默认为true,如果不配置webpack.config.jssymlinks=false,则默认开启symlinks=true,启用后,符号链接资源将解析为它们的实在门路,而不是它们的符号链接地位

// webpack.config.jsmodule.exports = {  //...  resolve: {    symlinks: true,  },};

resolve.symlinks默认为true,这个时候会查看是否存在软链状况(能够简略看作是window零碎的快捷方式),如果存在,则替换path成为实在门路

// SymlinkPlugin applyconst pathsResult = getPaths(request.path);const pathSegments = pathsResult.segments;const paths = pathsResult.paths;let containsSymlink = false;let idx = -1;forEachBail(    paths,    (path, callback) => {        idx++;        if (resolveContext.fileDependencies)            resolveContext.fileDependencies.add(path);        fs.readlink(path, (err, result) => {            if (!err && result) {                pathSegments[idx] = result;                containsSymlink = true;                // Shortcut when absolute symlink found                const resultType = getType(result.toString());                if (                    resultType === PathType.AbsoluteWin ||                    resultType === PathType.AbsolutePosix                ) {                    return callback(null, idx);                }            }            callback();        });    },    (err, idx) => {        if (!containsSymlink) return callback();        const resultSegments =            typeof idx === "number"                ? pathSegments.slice(0, idx + 1)                : pathSegments.slice();        const result = resultSegments.reduceRight((a, b) => {            return resolver.join(a, b);        });        const obj = {            ...request,            path: result        };        resolver.doResolve(            target,            obj,            "resolved symlink to " + result,            resolveContext,            callback        );    })

下面apply()办法的代码尽管有点长,然而逻辑是非常简单的
应用示例

request.path='/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src/entry1.js'

咱们能够失去

pathSegments=['entry1.js', 'src', 'js-enhanced_resolve', 'webpack-debugger',               'Frontend-Articles', 'blog', 'wcbbcc', 'Users', '/'];paths=[  '/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src/entry1.js',  '/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src',  ......]

下面的forEachBail循环中
idx=0时,path="/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src/entry1.js"
idx=1时,path="/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/src"
idx=2时,path="/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve"

如果fs.readlink(path)胜利,可能正确获取到理论的链接result时,咱们会应用pathSegments[idx]= result
比方idx=2可能胜利,那么

// pathSegments[2] = js-enhanced_resolve// path的最初一个单词被笼罩为resultpathSegments[2] = fs.readlink(path)的后果

获取目前的resultSegments=pathSegments.slice(0, idx + 1),即

resultSegments=['entry1.js', 'src', fs.readlink(path)的后果];const result = resultSegments.reduceRight((a, b) => {    return resolver.join(a, b);});result = fs.readlink(path)的后果 + '/src/entry1.js';const obj = {    ...request,    path: result};resolver.doResolve(target, obj, ...);

ResultPlugin

最初一个Plugin,返回后果!实现整个resolve流程

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);    }});

具体实例-一般目录解析

应用流程图展现所有Plugin的流向以及对Plugin的解决做简略的正文

具体实例-node_modules第三方模块解析

因为篇幅起因,这里不进行具体的流程图剖析,将应用简略的文字描述展现整体流程
import _ from "loadsh";// 第三方模块的resolve测试console.error( _.add(3, 4));

初始模块resolve

  • ParsePlugin: 初始化流程,应用resolver.parse(request.request)办法,解析出目前门路所属的类型以及所携带的参数
  • DescriptionFilePlugin: 解析package.json,获取package.json对应的门路以及它自身的内容,前面须要读取外面的内容进行resolve
  • NextPlugin: 因为aliasaliasFieldextensionAlias都不须要解决,因而间接跳到normal-resolve状态

模块解决normal-resolve

  • ConditionalPlugin: 触发module=true状态,进入raw-module状态
  • ModulesInHierarchicalDirectoriesPlugin: 依据目前import vue form "vue"所在的文件门路request.path,一层一层不停往parent目录找node_modules的地位,最终拼凑进去path=node_modules的绝对路径以及request="./vue"
  • JoinRequestPartPlugin: 检测import的库是否存在多层目录,比方不是"vue",而是"vue/test",将path拼接为原来的path+前缀的"/vue"relativePath拼接为原来的relativePath+前缀目录名"/vue"request拼接为门路的最初一部分,为"./test"

尝试这个模块是否是单文件,尝试从resolve-as-module转化为undescribed-raw-file(参考模块解决的流程图),即尝试是不是loadsh.js这种模式,通过一圈Plugin的调用,最终发现上面三种文件都不存在(参考AppendPlugin的剖析),无奈从resolve-as-module转化为undescribed-raw-file

  • /Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh.js
  • /Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh.json
  • /Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh.wasm
  • DirectoryExistsPlugin: 应用resolver.fileSystem.fs.stat进行以后path的判断,如果目录存在,则触发doResolve()
  • DescriptionFilePlugin: 解析package.json,获取package.json对应的门路以及它自身的内容,前面须要读取外面的内容进行resolvepackage.json"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/package.json"更改为"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh/package.json"
  • NextPlugin: 因为不须要解决resolve.exportsField字段,间接跳转到resolve-in-existing-directory状态
  • JoinRequestPlugin: 扭转pathrelativePathrequest,即path=path+requestrelativePath=relativePath+requestrequest=undefined,实质是将目前申请的门路加上申请文件的名称,造成申请文件的绝对路径

目录/文件门路解决relative

第1次relative

  • DescriptionFilePlugin: 解析package.json,获取package.json对应的门路以及它自身的内容

    • package.json"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh/package.json"更改为"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/package.json"
    • relativePath"."更改为"./node_modules/loadsh"
  • DirectoryExistsPlugin: 应用resolver.fileSystem.fs.stat进行以后path的判断,如果目录存在,则触发doResolve(),此时判断该目录loadsh存在!
  • DescriptionFilePlugin: 解析package.json,获取package.json对应的门路以及它自身的内容

    • package.json"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/package.json"更改为"/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh/package.json"
    • relativePath"./node_modules/loadsh"更改为"."
  • MainFieldPlugin: 解决resolve.mainFields数据,在本人我的项目的package.json中配置,用于指定从 npm 包中导入模块时,此选项将决定在 (npm包所在的)package.json中应用哪个字段进行入口文件的指定
留神,lodash这个第三方库的入口文件不是"index.js",而是"lodash.js",也就是"./node_modules/loadsh/lodash.js",通过resolve.mainFields指定!!!!!此时obj.request= "./lodash.js"
  • JoinRequestPlugin: 扭转pathrelativePathrequest,设置path=path+requestrelativePath=relativePath+requestrequest=undefined,实质是将目前申请的门路加上申请文件的名称,造成申请文件的绝对路径
obj.request并入到pathrelativePath中,path="/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js-enhanced_resolve/node_modules/loadsh/lodash.js"

第2次relative

JoinRequestPlugin的下一个状态又是relative!然而跟第1次relative相比拟,数据曾经从目录loadsh->文件loadsh/lodash.js,因而走的是跟第1次relative不同的路

因为不须要加后缀以及没有别名的替换逻辑,间接跳转到FileExistsPlugin(参考下面目录/文件门路解决的流程图即可明确)

  • FileExistsPlugin: 应用resolver.fileSystem.fs.stat进行以后path的判断,如果门路存在,阐明文件存在,则触发doResolve()

    没有软链,间接跳过SymlinkPlugin
  • ResultPlugin: 完结Plugin,返回后果