Webpack4 源码解析之文件resolve流程

652 阅读9分钟

Webpack源码解析

// 使用webpack版本

"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"

打包主流程分析

上一篇文章 compiler.run 做了什么 讲到了文件的 resolve,下面我们就看看一个普通文件的resolve流程。

普通文件的 doResolve

核心就是组装 stack,然后进行钩子调用,每次调用钩子都不同,上一个流程会提供下一个流程的 hook,hook 上所有 plugin 都会在node_modules/enhanced-resolve/lib/ResolverFactory.js中实例化,实例化时传入 target,调用插件时(执行 plugin 实例的 apply 方法)会通过调用 const target = resolver.ensureHook(this.target); 挂载钩子(node_modules/enhanced-resolve/lib/Resolver.js 第 87 行)。

// node_modules/enhanced-resolve/lib/Resolver.js

return hook.callAsync(request, innerContext, (err, result) => {
  if (err) return callback(err);
  if (result) return callback(null, result);
  callback();
});

1)第一个钩子是类 Resolver 挂载的 resolve 钩子,上面注册的钩子函数 UnsafeCachePlugin 查看是否存在 cacheEntry,存在的话直接返回缓存,不存在就 resolver.doResolve 继续回去执行 doResolve:

// node_modules/enhanced-resolve/lib/UnsafeCachePlugin.js

apply(resolver) {
  // 挂载新的钩子 newResolve
  const target = resolver.ensureHook(this.target);
  resolver
    .getHook(this.source)
    .tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {
      if (!this.filterPredicate(request)) return callback();
      // 查看是否有缓存
      // cacheId --> '{"context":"","path":"/Users/***/lagou-edu/webpack-entry","request":"./src/index.js"}'
      const cacheId = getCacheId(request, this.withContext);
      const cacheEntry = this.cache[cacheId];
      // 有缓存就执行回调,将缓存数据交由回调处理
      if (cacheEntry) {
        return callback(null, cacheEntry);
      }
      // 否则继续回去执行doResolve,只不过此时的hook变成了newResolve
      resolver.doResolve(
        target,
        request,
        null,
        resolveContext,
        (err, result) => {
          if (err) return callback(err);
          if (result) return callback(null, (this.cache[cacheId] = result));
          callback();
        }
      );
    });
}

此时 stack 中就存在一个内容:

0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"

2)然后就是 UnsafeCachePlugin 挂载的第二个钩子 newResolve, 上面注册的钩子函数 ParsePlugin,解析文件类型:

// node_modules/enhanced-resolve/lib/ParsePlugin.js

apply(resolver) {
  // 挂载钩子 parsedResolve
  const target = resolver.ensureHook(this.target);
  resolver
    .getHook(this.source)
    .tapAsync("ParsePlugin", (request, resolveContext, callback) => {
      // 解析文件类别
      // { request: './src/index.js', query: '', module: false, directory: false, file: false }
      const parsed = resolver.parse(request.request);
      const obj = Object.assign({}, request, parsed);
      // 合并参数
      if (request.query && !parsed.query) {
        obj.query = request.query;
      }
      // 如果打印日志的话就打印日志
      if (parsed && resolveContext.log) {
        if (parsed.module) resolveContext.log("Parsed request is a module");
        if (parsed.directory)
          resolveContext.log("Parsed request is a directory");
      }
      // 然后继续执行doResolve,此时的hook成为了parsedResolve
      resolver.doResolve(target, obj, null, resolveContext, callback);
    });
}

ParsePlugin 执行完毕会得到文件的类型,模块、文件、文件夹,和加载文件额外的请求参数,最终将信息合并到 request 中继续执行 doResolve,此时的 hook 变成了 parsedResolve。

此时 stack 中有两个内容:

0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"

3)第三个钩子是 ParsePlugin 挂载的 parsedResolve, 上面注册的函数 DescriptionFilePluginNextPlugin

DescriptionFilePlugin 是查找项目描述文件 package.json:

// node_modules/enhanced-resolve/lib/DescriptionFilePlugin.js

