likes
comments
collection
share

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

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

一、前文回顾

上文按照 ehanced-resolve 执行的流程注讲解其插件系统,这篇主要介绍了前 10 个插件:

  1. ParsePlugin:该插件用于解析原始的 request;
  2. DescriptionFilePlugin:读取 package.json 文件;
  3. NextPlugin:从一个 source 钩子推进到 target 钩子;
  4. AliasPlugin:处理 fallback、alias 等;
  5. AliasFieldPlugin:处理 package.json.browser 字段;

二、流水线注册的插件(2)

2.1 ExtensionAliasPlguin

该插件的实现很简单,改写原有的 request,将其中的扩展名部分改写成配置的扩展名进行尝试。

const forEachBail = require("./forEachBail");


module.exports = class ExtensionAliasPlugin {

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

 /**
  * @param {Resolver} resolver the resolver
  * @returns {void}
  */
 apply(resolver) {
  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) => {
     // 改写 request,将原有的扩展名替换掉,重新执行 resolve
     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);
    };
    // 根据 alias 配置不同情况处理
    // 1. alias 配置是一个字符串 extension: '.js', alias: '.ts' 
    if (typeof alias === "string") {
     resolve(alias, stoppingCallback);
    } else if (alias.length > 1) {
     // 2. alias 配置一个数组且长度大于1: extension '.js' alias: ['.jsx', '.tsx', '.mjs']
     forEachBail(alias, resolve, stoppingCallback);
    } else {
     // 3. alias 配置一个数组长度<= 1 的情况:extension '.js' alias: ['.jsx']
     resolve(alias[0], stoppingCallback);
    }
   });
 }
};

2.2 JoinRequestPlugin

拼接请求,把 request.path 和 request 拼接,相当于重新设定了 path,然后如果有 relativePath 同样拼接,还要置空原始 request。最后执行目标钩子。

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

 apply(resolver) {
  const target = resolver.ensureHook(this.target);
  resolver
   .getHook(this.source)
   .tapAsync("JoinRequestPlugin", (request, resolveContext, callback) => {
    const obj = {
     ...request,
     path: resolver.join(request.path, request.request), // request.path 是最终 resolver.resolve() 方法的路径解析结果
     relativePath: // 如果有 relativePath 就改写一次拼接当前的 request
      request.relativePath &&
      resolver.join(request.relativePath, request.request),
     request: undefined // 置空原始 request
    };
    resolver.doResolve(target, obj, null, resolveContext, callback);
   });
 }
};

2.3 ConditionalPlugin

判断本次 request 的类型是否满足条件(test),目前已有的匹配条件(test):

  1. { moduel: true }
  2. { internal: true }
  3. { directory: false, request: "." }
module.exports = class ConditionalPlugin {

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


 apply(resolver) {
  const target = resolver.ensureHook(this.target);
  const { test, message, allowAlternatives } = this;
  const keys = Object.keys(test);
  resolver
   .getHook(this.source)
   .tapAsync("ConditionalPlugin", (request, resolveContext, callback) => {
    // test { module: true } 
    // test { internal: true }
    for (const prop of keys) {
     // 不满足条件就不引导到目标钩子
     // 通过这种方式作为条件判断吗
     if (request[prop] !== test[prop]) return callback();
    }
    resolver.doResolve(
     target,
     request,
     message,
     resolveContext,
     allowAlternatives
      ? callback
      : (err, result) => {
        if (err) return callback(err);

        // Don't allow other alternatives
        if (result === undefined) return callback(null, null);
        callback(null, result);
        }
    );
   });
 }
};

2.4 RootsPlugin

替换 obj 中的 path,是把 '/some/module.js' 中的第一个 /(表示根目录) 改写成 resolve.roots 指向的目录,相当于以 resolve.roots 作为根目录查找。

const forEachBail = require("./forEachBail");

class RootsPlugin {

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

