“完备OR可扩展?”,webpack Resolver:“鱼和熊掌我都要!”

100 阅读5分钟

一、全流程

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

enhanced-resolve 工作流.png

一、注册流水线

我们大致回故一下从今天的流水线内容:

  1. resolve 开始解析:注册 UnsafeCachePlugin、ParsePlugin 插件;
  2. paresed-resolve:request 解析阶段;
  3. described-resolve 描述文件已解析;
  4. raw-resolve: 原始解析阶段,处理 alias/aliasFields/extension/extensionAlias;
  5. normal-resolve: 普通解析阶段,处理 preferRealative、preferAbsolute
  6. internal: 内部解析阶段,处理 importsFields 选项声明的能力;
  7. raw-module 阶段,处理 resolve.modules、exportsFields 选项声明的能力;
  8. module:处理带有 @ 符号的路径;
  9. resolve-as-module:处理 resolveToContext 配置项目;
  10. undescribed-resolve-in-package:重新读取 package.json;
  11. resolve-in-package:处理 mexportsFields 配置;
  12. resolve-in-existing-directory:
  13. relative:注册 DescriptionFilePlugin 插件;
  14. described-relative:target 为 directory hook;
  15. directory:注册 DirectoryExistsPlugin 插件;
  16. undescribed-existing-directory:根据有无 resolveToContext 配置注册不同插件;
  17. described-existing-directory:注册 MainFieldPlugin 和 UseFilePlugin;
  18. raw-file:处理扩展名相关,尝试匹配;
  19. file:处理 alias
  20. final-file:处理各种 dependencies;
  21. resolved:处理 restrictions 配置,符合就返回否则报错;

二、注册插件

本文按照 ehanced-resolve 执行的流程注讲解其插件系统,共包含了 23 个插件,以下为各个插件及其作用:

  1. ParsePlugin:该插件用于解析原始的 request;
  2. DescriptionFilePlugin:读取 package.json 文件;
  3. NextPlugin:从一个 source 钩子推进到 target 钩子;
  4. AliasPlugin:处理 fallback、alias 等;
  5. AliasFieldPlugin:处理 package.json.browser 字段;
  6. ExtensionAliasPlguin:拼接各个扩展名并尝试解析;
  7. JoinRequestPlugin:拼接请求,把 request.path 和 request 拼接;
  8. ConditionalPlugin:判断本次 request 的类型是否满足条件;
  9. RootsPlugin: 把的第一个 /(表示根目录) 改写成 resolve.roots 设定的目录;
  10. ImportsFieldPlugin:这个插件用于处理包的内部 request;
  11. SelfReferencePlugin:作用是结合 exportsFields 配置项和 package.json.exports 改写默认的 main 和子路径的(映射成别的路径);
  12. ModulesInHierarchicalDirectoriesPlugin:这个插件用作从当前目录一直向上查 node_modules,一直找到 / 目录下的 node_modules 终止,中间如果找到就中断;
  13. PnpPlugin:这个我也不会,也懒得看 👻👻👻👻👻👻👻👻
  14. JoinRequestPartPlugin:这个插件简化 @namespace/a/b/c 这种 request 为相对路径;
  15. DirectoryExistsPlugin:这个插件的作用主要是判断截止到当前流程,path 是不是一个真实存在的目录;
  16. ExportsFieldPlugin:结合 package.json.exports 字段是用于重新定义模块的导出规则;
  17. UseFilePlugin:处理导入目录时自动解析到目录下的 index.js;
  18. MainFieldPlugin:这个插件的作用是把包名和 package.json.main 字段解析拼接;
  19. TryNextPlugin:就是单纯的引导到下个钩子;
  20. AppendPlugin:给 path 和 relativePath 追加各个可能得扩展名;
  21. FileExistsPlugin:获取文件路径,检查其对应的 fs.stat 结果;
  22. SymlinkPlugin:将 path 当做一个 symlink 尝试读取,读到内容就是 symlink;
  23. ResultPlugin:触发 resolver.hooks.result 钩子,返回解析结果;

三、思考