apply(resolver) {
  // 挂载钩子 describedResolve
  const target = resolver.ensureHook(this.target);
  resolver
    .getHook(this.source)
    .tapAsync("DescriptionFilePlugin", (request, resolveContext, callback) => {
      // 获取问价夹路径(项目目录)
      const directory = request.path;
      // 加载描述文件package.json
      DescriptionFileUtils.loadDescriptionFile(
        resolver,
        directory,
        this.filenames,
        resolveContext,
        (err, result) => {
          // 加载出错返回错误信息
          if (err) return callback(err);
          // 没有结果的话执行_nextPlugin
          if (!result) {
            if (resolveContext.missing) {
              this.filenames.forEach((filename) => {
                resolveContext.missing.add(resolver.join(directory, filename));
              });
            }
            if (resolveContext.log)
              resolveContext.log("No description file found");
            return callback();
          }
          // 有结果的话将读取到 package.json 的信息和其所在的目录/路径信息,存入 request 中
          const relativePath =
            "." +
            request.path.substr(result.directory.length).replace(/\\/g, "/");
          const obj = Object.assign({}, request, {
            descriptionFilePath: result.path,
            descriptionFileData: result.content,
            descriptionFileRoot: result.directory,
            relativePath: relativePath,
          });
          // 继续执行事件流
          resolver.doResolve(
            target,
            obj,
            "using description file: " +
              result.path +
              " (relative path: " +
              relativePath +
              ")",
            resolveContext,
            (err, result) => {
              if (err) return callback(err);

              // Don't allow other processing
              if (result === undefined) return callback(null, null);
              callback(null, result);
            }
          );
        }
      );
    });
}

这里执行稍微复杂点,下面给出调用栈中的执行函数:

// VM46947652

(function anonymous(request, resolveContext, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  function _next0() {
    // 取出_nextPlugin执行
    var _fn1 = _x[1];
    _fn1(request, resolveContext, (_err1, _result1) => {
      if (_err1) {
        _callback(_err1);
      } else {
        if (_result1 !== undefined) {
          _callback(null, _result1);
        } else {
          _callback();
        }
      }
    });
  }
  // DescriptionFilePlugin 注册的函数
  var _fn0 = _x[0];
  // 先执行读取
  _fn0(request, resolveContext, (_err0, _result0) => {
    // 存在错误直接调用钩子的回调,返回错误
    if (_err0) {
      _callback(_err0);
    } else {
      // 如果有读取结果,将结果返回
      if (_result0 !== undefined) {
        _callback(null, _result0);
      } else {
        // 否则就调用_next0
        _next0();
      }
    }
  });
});

NextPlugin 起到衔接的作用,内部直接调用 doResolve,触发下一个事件。当 DescriptionFilePlugin 中未找到 package.json 文件时,会进入 NextPlugin,然后让事件流继续下去。

这一步的目的主要是为了下一步的路径别名解析做准备,因为路径别名依赖于 package.json。

如果 DescriptionFileUtils.loadDescriptionFile 调试不成功,反正我是没成功过(调试了无数遍,不知道原因,无法正常进入回调),在内部直接改成 resolver.doResolve(target, request, null, resolveContext, callback);,进行下一个步骤继续调试。

此时 stack 就有三个内容了:

0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"

4)第四个钩子是 DescriptionFilePlugin 挂载的 describedResolve,这个钩子上注册了 44 个函数:

AliasFieldPlugin --> 40 个 AliasPlugin --> ModuleKindPlugin --> JoinRequestPlugin --> RootPlugin