 /**
  * @param {Resolver} resolver the resolver
  * @returns {void}
  */
 apply(resolver) {
  const target = resolver.ensureHook(this.target);

  resolver
   .getHook(this.source)
   .tapAsync("RootsPlugin", (request, resolveContext, callback) => {
    const req = request.request;
    if (!req) return callback();
    if (!req.startsWith("/")) return callback();

    forEachBail(
     this.roots,
     (root, callback) => {
      const path = resolver.join(root, req.slice(1)); // 把第一个 / 替换成 root 对应的路径;
      const obj = {
       ...request,
       path, // path 是最终解析结果
       relativePath: request.relativePath && path
      };
      resolver.doResolve(
       target,
       obj,
       `root path ${root}`,
       resolveContext,
       callback
      );
     },
     callback
    );
   });
 }
}

module.exports = RootsPlugin;

2.5 ImportsFieldPlugin

这个插件用于处理包的内部 request 的,听着很复杂,其实干了一件很简单的事儿: 首先说内部请求:#anyKey 就是一个内部请求;在某些包的 package.json 定义了 imports 字段,值是个对象,这个对象中的 key 就是以 # 开头的字符串,你可以认为这就某个模块的别名,只不过这个模块只能在这个包内部以 #anyKey 的形式导入。

而这个插件的作用就是把以 # 开头的内部 request,替换成 package.json.imports[#anyKey] 对应的真实路径然后去解析。

鉴于全网搜了半天没有找到一个靠谱的例子,我自己鼓捣写了一个(这个特性没啥用,反正我的 node_modules 下面没有搜到一个用过这玩意儿的)。。。。

不过也得吐槽一下全网用机翻写webpack 相关博客的人,瞎JB翻译一通就发表,连个例子都不写

2.10.1 内部请求示例

  • webpack.config.js
resolve: {
  importsFields: ['imports']
},
  • src/index.js

    import g from 'qiang-sheng-group'
    console.log(g);
  • node_modules/qiang-sheng-group 包的文件树:

    .
    ├── lib
    │   ├── gaoqiqiang.js
    │   ├── tangxiaohu.js
    │   └── tangxiaolong.js
    └── package.json
  • node_modules/qiang-sheng-group/package.json 文件
{
  "name": "qiang-sheng-group",
  "version": "1.0.0",
  "description": "我怕风浪大?风浪越大,鱼越贵!",
  "main": "gaoqiqiang.js",
  "scripts": {
    "test": "anxin"
  },
  "imports": {
    "#xiaolong": "./lib/tangxiaolong.js", // imports 字段定义
    "#xiaohu": "./lib/tangxiaohu.js"
  },
  "keywords": [
    "see",
    "you",
    "again"
  ],
  "author": "Mr.Gao",
  "license": "ISC"
}

  • node_modules/qiang-sheng-group/lib/gaoqiqiang.js 采用 #xiaolong 调用内部模块:
import Xiaolong from '#xiaolong'; // 这会发起一个 内部 request

console.log(Xiaolong)

export default {
  ok: '就让这大风吹,大风吹~'
}

2.10.2 ImportsPlguin 实现细节

这是 debugger 运行中实例的 importsField,可以看到和上面 package.json.imports 是对应的: 涨薪面技:写个 enhanced-resolve 插件(9)

插件代码实现:

const path = require("path");
const DescriptionFileUtils = require("./DescriptionFileUtils");
const forEachBail = require("./forEachBail");
const { processImportsField } = require("./util/entrypoints");
const { parseIdentifier } = require("./util/identifier");
const { checkImportsExportsFieldTarget } = require("./util/path");


const dotCode = ".".charCodeAt(0);

