Webpack4源码解析之最终的buildModule

827 阅读6分钟

Webpack源码解析

// 使用webpack版本

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

打包主流程分析

上一篇文章 doResolve最终回调链路 分析了模块的 Loader和 Dependencies 经过插件解析最终经过层层回调到 _addModuleChain 中,此时一个完整的 module 信息已经形成,后续就是对这个 module 进行最终的 build。

_addModuleChain 中 module 创建完毕后的 buildModule

build 的流程不复杂,主要是加载 Loader,执行 Loader 的 pitch 函数和 normal 函数,对资源进行解析处理,最终将解析结果返回到 build 中的回调中,经过this.parser.parse()进行最终的解析,此时解析结果包含资源内容和资源其它依赖集合,将结果交由 addModuleChain 中 afterBuild 处理时会查找 module 是否存在其它依赖集合,存在的话会递归解析依赖,最终返回整个 module 的解析结果。

// node_modules/webpack/lib/Compilation.js

/**
 * Builds the module object
 *
 * @param {Module} module module to be built
 * @param {boolean} optional optional flag
 * @param {Module=} origin origin module this module build was requested from
 * @param {Dependency[]=} dependencies optional dependencies from the module to be built
 * @param {TODO} thisCallback the callback
 * @returns {TODO} returns the callback function with results
 */
buildModule(module, optional, origin, dependencies, thisCallback) {
  // 取出当前module打包的回调,这里是第一次打包,所以为undefined
  let callbackList = this._buildingModules.get(module);
  if (callbackList) {
    callbackList.push(thisCallback);
    return;
  }

  // 设置building回调列表
  this._buildingModules.set(module, (callbackList = [thisCallback]));

  // build后的最终回调,build已经完成,删除 module 对应的 building 回调
  // 然后依次执行回调
  const callback = (err) => {
    this._buildingModules.delete(module);
    for (const cb of callbackList) {
      // 执行回调,回到_addModuleChain
      cb(err);
    }
  };

  // 执行挂载在buildModule hook上的函数
  this.hooks.buildModule.call(module);

  // 进行build
  module.build(
    this.options, // 配置选项
    this, // Compilation实例
    this.resolverFactory.get("normal", module.resolveOptions), // NormalResolver {"normal|{}" => Resolver}
    this.inputFileSystem,
    (error) => {
      // 当回到这个回调的时候说明文件的AST树已经生成,并经过每个分支的解析,所有依赖全部添加至module,下面就可以进行依赖的加载处理
      // 错误处理
      const errors = module.errors;
      for (let indexError = 0; indexError < errors.length; indexError++) {
        const err = errors[indexError];
        err.origin = origin;
        err.dependencies = dependencies;
        if (optional) {
          this.warnings.push(err);
        } else {
          this.errors.push(err);
        }
      }

      // 警告处理
      const warnings = module.warnings;
      for (
        let indexWarning = 0;
        indexWarning < warnings.length;
        indexWarning++
      ) {
        const war = warnings[indexWarning];
        war.origin = origin;
        war.dependencies = dependencies;
        this.warnings.push(war);
      }

      // 依赖排序
      const originalMap = module.dependencies.reduce((map, v, i) => {
        map.set(v, i);
        return map;
      }, new Map());
      module.dependencies.sort((a, b) => {
        const cmp = compareLocations(a.loc, b.loc);
        if (cmp) return cmp;
        return originalMap.get(a) - originalMap.get(b);
      });
      if (error) {
        this.hooks.failedModule.call(module, error);
        return callback(error);
      }
      this.hooks.succeedModule.call(module);
      // 最终执行_addModuleChain中的afterBuild,存在依赖的话会递归加载依赖,加载过程和普通file类似,都是从NormalModule create开始
      return callback();
    }
  );
}

module.build 首先会到 ResolverFactory 模块中通过 get 方法找到 build 的模块:

// node_modules/webpack/lib/ResolverFactory.js

get(type, resolveOptions) {
  resolveOptions = resolveOptions || EMTPY_RESOLVE_OPTIONS;
  const ident = `${type}|${JSON.stringify(resolveOptions)}`;
  // 这里的ident是normal,在这里就是从cache2中取出NormalModule并返回,如果没有就创建一个
  // 此时再调用 module.build 是其实就是在调用 NormalModule 的 build 方法
  const resolver = this.cache2.get(ident);
  if (resolver) return resolver;
  const newResolver = this._create(type, resolveOptions);
  this.cache2.set(ident, newResolver);
  return newResolver;
}

获取到 NormalModule 后执行它的 build 方法,build 经过打包信息整合后调用 doBuild 进行打包:

// node_modules/webpack/lib/NormalModule.js