// node_modules/enhanced-resolve/lib/AliasFieldPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // target 挂载为resolve,替换完就会进行resolve,进入一个新文件、模块的加载解析
  resolver
    .getHook(this.source)
    .tapAsync("AliasFieldPlugin", (request, resolveContext, callback) => {
      // 如果没有描述文件就执行回调,进入下一个函数
      if (!request.descriptionFileData) return callback();
      // 解析出请求文件的路径,比如'./src/index.js'
      const innerRequest = c(resolver, request);
      // 不存在请求的话执行回调进行下一个函数
      if (!innerRequest) return callback();
      // 从package.json中取出字段对应的内容,这里的field为browser
      // browser 可定义成和 main/module 字段一一对应的映射对象,也可以直接定义为字符串
      // "browser": {
      //   "./lib/index.js": "./lib/index.browser.js", // browser+cjs
      //   "./lib/index.mjs": "./lib/index.browser.mjs"  // browser+mjs
      // },
      // 关于browser字段可以看这篇文章:[https://github.com/SunshowerC/blog/issues/8](https://github.com/SunshowerC/blog/issues/8)
      const fieldData = DescriptionFileUtils.getField(
        request.descriptionFileData,
        this.field
      );
      // 如果browser字段的内容不是一个对象的话提示配置错误,必须为一个对象,不会终止解析,会执行回调进行下一个函数
      if (typeof fieldData !== "object") {
        if (resolveContext.log)
          resolveContext.log(
            "Field '" +
              this.field +
              "' doesn't contain a valid alias configuration"
          );
        return callback();
      }
      // './src/index.js' 对应的值
      const data1 = fieldData[innerRequest];
      // "src/index.js" 对应的值
      const data2 = fieldData[innerRequest.replace(/^\.\//, "")];
      // 相对路径和绝对路径兼容
      const data = typeof data1 !== "undefined" ? data1 : data2;
      // 如果值和innerRequest相同就执行回调进行下一个函数,因为innerRequest已经resolve了
      if (data === innerRequest) return callback();
      // 如果是undefined执行回调进行下一个函数
      if (data === undefined) return callback();
      // 如果是false阻止将模块或文件加载到包中
      if (data === false) {
        const ignoreObj = Object.assign({}, request, {
          path: false,
        });
        return callback(null, ignoreObj);
      }
      // 将描述文件根路径和对应的文件路径合并到request
      const obj = Object.assign({}, request, {
        path: request.descriptionFileRoot,
        request: data,
      });
      // 进行resolve
      resolver.doResolve(
        target,
        obj,
        "aliased from description file " +
          request.descriptionFilePath +
          " with mapping '" +
          innerRequest +
          "' to '" +
          data +
          "'",
        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);
        }
      );
    });
}
// node_modules/enhanced-resolve/lib/AliasFieldPlugin.js