module.exports = class ImportsFieldPlugin {

 constructor(
  source,
  conditionNames,
  fieldNamePath,
  targetFile,
  targetPackage
 ) {
  this.source = source;
  this.targetFile = targetFile;
  this.targetPackage = targetPackage;
  this.conditionNames = conditionNames;
  this.fieldName = fieldNamePath;
  /** @type {WeakMap<any, FieldProcessor>} */
  this.fieldProcessorCache = new WeakMap();
 }


 apply(resolver) {
  const targetFile = resolver.ensureHook(this.targetFile);
  const targetPackage = resolver.ensureHook(this.targetPackage);

  resolver
   .getHook(this.source)
   .tapAsync("ImportsFieldPlugin", (request, resolveContext, callback) => {
    // When there is no description file, abort
    if (!request.descriptionFilePath || request.request === undefined) {
     return callback();
    }

    // 获取内部请求 例如 gaoqiqiang.js 中的 #xiaolong
    const remainingRequest =
     request.request + request.query + request.fragment;
  
     // 获取 package.json.imports 字段对应的映射对象 { "#xiaolong": "./lib/tangxiaolong.js", .... }
    const importsField = DescriptionFileUtils.getField(
     request.descriptionFileData,
     this.fieldName
    );
    if (!importsField) return callback();

    if (request.directory) {
     return callback(new Error());
    }

    let paths;

    try {
     
     let fieldProcessor = this.fieldProcessorCache.get(
      request.descriptionFileData
     );
     if (fieldProcessor === undefined) {
      fieldProcessor = processImportsField(importsField);
      this.fieldProcessorCache.set(
       request.descriptionFileData,
       fieldProcessor
      );
     }
     // 调用 filedProcessor 这个字段处理函数,
     // 结合 remainingRequest 和 this.conditionNames 获取到这个 internal request 对应的路径数组: ["./lib/tangxiaolong.js"]
     paths = fieldProcessor(remainingRequest, this.conditionNames);
    } catch (err) {
     if (resolveContext.log) {
      resolveContext.log(
       `Imports field in ${request.descriptionFilePath} can't be processed: ${err}`
      );
     }
     return callback(err);
    }

    if (paths.length === 0) {
     return callback(new Error());
    }

    // 遍历得到的 path 数组
    forEachBail(
     paths,
     (p, callback) => {
      const parsedIdentifier = parseIdentifier(p);

      if (!parsedIdentifier) return callback();

      const [path_, query, fragment] = parsedIdentifier;

      const error = checkImportsExportsFieldTarget(path_);

      if (error) {
       return callback(error);
      }
      // 根据路径的开头的第一个字符决定
      switch (path_.charCodeAt(0)) {
       // should be relative
       case dotCode: {
        // 如果以 . 开头当成相对路径解析
        const obj = {
         ...request,
         request: undefined,
         path: path.join(
          /** @type {string} */ (request.descriptionFileRoot),
          path_
         ),
         relativePath: path_,
         query,
         fragment
        };

        resolver.doResolve(
         targetFile,
         obj,
         "using imports field: " + p,
         resolveContext,
         callback
        );
        break;
       }

       // package resolving 否则当做包解析
       default: {
        const obj = {
         ...request,
         request: path_,
         relativePath: path_,
         fullySpecified: true,
         query,
         fragment
        };

        resolver.doResolve(
         targetPackage,
         obj,
         "using imports field: " + p,
         resolveContext,
         callback
        );
       }
      }
     },
     (err, result) => callback(err, result || null)
    );
   });
 }
};

三、总结

本文接着上文详细描述了包括 ExtensionAliasPlguin、JoinRequestPlugin、ConditionalPlugin、RootsPlugin 和 ImportsFieldPlugin 插件的具体工作原理,以下为各个插件的作用:

  1. ExtensionAliasPlguin:拼接各个扩展名并尝试解析;
  2. JoinRequestPlugin:拼接请求,把 request.path 和 request 拼接;
  3. ConditionalPlugin:判断本次 request 的类型是否满足条件;
  4. RootsPlugin: 把的第一个 /(表示根目录) 改写成 resolve.roots 设定的目录;
  5. ImportsFieldPlugin:这个插件用于处理包的内部 request; 最后我们还用 强盛集团 的例子讲了一下内部 request 的处理过程,这是全网最全的讲解了;

到这里,有关 enhanced-resolve 这个库的流水线部分注册的所有内部插件的细节部分就完结了,这部分内容相当抽象,调试过程也不是很顺利,希望各位看官有耐心啊~

转载自:https://juejin.cn/post/7366527815292256290
评论
请登录