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 路徑解析器,使用對外暴露的選項和插件,實現自定義的路徑查找規則。

相關文章
相關標籤/搜索