涨薪面技:写个 enhanced-resolve 插件(11)

149 阅读4分钟

一、前文回顾

本文主要接上文介绍了 enhance-resolve 中实现 resolve 功能的内部插件实现细节,本小结主要讨论了以下插件:

  1. SelfReferencePlugin:其作用是结合 exportsFields 配置项和 package.json.exports 字段改写默认的主入口以及子路径的解析规则;
  2. ModulesInHierarchicalDirectoriesPlugin:其作用是模拟 require.resolve 行为,从当前目录查找 node_moduels 一直到 / 下的这一行为;
  3. PnpPlugin:咱也不知道,也不想学(一言不合就开摆,就问你气不气😂)
  4. JoinRequestPartPlugin:将通过命名空间引用其包内部模块的 @ns/sub/module 语法 变成相对路径;
  5. DirectoryExistsPlugin:主要是判断截止到当前流程,path 是不是一个真实存在的目录;
  6. ExportsFieldPlugin:主要实现的是 package.json 中的 exports 字段相关能力的插件;
  7. UseFilePlugin:实现导入写只写到目录的情况下自动解析到该目录下的 index.js 能力;
  8. MainFieldPlugin:实现 package.json 中 main 字段相关能力的插件;

二、内部插件

这里主要接上文继续讲解 ehanced-resolve 内部实现 resolve 能力的插件!

2.19 TryNextPlugin

就是单纯的引导到下个钩子。。。看着个 NextPlugin 差不的啊。。。。what????

module.exports = class TryNextPlugin {

 constructor(source, message, target) {
  this.source = source;
  this.message = message;
  this.target = target;
 }


 apply(resolver) {
  const target = resolver.ensureHook(this.target);
  resolver
   .getHook(this.source)
   .tapAsync("TryNextPlugin", (request, resolveContext, callback) => {
    resolver.doResolve(
     target,
     request,
     this.message,
     resolveContext,
     callback
    );
   });
 }
};

2.20 AppendPlugin

给 path 和 relativePath 追加各个可能得扩展名?加完不再验证一下子是否存在? 后面的流水线中会有这个过程,用来校验这些路径对应的文件是否真实存在!

module.exports = class AppendPlugin {
 constructor(source, appending, target) {
  this.source = source;
  this.appending = appending;
  this.target = target;
 }


 apply(resolver) {
  const target = resolver.ensureHook(this.target);
  resolver
   .getHook(this.source)
   .tapAsync("AppendPlugin", (request, resolveContext, callback) => {
    const obj = {
     ...request,
     path: request.path + this.appending, // 追加扩展名
     relativePath:
      request.relativePath && request.relativePath + this.appending // 追加扩展名
    };
    resolver.doResolve(
     target,
     obj,
     this.appending,
     resolveContext,
     callback
    );
   });
 }
};

2.21 FileExistsPlugin

获取文件路径,调用 fs.stat 获取文件的 stat 对象:

  1. stat 显示 path 不是个文件,加入 missingDependencies;
  2. 正确得 path 对应的文件,加入 fileDependencies 中;
module.exports = class FileExistsPlugin {

 constructor(source, target) {
  this.source = source;
  this.target = target;
 }

 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 获取文件的 stat 对象
    fs.stat(file, (err, stat) => {
     if (err || !stat) {
      // 出错,加入 missingDependencies
      if (resolveContext.missingDependencies)
       resolveContext.missingDependencies.add(file);
      if (resolveContext.log) resolveContext.log(file + " doesn't exist");
      return callback();
     }
     if (!stat.isFile()) {
      // stat 显示 path 不是个文件,加入 missingDependencies
      if (resolveContext.missingDependencies)
       resolveContext.missingDependencies.add(file);
      if (resolveContext.log) resolveContext.log(file + " is not a file");
      return callback();
     }
     
     // 正确得 path 对应的文件,加入 fileDependencies 中
     if (resolveContext.fileDependencies)
      resolveContext.fileDependencies.add(file);
     resolver.doResolve(
      target,
      request,
      "existing file: " + file,
      resolveContext,
      callback
     );
    });
   });
 }
};

2.22 SymlinkPlugin

将 path 当做一个 symlink 尝试读取,读到内容就是 symlink,读不到就不是符号链接。


module.exports = class SymlinkPlugin {

 constructor(source, target) {
  this.source = source;
  this.target = target;
 }

 
 apply(resolver) {
  const target = resolver.ensureHook(this.target);
  const fs = resolver.fileSystem;
  resolver
   .getHook(this.source)
   .tapAsync("SymlinkPlugin", (request, resolveContext, callback) => {
    if (request.ignoreSymlinks) return callback();
    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);
      // 将这个 path 当成 symlink 然后读取,读取到内容就认定是符号链接
      // 读不到就不是
      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
      );
     }
    );
   });
 }
};

2.23 ResultPlugin

触发 resolver.hooks.result 钩子,传入 obj 和 resolverContext,obj.path 就是最后的解析路径结果。

module.exports = class ResultPlugin {
 constructor(source) {
  this.source = source;
 }

 apply(resolver) {
  this.source.tapAsync(
   "ResultPlugin",
   (request, resolverContext, callback) => {
    const obj = { ...request };
    if (resolverContext.log)
     resolverContext.log("reporting result " + obj.path);
    // 触发 resolver.hooks.result 钩子
    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);
     }
    });
   }
  );
 }
};

三、总结

本文完成了 ehanced-resolve 的所有插件注册工作,最后总结一下今天学习的各个插件的作用:

  1. SelfReferencePlugin:作用是结合 exportsFields 配置项和 package.json.exports 改写默认的 main 和子路径的(映射成别的路径);
  2. ModulesInHierarchicalDirectoriesPlugin:这个插件用作从当前目录一直向上查 node_modules,一直找到 / 目录下的 node_modules 终止,中间如果找到就中断;
  3. PnpPlugin:这个我也不会,也懒得看 👻👻👻👻👻👻👻👻
  4. JoinRequestPartPlugin:这个插件简化 @namespace/a/b/c 这种 request 为相对路径;
  5. DirectoryExistsPlugin:这个插件的作用主要是判断截止到当前流程,path 是不是一个真实存在的目录;
  6. ExportsFieldPlugin:结合 package.json.exports 字段是用于重新定义模块的导出规则;
  7. UseFilePlugin:处理导入目录时自动解析到目录下的 index.js;
  8. MainFieldPlugin:这个插件的作用是把包名和 package.json.main 字段解析拼接;
  9. TryNextPlugin:就是单纯的引导到下个钩子;
  10. AppendPlugin:给 path 和 relativePath 追加各个可能得扩展名;
  11. FileExistsPlugin:获取文件路径,检查其对应的 fs.stat 结果;
  12. SymlinkPlugin:将 path 当做一个 symlink 尝试读取,读到内容就是 symlink;
  13. ResultPlugin:触发 resolver.hooks.result 钩子,返回解析结果;

四、enhanced-resolve 工作全流程

这里结合一张流程图了解一下 enhanced-reolve 的工作全貌:

enhanced-resolve 工作流.png

到这里能回答这问题了:为啥有 enhanced-resolve?

这个问题我曾困惑了很久:因为完备且具备扩展性;

我一直很好奇为啥有 require.resolve() 就能解决的问题,为啥还要整这么多的活儿? 起初我以为是效率,后来证明是错误的,使用 enhanced-resolve 非但不快反而慢。后来经大神提示是因为完备可扩展。

enhanced-resolve 可以提升 resolve 效率,并且支持 alias 在内的各种牛逼的特效,不但可以查询,还可以便捷的更改。