Skip to content

resolve 的流程

在 webpack 中一切皆模块,在模块中我们可以通过模块化的方案去导出或者引入其他的模块。其中在 webpack 对于引入其他的模块提供了好几种方式

  1. import "./a.js"

  2. import "./a"

  3. import "@/a.js"(@ 符号通过 webpack 配置中 alias 设置)

  4. import "lodash/merge"

那么 node 如何知道上面 @ 、a、lodash 这三种分别对应什么文件夹、或者什么文件? 这就是 resolve 处理的部分。

当然其不仅仅正对于我们代码中的模块,还有配置中的 loader,只要涉及到文件的路径那么就依赖这个 resolve 去处理

webpack 官方配置

js
let config = {
  resolve: {
    extensions: ["*", ".js", ".json"],
    alias: {
      "@": path.resolve(__dirname, "src/"),
      vue$: path.resolve(__dirname, "src/"),
    },
  },
  // 这组选项与上面的 resolve 对象的属性集合相同,但仅用于解析 webpack 的 loader 包。
  resolveLoader: {
    modules: ["node_modules"],
    extensions: [".js", ".json"],
    mainFields: ["loader", "main"],
  },
}

源码分析

当然其第一步肯定也是 webpack 中的第一步,将用户的配置信息与 webpack 的默认配置进行合并

js
// WebpackOptionsDefaulter.js
class WebpackOptionsDefaulter extends OptionsDefaulter {
  constructor() {
    super()
    this.set("resolve", "call", value => Object.assign({}, value))
    this.set("resolve.unsafeCache", true)
    this.set("resolve.modules", ["node_modules"])
    this.set("resolve.extensions", [".wasm", ".mjs", ".js", ".json"])
    this.set("resolve.mainFiles", ["index"])
    this.set("resolve.aliasFields", "make", options => {
      if (options.target === "web" || options.target === "webworker" || options.target === "electron-renderer") {
        return ["browser"]
      } else {
        return []
      }
    })
    this.set("resolve.mainFields", "make", options => {
      if (options.target === "web" || options.target === "webworker" || options.target === "electron-renderer") {
        return ["browser", "module", "main"]
      } else {
        return ["module", "main"]
      }
    })
    this.set("resolve.cacheWithContext", "make", options => {
      return Array.isArray(options.resolve.plugins) && options.resolve.plugins.length > 0
    })

    this.set("resolveLoader", "call", value => Object.assign({}, value))
    this.set("resolveLoader.unsafeCache", true)
    this.set("resolveLoader.mainFields", ["loader", "main"])
    this.set("resolveLoader.extensions", [".js", ".json"])
    this.set("resolveLoader.mainFiles", ["index"])
    this.set("resolveLoader.cacheWithContext", "make", options => {
      return Array.isArray(options.resolveLoader.plugins) && options.resolveLoader.plugins.length > 0
    })
  }
}

可见如果我们不设置 resolve 或者 resolveLoader 其默认的配置信息如下:

js
let config = {
  resolve: {
    extensions: [".wasm", ".mjs", ".js", ".json"],
    unsafeCache: true,
    modules: ["node_modules"],
    mainFiles: ["index"],
    aliasFields: ["browser"],
    mainFields: ["browser", "module", "main"],
    cacheWithContext: false,
  },
  // 这组选项与上面的 resolve 对象的属性集合相同,但仅用于解析 webpack 的 loader 包。
  resolveLoader: {
    unsafeCache: true,
    mainFields: ["loader", "main"],
    extensions: [".js", ".json"],
    mainFiles: ["index"],
    cacheWithContext: false,
  },
}

然后我们在实例化 compiler 的时候也实例化了一个 ResolverFactory,其代码如下

js
class Compiler extends Tapable {
  constructor(context) {
    // 实例化  resolve工厂函数
    this.resolverFactory = new ResolverFactory()

    // TODO remove in webpack 5
    this.resolvers = {
      normal: {
        /**
         * 生成 调用 compiler.resolvers.normal.plugin() 调用就显示提示
         * 其实就是 不让使用  compiler.resolvers.normal.plugin() 而是应该使用 compiler.resolverFactory.plugin("resolver normal", resolver => {\n  resolver.plugin()})去定义,
         * 其本质还是在调用 compiler.resolvers.normal.plugin(hook , fn)的时候去执行 compiler.resolverFactory.plugin("resolver normal", resolver => { resolver.plugin(hook, fn)})
         * @type {[type]}
         */
        plugins: util.deprecate((hook, fn) => {
          // 触发我们在 resolverFactory constructor 中定义的 this._pluginCompat.tap("ResolverFactory", options => {})
          this.resolverFactory.plugin("resolver normal", resolver => {
            // 然后触发 resolverFactory 工厂函数中 resoler实例对象的 this._pluginCompat.tap("Resolver: before/after", options => {})
            resolver.plugin(hook, fn)
          })
        }, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n  resolver.plugin(/* … */);\n}); instead.'),
        apply: util.deprecate((...args) => {
          this.resolverFactory.plugin("resolver normal", resolver => {
            resolver.apply(...args)
          })
        }, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n  resolver.apply(/* … */);\n}); instead.'),
      },
      loader: {
        plugins: util.deprecate((hook, fn) => {
          this.resolverFactory.plugin("resolver loader", resolver => {
            resolver.plugin(hook, fn)
          })
        }, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n  resolver.plugin(/* … */);\n}); instead.'),
        apply: util.deprecate((...args) => {
          this.resolverFactory.plugin("resolver loader", resolver => {
            resolver.apply(...args)
          })
        }, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n  resolver.apply(/* … */);\n}); instead.'),
      },
      context: {
        plugins: util.deprecate((hook, fn) => {
          this.resolverFactory.plugin("resolver context", resolver => {
            resolver.plugin(hook, fn)
          })
        }, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n  resolver.plugin(/* … */);\n}); instead.'),
        apply: util.deprecate((...args) => {
          this.resolverFactory.plugin("resolver context", resolver => {
            resolver.apply(...args)
          })
        }, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n  resolver.apply(/* … */);\n}); instead.'),
      },
    }
  }
}