build(options, compilation, resolver, fs, callback) {
  // 打包时间戳
  this.buildTimestamp = Date.now();
  // 建成标识
  this.built = true;
  // 资源
  this._source = null;
  // 资源大小
  this._sourceSize = null;
  // AST树
  this._ast = null;
  // 打包hash
  this._buildHash = "";
  // 错误和告警
  this.error = null;
  this.errors.length = 0;
  this.warnings.length = 0;
  // 打包元信息
  this.buildMeta = {};
  // 打包信息
  this.buildInfo = {
    cacheable: false,
    fileDependencies: new Set(),
    contextDependencies: new Set(),
    assets: undefined,
    assetsInfo: undefined,
  };

  /**
   * options webpack 配置项
   * compilation Compilation实例
   * resolver NormalModuleResolve
   */
  return this.doBuild(options, compilation, resolver, fs, (err) => {
    this._cachedSources.clear();

    // 如果返回错误信息,表示创建module失败,退出打包
    if (err) {
      this.markModuleAsErrored(err);
      this._initBuildHash(compilation);
      return callback();
    }

    // 检测request是否是防止被解析的请求,如果是直接创建build hash,并执行回调终止解析
    // webpack可以通过配置 noParse 阻止一些资源被重复解析,有的第三方依赖本来就是一个独立的依赖模块
    // 已经被编译打包好的,无需再次经过编译解析、再次打包,可以节省资源消耗
    const noParseRule = options.module && options.module.noParse;
    if (this.shouldPreventParsing(noParseRule, this.request)) {
      this._initBuildHash(compilation);
      return callback();
    }

    // 解析失败结果
    const handleParseError = (e) => {
      const source = this._source.source();
      const loaders = this.loaders.map((item) =>
        contextify(options.context, item.loader)
      );
      const error = new ModuleParseError(this, source, e, loaders);
      this.markModuleAsErrored(error);
      this._initBuildHash(compilation);
      return callback();
    };

    // 解析成功结果,生成build hash,执行buildModule回调
    const handleParseResult = (result) => {
      this._lastSuccessfulBuildMeta = this.buildMeta;
      this._initBuildHash(compilation);
      return callback();
    };

    // 此时文件内容已经经过Loader解析,此时调用Module的parse方法进行最终的解析
    try {
      const result = this.parser.parse(
        // this._source:
        // _name:'/Users/xueyong/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js??ref--4!/Users/xueyong/lagou-edu/webpack-entry/src/index.js'
        // _value:'import vue from 'vue';\nconsole.log(vue);'
        // __proto__:Source
        // this._source 的 __proto__ 继承至 node_modules/webpack-sources/lib/OriginalSource.js
        // 它的source 方法返回 _value,即Loader处理后的文件内容
        this._ast || this._source.source(),
        {
          current: this, // 当前module
          module: this, // 当前module
          compilation: compilation, // 当前Compilation实例
          options: options, // 当前webpack参数选项
        },
        (err, result) => {
          if (err) {
            handleParseError(err);
          } else {
            handleParseResult(result);
          }
        }
      );
      // 经过各个分支的处理解析,最终将所有依赖全部添加至 module 的 depend 上:

      // lastHarmonyImportOrder:1
      // harmonySpecifier:Map(1) {vue => {source: 'vue', …}}
      // harmonyParserScope:{}
      // current:NormalModule {dependencies: Array(5), blocks: Array(0), variables:  …}
      // compilation:Compilation {_pluginCompat: SyncBailHook, hooks: {…}, name: undefined, …}
      // module:NormalModule {dependencies: Array(5), blocks: Array(0), variables: Array(0), type: 'javascript/auto', …}
      // options:{entry: './src/index.js', output: {…}, mode: 'development', devtool: 'none', plugins: Array(1), …}
      // __proto__:Object

      // 然后调用handleParseResult将最终结果返回

      if (result !== undefined) {
        // parse is sync
        handleParseResult(result);
      }
    } catch (e) {
      handleParseError(e);
    }
  });
}

doBuild(options, compilation, resolver, fs, callback) {
  // 生成Loader对象
  // _compilation:Compilation {_pluginCompat: SyncBailHook, hooks: {…}, name: ... }
  // _compiler:Compiler {_pluginCompat: SyncBailHook, hooks: {…}, name: undefined, parentCompilation: ...}
  // _module:NormalModule {dependencies: Array(0), blocks: Array(0), variables: Array(0), type: 'javascript/auto', ...}
  // emitError:error => {...}
  // emitFile:(name, content, sourceMap, assetInfo) => {...}
  // emitWarning:warning => {...}
  // exec:(code, filename) => {...}
  // fs:CachedInputFileSystem {fileSystem: NodeJsInputFileSystem,_statStorage:...}
  // getResolve:getResolve(options) {...}
  // getLogger:name => {..}
  // loadModule:(request, callback) => {…}
  // mode:'development'
  // resolve:ƒ resolve(context, request, callback) {\n\t\t\t\tresolver.resolve({}, context, request, {}, callback);\n\t\t\t}
  // rootContext:'/Users/---/lagou-edu/webpack-entry'
  // sourceMap:false
  // target:'web'
  // version:2
  // webpack:true
  // __proto__:Object
  const loaderContext = this.createLoaderContext(
    resolver,
    options,
    compilation,
    fs
  );

  // 运行Loaders
  runLoaders(
    {
      resource: this.resource,
      loaders: this.loaders,
      context: loaderContext,
      readResource: fs.readFile.bind(fs),
    },
    (err, result) => {
      // loader pitch 和 normal处理结果 result:
      // cacheable:true
      // fileDependencies:(1) ['/Users/---/lagou-edu/webpack-entry/src/index.js']
      // resourceBuffer:Buffer(39) [...]
      // result:(2) ['import vue from 'vue';\nconsole.log(vue);', null]
      // contextDependencies:(0) []
      // __proto__:Object

      // 设置缓存和依赖信息
      if (result) {
        this.buildInfo.cacheable = result.cacheable;
        this.buildInfo.fileDependencies = new Set(result.fileDependencies);
        this.buildInfo.contextDependencies = new Set(
          result.contextDependencies
        );
      }

      // 接收到runLoaders的错误信息,反馈至build函数回调,终止打包,抛出错误信息
      if (err) {
        if (!(err instanceof Error)) {
          err = new NonErrorEmittedError(err);
        }
        const currentLoader = this.getCurrentLoader(loaderContext);
        const error = new ModuleBuildError(this, err, {
          from:
            currentLoader &&
            compilation.runtimeTemplate.requestShortener.shorten(
              currentLoader.loader
            ),
        });
        return callback(error);
      }

      // 资源buffer
      const resourceBuffer = result.resourceBuffer;
      // 资源内容
      const source = result.result[0];
      // 设置sourceMap
      const sourceMap = result.result.length >= 1 ? result.result[1] : null;
      // 设置额外的信息
      const extraInfo = result.result.length >= 2 ? result.result[2] : null;

      // 如果返回的解析内容既不是一个buffer,也不是一个string,返回错误信息
      if (!Buffer.isBuffer(source) && typeof source !== "string") {
        const currentLoader = this.getCurrentLoader(loaderContext, 0);
        const err = new Error(
          `Final loader (${
            currentLoader
              ? compilation.runtimeTemplate.requestShortener.shorten(
                  currentLoader.loader
                )
              : "unknown"
          }) didn't return a Buffer or String`
        );
        const error = new ModuleBuildError(this, err);
        return callback(error);
      }

      // 创建一个source对象,继承至OriginSource
      // this._source:
      // _name:'/Users/---/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js??ref--4!/Users/---/lagou-edu/webpack-entry/src/index.js'
      // _value:'import vue from 'vue';\nconsole.log(vue);'
      // __proto__:Source
      this._source = this.createSource(
        this.binary ? asBuffer(source) : asString(source),
        resourceBuffer,
        sourceMap
      );
      this._sourceSize = null;
      // 如果额外信息有AST信息的话取出来
      this._ast =
        typeof extraInfo === "object" &&
        extraInfo !== null &&
        extraInfo.webpackAST !== undefined
          ? extraInfo.webpackAST
          : null;
      return callback();
    }
  );
}

