likes
comments
collection
share

Webpack 中 enhanced-resolve 路径解析流程详解

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

作者:赵一鸣

前言

webpack 使用 enhanced-resolve 进行路径解析。它的作用类似于一个异步的 require.resolve 方法,将 require / import 语句中引入的字符串,解析为引入文件的绝对路径。

// 绝对路径
const moduleA = require('/Users/didi/Desktop/github/test-enhanced-resolve/src/moduleA.js')

// 相对路径
const moduleB = require('../moduleB.js')

// 模块路径,npm 包名或者是通过 alias 配置的别名
const moduleC = require('moduleC')

在其官方文档中,将其描述为高度可配置,这得益于它完善的插件系统。事实上,enhanced-resolve 的所有内置功能都是通过插件实现的。

webpack 如何集成 enhanced-resolve

在 WebpackOptionsApply.js 中,合并 webpack.config.js 中的 resolve 选项:

compiler.resolverFactory.hooks.resolveOptions
  .for("normal")
  .tap("WebpackOptionsApply", resolveOptions => {
    return Object.assign(
      {
        fileSystem: compiler.inputFileSystem
      },
      cachedCleverMerge(options.resolve, resolveOptions)
    );
  });
  
compiler.resolverFactory.hooks.resolveOptions
  .for("context")
  .tap("WebpackOptionsApply", resolveOptions => {
    return Object.assign(
      {
        fileSystem: compiler.inputFileSystem,
        resolveToContext: true
      },
      cachedCleverMerge(options.resolve, resolveOptions)
    );
  });
  
compiler.resolverFactory.hooks.resolveOptions
  .for("loader")
  .tap("WebpackOptionsApply", resolveOptions => {
    return Object.assign(
      {
        fileSystem: compiler.inputFileSystem
      },
      cachedCleverMerge(options.resolveLoader, resolveOptions)
    );
  });

创建 normalModuleFactory 和 contextModuleFactory 的时候传入 resolverFactory,normalModuleFactory 和 contextModuleFactory 在解析路径时就可以使用 enhanced-resolve 的功能。

createNormalModuleFactory() {
  const normalModuleFactory = new NormalModuleFactory(
    this.options.context,
    this.resolverFactory,
    this.options.module || {}
  );
  this.hooks.normalModuleFactory.call(normalModuleFactory);
  return normalModuleFactory;
}

createContextModuleFactory() {
  const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
  this.hooks.contextModuleFactory.call(contextModuleFactory);
  return contextModuleFactory;
}

用一张图来展示这部分流程:

 Webpack 中 enhanced-resolve 路径解析流程详解

核心功能

1、通过同步或异步的方式获取模块的绝对路径,并且可以判断模块是否存在。

 Webpack 中 enhanced-resolve 路径解析流程详解

create 方法允许我们传入 options,用于自定义解析规则。

 Webpack 中 enhanced-resolve 路径解析流程详解

2、继承 Tapable,对外暴露自定义插件功能,实现更灵活的模块解析规则。

3、灵活的自定义文件系统,enhanced-resolve 自带 NodeJsInputFileSystem、CachedInputFileSystem。

 Webpack 中 enhanced-resolve 路径解析流程详解

路径解析流程

以 5.4.0 版本为例,enhanced-resolve 的原理可以简单理解成是一个管道pipeline进行解析,从最初的地方传入要解析的路径,经过一个个插件解析,最终返回文件路径或报错。

在 Resolver.js 中,enhance-resolve 默认只有 4 个 hook:

class Resolver {
  constructor(fileSystem, options) {
    this.fileSystem = fileSystem;
    this.options = options;
    this.hooks = {
      // 每执行一个插件都会调用
      resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
      // 没有找到具体文件或目录
      noResolve: new SyncHook(["request", "error"], "noResolve"),
      // 开始解析
      resolve: new AsyncSeriesBailHook(
        ["request", "resolveContext"],
        "resolve"
      ),
      // 解析完成
      result: new AsyncSeriesHook(["result", "resolveContext"], "result")
    }
  }
}

可以看到,与解析流程相关的只有开始resolve结束result两个hook,其余都是在 ResolverFactory.js 中手动加入的。