对于上面的源码我们需要首先了解 util.deprecate 的作用的作用和Tapable 中 pluginCompat 的使用流程

那么上面 compiler.resolvers.normal.plugins()的作用是什么?

我们以一个例子作为参考:

js
compiler.resolvers.normal.plugins("resolve", fn)

其触发 this.resolverFactory.plugin('resolver normal', resolver => { resolver.plugin(hook, fn); });,然后我们看 resolverFactory 实例对象中的定义的 resolverFactory._pluginCompat()

resolverFactory._pluginCompat() 通过判断事件流的名称是否以resolve-optionsresolver开头的来决定其事件添加在 resolverFactory.hooks`[resolver|resolveOptions]上,并且返回 ture 来阻止 tapable 内置的 Tapable camelCase , "Tapable this.hooks" 两个事件。

然后在其他的地方 **resolverFactory.hooks[resolver|resolveOptions].call()**执行的时候,其也会触发前面在 compiler 上定义的 resolver => { resolver.plugin(hook, fn); },这时候又上面类型的流程。

js
module.exports = class ResolverFactory extends Tapable {
  constructor() {
    super()
    this.hooks = {
      resolveOptions: new HookMap(() => new SyncWaterfallHook(["resolveOptions"])),
      resolver: new HookMap(() => new SyncHook(["resolver", "resolveOptions"])),
    }
    this._pluginCompat.tap("ResolverFactory", options => {
      // 在 compiler 中定义了 	this.resolverFactory.plugin("resolver normal", resolver => {}) 的函数,其执行的时候 触发此事件
      let match
      match = /^resolve-options (.+)$/.exec(options.name)
      if (match) {
        this.hooks.resolveOptions.for(match[1]).tap(options.fn.name || "unnamed compat plugin", options.fn)
        return true
      }
      //  "resolver normal"  匹配成功
      match = /^resolver (.+)$/.exec(options.name)
      if (match) {
        // 那么在 ResolverFactory.hooks.resolver上就会添加一个 key为 normal的事件流 事件的名称为 "unnamed compat plugin"
        // options.fn 为上面的 resolver => {}
        this.hooks.resolver.for(match[1]).tap(options.fn.name || "unnamed compat plugin", options.fn)
        // 不执行 tapable内置的 Tapable camelCase , "Tapable this.hooks" 两个事件
        return true
      }
    })
  }
}

执行了 resolver.plugin(hook, fn)

源码如下:

js
class Resolver extends Tapable {
  constructor(fileSystem) {
    super()
    this.fileSystem = fileSystem
    this.hooks = {
      // 生成 resolver.hooks.resolveStep : new SyncHook(["hook", "request"]) 只是修改 hook.name === 'resolveStep'
      resolveStep: withName("resolveStep", new SyncHook(["hook", "request"])),
      noResolve: withName("noResolve", new SyncHook(["request", "error"])),
      resolve: withName("resolve", new AsyncSeriesBailHook(["request", "resolveContext"])),
      result: new AsyncSeriesHook(["result", "resolveContext"]),
    }

    /*
		 	处理通过  resolver.plugin()的方式添加的事件
				1. 通过添加的目标钩子事件流名称前加 before- after- 去决定当前事件在事件流中执行的顺序
		 */
    this._pluginCompat.tap("Resolver: before/after", options => {
      // 处理  resolver.plugin("before-no-resolve" , fn)方式添加到 noResolve钩子事件流上的事件
      // 因为其事件流的名称是 resolveStep、noResolve、resolve等,那么 before-no-resolve就找不到noResolve
      if (/^before-/.test(options.name)) {
        // 修改目标钩子事件流的名称  before-no-resolve -> no-resolve
        options.name = options.name.substr(7)
        // 将事件流的权重设置为 负数,  较高
        options.stage = -10
      } else if (/^after-/.test(options.name)) {
        // 一样的方式  修改目标钩子事件流的名称  after-no-resolve -> no-resolve
        options.name = options.name.substr(6)
        // 将事件流的权重设置为 10,  较低
        options.stage = 10
      }
    })
    /*


		 */
    this._pluginCompat.tap("Resolver: step hooks", options => {
      // 获取目标钩子事件流的名称,已经经过上面 "Resolver: before/after"事件处理 ,所以当前 options.name 为 no-resolve
      const name = options.name
      // 处理 resolveStep no-resolve 两种类型的钩子事件流
      const stepHook = !/^resolve(-s|S)tep$|^no(-r|R)esolve$/.test(name)
      if (stepHook) {
        // 事件通过异步的方式添加
        options.async = true
        // 获取 name 类型的钩子事件流
        this.ensureHook(name)
        // 获取事件回调函数 fn
        const fn = options.fn
        // 重写事件的回调函数  TODO: 确定作用和意义
        options.fn = (request, resolverContext, callback) => {
          const innerCallback = (err, result) => {
            if (err) return callback(err)
            if (result !== undefined) return callback(null, result)
            callback()
          }
          for (const key in resolverContext) {
            innerCallback[key] = resolverContext[key]
          }
          fn.call(this, request, innerCallback)
        }
      }
    })
  }
}

上面我们执行的compiler.resolvers.normal.plugins('resolve', fn);,那么这时候 hook === resolve,然后就会经历上面的 resover 实例对象中的 name 中存在 before 或者 after 设置事件的权重不同、处理 resolveStep no-resolve 两种类型的钩子事件流、Tapable 内置的 name 的驼峰命名转换、Tapable 内置的创建或者返回 resolver.hooks.resolve 的钩子事件流四个流程,然后添加到对应的事件流中。那么我们就可以通过 **resolvers.hooks.resolve.call()**去执行 resolve 事件流。

然后我们进过 compiler -> compilation -> nomalModuleFactory.create -> NormalModuleFactory.resolve 的流程

js
class NormalModuleFactory extends Tapable {
  constructor(context, resolverFactory, options) {
    super()
    this.resolverFactory = resolverFactory
    this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
      let resolver = this.hooks.resolver.call(null)
      // Ignored
      if (!resolver) return callback()
      resolver(result, (err, data) => {})
    })
    this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
      const contextInfo = data.contextInfo
      const context = data.context
      const request = data.request

      const loaderResolver = this.getResolver("loader")
      const normalResolver = this.getResolver("normal", data.resolveOptions)

      let matchResource = undefined
      let requestWithoutMatchResource = request
      const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request)
      if (matchResourceMatch) {
        matchResource = matchResourceMatch[1]
        if (/^\.\.?\//.test(matchResource)) {
          matchResource = path.join(context, matchResource)
        }
        requestWithoutMatchResource = request.substr(matchResourceMatch[0].length)
      }

      const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!")
      const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!")
      const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!")
      let elements = requestWithoutMatchResource.replace(/^-?!+/, "").replace(/!!+/g, "!").split("!")
      let resource = elements.pop()
      elements = elements.map(identToLoaderRequest)

      asyncLib.parallel(
        [
          callback => this.resolveRequestArray(contextInfo, context, elements, loaderResolver, callback),
          callback => {
            if (resource === "" || resource[0] === "?") {
              return callback(null, {
                resource,
              })
            }

            normalResolver.resolve(contextInfo, context, resource, {}, (err, resource, resourceResolveData) => {
              if (err) return callback(err)
              callback(null, {
                resourceResolveData,
                resource,
              })
            })
          },
        ],
        (err, results) => {
          if (err) return callback(err)
          let loaders = results[0]
          const resourceResolveData = results[1].resourceResolveData
          resource = results[1].resource

          // translate option idents
          try {
            for (const item of loaders) {
              if (typeof item.options === "string" && item.options[0] === "?") {
                const ident = item.options.substr(1)
                item.options = this.ruleSet.findOptionsByIdent(ident)
                item.ident = ident
              }
            }
          } catch (e) {
            return callback(e)
          }

          if (resource === false) {
            // ignored
            return callback(
              null,
              new RawModule("/* (ignored) */", `ignored ${context} ${request}`, `${request} (ignored)`)
            )
          }

          const userRequest =
            (matchResource !== undefined ? `${matchResource}!=!` : "") +
            loaders.map(loaderToIdent).concat([resource]).join("!")

          let resourcePath = matchResource !== undefined ? matchResource : resource
          let resourceQuery = ""
          const queryIndex = resourcePath.indexOf("?")
          if (queryIndex >= 0) {
            resourceQuery = resourcePath.substr(queryIndex)
            resourcePath = resourcePath.substr(0, queryIndex)
          }

          const result = this.ruleSet.exec({
            resource: resourcePath,
            realResource: matchResource !== undefined ? resource.replace(/\?.*/, "") : resourcePath,
            resourceQuery,
            issuer: contextInfo.issuer,
            compiler: contextInfo.compiler,
          })
          const settings = {}
          const useLoadersPost = []
          const useLoaders = []
          const useLoadersPre = []
          for (const r of result) {
            if (r.type === "use") {
              if (r.enforce === "post" && !noPrePostAutoLoaders) {
                useLoadersPost.push(r.value)
              } else if (r.enforce === "pre" && !noPreAutoLoaders && !noPrePostAutoLoaders) {
                useLoadersPre.push(r.value)
              } else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) {
                useLoaders.push(r.value)
              }
            } else if (
              typeof r.value === "object" &&
              r.value !== null &&
              typeof settings[r.type] === "object" &&
              settings[r.type] !== null
            ) {
              settings[r.type] = cachedCleverMerge(settings[r.type], r.value)
            } else {
              settings[r.type] = r.value
            }
          }
          asyncLib.parallel(
            [
              this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, loaderResolver),
              this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, loaderResolver),
              this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, loaderResolver),
            ],
            (err, results) => {
              if (err) return callback(err)
              if (matchResource === undefined) {
                loaders = results[0].concat(loaders, results[1], results[2])
              } else {
                loaders = results[0].concat(results[1], loaders, results[2])
              }
              process.nextTick(() => {
                const type = settings.type
                const resolveOptions = settings.resolve
                callback(null, {
                  context: context,
                  request: loaders.map(loaderToIdent).concat([resource]).join("!"),
                  dependencies: data.dependencies,
                  userRequest,
                  rawRequest: request,
                  loaders,
                  resource,
                  matchResource,
                  resourceResolveData,
                  settings,
                  type,
                  parser: this.getParser(type, settings.parser),
                  generator: this.getGenerator(type, settings.generator),
                  resolveOptions,
                })
              })
            }
          )
        }
      )
    })
  }
}

在其中有几步比较重要

this.getResolver("loader")

作用

流程

  1. const loaderResolver = this.getResolver("loader");

其访问的是

js
normalModuleFactory.getResolver = (type, resolveOptions) => {
  return this.resolverFactory.get(type, resolveOptions || EMPTY_RESOLVE_OPTIONS)
}

此处 type === 'normal', resolveOptions === {},然后其访问 resolverFactory 工厂的 get 方法

我们看过程也是调用 this.resolverFactory.get( 'loader' , resolveOptions || EMPTY_RESOLVE_OPTIONS);

js
/*
	 resolve(解析)

	 配置如何解析模块

	 resolve:{
	 alias :  ""   , // 创建 import 或 require 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块
	 aliasFields : ''   // 指定一个字段,例如 browser,根据此规范进行解析。默认:
	 extensions: [".js", ".json"] ,    // 配置解析文件的拓展名
	 enforceExtension :
 }

 */
module.exports = class ResolverFactory extends Tapable {
  constructor() {
    super()
    this.cache2 = new Map()
  }

  /**
   * 获取指定类型的的resolver 如果没有就创建这个类型的 resolver
   * [get description]
   * @param  {[type]} type           resolver 的type
   * @param  {[type]} resolveOptions [description]
   * @return {[type]}                [description]
   */
  get(type, resolveOptions) {
    // resolve 的options
    resolveOptions = resolveOptions || EMTPY_RESOLVE_OPTIONS
    // 生成当前type的id  不能只按照 type 去处理
    const ident = `${type}|${JSON.stringify(resolveOptions)}`
    const resolver = this.cache2.get(ident)
    if (resolver) return resolver
    const newResolver = this._create(type, resolveOptions)
    this.cache2.set(ident, newResolver)
    return newResolver
  }

  /**
   * 创建一个 resolve 实例对象
   *
   * {
   * 		fileSystem:
   * 		withOptions
   * 		hooks
   * }
   * @param  {[type]} type           [description]
   * @param  {[type]} resolveOptions [description]
   * @return {[type]}                [description]
   */
  _create(type, resolveOptions) {
    // 生成一个新的 options
    const originalResolveOptions = Object.assign({}, resolveOptions)
    // 按照 loader normal context 三种类型获取对于的 resolveOptions
    resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions)
    // 创建 resolver
    const resolver = Factory.createResolver(resolveOptions)
    if (!resolver) {
      throw new Error("No resolver created")
    }
    /** @type {Map<Object, Resolver>} */
    const childCache = new Map()
    resolver.withOptions = options => {
      const cacheEntry = childCache.get(options)
      if (cacheEntry !== undefined) return cacheEntry
      const mergedOptions = cachedCleverMerge(originalResolveOptions, options)
      const resolver = this.get(type, mergedOptions)
      childCache.set(options, resolver)
      return resolver
    }
    //
    this.hooks.resolver.for(type).call(resolver, resolveOptions)
    return resolver
  }
}

从中看出缓存(cache2)中没有 type 为 'loader' , ident : 'loader|{}'的缓存,所以需要去通过this._create(type, resolveOptions)去创建这一类型的 resolver,具体可以看 enhanced-resolve 如果通过 ResolverFactory 去创建一个 resolve 的过程。

resolveFactory._create()的过程中其存在两个比较特殊的过程

  1. resolver.withOptions
js
resolver.withOptions = options => {
  const cacheEntry = childCache.get(options)
  if (cacheEntry !== undefined) return cacheEntry
  const mergedOptions = cachedCleverMerge(originalResolveOptions, options)
  const resolver = this.get(type, mergedOptions)
  childCache.set(options, resolver)
  return resolver
}
  • this.hooks.resolver.for(type).call(resolver, resolveOptions);

在上面的例子中 type 为 normal,那么其在什么时候在 compiler.hooks.resolver 添加了 normal 类型的事件流?

这个还得回到我们的 **WebpackOptionsApply().process()**的时候,其根据 webpack 的 options 添加了很多钩子函数,并且通过 plugins.apply()去应用这些钩子函数,其中就有如同:

normal类型的事件流

发现其虽然有 38 个之多但是基本上都是NodeSourcePlugin的事件,那么我们看一下NodeSourcePlugin是干嘛的?

NodeSourcePlugin

js
/*
	我们先看一下 nodeLibsBrowser的内容是什么?
		os:"d:\guzhangh\webpack\webpack\node_modules\os-browserify\browser.js"
		path:"d:\guzhangh\webpack\webpack\node_modules\path-browserify\index.js"
		process:"d:\guzhangh\webpack\webpack\node_modules\process\browser.js"
		punycode:"d:\guzhangh\webpack\webpack\node_modules\node-libs-browser\node_modules\punycode\punycode.js"
		querystring:"d:\guzhangh\webpack\webpack\node_modules\querystring-es3\index.js"
		readline:null
		repl:null
		stream:"d:\guzhangh\webpack\webpack\node_modules\stream-browserify\index.js"
		string_decoder:"d:\guzhangh\webpack\webpack\node_modules\string_decoder\lib\string_decoder.js"
		sys:"d:\guzhangh\webpack\webpack\node_modules\node-libs-browser\node_modules\util\util.js"
		timers:"d:\guzhangh\webpack\webpack\node_modules\timers-browserify\main.js"
		tls:null
		tty:"d:\guzhangh\webpack\webpack\node_modules\tty-browserify\index.js"
		url:"d:\guzhangh\webpack\webpack\node_modules\url\url.js"
		util:"d:\guzhangh\webpack\webpack\node_modules\node-libs-browser\node_modules\util\util.js"
		vm:"d:\guzhangh\webpack\webpack\node_modules\vm-browserify\index.js"
	 其都是一些我们可以在浏览器访问的node提供的属性和其源码地址
 */
compiler.hooks.afterResolvers.tap("NodeSourcePlugin", compiler => {
  for (const lib of Object.keys(nodeLibsBrowser)) {
    if (options[lib] !== false) {
      // 那么这时候我们就可以添加很多在 compilercompiler.resolverFactory.hooks.resolver.mormal上的事件,
      // 其内容是支持浏览器访问使用的Node的lib工作方法
      // 如上面的 asset
      compiler.resolverFactory.hooks.resolver.for("normal").tap("NodeSourcePlugin", resolver => {
        new AliasPlugin(
          "described-resolve",
          {
            // 名称  asset
            name: lib,
            // 这是一个 module 而不是一个路径  就想我们alias 中 "vue$":"" 这种声明的别名一样
            onlyModule: true,
            // asset的源码路径 "d:\guzhangh\webpack\webpack\node_modules\assert\assert.js"
            alias: getPathToModule(lib, options[lib]),
          },
          // 定义在 resolver 钩子事件流的名称  即这个事件注册到 resolver.resolve钩子事件流上
          "resolve"
        ).apply(resolver)
      })
    }
  }
})

可见其实 NodeSourcePlugin的作用也就是添加了很多 module 类型的 Node 系统支持的插件,所以我们在 webpack 中写代码中可以使用如process.env.xxxcluster

AMDPlugin

js
/*
			添加webpack支持的 AMD 模块加载依赖的插件路径
		 */
compiler.hooks.afterResolvers.tap("AMDPlugin", () => {
  // 在 normal上注册 amdefine 类型的 resolver事件
  compiler.resolverFactory.hooks.resolver.for("normal").tap("AMDPlugin", resolver => {
    // 定义  const amd = reuquire("amdefine")  -> "d:\guzhangh\webpack\webpack\node_modules\webpack\buildin\amd-define.js"
    new AliasPlugin(
      "described-resolve",
      {
        name: "amdefine",
        alias: path.join(__dirname, "..", "..", "buildin", "amd-define.js"),
      },
      "resolve"
    ).apply(resolver)
    // 定义  const amd = reuquire("amdefine")  -> "d:\guzhangh\webpack\webpack\node_modules\webpack\buildin\amd-define.js"
    new AliasPlugin(
      "described-resolve",
      {
        name: "webpack amd options",
        alias: path.join(__dirname, "..", "..", "buildin", "amd-options.js"),
      },
      "resolve"
    ).apply(resolver)
    // 定义  const amd = reuquire("amdefine")  -> "d:\guzhangh\webpack\webpack\node_modules\webpack\buildin\amd-define.js"
    new AliasPlugin(
      "described-resolve",
      {
        name: "webpack amd define",
        alias: path.join(__dirname, "..", "..", "buildin", "amd-define.js"),
      },
      "resolve"
    ).apply(resolver)
  })
})

其主要是添加 AMD 模块化加载依赖的插件文件

通过上面的创建 resolver、加载应用相应插件、在WebpackOptionsApply().process()的时候还有两个插件添加了 Node 支持的浏览器端属性和方法的插件与 AMD 模块化依赖的插件等就形成了 type 为 normal 类型的 resolver 实例化对象

normal类型的resolver对象

normalResolver.resolve( contextInfo, context, resource, {}, (err, resource, resourceResolveData) => { });

下面我们以 request === './resolve/src/main.js'为例子讲解 normalResolver.resolve()的过程。

首先在 NormalModuleFactory 中通过 normalResolver 去使用对应类型的 resolver 实例对象去 resolve 加载相应的文件,这是一个非常好玩的过程,在 resolver 、 UnsafeCachePlugin 、ParsePlugin 之间来回纠缠。

当生成对应类型的 resolver 的时候其通过 normalResolver.resolve() 去加载相应文件的时候,经历一下过程

UnsafeCachePlugin和ParsePlugin

  1. resolve 钩子事件流

第一次 normalResolver.resolve()触发 resoler.resolve('resolve'),这时候 hooks 为 resolver.hooks.resolve。所以执行 resolver.doResolve('resolve Hooks')

这时候 resolver.hooks.resolve 下有 UnsafeCachePlugin 插件添加的 resolve 事件(根据文件的 request 和 context 生成文件的 ID 进行缓存)。

然后进行 resoler.doResolve('new-resolve')。

  1. newResolve 钩子事件流

进行 new-resolve 的事件流过程,触发resolver.hooks.newResolve.callAsync(request),这时候 ParsePlugin 插件添加的 newResolve 事件

根据请求的路径生成 parsed 对象,包含 query、module、directory、request 等信息

然后触发 parsedResolve类型的 doResolve

  1. parsedResolve 钩子事件流

这时候 request 对象内容是

在这个事件流中存在两个事件:

  • DescriptionFilePlugin(加载描述信息文件)

  • NextPlugin

parsedResolve 是一个异步串行执行,下一个事件依赖于上一个事件的 callback,所以下一步执行的是 DescriptionFilePlugin 中的 resolver.doResolve('described-resolve')

  1. describedResolve 钩子事件流

可见在 describedResolve 事件流中主要处理 alias,在前面我们处理NodeSourcePluginAMDPlugin添加了很多 aliasPlugin,然后我们 AliasPlugin 这时候就有用了

js
module.exports = class AliasPlugin {
  constructor(source, options, target) {
    // 实例化的时候 source为 described-resolve
    this.source = source
    this.options = Array.isArray(options) ? options : [options]
    this.target = target
  }

  apply(resolver) {
    const target = resolver.ensureHook(this.target)
    // this.source === described-resolve 说明其注册在 resolver.hooks.describedResolve 钩子上  不就是我们3.1步骤的事件流
    resolver.getHook(this.source).tapAsync("AliasPlugin", (request, resolveContext, callback) => {
      const innerRequest = request.request || request.path
      if (!innerRequest) return callback()
      for (const item of this.options) {
        if (innerRequest === item.name || (!item.onlyModule && startsWith(innerRequest, item.name + "/"))) {
          if (innerRequest !== item.alias && !startsWith(innerRequest, item.alias + "/")) {
            const newRequestStr = item.alias + innerRequest.substr(item.name.length)
            const obj = Object.assign({}, request, {
              request: newRequestStr,
            })
            return resolver.doResolve(
              target,
              obj,
              "aliased with mapping '" + item.name + "': '" + item.alias + "' to '" + newRequestStr + "'",
              resolveContext,
              (err, result) => {
                if (err) return callback(err)

                // Don't allow other aliasing or raw request
                if (result === undefined) return callback(null, null)
                callback(null, result)
              }
            )
          }
        }
      }
      return callback()
    })
  }
}

第一个 Alias

其主要判断当前用户 alias 的配置中是否与此路径相匹配。如果匹配就进入 resolve 事件流,此处 "@"与"./resolve/src/main.js"不匹配,所以使用 return callback(),进入describedResolve 钩子事件流的下一个事件

第二个: AliasFieldPlugin 主要处理 DescriptionFileUtils.brower 中

第三个: AliasPlugin 处理的是我们前面 NodeSourcePlugin 定义的 Node 内置的第三方插件

第四个: AliasPlugin 处理的是我们前面 NodeSourcePlugin 定义的 Node 内置的第三方插件(buffer)

... child_process、cluster

第 41 个:: AliasPlugin 处理的是定义的 alias 别名是否与其相匹配

第 42 个: ModuleKindPlugin

第 43 个:JoinRequestPlugin

  • 生成 module 的全路径

  • 处理文件路径的分隔符

d:\guzhangh\webpack\webpack\resolve\src/modules/module2 -> "d:\guzhangh\webpack\webpack\resolve\src\modules\module2"

js
module.exports = class JoinRequestPlugin {
  apply(resolver) {
    const target = resolver.ensureHook(this.target)
    resolver.getHook(this.source).tapAsync("JoinRequestPlugin", (request, resolveContext, callback) => {
      const obj = Object.assign({}, request, {
        // 根据项目路径与module的路径生成module的全路径
        path: resolver.join(request.path, request.request),
        relativePath: request.relativePath && resolver.join(request.relativePath, request.request),
        request: undefined,
      })
      resolver.doResolve(target, obj, null, resolveContext, callback)
    })
  }
}
  1. relative 事件流

在这个事件流中也存在两个事件:

  • DescriptionFilePlugin(加载描述信息文件)

这时候的 DescriptionFilePlugin 插件 doResolve 的事件流不是前面的 describedResolve 而是 describedRelative 事件流了

  • NextPlugin
  1. describedRelative 事件流

在这个事件流中存在两个事件

  • FileKindPlugin

这个插件主要的作用就是判断是否是文件,是文件就进入下一个事件流 rawFile

  1. rawFile 事件流

插件事件很多

  • TryNextPlugin

直接进入下一个事件流 file

  • AppendPlugin
  1. file 事件流

从 rawFile 事件流的第一个事件直接进入了 file 事件流

  • AliasPlugin

在前面describedResolve 钩子事件流中进行了很多 Alias 的判断去处理 request.request。但是那个时候处理的路径只是用户 引用的文件的路径,但是在describedResolve 钩子事件流最后一个事件JoinRequestPlugin 其一方面通过 request.path 和 request.request 去生成文件的全路径,一方面将 request.request 赋值为 undefined,那么这时候处理 alias 就是处理的文件的全路径而不是相对路径或者别名路径。

  • AliasFieldPlugin

describedResolve 钩子事件流处理 package.json 中定义的 brower 路径,进入下一个钩子

  • SymlinkPlugin

异步的方式去读取链接

  • FileExistsPlugin

判断文件是否存在,如果存在就进入existingFile 事件流

  1. existingFile 事件流
  • NextPlugin

就是进入下一个事件流插件(resolved)

  1. resolved 事件流
  • ResultPlugin

作用很简单 触发 result 事件流

js
module.exports = class ResultPlugin {
  apply(resolver) {
    this.source.tapAsync("ResultPlugin", (request, resolverContext, callback) => {
      const obj = Object.assign({}, request)
      if (resolverContext.log) resolverContext.log("reporting result " + obj.path)
      // 触发 result的事件流
      resolver.hooks.result.callAsync(obj, resolverContext, err => {
        if (err) return callback(err)
        callback(null, obj)
      })
    })
  }
}

下面开始执行回调了,不断的回调前面的 callback 直到 normalResolver.resolve()的 callback

通过上面 10 个事件流的过程 resolve -> newResolve -> parsedResolve -> NextPlugin -> describedResolve -> relative -> describedRelative -> rawFile -> file -> existingFile -> resolved 发现其主要作用就是

  • 解析其相对路径或者别名路径或者解析成全路径 request.path 去匹配定义的和 Node 系统,AMD 模块化定义的别名匹配

  • 加载描述文件(package.json)中相应的属性(brower)是否存在路径别名信息

  • 解析成绝对路径

  • 判断文件和文件夹是否存在

最后附上一个解析完成的图片

下面是通过文件的路径获取相应的 loaders 的过程,详情请看 loader 篇

下面我们看一下 存在别名的例子

如 "@/modules/module2" 其经历了上面的 resolve -> newResolve -> parsedResolve -> NextPlugin -> describedResolve 然后遇到匹配的@别名事件

js
module.exports = class AliasPlugin {
  apply(resolver) {
    const target = resolver.ensureHook(this.target)
    // this.source === described-resolve 说明其注册在 resolver.hooks.describedResolve 钩子上  不就是我们3.1步骤的事件流
    resolver.getHook(this.source).tapAsync("AliasPlugin", (request, resolveContext, callback) => {
      // innerRequest 为文件的加载路径
      const innerRequest = request.request || request.path
      // 遍历设置的所有的alias
      for (const item of this.options) {
        // 如果 路径直接相同 那就是这种  import vue from "vue"  在 alias上  "vue$" : 'vue/xxx.js'
        // 或者如果 别名不是一个module 那么就去判断当前文件的路径前缀是不是以这个别名开头的
        // 如果不是一个路径  那就是一个文件  如 import { http } from "@/api/index.js" 那么这时候innerRequest不是不是只是module 且 以 @开始
        if (innerRequest === item.name || (!item.onlyModule && startsWith(innerRequest, item.name + "/"))) {
          if (innerRequest !== item.alias && !startsWith(innerRequest, item.alias + "/")) {
            // 如  "@/modules/module2"  其会使用定义的 @的值和别名路径截取后的值生成全路径
            // d:\guzhangh\webpack\webpack\resolve\src/modules/module2
            const newRequestStr = item.alias + innerRequest.substr(item.name.length)
            // 下面事件流中 request.request的值就是上面的处理后的路径了
            const obj = Object.assign({}, request, {
              request: newRequestStr,
            })
            return resolver.doResolve(
              target, // resolve
              obj,
              "aliased with mapping '" + item.name + "': '" + item.alias + "' to '" + newRequestStr + "'",
              resolveContext,
              (err, result) => {
                if (err) return callback(err)

                // Don't allow other aliasing or raw request
                if (result === undefined) return callback(null, null)
                callback(null, result)
              }
            )
          }
        }
      }
    })
  }
}

这时候的重点是返回的是 resolver.doResolve("resolve") 又是 resolve 事件流,只是其 request.request 修改成了 d:\guzhangh\webpack\webpack\resolve\src/modules/module2,我们发现了一个什么问题,就是在 window 系统中路径的定义方式是不同的,所以需要去处理文件的路径,这就是我们上面的 describedResolve 钩子事件流的 JoinRequestPlugin 事件,其会通过 resolver.join(request.path, request.request)将绝对路径的文件分隔符修改成 d:\guzhangh\webpack\webpack\resolve\src\modules\module2

resolver 的创建过程

  1. 获取对应类型的resolveOptions

在我们根据用户的配置合并 webpack 的默认配置后通过 **WebpackOptionsApply().process()**去根据配置信息加载相应的依赖插件

js
class WebpackOptionsApply extends OptionsApply {
  constructor() {
    super()
  }
  process(options, compiler) {
    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)
      )
    })
    compiler.hooks.afterResolvers.call(compiler)
    return options
  }
}

module.exports = WebpackOptionsApply

在加载依赖的最后通过 compiler.resolverFactory.hooks.resolveOptions.for("normal|context|loader")分别为 loader、normal、context 三种类型的 resolve 添加一个一个合并处理后的 options.resolve 和 resolveOptions 的事件

那么在上面我们创建相应的 resolve 的时候,其resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions);就会执行上面对应类型的钩子事件流,然后返回合并处理后的resolveOptions, 这就是 webpack 中 resolve 如何获取不同 type 的 reolveOptions 的过程。

获取到其resolveOptions,那么就可以根据配置信息去创建 resolve(const resolver = Factory.createResolver(resolveOptions);)。

  1. 创建 resolver
js
exports.createResolver = function (options) {
  //// OPTIONS ////
  let modules = options.modules || ["node_modules"]
  // ...
  if (!resolver) {
    resolver = new Resolver(useSyncFileSystemCalls ? new SyncAsyncFileSystemDecorator(fileSystem) : fileSystem)
  }
  // ...
  // 处理alias属性
  if (typeof alias === "object" && !Array.isArray(alias)) {
    alias = Object.keys(alias).map(key => {
      let onlyModule = false
      let obj = alias[key]
      if (/\$$/.test(key)) {
        onlyModule = true
        key = key.substr(0, key.length - 1)
      }
      if (typeof obj === "string") {
        obj = {
          alias: obj,
        }
      }
      obj = Object.assign(
        {
          name: key,
          onlyModule: onlyModule,
        },
        obj
      )
      return obj
    })
  }
  // 生成一下 类型的钩子事件流
  resolver.ensureHook("resolve")
  // ...
  resolver.ensureHook("resolved")

  // 下面是按照配置添加一大堆的插件
  aliasFields.forEach(item => {
    plugins.push(new AliasFieldPlugin("described-resolve", item, "resolve"))
  })
  //  将上面添加的所有的钩子 调用其apply方法
  plugins.forEach(plugin => {
    plugin.apply(resolver)
  })

  return resolver
}

class Resolver extends Tapable {
  constructor(fileSystem) {
    super()
    this.fileSystem = fileSystem
    this.hooks = {
      // 生成 resolver.hooks.resolveStep : new SyncHook(["hook", "request"]) 只是修改 hook.name === 'resolveStep'
      resolveStep: withName("resolveStep", new SyncHook(["hook", "request"])),
      noResolve: withName("noResolve", new SyncHook(["request", "error"])),
      resolve: withName("resolve", new AsyncSeriesBailHook(["request", "resolveContext"])),
      result: new AsyncSeriesHook(["result", "resolveContext"]),
    }

    /*
		 	处理通过  resolver.plugin()的方式添加的事件
				1. 通过添加的目标钩子事件流名称前加 before- after- 去决定当前事件在事件流中执行的顺序
		 */
    this._pluginCompat.tap("Resolver: before/after", options => {
      // 处理  resolver.plugin("before-no-resolve" , fn)方式添加到 noResolve钩子事件流上的事件
      // 因为其事件流的名称是 resolveStep、noResolve、resolve等,那么 before-no-resolve就找不到noResolve
      if (/^before-/.test(options.name)) {
        // 修改目标钩子事件流的名称  before-no-resolve -> no-resolve
        options.name = options.name.substr(7)
        // 将事件流的权重设置为 负数,  较高
        options.stage = -10
      } else if (/^after-/.test(options.name)) {
        // 一样的方式  修改目标钩子事件流的名称  after-no-resolve -> no-resolve
        options.name = options.name.substr(6)
        // 将事件流的权重设置为 10,  较低
        options.stage = 10
      }
    })
    /*


		 */
    this._pluginCompat.tap("Resolver: step hooks", options => {
      // 获取目标钩子事件流的名称,已经经过上面 "Resolver: before/after"事件处理 ,所以当前 options.name 为 no-resolve
      const name = options.name
      // 处理 resolveStep no-resolve 两种类型的钩子事件流
      const stepHook = !/^resolve(-s|S)tep$|^no(-r|R)esolve$/.test(name)
      if (stepHook) {
        // 事件通过异步的方式添加
        options.async = true
        // 获取 name 类型的钩子事件流
        this.ensureHook(name)
        // 获取事件回调函数 fn
        const fn = options.fn
        // 重写事件的回调函数  TODO: 确定作用和意义
        options.fn = (request, resolverContext, callback) => {
          const innerCallback = (err, result) => {
            if (err) return callback(err)
            if (result !== undefined) return callback(null, result)
            callback()
          }
          for (const key in resolverContext) {
            innerCallback[key] = resolverContext[key]
          }
          fn.call(this, request, innerCallback)
        }
      }
    })
  }

  /**
   * 1. 通过钩子事件流名称来确定事件的权重
   *   before- 开头的权重较高 为 -10
   *   after- 开头的权重较低 为 10
   * 2. 返回 this.hooks[name]
   *   如果没有这个类型的钩子事件流 那么就创建一个以 ["request", "resolveContext"] 的异步串行事件流
   * @param  {[type]} name [description]
   * @return {[type]}      [description]
   */
  ensureHook(name) {
    if (typeof name !== "string") return name
    // 驼峰转换
    name = toCamelCase(name)
    if (/^before/.test(name)) {
      return this.ensureHook(name[6].toLowerCase() + name.substr(7)).withOptions({
        stage: -10,
      })
    }
    if (/^after/.test(name)) {
      return this.ensureHook(name[5].toLowerCase() + name.substr(6)).withOptions({
        stage: 10,
      })
    }
    // 获取 name 类型的钩子事件流
    const hook = this.hooks[name]
    if (!hook) {
      // 如果不存在 那么就创建异步串行事件流
      return (this.hooks[name] = withName(name, new AsyncSeriesBailHook(["request", "resolveContext"])))
    }
    return hook
  }
}

根据上面 ResolverFactory.createResolver,Resolver 实例化两个流程,发现其实际上也跟 compiler、NomalModuleFactory 等一样,也是通过一下步骤option 的处理根据 option 创建 resolver 实例对象根据 option 给 resolver 添加钩子事件流和相应的钩子函数

resolve 的钩子

名称名称sourcetarget类型地址调用时间说明
1UnsafeCachePluginresolvenew-resolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
2ParsePluginnew-resolveparsed-resolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
3DescriptionFilePluginparsed-resolvedescribed-resolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
4NextPluginafter-parsed-resolvedescribed-resolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
5AliasPlugindescribed-resolveresolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
6AliasFieldPlugindescribed-resolveresolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
7ModuleKindPluginafter-described-resolveraw-moduleAsyncSeriesHook中等文本中等文本稍微长一点的文本
8JoinRequestPluginafter-described-resolverelativeAsyncSeriesHook中等文本中等文本稍微长一点的文本
9TryNextPluginraw-modulemoduleAsyncSeriesHook中等文本中等文本稍微长一点的文本
10ModulesInHierachicDirectoriesPluginmoduleresolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
11DescriptionFilePluginrelativedescribed-relativeAsyncSeriesHook中等文本中等文本稍微长一点的文本
12NextPluginafter-relativedescribed-relativeAsyncSeriesHook中等文本中等文本稍微长一点的文本
13FileKindPlugindescribed-relativeraw-fileAsyncSeriesHook中等文本中等文本稍微长一点的文本
14TryNextPlugindescribed-relativedirectoryAsyncSeriesHook中等文本中等文本稍微长一点的文本
15DirectoryExistsPlugindirectoryexisting-directoryAsyncSeriesHook中等文本中等文本稍微长一点的文本
16MainFieldPluginexisting-directoryresolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
17MainFieldPluginexisting-directoryresolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
18MainFieldPluginexisting-directoryresolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
19UseFilePluginexisting-directoryundescribed-raw-fileAsyncSeriesHook中等文本中等文本稍微长一点的文本
20DescriptionFilePluginundescribed-raw-fileraw-fileAsyncSeriesHook中等文本中等文本稍微长一点的文本
21NextPluginafter-undescribed-raw-fileraw-fileAsyncSeriesHook中等文本中等文本稍微长一点的文本
22TryNextPluginraw-filefileAsyncSeriesHook中等文本中等文本稍微长一点的文本
23AppendPluginraw-filefileAsyncSeriesHook中等文本中等文本稍微长一点的文本
24AppendPluginraw-filefileAsyncSeriesHook中等文本中等文本稍微长一点的文本
25AppendPluginraw-filefileAsyncSeriesHook中等文本中等文本稍微长一点的文本
26AliasPluginfileresolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
27AliasFieldPluginfileresolveAsyncSeriesHook中等文本中等文本稍微长一点的文本
28SymlinkPluginfilerelativeAsyncSeriesHook中等文本中等文本稍微长一点的文本
29FileExistsPluginfileexisting-fileAsyncSeriesHook中等文本中等文本稍微长一点的文本
30NextPluginexisting-fileresolvedAsyncSeriesHook中等文本中等文本稍微长一点的文本
31ResultPluginAsyncSeriesBailHook""AsyncSeriesHook中等文本中等文本稍微长一点的文本
  • UnsafeCachePlugin

  • ParsePlugin

  • DescriptionFilePlugin

  • NextPlugin

  • AliasPlugin

  • AliasFieldPlugin

  • ModuleKindPlugin

  • JoinRequestPlugin

  • TryNextPlugin

  • ModulesInHierachicDirectoriesPlugin

  • DescriptionFilePlugin

上面是在