module.noParse 防止 webpack 解析那些任何与给定正则表达式相匹配的文件。忽略的文件中不应该含有 import, require, define 的调用,或任何其他导入机制。忽略大型的 library 可以提高构建性能。

// webpack.config.js

module.exports = {
  //...
  module: {
    noParse: /jquery|lodash/,
  },
};

module.exports = {
  //...
  module: {
    noParse: (content) => /jquery|lodash/.test(content),
  },
};

下面就是递归执行 Loader 的 pitch、normal 函数对资源进行加载:

// node_modules/loader-runner/lib/LoaderRunner.js

function runSyncOrAsync(fn, context, args, callback) {
  var isSync = true;
  var isDone = false;
  var isError = false; // internal error
  var reportedError = false;
  // 提供异步解析,上下文上绑定async方法,提供一个内部回调
  context.async = function async() {
    if (isDone) {
      if (reportedError) return; // ignore
      throw new Error("async(): The callback was already called.");
    }
    isSync = false;
    return innerCallback;
  };
  // 内部回调修改上下文回调,内部执行最终回调将解析结果返回
  var innerCallback = (context.callback = function () {
    if (isDone) {
      if (reportedError) return; // ignore
      throw new Error("callback(): The callback was already called.");
    }
    isDone = true;
    isSync = false;
    try {
      callback.apply(null, arguments);
    } catch (e) {
      isError = true;
      throw e;
    }
  });
  try {
    // 执行pitch\normal函数
    var result = (function LOADER_EXECUTION() {
      return fn.apply(context, args);
    })();
    if (isSync) {
      isDone = true;
      if (result === undefined) return callback();
      if (
        result &&
        typeof result === "object" &&
        typeof result.then === "function"
      ) {
        return result.then(function (r) {
          callback(null, r);
        }, callback);
      }
      return callback(null, result);
    }
  } catch (e) {
    if (isError) throw e;
    if (isDone) {
      // loader is already "done", so we cannot use the callback function
      // for better debugging we print the error on the console
      if (typeof e === "object" && e.stack) console.error(e.stack);
      else console.error(e);
      return;
    }
    isDone = true;
    reportedError = true;
    callback(e);
  }
}

// 默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。
// 每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。complier 将会把它们在 loader 之间相互转换。
function convertArgs(args, raw) {
  // 如果raw设置为false的话,就转换为utf-8字符串
  if(!raw && Buffer.isBuffer(args[0]))
    args[0] = utf8BufferToString(args[0]);
  else if(raw && typeof args[0] === "string")
    args[0] = new Buffer(args[0], "utf-8"); // eslint-disable-line
}

// pitching-loader 相关请查看 https://webpack.docschina.org/api/loaders/#pitching-loader

// 比如”use: ["style-loader", "css-loader"],“,css-loader 最终会导出一段包含css代码的js字符串,
// 里面可能还包含一些动态的导入,将其他的css样式代码文件导入,这些在style-loader中都是无法直接处理的,
// style-loader本身只会创建一个style标签,将最终的css代码嵌入进html中,而在嵌入之前的处理就需要交由pitch函数处理

/**
 * 递归执行Loader的pitch函数
 */
