webpack 流程解析 (5) module reslove

csywweb發表於2021-10-26

前言

上文說道我們拿到了構建modlule的factory,和依賴等關鍵資料,通過addModuleTree經過factorizeQueue的控制走到了factory.create。這個時候就開始了reslove過程。
本文主要分析,NormalModuleFactory 內部 beforeResolve,factorize,resolve, afterResolve 這幾個鉤子。

配置檔案

本文圍繞的配置檔案如下:

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.(js?|tsx?|ts?)$/,
        use: [
            {
                loader: 'babel-loader',
            },
        ],
    },
    ]
  },
  resolve: {
    extensions: ['.js', '.ts'],
    alias: {
      demo: path.resolve(__dirname, 'src/second'),
    },
  },
};

factory.create

入口從factory.create開始,這裡的 factory 是之前 addModuleTree 獲取到的 NormalModuleFactory
image.png

NormalModuleFactory 先觸發了其內部的 beforeResolve 鉤子,然後在回撥裡執行了 factorize 鉤子函式。
factorize 鉤子內有又呼叫了resolve

這裡看起來比較繞,簡單解釋一下:
鉤子的呼叫順序,就像是這樣。
beforeResolve -> factorize -> resolve

  • beforeResolve 沒找到之前註冊過的地方,看起來什麼都沒幹,也有可能是我沒找到
  • factorize 之前在 ExternalModuleFactoryPlugin外掛中註冊過,這裡會處理下external的資訊。
  • resolve 鉤子註冊在 NormalModuleFactory 內部,用於解析這個module,生成對應的loader和依賴資訊,這裡的重點就在resolve

resolve

getLoaderResolver

進來的第一步resolve 鉤子先呼叫了this.getResolver("loader") 返回loaderResolver,這個可以理解為是解析loader的方法。
簡單過一下分為以下幾步:

  • 呼叫到了 ResolverFactory裡的get方法
  • 判斷是否有對應型別的快取
  • 建立 resolveOptions,
  • 呼叫 require("enhanced-resolve").ResolverFactory 建立了一個 resolver,然後返回 NormalModuleFactory繼續執行程式碼。
const loaderResolver = this.getResolver("loader");

loaderResolver暴露了一個resolver方法,用於解析loader

normalResolver

接著往下走,略過一些判斷,直接走到了defaultResolve這個方法,這裡會根據webpack配置檔案中的resolve選項,生成一個 normalResolver。同樣的,這個normalResolver也是require("enhanced-resolve").ResolverFactory的例項,也暴露出了一個resolve方法。

const normalResolver = this.getResolver(
    "normal",
    dependencyType
        ? cachedSetProperty(
            resolveOptions || EMPTY_RESOLVE_OPTIONS,
            "dependencyType",
            dependencyType
      )
    : resolveOptions
);

接下來會把這個normalResolver 和一些上下文資訊傳給resolveResource方法,這裡最終會呼叫到node_modules/enhanced-resolve/lib/Resolver.jsdoResolve

this.resolveResource(
    contextInfo,
    context,
    unresolvedResource,
    normalResolver,
    resolveContext,
    (err, resolvedResource, resolvedResourceResolveData) => {
        if (err) return continueCallback(err);
        if (resolvedResource !== false) {
            resourceData = {
                resource: resolvedResource,
                data: resolvedResourceResolveData,
                ...cacheParseResource(resolvedResource)
            };
        }
        continueCallback();
    }
);

然後根據doResolve返回的resolvedResourceresolvedResourceResolveData一起拼裝成resourceData。我們在後續解析loader的時候還會用到這個。

resourceData資料結構
image.png

解析loader

resolvedResource的回撥裡繼續執行

const result = this.ruleSet.exec({
    resource: resourceDataForRules.path,
    realResource: resourceData.path,
    resourceQuery: resourceDataForRules.query,
    resourceFragment: resourceDataForRules.fragment,
    scheme,
    assertions,
    mimetype: matchResourceData
    ? ""
    : resourceData.data.mimetype || "",
    dependency: dependencyType,
    descriptionData: matchResourceData
    ? undefined
    : resourceData.data.descriptionFileData,
    issuer: contextInfo.issuer,
    compiler: contextInfo.compiler,
    issuerLayer: contextInfo.issuerLayer || ""
});

這裡會根據配置檔案裡的rules得到需要的loader,這個例子裡,我們的result
image.png

接下來會通過這個result的遍歷,生成useLoadersPost, useLoaders, useLoadersPre
然後呼叫resolveRequestArray得到postLoaders, normalLoaders, preLoaders

this.resolveRequestArray(
  contextInfo,
  this.context,
  useLoaders,
  loaderResolver,
  resolveContext,
  (err, result) => {
    normalLoaders = result;
    continueCallback(err);
  }
);

當前例子並沒有postLoaderspreLoaders,這裡只有normalLoadersresolveRequestArray內部呼叫loaderResolver.resolve解析useLoaders,最後結果就是把result裡的loader替換成了對應的真實檔案地址。

{
  ident:undefined
  loader:'/Users/csy/Code/webpack5/node_modules/babel-loader/lib/index.js'
  options:undefined
}

生成回撥資料

最後在continueCallback處理下已經生成好的資料,首先是對loader的合併。把postLoaders, normalLoaders, preLoaders這幾個合併。然後assign一下data.createData, 這個data來自於鉤子的入口傳入的data。

Object.assign(data.createData, {
    layer:
        layer === undefined ? contextInfo.issuerLayer || null : layer,
    request: stringifyLoadersAndResource(
        allLoaders,
        resourceData.resource
    ),
    userRequest,
    rawRequest: request,
    loaders: allLoaders,
    resource: resourceData.resource,
    context:
        resourceData.context || getContext(resourceData.resource),
    matchResource: matchResourceData
        ? matchResourceData.resource
        : undefined,
    resourceResolveData: resourceData.data,
    settings,
    type,
    parser: this.getParser(type, settings.parser),
    parserOptions: settings.parser,
    generator: this.getGenerator(type, settings.generator),
    generatorOptions: settings.generator,
    resolveOptions
});

這裡著重講一下getParsergetGenerator, 這兩個方法返回的是對應檔案的解析器和構建模板的方法。按照當前示例,返回的是JavascriptParserJavascriptGenerator

然後這個createData將被用於createModule
在執行完NormalModuleFactoryafterResolve鉤子後

const createData = resolveData.createData;
this.hooks.createModule.callAsync(//something)

reslove結束了,即將開始下一步,建立module!

小結

  • module resolve 流程用於獲得各 loader 和模組的絕對路徑等資訊。
  • resolver鉤子裡,先通過 enhanced-resolve 獲取 loaderResolver,提供 resolve 方法
  • defaultResolve 方法裡,獲取 normalResolver, 提供 resolve 方法。
  • 解析 unresolvedResource,得到檔案的絕對路徑等資訊
  • 根據rules得到 loader
  • 使用 loaderResolver 得到loader的絕對路徑等資訊
  • 合併 loader, 拼接資料,
  • 呼叫 NormalModuleFactoryafterResolve鉤子,結束 resolve 流程。

相關文章