likes
comments
collection
share

通过enhanced-resolve领略webpack的插件架构

作者站长头像
站长
· 阅读数 29

前言

大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.

我的宗旨就是 万物皆可手写

今天我们来看一看webpack内部的核心库enhanced-resolve,对webpack的可插拔架构有一个更深入的认知

新手创作不易,有问题欢迎指出和轻喷,谢谢


enhanced-resolve库

我们可以在webpack源码的依赖中看到这个玩意

// package.json
{
  "name": "webpack",
  "version": "5.74.0",
  "dependencies": {
    "enhanced-resolve": "^5.10.0",
    ...
}

我们也可以在webpack的主类Compiler中看到这个玩意

class Compiler {
    constructor(context, options = /** @type {WebpackOptions} */ ({})) {
    this.hooks = {}
    /** @type {ResolverFactory} */
    this.resolverFactory = new ResolverFactory();
    }
    ......
}

看一下Readme,可以发现,这个库主要做webpack模块的路径解析 可以对比node的 require.resolve 和我们的enhanced-resolve(增强版resolve)

增强的功能

原版require.resolve只能运行在node环境中,而且无法应对webpack多样化的打包需求.

  1. 可解析更多的后缀 比如./index没有后缀,node会解析.js,.json,.node等, 而本包可以配置解析更多的后缀名

  2. require.resolve的返回值只有一个完整路径,enhanced-resolve的返回值还包含了描述文件等较为丰富的数据。

  3. enhanced-resolve支持配置文件/文件夹别名

  4. require.resolve只会去解析文件的完整路径,但是enhanced-resolve既可以查询文件也可以查询文件夹。 .....

使用

基本

const { ResolverFactory, CachedInputFileSystem } = require("enhanced-resolve");
const fs = require("fs");
const path = require("path");

const myResolver = ResolverFactory.createResolver({ 
fileSystem: new CachedInputFileSystem(fs, 4000), 
extensions: [".json", ".js", ".ts"], 
})

const context = {};
const resolveContext = {};
const lookupStartPath = path.resolve(__dirname);
const request = "./test"; // 访问./test文件  获取路径

myResolver.resolve(
 context,
 lookupStartPath,
 request,
 resolveContext,
 (err, path, result) => {
   if (err) {
     console.log("createResolve err: ", err);
   } else {// 这里可获得解析好的路径
     console.log("createResolve path: ", path);
   }
 }
);

创建Resolver

resolverFactory是一个典型的工厂模式,通过options创建Resolver并进行修改,最终返回

  1. 首先我们解析配置options,创建Resolver,(CreateResolver),并挂载hooks
exports.createResolver = function (options) {
       // 解析options
   const normalizedOptions = createOptions(options);
   const {...} = normalizedOptions;
       // 准备plugins
   const plugins = userPlugins.slice();
       // new Resolver
   const resolver = customResolver
   	? customResolver
   	: new Resolver(fileSystem, normalizedOptions);
       // 给resolver挂载各种hooks
   resolver.ensureHook("resolve");
   ....
       
       }

  1. 其中ensureHook内部会创建一个AsyncSeriesBailHook类型的tapableHook并挂载到Resolver上, hook对应了不同的执行流程 AsyncSeriesBailHook是一个异步串行的 Hook:用于注册异步回调函数,并且这些回调函数将按照它们被注册的顺序依次执行。
ensureHook(name) {
       ...
       return (this.hooks[name] = new AsyncSeriesBailHook();
   }

画成图也就是这样 通过enhanced-resolve领略webpack的插件架构 3. resolver上的Hook大概有这些(2023年的版本) 而他们会被按照顺序依次执行(AsyncSeriesBailHook)

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

Hooks们订阅plugins

  1. 之后我们会将各种内置plugins (根据配置) 推入对应的hook中(hook订阅plugins), plugins会指定下一个流转的hook
// 创建并订阅ParsePlugin(先推入plugins数组)
 plugins.push(new ParsePlugin(// 
     "resolve",   // 订阅本插件的hook
     resolveOptions, 
     "parsed-resolve"// 流转的下一个hook
 ));
  
// 根据配置判断是否推入
if (options.alias.length > 0) {
     plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve"));
}
 // 推入NextPlugin到after-parsed-resolve Hook中
 plugins.push(new NextPlugin("after-parsed-resolve", "described-resolve"));
 // 其他插件
......

// 最后会批量订阅plugins数组中的插件到hook中(plugins内部订阅)
for (const plugin of plugins) {
	if (typeof plugin === "function") {
		plugin.call(resolver, resolver);
	} else {
		plugin.apply(resolver);
	}
}

  1. 每个plugins内部中都会传入我们的resolver,(想必写过webpack插件的小伙伴都懂,我们自己写的插件中的apply方法会直接接收compiler作为参数,这里则接收resolver)
class ParsePlugin {
   constructor(source, requestOptions, target) {}
    ... 
    apply(resolver) { 
        // 获取/创建下一个hook(target为传入的下一个hook名称)
       	const target = resolver.ensureHook(this.target);
	resolver // 获取当前hook,将逻辑tapAsync到该hook中
	  .getHook(this.source)
	  .tapAsync("ParsePlugin", (request, resolveContext, callback) => {
           //插件的处理逻辑
           ...
          }
    }
}

画成图也就是这样

通过enhanced-resolve领略webpack的插件架构

执行Resolver

此时Resolver已经创建完毕 ,经过了两大步骤

  1. 注册hooks
  2. hooks订阅各种plugins

还记得使用方法吗?

// 最终会执行resolver.resolve
 myResolver.resolve()

进去可以看到

resolve(context, path, request, resolveContext, callback) {
        // 最终返回doResolve
	return this.doResolve()
}

// doResolve最终会返回hook.callAsync,并执行回调
doResolve(){
    return hook.callAsync(request, innerContext, (err, result) => {
	if (err) return callback(err);
	if (result) return callback(null, result);
        // 在回调中会流转到下一个hook回调
        // 这个callback是tapableHook中触发下一个hook回调的方法,执行doResolve时传入
        // (详细请参考tapable中AsyncSeriseHook的用法)
	callback();
    })
}

画成流程图也就是这样

通过enhanced-resolve领略webpack的插件架构

最后我们可以结合几张图来看这个流程

1.hooks就像是弹夹,依次向下,一个弹夹打空了就换另一个

2.plugin就是子弹,通过回调依次射出(有些是通过nextPlugin进行流转)

3.Resolve方法就是机枪的启动开关

通过enhanced-resolve领略webpack的插件架构

Plugins

最后来看看作为子弹的plugins,他们分别都干了些什么? 可以看到在本库中,(可能)使用到的插件有这些

const AliasFieldPlugin = require("./AliasFieldPlugin");
const AliasPlugin = require("./AliasPlugin");
const AppendPlugin = require("./AppendPlugin");
const ConditionalPlugin = require("./ConditionalPlugin");
const DescriptionFilePlugin = require("./DescriptionFilePlugin");
const DirectoryExistsPlugin = require("./DirectoryExistsPlugin");
const ExportsFieldPlugin = require("./ExportsFieldPlugin");
const ExtensionAliasPlugin = require("./ExtensionAliasPlugin");
const FileExistsPlugin = require("./FileExistsPlugin");
const ImportsFieldPlugin = require("./ImportsFieldPlugin");
const JoinRequestPartPlugin = require("./JoinRequestPartPlugin");
const JoinRequestPlugin = require("./JoinRequestPlugin");
const MainFieldPlugin = require("./MainFieldPlugin");
const ModulesInHierarchicalDirectoriesPlugin = require("./ModulesInHierarchicalDirectoriesPlugin");
const ModulesInRootPlugin = require("./ModulesInRootPlugin");
const NextPlugin = require("./NextPlugin");
const ParsePlugin = require("./ParsePlugin");
const PnpPlugin = require("./PnpPlugin");
const RestrictionsPlugin = require("./RestrictionsPlugin");
const ResultPlugin = require("./ResultPlugin");
const RootsPlugin = require("./RootsPlugin");
const SelfReferencePlugin = require("./SelfReferencePlugin");
const SymlinkPlugin = require("./SymlinkPlugin");
const TryNextPlugin = require("./TryNextPlugin");
const UnsafeCachePlugin = require("./UnsafeCachePlugin");
const UseFilePlugin = require("./UseFilePlugin");

单个插件功能粒度很低,一般也就一百多行代码,可以理解为逻辑块, 这里简单介绍几个

NextPlugin 引导插件到下一个Hook

RootsPlugin 其作用是在解析模块的路径时,将源路径中以斜杠(/)开头的请求与一组根路径进行匹配,并将匹配到的路径替换成根路径加上请求路径而得到的新路径。

ImportsFieldPlugin 其作用是在描述文件中(如package.json)中查找imports字段并根据其内容进行路径解析。

ParsePlugin 请求路径解析插件

DescriptionFilePlugin 查找file的描述文件插件

AliasPlugin 别名解析插件

JoinRequestPlugin 可解析并输出输出path和ralitivePath

ConditionalPlugin 用于判断跳过某个步骤

后记

想必现在大家再看看webpack官网中的那句话 "webpack本身也是由大量的插件所组成的" 是否有了更好的理解

本人也手写了一个简易的enhance-resolve,使用在自己的手写miniWebpack中,并逐渐miniWebpack本体插件化。

想看一下具体的代码实现可以查看我的git仓库。

手写简易enhanceed-resolve