function iteratePitchingLoaders(options, loaderContext, callback) {
  // loaderIndex 记录loader pitch函数的执行索引,当执行到最后一个之后就执行Loader
  if (loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

  // 取出当前的Loader对象

  // normal:null
  // ident:'ref--4'
  // data:null
  // normalExecuted:false
  // options:{presets: Array(1)}
  // path:'/Users/---/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js'
  // pitch:null
  // pitchExecuted:false
  // query:'??ref--4'
  // raw:null
  // request (get):ƒ () {\n\t\t\treturn obj.path + obj.query;\n\t\t}
  // request (set):ƒ (value) {...}
  // __proto__:Object
  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

  // 如果当前Loader的pitch函数已经执行过了,递归下一个Loader
  if (currentLoaderObject.pitchExecuted) {
    loaderContext.loaderIndex++;
    return iteratePitchingLoaders(options, loaderContext, callback);
  }

  // 加载Loader模块
  loadLoader(currentLoaderObject, function (err) {
    if (err) {
      loaderContext.cacheable(false);
      return callback(err);
    }
    // 取pitch函数
    var fn = currentLoaderObject.pitch;
    // 标注当前Loader的pitch执行
    currentLoaderObject.pitchExecuted = true;
    // 如果不存在pitch函数就直接递归下一个Loader
    if (!fn) return iteratePitchingLoaders(options, loaderContext, callback);

    // 异步或者同步执行pitch函数,数组中的选项会作为pitch函数的执行实参传入
    // remainingRequest前置请求
    // previousRequest后置请求
    // currentLoaderObject.data 当前loader处理数据
    runSyncOrAsync(
      fn,
      loaderContext,
      [
        loaderContext.remainingRequest,
        loaderContext.previousRequest,
        (currentLoaderObject.data = {}),
      ],
      function (err) {
        if (err) return callback(err);
        // 如果当前pitch函数返回结果不为undefined,那么就终止后续Loader的pitch和normal的执行
        // 直接执行上一个Loader的normal
        // 这里需要区别的是:pitch的执行是从上(左)往下(右),normal的执行是从下(右)往上(左)(我们一般配置是按normal执行顺序配置的)
        var args = Array.prototype.slice.call(arguments, 1);
        if (args.length > 0) {
          loaderContext.loaderIndex--;
          iterateNormalLoaders(options, loaderContext, args, callback);
        } else {
          // 返回结果为undefined的话就继续进行递归,执行下一个Loader的pitch
          iteratePitchingLoaders(options, loaderContext, callback);
        }
      }
    );
  });
}

/**
 * 加载请求资源,获取buffer,然后交由Loader处理
 * @param options {
 *   resourceBuffer: null,
 *   readResource: readResource,
 * }
 * @param loaderContext loaders控制对象
 */
function processResource(options, loaderContext, callback) {
  // 设置最后一个Loader,因为Loader normal从右往左执行
  loaderContext.loaderIndex = loaderContext.loaders.length - 1;

  var resourcePath = loaderContext.resourcePath;
  if (resourcePath) {
    // runLoader中fileDependencies中加入依赖文件
    loaderContext.addDependency(resourcePath);
    // 读取文件buffer(var readResource = options.readResource || readFile;)
    options.readResource(resourcePath, function (err, buffer) {
      if (err) return callback(err);
      // 设置buffer并交由normal处理
      options.resourceBuffer = buffer;
      iterateNormalLoaders(options, loaderContext, [buffer], callback);
    });
  } else {
    iterateNormalLoaders(options, loaderContext, [null], callback);
  }
}

/**
 * normal loader处理文件buffer
 * @param options {
 *   resourceBuffer: null,
 *   readResource: readResource,
 * }
 * @param loaderContext loaders控制对象
 * @param args 文件buffer
 */
function iterateNormalLoaders(options, loaderContext, args, callback) {
  // loaderIndex < 0 表示所有Loader的normal执行完毕,执行回调,返回处理后的args
  // iteratePitchingLoaders callback -> doBuild runLoaders callback -> build callback handleParseResult(result);
  if (loaderContext.loaderIndex < 0) return callback(null, args);

  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

  // 如果当前Loader normal执行过就递归执行下一个 normal 函数
  if (currentLoaderObject.normalExecuted) {
    loaderContext.loaderIndex--;
    return iterateNormalLoaders(options, loaderContext, args, callback);
  }

  // 取出normal函数
  var fn = currentLoaderObject.normal;
  // 执行标识位
  currentLoaderObject.normalExecuted = true;
  // 不存在normal直接进入下一个Loader normal
  if (!fn) {
    return iterateNormalLoaders(options, loaderContext, args, callback);
  }

  // 转换为String还是Buffer
  convertArgs(args, currentLoaderObject.raw);

  // 执行normal,以我们的babel-loader加载index.js为例:
  // runSyncOrAsync 中 ”fn.apply(context, args)“,执行fn时其实执行的是babel-loader的makeLoader函数,this指向context
  // function makeLoader(callback) {
  //   const overrides = callback ? callback(babel) : undefined;
  //   return function (source, inputSourceMap) {
  //     // Make the loader async
  //     const callback = this.async();
  //     loader.call(this, source, inputSourceMap, overrides).then(args => callback(null, ...args), err => callback(err));
  //   };
  // }
  // 此时contest上是挂载有async函数的,就是执行自定义的innerCallback回调,将解析结果args返回

  runSyncOrAsync(fn, loaderContext, args, function (err) {
    if (err) return callback(err);
    // 将执行结果传递给下一个Loader的normal递归执行
    var args = Array.prototype.slice.call(arguments, 1);
    iterateNormalLoaders(options, loaderContext, args, callback);
  });
}


exports.runLoaders = function runLoaders(options, callback) {
  // 读取配置项
  var resource = options.resource || ""; // 需要处理的资源
  var loaders = options.loaders || []; // loaders配置项
  var loaderContext = options.context || {}; // 所有loader工作时共享的一份数据
  var readResource = options.readResource || readFile; // 读取文件方法

  // 获取路径和路径参数
  var splittedResource = resource && splitQuery(resource);
  var resourcePath = splittedResource ? splittedResource[0] : undefined;
  var resourceQuery = splittedResource ? splittedResource[1] : undefined;
  var contextDirectory = resourcePath ? dirname(resourcePath) : null;

  // 执行状态
  var requestCacheable = true; // 缓存标识位
  var fileDependencies = []; // 文件依赖的缓存
  var contextDependencies = []; // 目录依赖的缓存

  // 准备Loader对象
  loaders = loaders.map(createLoaderObject);
  loaders 如下
  // [
  //   {
  //     data:null
  //     ident:'ref--4'
  //     normal:null
  //     normalExecuted:false
  //     options:{presets: Array(1)}
  //     path:'/Users/---/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js'
  //     pitch:null
  //     pitchExecuted:false
  //     query:'??ref--4'
  //     raw:null
  //     request (get):ƒ () {\n\t\t\treturn obj.path + obj.query;\n\t\t}
  //     request (set):ƒ (value) {...}
  //     __proto__:Object
  //   }
  // ]

  ...

  // loaderContext上扩展一些属性和方法
  // 其中 loaderIndex 是一个指针,它控制了所有 loaders 的 pitch 与 normal 函数的执行

  ...

  // Object.preventExtensions()(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions)方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
  // 到这里Loader的上下文环境已经全部整合完毕,闭合LoaderContext,解析过程中它不能够再被扩展修改
  if (Object.preventExtensions) {
    Object.preventExtensions(loaderContext);
  }

  var processOptions = {
    resourceBuffer: null,
    readResource: readResource,
  };
  // 迭代 PitchingLoaders
  iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
    // 如果出错会执行回调,通过doBuild最终到build函数的回调中终止解析抛出错误信息
    if (err) {
      return callback(err, {
        cacheable: requestCacheable,
        fileDependencies: fileDependencies,
        contextDependencies: contextDependencies,
      });
    }
    // 不出错的情况下将结果通过回调传给doBuild
    callback(null, {
      result: result,
      resourceBuffer: processOptions.resourceBuffer,
      cacheable: requestCacheable,
      fileDependencies: fileDependencies,
      contextDependencies: contextDependencies,
    });
  });
};