resolver.ensureHook("resolve");
resolver.ensureHook("internalResolve");
resolver.ensureHook("newInteralResolve");
resolver.ensureHook("parsedResolve");
resolver.ensureHook("describedResolve");
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");

enhanced-resolve 允许我们通过传入配置和编写插件的形式非常灵活的自定义路径解析方式,以上 hooks 除了 resolve 和 result 两个钩子是固定的在开始和结束时被调用,其余的 hooks 可能没有固定的执行顺序

对于以下 demo 来说,hook 调用顺序是固定的:

const { CachedInputFileSystem, ResolverFactory } = require('enhanced-resolve')
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= './a'
myResolver.resolve(context, lookupStartPath, request, resolveContext, (err, path, result) => {
	if (err) {
    console.log('createResolve err: ', err)
  } else {
    console.log('createResolve path: ', path)
  }
});

在 ResultPlugin.js 中 debugger 看以上 demo 在解析路径的过程中调用了哪些 hooks:

 Webpack 中 enhanced-resolve 路径解析流程详解

以上 demo 调用 myResolver.resolve 时,在 resolve 方法内部主动调用了 doResolve 方法,并且使用 resolve 钩子。

class Resolver {
  resolve (context, path, request, resolveContext, callback) {
    // ...
    if (resolveContext.log) {
      const parentLog = resolveContext.log;
      const log = [];
      return this.doResolve(
        // ----------- 这里 -----------
        this.hooks.resolve,
        obj,
        message,
        {
          log: msg => {
            parentLog(msg);
            log.push(msg);
          },
          fileDependencies: resolveContext.fileDependencies,
          contextDependencies: resolveContext.contextDependencies,
          missingDependencies: resolveContext.missingDependencies,
          stack: resolveContext.stack
        },
        (err, result) => {
          if (err) return callback(err);

          if (result) return finishResolved(result);

          return finishWithoutResolve(log);
        }
      );
    } else {
      // ...
    }
  }
}

enhanced-resolve 通过不同的配置来初始化不同的插件,在插件内部注册一个 hook,然后使用 doResolve 方法调用下一个 hook 将整个解析流程串联起来。

 Webpack 中 enhanced-resolve 路径解析流程详解

插件编写方式:

一个插件依赖三个信息:

1、上游hook:上一个 hook 处理完信息,轮到我来接着处理,所以需要注册一个 hook 的 tap。

2、配置信息:在该 plugin 处理逻辑时,用到的参数。

3、下游hook:该 plugin 处理完逻辑时,通知下游 hook, 来 call 它注册的tap。

class ResolvePlugin {
  constructor (source, option, target) {
    this.source = source // 当前插件挂在哪个钩子下
    this.target = target // 触发的下一个钩子
    this.option = option
  }
  
  apply (resolver) {
    const target = resolver.ensureHook(this.target)
    
    resolver.getHook(this.source).tapAsync('ResolvePlugin', (request, resolveContext, callback) => {
      const resource = request.request
      const resourceExt = path.extname(request.request)
      const obj = Object.assign({}, request, {})
      const message = null

      // 触发下一个钩子
      resolver.doResolve(target, obj, message, resolveContext, callback)
    })
  }
}

在插件中可以自定义下一个要触发的钩子,所以 hooks 可能没有固定的执行顺序

Mpx 通过 enhanced-resolve 插件实现文件维度的条件编译

在团队自研的增强型跨端小程序框架 Mpx 中,也有对于enhanced-resolve的应用。

Mpx 支持以微信小程序语法为基础,通过读取用户传入的 mode 和 srcMode 来构建输出其他平台的小程序代码。但是不同平台的部分组件或 API 可能差异比较大,通过简单的 if / else 无法抹平差异。例如在滴滴出行小程序中微信转支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,这时我们可以在相同的位置新建一个 map.ali.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的 mode 来加载对应模块,当 mode 为ali时,会优先加载 map.ali.mpx,反之则会加载 map.mpx。

其原理就是通过自定义插件 AddModePlugin 实现对不同 mode 文件的优先匹配加载。

总结

webpack 使用 enhanced-resolve 模块进行路径解析,它是一个高度可配置的 require.resolve 路径解析器,使用对外暴露的选项和插件,实现自定义的路径查找规则。

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