本文到治理算是完成了 ehanced-resolve 的全部讲解工作,最后我们回过头来思考一个问题:

为啥要有 enhanced-resolve?

这个问题我曾困惑了很久,到这里能回答这问题了:

答:因为完备且具备扩展性;

3.1 模块规范的差异

说到解析规则,大家对 require.resolve 一定不陌生。初次接触 ehanced-resolve 的时候,我一直很好奇为啥有 require.resolve() 就能解决的问题,为啥还要整这么多的活儿?

起初我以为是效率,后来证明是错误的,使用 enhanced-resolve 非但不快反而慢。后来经大神提示是因为完备可扩展。

前端因为其特殊的历史原因,ECMA 早期没有实现模块的规范,导致历史上的模块规范都是社区的产物,包括 AMD/CMD/UMD.... 各家规范相对独立,像前面提及的 require.resolve 只是 CommonJS 在 Node.JS 内部实现的解析,其他环境则没有这个能力,或者需要 runtime 去实现一套。

而 webpack 作为构建工具,没打算限制用户的模块规范。因此它需要去抹平各个规范下的模块定义和解析规则,使得无论用户使用哪种规范,webpack 都能正确的理解并在打包时获取到符合用户预期的模块。

ehanced-resolve 完全实现了 CommonJS 和 ESM 的解析规则,无论你的项目中使用某一种或者某几种,webpack 借助 ehanced-resolve 都可以覆盖掉方方面面的路径解析场景。

3.2 可扩展性

思考一下,你在日常开发中遇到过哪些需要处理路径解析的场景?

  1. 使用别名,简化导入时的路径,提升开发效率,降低错误引用的几率;
  2. 跨平台项目构建时,自动取用不同对应平台配置;
  3. 极端场景下,用自己的包替代三方包或者其中的某些模块;
  4. 出于某种原因,你需要修改模块的解析结果路径;
  5. ....

这只是列举了几个很常见场景的例子,那在日常开发中,enhanced-resolve 可以扩展模块解析 这个阶段的能力。比如 webpack 通过配置支持 alias、extension 在内的各种简洁的能力,大大降低开发成本。

此外,enhanced-resolve 创建的 Resolver 同样继承自 tappable 意味着它内部提供的这些钩子同样可以订阅,这就为我们开发者打开了介入解析工作流程的机会。

比如,你有一个跨平台的项目,当 mode=wx 时,自动取用 .wx.js 后缀的模块,mode=ali 时自动取用 .ali.js 模块,当然如果都没有再取用普通的 .js 模块;

这个例子取用的是 Mpx 框架中的已经实现的能力:Mpx 中的 Mode 支持

module.exports = class AddModePlugin {
  constructor (source, mode, fileConditionRules, target) {
    this.source = source
    this.target = target
    this.mode = mode
    this.fileConditionRules = fileConditionRules
  }

  apply (resolver) {
    const target = resolver.ensureHook(this.target)
    const mode = this.mode
    resolver.getHook(this.source).tapAsync('AddModePlugin', (request, resolveContext, callback) => {
      if (request.mode || request.env) {
        return callback()
      }
      const obj = {
        mode
      }
      const resourcePath = request.path
      let extname = ''
      if (resourcePath.endsWith(JSON_JS_EXT)) {
        extname = JSON_JS_EXT
      } else {
        extname = path.extname(resourcePath)
      }
      // 当前资源没有后缀名或者路径不符合fileConditionRules规则时,直接返回
      if (!extname || !matchCondition(resourcePath, this.fileConditionRules)) return callback()
      const queryObj = parseQuery(request.query || '?')
      queryObj.mode = mode
      queryObj.infix = `${queryObj.infix || ''}.${mode}`
      obj.query = stringifyQuery(queryObj)
      obj.path = addInfix(resourcePath, mode, extname)
      obj.relativePath = request.relativePath && addInfix(request.relativePath, mode, extname)
      resolver.doResolve(target, Object.assign({}, request, obj), 'add mode: ' + mode, resolveContext, callback)
    })
  }
}

除了前面提及的完备性,webpack 强大可扩展性在路径解析这个领域得到了延续!