加载 Loader 信息:

// node_modules/loader-runner/lib/loadLoader.js

// LoaderRunner.js 中的 iteratePitchingLoaders 方法会调用 loadLoader 来加载 load module
// loader是一个Loader对象
// 这里主要是获取Loader的三个属性,normalLoader,pitchLoader和raw
module.exports = function loadLoader(loader, callback) {
  if (typeof System === "object" && typeof System.import === "function") {
    // System.js加载模块
    System.import(loader.path)
      .catch(callback)
      .then(function (module) {
        ...
        callback();
      });
  } else {
    try {
      var module = require(loader.path);
    } catch (e) {
      ...
      return callback(e);
    }
    // module必须是一个function或者标准的es6模块
    if (typeof module !== "function" && typeof module !== "object") {
      return callback(
        new LoaderLoadingError(
          "Module '" +
            loader.path +
            "' is not a loader (export function or es6 module)"
        )
      );
    }

    // babel-loader部分源码:

    /**
     * module.exports = makeLoader();
     * module.exports.custom = makeLoader;
     *
     * function makeLoader(callback) {
     *   const overrides = callback ? callback(babel) : undefined;
     *   return function (source, inputSourceMap) {
     *     // Make the loader async
     *     const callback = this.async();
     *     loader.call(this, source, inputSourceMap, overrides).then(args => callback(null, ...args), err => callback(err));
     *   };
     * }
     *
     * function loader(_x, _x2, _x3) {
     *   return _loader.apply(this, arguments);
     * }
     *
     * function _loader() {
     *   ...
     * }
     */

    // 最终的normal就对应于_loader函数

    loader.normal = typeof module === "function" ? module : module.default;
    loader.pitch = module.pitch;
    loader.raw = module.raw;
    // normalLoader和pitch只要存在就必须是函数
    if (
      typeof loader.normal !== "function" &&
      typeof loader.pitch !== "function"
    ) {
      return callback(
        new LoaderLoadingError(
          "Module '" +
            loader.path +
            "' is not a loader (must have normal or pitch function)"
        )
      );
    }
    // 这里的loader是引用类型数据,会直接修改值,无需回传loader,直接执行iteratePitchingLoaders中loadLoader的回调
    callback();
  }
};

可以看到上面的整个流程的核心就是 Loader 的 pitch 和 normal 方法的执行,这个处理完毕会生成一个文件被 Loader 解析的结果:

  cacheable:truefileDependencies:(1) ['/Users/---/lagou-edu/webpack-entry/src/index.js']
▶ resourceBuffer:Buffer(39) [...]
▶ result:(2) ['import vue from 'vue';\nconsole.log(vue);', null]
▶ contextDependencies:(0) []
▶ __proto__:Object

这就是 LoaderRunner.jsrunLoaders 的结果,最终通过执行回调,将结果传回 NormalModule.jsdoBuild,在 runLoaders 的回调中通过 this.createSource(...) 创建一个 OriginSource 实例:

  _name:'/Users/xueyong/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js??ref--4!/Users/xueyong/lagou-edu/  webpack-entry/src/index.js'
  _value:'import vue from 'vue';\nconsole.log(vue);'__proto__:Source

然后再次执行回调,此时进入 build 中进行最终的 parse:

// 目前这里的source就是文件内容_value:'import vue from 'vue';\nconsole.log(vue);'
parse(source, initialState) {
  let ast;
  let comments;
  if (typeof source === "object" && source !== null) {
    ast = source;
    comments = source.comments;
  } else {
    // 注释
    comments = [];
    // 生成AST树,结构如下:

    // body:(2) [Node, Node]
    //   0:Node {type: 'ImportDeclaration', start: 0, end: 22, loc: SourceLocation, range: Array(2), …}
    //   1:Node {type: 'ExpressionStatement', start: 23, end: 40, loc: SourceLocation, range: Array(2), …}
    //   length:2
    // start:0
    // end:40
    // range:(2) [0, 40]
    // loc:SourceLocation {start: Position, end: Position}
    // sourceType:'module'
    // type:'Program'

    ast = Parser.parse(source, {
      sourceType: this.sourceType,
      onComment: comments,
    });
  }

  // 缓存parse的初始信息
  const oldScope = this.scope;
  const oldState = this.state;
  const oldComments = this.comments;

  // 每次parse都会记录当前parse的一些信息,保存在scope中
  this.scope = {
    // 是否是顶级作用域,解析模块中的函数、class类时,它们就属于非顶级作用域
    topLevelScope: true,
    // 当前解析是否在try语句中
    inTry: false,
    // 对象表达式中是否是缩减形式,即 {x: x, y: y, fun: () => {}} 等价于 { x, y, () => {} }
    inShorthand: false,
    // 是否是严格模式
    isStrict: false,
    // 是否是asm.js
    isAsmJs: false,
    // 模块内定义的变量
    definitions: new StackedSetMap(),
    // 模块内可以重命名的变量
    renames: new StackedSetMap(),
  };

  // 保存此次parse的module、compilation、webpack options信息
  const state = (this.state = initialState || {});
  this.comments = comments;

  // 执行program钩子上注册的两个函数:
  // 0:{type: 'sync', fn: ƒ, name: 'HarmonyDetectionParserPlugin'}
  // 1:{type: 'sync', fn: ƒ, name: 'UseStrictPlugin'}
  if (this.hooks.program.call(ast, comments) === undefined) {
    // program hook没有返回值时顺序执行下面四个函数

    // 判断是isStrict,还是isAsmjs
    this.detectMode(ast.body);

    // 迭代普通变量声明(主要解析块级作用域之类的,比如条件语句、循环语句、export、函数、try/catch等)
    this.prewalkStatements(ast.body);

    // 迭代块变量的声明(变量、export、class)
    this.blockPrewalkStatements(ast.body);

    // 迭代迭代对象、表达式(主要解决迭代语句、表达式等的解析)
    this.walkStatements(ast.body);
  }

  // 重置parse信息
  this.scope = oldScope;
  this.state = oldState;
  this.comments = oldComments;

  return state;
}

