乐趣区

关于webpack:Webpack5源码enhancedresolve路径解析库源码分析

本文内容基于 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") 进行解析?答案是不会的

type normal loader
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.js
compiler.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.js
resolve(context, path, request, resolveContext, callback) {return this.doResolve(this.hooks.resolve, ...args);
}
// node_modules/enhanced-resolve/lib/Resolver.js
doResolve(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.js
parse(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.js
const 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.js
alias: {aliasTest: resolve(__dirname, 'src/item'),
}

// src/entry1.js
import {getG} from "aliasTest/common_____g";
  • 别名替换的规定是,咱们的门路innerRequest.startsWith("aliasTest"),也就是咱们门路的第一个单词必须是别名,能力失常匹配
  • 而后就是替换逻辑,将 "aliasTest" 替换为咱们在 webpack.config.js 配置的门路,造成新的门路newRequestStr
  • 最初替换咱们的 obj 数据中对应的 request 参数,而后进入下一个Plugin

AliasFieldPlugin

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

type normal loader
resolveOptions

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

// webpack.config.js
resolve: {
    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.js
module.exports = {
  //...
  resolve: {
    extensionAlias: {'.js': ['.ts', '.js'],
      '.mjs': ['.mts', '.mjs'],
    },
  },
};

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

RootsPlugin

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

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

// RootsPlugin apply
if (!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.js
module.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.js
module.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.js
exports.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 apply
const 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.js
module.exports = {
  //...
  resolve: {mainFiles: ['test'],
  },
};

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

// UseFilePlugin apply
const 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.js
module.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.js
module.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.js
module.exports = {
  //...
  resolve: {symlinks: true,},
};

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

// SymlinkPlugin apply
const 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 的最初一个单词被笼罩为 result
pathSegments[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,返回后果
退出移动版