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

做者:赵一鸣javascript

前言

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

// 绝对路径
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

webpack 如何集成 enhanced-resolve

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

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 的功能。github

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;
}
复制代码

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

image

核心功能

一、经过同步或异步的方式获取模块的绝对路径,而且能够判断模块是否存在。npm

image

create 方法容许咱们传入 options,用于自定义解析规则。json

image

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

三、灵活的自定义文件系统,enhanced-resolve 自带 NodeJsInputFileSystem、CachedInputFileSystem。微信小程序

image

路径解析流程

以 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:

image

以上 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 将整个解析流程串联起来。

image

插件编写方式:

一个插件依赖三个信息:

一、上游hook:上一个 hook 处理完信息,轮到我来接着处理,因此须要注册一个 hook 的 tap。

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

三、下游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 路径解析器,使用对外暴露的选项和插件,实现自定义的路径查找规则。

相关文章
相关标签/搜索