parse 主要就是对文件内容解析生成 AST 树,然后就是执行 program hook 上的两个函数 HarmonyDetectionParserPluginUseStrictPlugin,进行依赖收集,和严格模式标注。

node_modules/webpack/lib/dependencies 目录下包含很多 webpack 的依赖模板,包含 AMD、CommonJS、ESModule 三种不同的依赖导入的解析,这里因为我们是使用的 ESModule 导入的 Vue,所以这里会采用 HarmonyDetectionParserPlugin 插件来解析依赖,下面的 loc 指的是 location,代码的位置:

// node_modules/webpack/lib/dependencies/HarmonyDetectionParserPlugin.js

module.exports = class HarmonyDetectionParserPlugin {
  // program 上注册的HarmonyDetectionParserPlugin函数
  apply(parser) {
    parser.hooks.program.tap("HarmonyDetectionParserPlugin", (ast) => {
      /**
       * import vue from 'vue'
       *
       * console.log(vue)
       */

      // ast body里面就是两个node:

      /**
       * 1. 'ImportDeclaration' 一个import声明
       * 2. 'ExpressionStatement' 一个表达式
       */

      // 创建module的时候type设置成功`javascript/auto`,没有使用严格模式
      const isStrictHarmony = parser.state.module.type === "javascript/esm";

      // 判断是否是ESModule
      const isHarmony =
        isStrictHarmony ||
        ast.body.some(
          (statement) =>
            statement.type === "ImportDeclaration" ||
            statement.type === "ExportDefaultDeclaration" ||
            statement.type === "ExportNamedDeclaration" ||
            statement.type === "ExportAllDeclaration"
        );

      // 如果是ESModule,创建一个兼容的ESModule依赖模板
      if (isHarmony) {
        const module = parser.state.module;
        const compatDep = new HarmonyCompatibilityDependency(module);
        compatDep.loc = {
          start: {
            line: -1,
            column: 0,
          },
          end: {
            line: -1,
            column: 0,
          },
          index: -3,
        };
        // 添加到module的依赖中
        module.addDependency(compatDep);
        // 创建一个初始依赖模板
        const initDep = new HarmonyInitDependency(module);
        initDep.loc = {
          start: {
            line: -1,
            column: 0,
          },
          end: {
            line: -1,
            column: 0,
          },
          index: -2,
        };
        // 添加到module的依赖中
        module.addDependency(initDep);
        parser.state.harmonyParserScope = parser.state.harmonyParserScope || {};
        parser.scope.isStrict = true;
        // 打包信息中修改导出方式和模块信息为CommonJS
        module.buildMeta.exportsType = "namespace";
        module.buildInfo.strict = true;
        module.buildInfo.exportsArgument = "__webpack_exports__";
        if (isStrictHarmony) {
          module.buildMeta.strictHarmonyModule = true;
          module.buildInfo.moduleArgument = "__webpack_module__";
        }
      }
    });
  }
};

然后就是标注是否是严格模式:

// node_modules/webpack/lib/UseStrictPlugin.js

class UseStrictPlugin {
  /**
   * @param {Compiler} compiler Webpack Compiler
   * @returns {void}
   */
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "UseStrictPlugin",
      (compilation, { normalModuleFactory }) => {
        const handler = (parser) => {
          parser.hooks.program.tap("UseStrictPlugin", (ast) => {
            const firstNode = ast.body[0];
            if (
              firstNode &&
              firstNode.type === "ExpressionStatement" &&
              firstNode.expression.type === "Literal" &&
              firstNode.expression.value === "use strict"
            ) {
              // 删除“use strict”表达式, 稍后渲染器将再次添加它。
              // 为了在 webpack 预先添加代码时不破坏严格模式,这是必要的。
              // @see https://github.com/webpack/webpack/issues/1970
              const dep = new ConstDependency("", firstNode.range);
              dep.loc = firstNode.loc;
              parser.state.current.addDependency(dep);
              // 在打包信息中标注它是严格模式
              parser.state.module.buildInfo.strict = true;
            }
          });
        };
      }
    );
  }
}

依赖模板生成完毕,如果没有返回值就会进入条件内部执行四个函数,第一个是判断 isStrict\isAsmJs 的,没有特别的意义,暂且不看,第二个函数 prewalkStatements 迭代 AST body 内容,然后执行 prewalkStatement 函数进行类别分支控制,进入不同类型语句的解析:

// node_modules/webpack/lib/Parser.js

function prewalkStatement(statement) {
  switch (statement.type) {
    case "BlockStatement":
      this.prewalkBlockStatement(statement);
      break;
    case "DoWhileStatement":
      this.prewalkDoWhileStatement(statement);
      break;

   ...

    case "IfStatement":
      this.prewalkIfStatement(statement);
      break;
    case "ImportDeclaration":
      this.prewalkImportDeclaration(statement);
      break;
    case "LabeledStatement":
      this.prewalkLabeledStatement(statement);
      break;

    ...
  }
}

这里第一行是 import vue from 'vue',所以会走 ImportDeclaration,执行 prewalkImportDeclaration 执行 import 声明的处理:

// node_modules/webpack/lib/Parser.js