/*
 * 源配置
 * alias: {
 *   'vue$': 'vue/dist/vue.esm.js',
 *   '@': '../src'
 * }
 * 转换后为
 * alias:[
 *   {
 *     name: 'vue',
 *     onlyModule: true,
 *     alias: 'vue/dist/vue.esm.js'
 *   },
 *   {
 *     name: '@',
 *     onlyModule: false,
 *     alias: '../src'
 *   }
 * ]
*/
apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // target 挂载为resolve
  resolver
    .getHook(this.source)
    .tapAsync("AliasPlugin", (request, resolveContext, callback) => {
      // 取出内部请求,比如 @/user/login.js
      const innerRequest = request.request || request.path;
      // 没有内部请求就执行回调,进行下一个alias解析
      if (!innerRequest) return callback();
      // 以这个为例 {name: '@', onlyModule: false, alias: '../src'}
      for (const item of this.options) {
        // request 是以 @/ 开头 或者就是 @(比如src/index.js 直接可以写成 @)
        if (
          innerRequest === item.name ||
          (!item.onlyModule && startsWith(innerRequest, item.name + "/"))
        ) {
          // 尚未被替换掉
          if (
            innerRequest !== item.alias &&
            !startsWith(innerRequest, item.alias + "/")
          ) {
            // 将name替换为alias @ ---> ../src
            const newRequestStr =
              item.alias + innerRequest.substr(item.name.length);
            // 将新的请求合并至request
            const obj = Object.assign({}, request, {
              request: newRequestStr,
            });
            // 替换完就进行resolve
            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();
    });
}

这一步就是处理别名的,它依赖于 package.json 中的配置,所以放在了 DescriptionFilePlugin 后面执行。AliasFieldPlugin 会先解析出 package.json中的 alias ,也就是 browser 字段设置的路径映射(CommonJS 和 ESModule)。除了我们自己配置的别名外,webpack 内部还有一些自带的 alias,每一个 alias 都会注册一个 AliasPlugin 函数进行处理,一旦匹配到 alias 的 name 就用新的别名 alias 替换 request 参数,然后进行 resolve,如果没有匹配到则进入 ModuleKindPlugin 函数。

ModuleKindPlugin 会根据 request.module(上面 ParsePlugin 返回的 module 值) 的值走不同的分支。如果是 module,则后续进入 rawModule 的逻辑,进行模块的 resolve。否则的话就返回 undefined,继续进入下一个处理函数 JoinRequestPlugin

// node_modules/enhanced-resolve/lib/ModuleKindPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // 挂载 module hook
  resolver
    .getHook(this.source)
    .tapAsync("ModuleKindPlugin", (request, resolveContext, callback) => {
      // 如果不是module就执行下一个函数
      if (!request.module) return callback();
      // 如果是module就合并request,删除module字段然后以加载module的方式执行resolve
      const obj = Object.assign({}, request);
      delete obj.module;
      resolver.doResolve(
        target,
        obj,
        "resolve as module",
        resolveContext,
        (err, result) => {
          if (err) return callback(err);
          // Don't allow other alternatives
          if (result === undefined) return callback(null, null);
          callback(null, result);
        }
      );
    });
}

JoinRequestPlugin 将 request 中 path 和 request 合并起来,将 request 中 relativePath 和 request 合并起来,得到两个完整的路径:

// node_modules/enhanced-resolve/lib/JoinRequestPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // 挂载 relative hook
  resolver
    .getHook(this.source)
    .tapAsync("JoinRequestPlugin", (request, resolveContext, callback) => {
      // 合并路径得到 path: '/Users/***/lagou-edu/webpack-entry/src/index.js' relativePath: undefind
      const obj = Object.assign({}, request, {
        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);
    });
}

JoinRequestPlugin 合并完毕后将合并结果合并至 request,然后进行 resolve,执行挂载的钩子 relative,这个钩子上同样注册了两个钩子函数:DescriptionFilePluginNextPlugin,不过与上面 DescriptionFilePlugin不同的是 ,此时的 request.path 变成了 /Users/***/lagou-edu/webpack-entry/src/index.js,由于 path 改变了,所以需要再次查找一下 package.json,进行路径映射匹配,最终进行 resolve,此时的 hook 已经挂载为 describedRelative

执行完 JoinRequestPlugin 的 stack 里面就有 5 个内容了:

0:"resolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "

5)第五个钩子就是上面第二次执行 DescriptionFilePlugin 时挂载的钩子 describedRelative,它上面注册有钩子函数 FileKindPluginTryNextPlugin

FileKindPlugin 函数判断目标是否为一个 directory,如果是则返回 undefined, 进入下一个 tryNextPlugin 函数,这时会进入加载 directory 的分支。否则,则表明是一个文件,进入 rawFile 事件:

// node_modules/enhanced-resolve/lib/FileKindPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // 挂载 rawFile hook
  resolver
    .getHook(this.source)
    .tapAsync("FileKindPlugin", (request, resolveContext, callback) => {
      // 如果是一个文件夹,执行回调,返回undefined,会进入下一个函数tryNextPlugin
      if (request.directory) return callback();
      // 否则的话就是一个file,不可能是module了,因为前面已经判断过module的逻辑
      // 合并request,进行resolve
      const obj = Object.assign({}, request);
      // 因为不是文件夹,删除它的directory
      delete obj.directory;
      resolver.doResolve(target, obj, null, resolveContext, callback);
    });
}

tryNextPlugin 函数起一个衔接的作用,直接进行 resolve,进入下一个流程:

// node_modules/enhanced-resolve/lib/TryNextPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // 挂载 directory hook(上面判断是否为文件夹,如果是,这里的hook就挂载为directory,然后进行resolve,执行directory上注册的函数)、
  // node_modules/enhanced-resolve/lib/ResolverFactory.js 275行
  // plugins.push(
  //   new TryNextPlugin("described-relative", "as directory", "directory")
  // );
  resolver
    .getHook(this.source)
    .tapAsync("TryNextPlugin", (request, resolveContext, callback) => {
      resolver.doResolve(
        target,
        request,
        this.message,
        resolveContext,
        callback
      );
    });
}

describedRelative 执行完 stack 就有 6 个内容了:

0:"resolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js)

6)第六个就是 FileKindPlugin 挂载的钩子 rawFile,它上面注册了 5 个函数:

TryNextPlugin --> AppendPlugin --> AppendPlugin --> AppendPlugin --> AppendPlugin

这里的 TryNextPlugin 主要是挂载 file hook:

// node_modules/enhanced-resolve/lib/TryNextPlugin.js
apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // 挂载 file hook
  resolver
    .getHook(this.source)
    .tapAsync("TryNextPlugin", (request, resolveContext, callback) => {
      // 过渡到file hook 执行流程
      resolver.doResolve(
        target,
        request,
        this.message,
        resolveContext,
        callback
      );
    });
}

webpack 的 resolve.enforceExtension 决定加载文件时是否可以省略扩展名,默认为 false,也就是默认可以省略的,如果设置为 true 的话,我们加载文件就必须带上文件扩展名,否则就会找不到文件;resolve.extensions 尝试按顺序解析这些后缀名,如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首个匹配到的后缀的文件并跳过其余的后缀,比如 ['.js', '.json', '.scss'],当存在相同命名但后缀不同的文件 ”user.js“、”user.json“、”user.scss“ 时,如果我们导入文件时省略了后缀 import/src/user ,那么 webpack 在解析到”user.js“后就会跳过数组后面的几个后缀。

AppendPlugin 主要是添加上文件扩展名,将 extensions 中的匹配的路径拼接到 request.pathrequest.relativePath 上:

// node_modules/enhanced-resolve/lib/AppendPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // let extensions = options.extensions || [".js", ".json", ".node"];
  // extensions.forEach(item => {
  //   plugins.push(new AppendPlugin("raw-file", item, "file"));
  // });
  // 这里根据extensions生成AppendPlugin插件
  // 挂载file hook,后缀添加完回到 file 流程
  resolver
    .getHook(this.source)
    .tapAsync("AppendPlugin", (request, resolveContext, callback) => {
      const obj = Object.assign({}, request, {
        path: request.path + this.appending,
        relativePath:
          request.relativePath && request.relativePath + this.appending
      });
      resolver.doResolve(
        target,
        obj,
        this.appending,
        resolveContext,
        callback
      );
    });
}

此时 stack 就有 7 个内容了:

0:"resolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
6:"rawFile: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "

7)第七个就是 AppendPlugin 挂载的钩子 file,它上面注册了 3 个钩子函数:

AliasFieldPlugin --> SymlinkPlugin --> FileExistsPlugin

此时路径后缀已经添加上了,又回到了 AliasFieldPlugin 处理 alias 的逻辑,这里只会处理 package.json 中的 browser 配置的路径映射,webpack 自带的 alias 不再重复处理,如果匹配到就进行 resolve(AliasFieldPlugin 里的 target 挂载为 resolve),否则就执行回调,返回 undefined,进入 SymlinkPlugin

SymlinkPlugin 用来处理路径中存在 link 的情况,webpack 默认是按照真实路径进行解析的,如果解析途中遇到 link 会替换为真实路径:

// node_modules/enhanced-resolve/lib/SymlinkPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // 挂载 relative hook,替换完进入 relative 流程,因为 path 改变了,需要重新加载 package.json 查找路径映射
  const fs = resolver.fileSystem;
  resolver
    .getHook(this.source)
    .tapAsync("SymlinkPlugin", (request, resolveContext, callback) => {
      // 分解path,得到两个数组 paths 和 seqments
      const pathsResult = getPaths(request.path);

      // ['index.js', 'src', 'webpack-entry', 'lagou-edu', 'xxx', 'Users', '/']
      const pathSeqments = pathsResult.seqments;

      // 0:'/Users/---/lagou-edu/webpack-entry/src/index.js'
      // 1:'/Users/---/lagou-edu/webpack-entry/src'
      // 2:'/Users/---/lagou-edu/webpack-entry'
      // 3:'/Users/---/lagou-edu'
      // 4:'/Users/---'
      // 5:'/Users'
      // 6:'/'
      const paths = pathsResult.paths;
      // 包含link标识,默认为false
      let containsSymlink = false;
      forEachBail.withIndex(
        paths,
        (path, idx, callback) => {
          // 读取link
          fs.readlink(path, (err, result) => {
            if (!err && result) {
              // 将pathSeqments里面的值替换为读取到的结果
              pathSeqments[idx] = result;
              // 设置标识为true
              containsSymlink = true;
              // 找到绝对符号链接的快捷路径,就进入下一个函数处理link(symlink必须是一个绝对路径,不可能是相对路径)
              // 这个正则就是检测结果是不是一个绝对路径,是的话才进入下面的函数处理link
              if (/^(\/|[a-zA-Z]:($|\\))/.test(result))
                return callback(null, idx);
            }
            // 读取不到就执行回调,返回undefined,执行下一个函数FileExistsPlugin
            callback();
          });
        },
        (err, idx) => {
          // 如果不存在link执行下一个函数FileExistsPlugin
          if (!containsSymlink) return callback();
          // 将link替换为fs.readlink读取到的真实路径
          const resultSeqments =
            typeof idx === "number"
              ? pathSeqments.slice(0, idx + 1)
              : pathSeqments.slice();
          const result = resultSeqments.reverse().reduce((a, b) => {
            return resolver.join(a, b);
          });
          // 将替换后的路径合并至request
          const obj = Object.assign({}, request, {
            path: result,
          });
          // 进入下一个流程 relative
          resolver.doResolve(
            target,
            obj,
            "resolved symlink to " + result,
            resolveContext,
            callback
          );
        }
      );
    });
}

resolve.symlink 是否将符号链接(symlink)解析到它们的符号链接位置(symlink location)。启用时,符号链接(symlink)的资源,将解析为其 真实 路径,而不是其符号链接(symlink)的位置。注意,当使用创建符号链接包的工具(如 npm link)时,这种方式可能会导致模块解析失败。所以这种方式只能在开发期间使用,正式环境下需要将独立开发的包发布到 npm,然后安装到使用项目中。

关于 symlink,在使用 lerna 管理多个包的开发时,在某个包下使用 npm link 创建这个包的软链,在使用这个包的 package 下通过 npm link packageName 来创建一个链接,此时在 node_modules 下就会存在这个名称为 packageName 的包,不过它不是真实存在于这个使用方的依赖下,而是一个 symlink,当使用 webpack 解析到这个请求时,就是尝试使用 SymlinkPluginrequest path 替换为真实的路径,然后重新回到 relative 流程,查找是否存在路径映射。

如果不存在 symlink,或者解析异常,路径不是绝对路径都会执行 callback,返回 undefined,进入下一个函数 FileExistsPlugin

FileExistsPlugin 检测文件是否存在,存在的话就会进入下一个流程 existing-file :

// node_modules/enhanced-resolve/lib/FileExistsPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // 挂载 existing-file hook

  const fs = resolver.fileSystem;
  resolver
    .getHook(this.source)
    .tapAsync("FileExistsPlugin", (request, resolveContext, callback) => {
      const file = request.path;
      // 检测路径
      fs.stat(file, (err, stat) => {
        // 如果检测出错或者没有检测结果,记录错误并打印日志(配置了的话),执行回调
        if (err || !stat) {
          if (resolveContext.missing) resolveContext.missing.add(file);
          if (resolveContext.log) resolveContext.log(file + " doesn't exist");
          return callback();
        }
        // 如果检测的结果不是一个文件,记录错误并打印日志(配置了的话),执行回调
        if (!stat.isFile()) {
          if (resolveContext.missing) resolveContext.missing.add(file);
          if (resolveContext.log) resolveContext.log(file + " is not a file");
          return callback();
        }
        // 否则进行下一个流程existing-file
        resolver.doResolve(
          target,
          request,
          "existing file: " + file,
          resolveContext,
          callback
        );
      });
    });
}