prewalkImportDeclaration(statement) {
  // 取出导入变量,这里就是 vue
  const source = statement.source.value;
  // 执行import钩子上注册的函数
  this.hooks.import.call(statement, source);
  // 其它dep添加完毕后,判断是默认导入、导入某个特定的导出、导入命名空间
  for (const specifier of statement.specifiers) {
    const name = specifier.local.name;
    // `import vue from 'vue'` 这里的导入变量vue是可以重命名的,添加入renames中
    this.scope.renames.set(name, null);
    this.scope.definitions.add(name);
    switch (specifier.type) {
      // 这里是默认导入,走这个分支
      case "ImportDefaultSpecifier":
        this.hooks.importSpecifier.call(statement, source, "default", name);
        break;
      case "ImportSpecifier":
        this.hooks.importSpecifier.call(
          statement,
          source,
          specifier.imported.name,
          name
        );
        break;
      case "ImportNamespaceSpecifier":
        this.hooks.importSpecifier.call(statement, source, null, name);
        break;
    }
  }
}

prewalkImportDeclaration 第二行在取出 import 变量后执行了挂载在 import 钩子上的函数,主要就是添加两个额外的依赖,用来清除 import 语句和处理特殊的模块引用方式:

// node_modules/webpack/lib/dependencies/HarmonyImportDependencyParserPlugin.js

parser.hooks.import.tap(
  "HarmonyImportDependencyParserPlugin",
  (statement, source) => {
    parser.state.lastHarmonyImportOrder =
      (parser.state.lastHarmonyImportOrder || 0) + 1;
    // clearDep 清除依赖导入,import 最终会被替换为真实的依赖模块,所有原有的import语句要被删除
    const clearDep = new ConstDependency("", statement.range);
    clearDep.loc = statement.loc;
    parser.state.module.addDependency(clearDep);
    // sideEffectDep 用来处理 ”import b from 'b'; a.use(b);“
    // 最终将a.use(b) 替换为b的代码片段。
    const sideEffectDep = new HarmonyImportSideEffectDependency(
      source,
      parser.state.module,
      parser.state.lastHarmonyImportOrder,
      parser.state.harmonyParserScope
    );
    sideEffectDep.loc = statement.loc;
    parser.state.module.addDependency(sideEffectDep);
    return true;
  }
);

上面执行完毕就会进入 switch/case 分支进行导入判断,区分默认导入、特定导入和命名空间导入,我们这里是默认导入,所以执行 importSpecifier 钩子时传入的 id 就是 default

// node_modules/webpack/lib/dependencies/HarmonyImportDependencyParserPlugin.js

parser.hooks.importSpecifier.tap(
  "HarmonyImportDependencyParserPlugin",
  (statement, source, id, name) => {
    // 将定义的import变量删除
    parser.scope.definitions.delete(name);
    // 重新命名为 imported var
    parser.scope.renames.set(name, "imported var");

    // 设置一个依赖标识符
    if (!parser.state.harmonySpecifier) {
      parser.state.harmonySpecifier = new Map();
    }
    // parser.state 最终结果:

    // options:{entry: './src/index.js', output: {…}, mode: 'development', devtool: 'none', plugins: Array(1), …}
    // module:NormalModule {dependencies: Array(4), blocks: Array(0),  …}
    // compilation:Compilation {_pluginCompat: SyncBailHook, hooks: {…}, …}
    // current:NormalModule {dependencies: Array(4), blocks: Array(0),  …}
    // harmonyParserScope:{}
    // harmonySpecifier:Map(1) {vue => {source: 'vue', …}}
    // lastHarmonyImportOrder:1
    // __proto__:Object
    parser.state.harmonySpecifier.set(name, {
      source,
      id,
      sourceOrder: parser.state.lastHarmonyImportOrder,
    });
    return true;
  }
);

以上 import 语句处理完毕,迭代处理第二个 node,这时第二个 node 是一个表达式 console.log(vue),不符合 prewalkStatement 任何一个分支,所以退出来执行第三个函数 blockPrewalkStatements,同样进行 AST body 迭代然后执行 blockPrewalkStatement 函数进行分支处理:

// node_modules/webpack/lib/Parser.js

blockPrewalkStatement(statement) {
  switch (statement.type) {
    // 变量
    case "VariableDeclaration":
      this.blockPrewalkVariableDeclaration(statement);
      break;
    // 默认导出
    case "ExportDefaultDeclaration":
      this.blockPrewalkExportDefaultDeclaration(statement);
      break;
    // 特定导出
    case "ExportNamedDeclaration":
      this.blockPrewalkExportNamedDeclaration(statement);
      break;
    // class类
    case "ClassDeclaration":
      this.blockPrewalkClassDeclaration(statement);
      break;
  }
}

我们这里的两个 node 皆不满足条件,所以不会执行任何一个分支,直接结束迭代,然后执行第四个函数 walkStatements,迭代 AST body 执行 walkStatement 函数进行分支处理:

// node_modules/webpack/lib/Parser.js
function walkStatement(statement) {
  // 如果自定义处理了,且返回结果不为undefined,就return掉
  if (this.hooks.statement.call(statement) !== undefined) return;
  switch (statement.type) {
    case "BlockStatement":
      this.walkBlockStatement(statement);
      break;
    ...
    case "ExpressionStatement":
      this.walkExpressionStatement(statement);
      break;
    ...
    case "FunctionDeclaration":
      this.walkFunctionDeclaration(statement);
      break;
    case "IfStatement":
      this.walkIfStatement(statement);
      break;
    ...
    case "WhileStatement":
      this.walkWhileStatement(statement);
      break;
    case "WithStatement":
      this.walkWithStatement(statement);
      break;
  }
}

第一条语句不满足任何分支条件,第二条语句 console.log() 是一个表达式,会执行 walkExpressionStatement 函数,该函数内部直接调用了 this.walkExpression 进行表达式判断:

// node_modules/webpack/lib/Parser.js
function walkExpression(expression) {
  switch (expression.type) {
    case "ArrayExpression":
      this.walkArrayExpression(expression);
      break;
    case "ArrowFunctionExpression":
      this.walkArrowFunctionExpression(expression);
      break;
    ...
    case "AwaitExpression":
      this.walkAwaitExpression(expression);
      break;
    case "BinaryExpression":
      this.walkBinaryExpression(expression);
      break;
    case "CallExpression":
      this.walkCallExpression(expression);
      break;
    case "Identifier":
      this.walkIdentifier(expression);
      break;
    ...
    case "YieldExpression":
      this.walkYieldExpression(expression);
      break;
  }
}

console 也相当于函数调用,会进入 CallExpression 分支执行 walkCallExpression 函数:

// node_modules/webpack/lib/Parser.js

function walkCallExpression(expression) {
  if (
    expression.callee.type === "MemberExpression" &&
    expression.callee.object.type === "FunctionExpression" &&
    !expression.callee.computed &&
    (expression.callee.property.name === "call" ||
      expression.callee.property.name === "bind") &&
    expression.arguments.length > 0
  ) {
    // (function(…) { }.call/bind(?, …))
    this._walkIIFE(
      expression.callee.object,
      expression.arguments.slice(1),
      expression.arguments[0]
    );
  } else if (expression.callee.type === "FunctionExpression") {
    // (function(…) { }(…))
    this._walkIIFE(expression.callee, expression.arguments, null);
  } else if (expression.callee.type === "Import") {
    let result = this.hooks.importCall.call(expression);
    if (result === true) return;

    if (expression.arguments) this.walkExpressions(expression.arguments);
  } else {
    // console.log()都不符合前两种调用方式,也不是import,所以执行下面的代码

    // 鉴定表达式
    // BasicEvaluatedExpression {type: 9, identifier:'console.log', range: Array(2), falsy: false, truthy: false ... }
    const callee = this.evaluateExpression(expression.callee);
    // 判断表达式有没有标识符
    if (callee.isIdentifier()) {
      // 看看表达式有没有注册hook,有就执行
      const callHook = this.hooks.call.get(callee.identifier);
      if (callHook !== undefined) {
        let result = callHook.call(expression);
        if (result === true) return;
      }
      // 截取 . 符号之前的表达式
      let identifier = callee.identifier.replace(/\.[^.]+$/, "");
      // 如果截取的表达式和源表达式不同,找出截取的表达式注册的钩子,有就执行它
      if (identifier !== callee.identifier) {
        const callAnyHook = this.hooks.callAnyMember.get(identifier);
        if (callAnyHook !== undefined) {
          let result = callAnyHook.call(expression);
          if (result === true) return;
        }
      }
    }

    // 最终将表达式和参数进行处理
    // 这里如果存在多个点调用的话会递归依次处理
    if (expression.callee) this.walkExpression(expression.callee);
    // 每次调用都需要对参数进行处理
    if (expression.arguments) this.walkExpressions(expression.arguments);
  }
}

递归 点调用(a.b.c()) 的逻辑重复,我们暂且不提,只看一下对于参数的处理,这里会和前面的 import 变量联通,walkExpressions 对传入的参数进行迭代,然后交由函数 walkExpression 处理,参数的 type 对应 Identifier,会走 walkIdentifier 这个函数处理参数标识符:

// node_modules/webpack/lib/Parser.js

function walkIdentifier(expression) {
  // 我们以上在处理import声明的时候已经将导入变量修改了:
  // parser.scope.definitions.delete(name);
  // parser.scope.renames.set(name, "imported var");
  // 这里判断主要是保证import语句已经被处理过了,以便进行后面的参数替换
  if (!this.scope.definitions.has(expression.name)) {
    // 此时的name其实已经被改变为 `imported va`
    // 所以是取出 `imported va` hook
    const hook = this.hooks.expression.get(
      this.scope.renames.get(expression.name) || expression.name
    );
    // 然后就是执行 `imported va` hook
    if (hook !== undefined) {
      const result = hook.call(expression);
      if (result === true) return;
    }
  }
}
// node_modules/webpack/lib/dependencies/HarmonyImportDependencyParserPlugin.js

parser.hooks.expression
  .for("imported var")
  .tap("HarmonyImportDependencyParserPlugin", (expr) => {
    // 取出参数名称,这里就是vue
    const name = expr.name;
    // 取出标识符设置:{source: 'vue', id: 'default', sourceOrder: 1}
    const settings = parser.state.harmonySpecifier.get(name);
    // 创建一个依赖模板
    const dep = new HarmonyImportSpecifierDependency(
      settings.source,
      parser.state.module,
      settings.sourceOrder,
      parser.state.harmonyParserScope,
      settings.id,
      name,
      expr.range,
      this.strictExportPresence
    );
    // 设置填充范围并添加到module的依赖中
    dep.shorthand = parser.scope.inShorthand;
    dep.directImport = true;
    dep.loc = expr.loc;
    parser.state.module.addDependency(dep);
    return true;
  });

到这里内部的四个函数全部执行完毕了,后续就对本次的parse信息进行重置,然后返回结果至最上面的parse:

// result:

// lastHarmonyImportOrder:1
// harmonySpecifier:Map(1) {vue => {source: 'vue', …}}
// harmonyParserScope:{}
// current:NormalModule {dependencies: Array(5), blocks: Array(0), variables:  …}
// compilation:Compilation {_pluginCompat: SyncBailHook, hooks: {…}, name: undefined, …}
// module:NormalModule {dependencies: Array(5), blocks: Array(0), variables: Array(0), type: 'javascript/auto', …}
// options:{entry: './src/index.js', output: {…}, mode: 'development', devtool: 'none', plugins: Array(1), …}
// __proto__:Object

到此整个webpack的核心打包流程就分析完毕了,总结在上篇文章已经说明,可以去看看。