此时 stack 就有 8 个内容了:

0:"resolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/xxx/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
6:"rawFile: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "
7:"file: (/Users/xxx/lagou-edu/webpack-entry/src/index.js) "

8)第八个就是 FileExistsPlugin 挂载的钩子 existing-file,它上面注册了一个函数 NextPlugin,直接进入下一个流程:

// node_modules/enhanced-resolve/lib/NextPlugin.js

apply(resolver) {
  const target = resolver.ensureHook(this.target);
  // 挂载 resolved hook
  resolver
    .getHook(this.source)
    .tapAsync("NextPlugin", (request, resolveContext, callback) => {
      resolver.doResolve(target, request, null, resolveContext, callback);
    });
}

此时 stack 就有 9 个内容了:

0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
6:"rawFile: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
7:"file: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
8:"existingFile: (/Users/---/lagou-edu/webpack-entry/src/index.js) "

9)第九个就是 FileExistsPlugin 挂载的钩子 resolved,它上面注册了一个函数 ResultPlugin

// node_modules/enhanced-resolve/lib/ResultPlugin.js

apply(resolver) {
  this.source.tapAsync("ResultPlugin", (request, resolverContext, callback) => {
    // 合并request
    const obj = Object.assign({}, request);
    // 报告结果路径
    if (resolverContext.log)
      resolverContext.log("reporting result " + obj.path);
    // 执行resolver钩子上挂载的result钩子,如果有注册函数就执行没有就直接执行回调
    // 这里的回调时doResolve hook执行的最终回调
    // return hook.callAsync(request, innerContext, (err, result) => {
    //   if (err) return callback(err);
    //   if (result) return callback(null, result);
    //   callback();
    // });
    resolver.hooks.result.callAsync(obj, resolverContext, (err) => {
      if (err) return callback(err);
      callback(null, obj);
    });
  });
}

ResultPlugin 最终先执行 result 钩子上注册的函数,没有注册或者执行完会最终执行函数 doResolve 中 hook 执行的最终回调,然后走到 resolve 中调用 doResolve 的回调

此时 stack 就有 10 个内容了:

0:"resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
1:"newResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
2:"parsedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
3:"describedResolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js"
4:"relative: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
5:"describedRelative: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
6:"rawFile: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
7:"file: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
8:"existingFile: (/Users/---/lagou-edu/webpack-entry/src/index.js) "
9:"resolved: (/Users/---/lagou-edu/webpack-entry/src/index.js) "

最后我们看看 ResolverFactory 中挂载的钩子:

// node_modules/enhanced-resolve/lib/ResolverFactory.js

//// pipeline ////
resolver.ensureHook("resolve");
resolver.ensureHook("parsedResolve");
resolver.ensureHook("describedResolve");
resolver.ensureHook("rawModule");
resolver.ensureHook("module");
resolver.ensureHook("relative");
resolver.ensureHook("describedRelative");
resolver.ensureHook("directory");
resolver.ensureHook("existingDirectory");
resolver.ensureHook("undescribedRawFile");
resolver.ensureHook("rawFile");
resolver.ensureHook("file");
resolver.ensureHook("existingFile");
resolver.ensureHook("resolved");

除了 module、directory、existingDirectory、undescribedRawFile 几个钩子外,其它的 10 个钩子全部在我们最终的 stack 中,这就是我们加载一个普通 file 的整个流程,也就是所谓的 pipeline(管道思想)。

module的doResolve

ParsePlugin 的时候就会标识加载的目标是不是一个module,模块其实就类似于import vue from 'vue'这样的第三方模块加载,它也是执行挂载在hook上的钩子函数,一步步处理,最终会转化为directory来处理。而load最终也会转化为一个普通module来处理。