likes
comments
collection
share

进阶开发,跟我一起拿捏webpack loader原理

作者站长头像
站长
· 阅读数 22

前言

上一篇是日常开发,我该掌握哪些webpack loader知识,这一篇主要从源码层面分析,webpack内部是怎么控制loader执行,让我们不仅知道怎么用,还能知道为什么这么用,最后还会分析常用loader的内部实现原理,帮助我们更好的在项目中使用loader

loader 执行原理

loader存在多种形式,比如inline loaderpre loaderpost loader等,这些loader在执行的时候会按照指定的顺序执行,那么我们来看下webpack内部是怎样获取到资源匹配的loader,及怎么控制获取到的loader执行,以webpack处理js文件为例,主要分为两步

  • 第一步:webpack在构建时候,首先js文件会从资源链接上获取inline loader,然后在根据rules匹配到对应的rule,在根据rule获取loader配置,最后根据prepostinlinenormal按照一定的规则组合loader数组
  • 第二步:将上一步组合之后的loader数组,传递给loader-runnerloader-runner内部控制loader数组执行

简单概括如下图所示 进阶开发,跟我一起拿捏webpack loader原理

下面将分别介绍

  • webpack组装loaders数组原理
  • loader-runner执行loaders数组原理
  • 同步与异步loader原理
  • picth loader阻断原理
  • raw loader原理

webpack组装loaders数组原理

webpack内部获取loaders的原理,以normalModule为例:

  1. compilcation.resolve hook上注册callback,在这个callback内开始获取匹配当前moduleloaders
  2. 先获取inline loader,然后在通过resolveRequestArray方法将匹配到的loader转换为loader对象
  3. 在通过this.ruleSet.exec获取rules中配置的loader,包括pre loadernormal loaderpost loader
  4. 然后在通过resolveRequestArray方法将匹配到的normal loader等转换为loader对象
  5. 在然后将获取到的所有loader按照postinlinenormalpre 的顺序进行排列
  6. 最后runLoads执行的时候传入的loaders数组执行loader转换逻辑

流程图如下所示 进阶开发,跟我一起拿捏webpack loader原理

以normalModule为例,点击展开精简后的代码
this.hooks.resolve.tapAsync(
{
  name: "NormalModuleFactory",
  stage: 100
},
(data, callback) => {
  
  ...
  const loaderResolver = this.getResolver("loader");

  const firstChar = requestWithoutMatchResource.charCodeAt(0);
  const secondChar = requestWithoutMatchResource.charCodeAt(1);
  // 判断line loader的三种前缀
  noPreAutoLoaders = firstChar === 45 && secondChar === 33; // startsWith "-!"
  noAutoLoaders = noPreAutoLoaders || firstChar === 33; // startsWith "!"
  noPrePostAutoLoaders = firstChar === 33 && secondChar === 33; // startsWith "!!";
  const rawElements = requestWithoutMatchResource
    .slice(
      noPreAutoLoaders || noPrePostAutoLoaders
        ? 2
        : noAutoLoaders
        ? 1
        : 0
    )
    .split(/!+/);
  unresolvedResource = rawElements.pop();
  // 获取inline loader
  elements = rawElements.map(el => {
    const { path, query } = cachedParseResourceWithoutFragment(el);
    return {
      loader: path,
      options: query ? query.slice(1) : undefined
    };
  });


  // 将inline loader转化为内部loader对象,包含loader的绝对路径及option
  this.resolveRequestArray(
    elements,
    loaderResolver
  );


  // 调用this.ruleSet.exec根据resourceData.path及loader的test等参数匹配符合的loader
  const result = this.ruleSet.exec({
    resource: resourceDataForRules.path,
    realResource: resourceData.path,
    resourceQuery: resourceDataForRules.query,
    ...
  });
  
  // 将上一步匹配到的loader,分布放到对应的数组中,便于组合loader的顺序
  for (const r of result) {
    if (r.type === "use") {
      // 配置中的normal loader,在inline loader的逻辑那里会处理noAutoLoaders、noPrePostAutoLoaders、noPreAutoLoaders的值
      if (!noAutoLoaders && !noPrePostAutoLoaders) {
        useLoaders.push(r.value);
      }
    } else if (r.type === "use-post") {
      if (!noPrePostAutoLoaders) {
        // 配置中的post loader
        useLoadersPost.push(r.value);
      }
    } else if (r.type === "use-pre") {
      if (!noPreAutoLoaders && !noPrePostAutoLoaders) {
        // // 配置中的pre loader
        useLoadersPre.push(r.value);
      }
    }
  }


  let postLoaders, normalLoaders, preLoaders;

  const continueCallback = needCalls(3, err => {
    if (err) {
      return callback(err);
    }
    // 为了保证loader的执行顺序,所以loader的数组顺序为[postLoaders, loaders, normalLoaders, preLoaders] 这里的loader是inline loader
    // 先赋值postLoaders,保证postLoaders在最前面
    const allLoaders = postLoaders;

    // 处理inline loader
    for (const loader of loaders) allLoaders.push(loader);

    // 处理normal loader
    for (const loader of normalLoaders) allLoaders.push(loader);

    // 保证preLoaders在数组的最后
    for (const loader of preLoaders) allLoaders.push(loader);

    try {
      // resolveData.createData做对象合并
      Object.assign(data.createData, {
        ...
        loaders: allLoaders,
      });
    } catch (e) {
      return callback(e);
    }
    callback();
  });
  // 将post loader转换为内部loader对象
  this.resolveRequestArray(
    useLoadersPost,
    loaderResolver
  );
  // 将normal loader转换为内部loader对象
  this.resolveRequestArray(
    useLoaders,
    loaderResolver
  );
  // 将pre loader转换为内部loader对象
  this.resolveRequestArray(
    useLoadersPre,
    loaderResolver
  );

);

// 将inline loader、pre loader等转化为绝对路径的loader
resolveRequestArray(
  contextInfo,
  context,
  array,
  resolver,
) {
  asyncLib.map(
    array,
    (item, callback) => {
      // 调用loaderResolver.resolve方法查找loader文件
      resolver.resolve(
        contextInfo,
        context,
        item.loader,
        resolveContext,
        (err, result) => {
          const parsedResult = this._parseResourceWithoutFragment(result);
          // 格式化loader,将每个loader转化成下面的对象形式
          const resolved = {
            loader: parsedResult.path,
            options: item.options,
            ident: item.options === undefined ? undefined : item.ident
          };
          return callback(null, resolved);
        }
      );
    },
    callback
  );
}

loader-runner执行loaders数组原理

webpackloader的执行,单独封装了一个loader-runner的库,专门来执行loader相关的逻辑,这里以loader-runner@4.3.0的源代码为例

run loader核心原理就是

  1. webpack传入的loaders数组,转换成对象数组
  2. 然后控制loaderIndex0-loaders.length - 1顺序调用pitch loader,通过判断pitch loader是否返回非undefiend的值,如果是则跳过后续loader执行,否则继续执行picth loader的执行
  3. picth loader执行完之后,先获取module source,然后在开始按照loaderIndexloaders.length - 1 => 0的顺序依次调用normal loader,当所有normal loader调用完毕之后,调用run loader传入的callback,表示当前模块对应的所有loader执行完毕

流程图如下图所示 进阶开发,跟我一起拿捏webpack loader原理

点击查看精简后的runLoaders代码
exports.runLoaders = function runLoaders(options, callback) {
  // read options
  var processResource = options.processResource || ((readResource, context, resource, callback) => {
    context.addDependency(resource);
    readResource(resource, callback);
  }).bind(null, options.readResource || readFile);

  // 初始化loaders,将webpack传入的loaders数组,转换成内部的obj数组对象
  loaders = loaders.map(createLoaderObject);

  // loader执行下标,loader的执行顺序,全依赖该loaderIndex
  loaderContext.loaderIndex = 0;
  loaderContext.loaders = loaders;

  // 省略部分代码...
  var processOptions = {
    resourceBuffer: null,
    processResource: processResource
  };

  // 先执行pitch loader
  iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
    if(err) {
      return callback(err);
    }
    callback(null, {
      result: result,
    });
  });
};


function iteratePitchingLoaders(options, loaderContext, callback) {
  // 如果所有的pitch loader执行完了,就读取module source,然后执行normal loader
  if(loaderContext.loaderIndex >= loaderContext.loaders.length)
    return processResource(options, loaderContext, callback);

  // 从0 -> loaderContext.loaders.length 执行picth loader
  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

  // 如果当前pitch loader执行完了,则继续递归调用iteratePitchingLoaders
  if(currentLoaderObject.pitchExecuted) {
    loaderContext.loaderIndex++;
    return iteratePitchingLoaders(options, loaderContext, callback);
  }

  // 获取loader模块暴露的属性,比如picth loader function
  loadLoader(currentLoaderObject, function(err) {
    if(err) {
      return callback(err);
    }
    var fn = currentLoaderObject.pitch;
    currentLoaderObject.pitchExecuted = true;
    // 如果当前loader没有暴露picth loader,则继续递归执行iteratePitchingLoaders
    if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);

    // 如果当前loader暴露了picth loader,则使用runSyncOrAsync函数调用picth loader function
    runSyncOrAsync(
      fn,
      // 构造传入picth loader的参数
      loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
      function(err) {
        if(err) return callback(err);
        var args = Array.prototype.slice.call(arguments, 1);
        // 如果调用的picth loader function 返回了非undefined的值,则loaderIndex--,并开始调用normal loader,否则继续递归调用iteratePitchingLoaders
        var hasArg = args.some(function(value) {
          return value !== undefined;
        });
        if(hasArg) {
          loaderContext.loaderIndex--;
          iterateNormalLoaders(options, loaderContext, args, callback);
        } else {
          iteratePitchingLoaders(options, loaderContext, callback);
        }
      }
    );
  });
}

function runSyncOrAsync(fn, context, args, callback) {
  var isSync = true;
  var isDone = false;
  var isError = false; // internal error
  var reportedError = false;

  // 在loader context 上暴露this.async方法,调用该方法返回一个callback function
  context.async = function async() {
    if(isDone) {
      if(reportedError) return; // ignore
      throw new Error("async(): The callback was already called.");
    }
    isSync = false;
    return innerCallback;
  };
  // 处理this.callback调用场景
  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 {
    // 调用picth loader function or normal loader function
    var result = (function LOADER_EXECUTION() {
      return fn.apply(context, args);
    }());

    // 处理loader内直接return source 及 return Promise<source>
    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) {
    ...
    callback(e);
  }

}


function processResource(options, loaderContext, callback) {
  // 调用normal loader之前将loaderIndex重置为loaderContext.loaders.length - 1,保证normal loader的执行顺序
  loaderContext.loaderIndex = loaderContext.loaders.length - 1;

  var resourcePath = loaderContext.resourcePath;
  if(resourcePath) {
    // 获取module source
    options.processResource(loaderContext, resourcePath, function(err) {
      if(err) return callback(err);
      var args = Array.prototype.slice.call(arguments, 1);
      options.resourceBuffer = args[0];
      // 调用normal loader处理module source
      iterateNormalLoaders(options, loaderContext, args, callback);
    });
  } else {
    iterateNormalLoaders(options, loaderContext, [null], callback);
  }
}

function iterateNormalLoaders(options, loaderContext, args, callback) {
  // 如果loaderIndex小于0,表示所有normal loader执行完毕,应该直接调用run loader callback
  if(loaderContext.loaderIndex < 0)
    return callback(null, args);

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

  // 如果当前normal loader执行过了,则loaderIndex--之后继续递归调用iterateNormalLoaders
  if(currentLoaderObject.normalExecuted) {
    loaderContext.loaderIndex--;
    return iterateNormalLoaders(options, loaderContext, args, callback);
  }

  var fn = currentLoaderObject.normal;
  currentLoaderObject.normalExecuted = true;
  if(!fn) {
    return iterateNormalLoaders(options, loaderContext, args, callback);
  }

  // 通过loader暴露的raw字段,决定当前loader接受的source是string类型还是buffer类型
  convertArgs(args, currentLoaderObject.raw);

  // 通过runSyncOrAsync执行normal loader function
  runSyncOrAsync(fn, loaderContext, args, function(err) {
    if(err) return callback(err);

    var args = Array.prototype.slice.call(arguments, 1);
    // 继续递归调用iterateNormalLoaders
    iterateNormalLoaders(options, loaderContext, args, callback);
  });
}

同步与异步loader的原理

  • 如果是异步loader,在loader内调用this.async时会将isSync设置为false,避免被当成同步loader处理,最后会返回一个innerCallback,而这个innerCallback调用的时候,我们会传入处理后的参数,在innerCallback内会调用runSyncOrAsync传入的callback
  • 如果是同步loader,则直接调用runSyncOrAsync传入的callback
点击查看精简后的代码
context.async = function async() {
  if(isDone) {
    if(reportedError) return; // ignore
    throw new Error("async(): The callback was already called.");
  }
  isSync = false;
  return innerCallback;
};

// 处理this.callback调用场景
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;
  }
};

// 调用picth方法或者normal方法
var result = (function LOADER_EXECUTION() {
  return fn.apply(context, args);
}());

// 如果是同步loader,也就是未调用this.async方法的场景,则会直接调用runSyncOrAsync传入的callback
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);
}



// 异步loader
module.exports = function (content) {
  // 异步loader
  const callback = this.async() // 获取一个callback具柄
  asyncOpration(xxxx, (result) => {
    // 必须要调用callback,告诉runLoader当前loader跳用完成了
    callback(null, result)
  })
}

// 同步loader
module.exports = function (content) {
  // 直接返回
  return content
}

module.exports = function (content) {
  // 调用this.callback返回
  this.callback(null, content)
}

picth loader阻断原理

  • runLoaders会按照loaders数组的顺序,先执行picth loader
  • picth方法执行完之后,会判断返回值是否为undefined,如果是非undefined的值,就loaderIndex--,并开始调用iterateNormalLoaders开始执行normal loader的逻辑

执行示例

use: ['a-loader', 'b-loader', 'c-loader'],

// 假设b-loader有picth方法,并返回非undefined的值,那么最终的执行顺序如下所示
|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution
精简后的picth loader阻断原理代码
exports.runLoaders = function runLoaders(options, callback) {
  ...
  // 先执行pitch loader
  iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
    if(err) {
      return callback(err);
    }
    callback(null, {
      result: result,
    });
  });
};

function iteratePitchingLoaders(options, loaderContext, callback) {
  // 获取loader模块暴露的属性,比如picth loader function
  loadLoader(currentLoaderObject, function(err) {

    var fn = currentLoaderObject.pitch;

    // 如果当前loader暴露了picth loader,则使用runSyncOrAsync函数调用picth loader function
    runSyncOrAsync(
      fn,
      // 构造传入picth loader的参数
      loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
      function(err) {
        var args = Array.prototype.slice.call(arguments, 1);
        // 如果调用的picth loader function 返回了非undefined的值,则loaderIndex--,并开始调用normal loader,否则继续递归调用iteratePitchingLoaders
        var hasArg = args.some(function(value) {
          return value !== undefined;
        });
        if(hasArg) {
          loaderContext.loaderIndex--;
          iterateNormalLoaders(options, loaderContext, args, callback);
        } else {
          iteratePitchingLoaders(options, loaderContext, callback);
        }
      }
    );
  });
}

raw loader原理

  • 只对normal loader生效,根据暴露的loader模块上是否有raw属性来做处理
  • 如果rawtrue则将内容处理成buffer类型,否则处理成string类型
点击查看精简后的代码
// 如果raw为true则将内容处理成buffer类型,如果raw为false,则将内容处理成string类型
function convertArgs(args, raw) {
  // 如果没有设置raw,且内容是buffer,则转化成string
	if(!raw && Buffer.isBuffer(args[0]))
		args[0] = utf8BufferToString(args[0]);
  // 如果设置raw,且内容是string,则转化成buffer
	else if(raw && typeof args[0] === "string")
		args[0] = Buffer.from(args[0], "utf-8");
}

// 执行normal loader逻辑
function iterateNormalLoaders(options, loaderContext, args, callback) {
  // 通过loader暴露的raw字段,决定当前loader接受的source是string类型还是buffer类型
	convertArgs(args, currentLoaderObject.raw);

  // 通过runSyncOrAsync执行normal loader function
	runSyncOrAsync(fn, loaderContext, args, function(err) {
		if(err) return callback(err);

		var args = Array.prototype.slice.call(arguments, 1);
    // 继续递归调用iterateNormalLoaders
		iterateNormalLoaders(options, loaderContext, args, callback);
	});
}

常用loader及原理

了解那么多,当然需要看下实际项目中使用的loader都是怎么实现,方便我们更近一步使用与理解loader

babel-loader@8.2.5

babel-loader可以说是我们用的最多的loader之一了,主要作用将ts语法转换成js语法、es6+语法转换成es5语法等

内部处理流程如下图所示 进阶开发,跟我一起拿捏webpack loader原理

点击babel-loader精简后的代码

async function loader(source, inputSourceMap, overrides) {
  const filename = this.resourcePath;

  let loaderOptions = loaderUtils.getOptions(this);

  // 获取babel加载config的function
  const { loadPartialConfigAsync = babel.loadPartialConfig } = babel;

  // 获取babel config
  const config = await loadPartialConfigAsync(
    injectCaller(programmaticOptions, this.target),
  );

  let result;
  // 如果当前module存在缓存,则直接返回,否则,使用babel.transform进行处理
  if (cacheDirectory) {
    result = await cache({source});
  } else {
    result = await transform(source, options);
  }

  if (result) {
    // 将babel获取到的代码,return 出去
    const { code, map, metadata } = result;
    return [code, map];
  }
}

从源码可以看到babel-loader是一个normal loader,内部是同步返回babel处理之后的内容

less-loader@11.0.0

less-loader也是我们用的比较多的一个loader,专门用来处理less样式

内部处理流程如下图所示 进阶开发,跟我一起拿捏webpack loader原理

点击查看less-loader精简后的代码
async function lessLoader(source) {
  const options = this.getOptions(schema);

  // 调用this.async,使用异步 loader的方式返回result
  const callback = this.async();

  const lessOptions = getLessOptions(this, options, implementation);

  let data = source;

  let result;

  try {
    // 调用less.render方法将less语法转换为css语法
    result = await implementation.render(data, lessOptions);
  } catch (error) {
    callback(new LessError(error));
    return;
  }

  const { css, imports } = result;

  // 如果less文件内部有@import这样的语法,那么将@import 的file,通过this.addDependency传入,让webpack能够监视到文件变化
  imports.forEach((item) => {

    // Custom `importer` can return only `contents` so item will be relative
    if (path.isAbsolute(normalizedItem)) {
      this.addDependency(normalizedItem);
    }
  });

  let map = typeof result.map === "string" ? JSON.parse(result.map) : result.map;

  // 将less处理的结果返回
  callback(null, css, map);
}

export default lessLoader;

从源码可以看到less-loader是一个normal loader,内部是异步返回less处理之后的内容

css-loader@6.7.4

css-loader是基础loader,专门用来处理css样式,将css模块转化为webpack能够处理的内容

内部处理流程如下图所示 进阶开发,跟我一起拿捏webpack loader原理

点击查看精简后的css-loader代码
export default async function loader(content, map, meta) {
  const rawOptions = this.getOptions(schema);
  const callback = this.async();

  // Reuse CSS AST (PostCSS AST e.g 'postcss-loader') to avoid reparsing
  if (meta) {
    const { ast } = meta;

    if (
      ast &&
      ast.type === "postcss" &&
      satisfies(ast.version, `^${postcssPkg.version}`)
    ) {
      // eslint-disable-next-line no-param-reassign
      content = ast.root;
    }
  }

  const { resourcePath } = this;

  let result;

  try {
    result = await postcss(plugins).process(content, {});
  } catch (error) {
    return;
  }


  const imports = []
    .concat(icssPluginImports.sort(sort))
    .concat(importPluginImports.sort(sort))
    .concat(urlPluginImports.sort(sort));
  const api = []
    .concat(importPluginApi.sort(sort))
    .concat(icssPluginApi.sort(sort));

  const importCode = getImportCode(imports, options);

  let moduleCode;

  try {
    moduleCode = getModuleCode(result, api, replacements, options, this);
  } catch (error) {
    callback(error);

    return;
  }

  const exportCode = getExportCode(
    exports,
    replacements,
    needToUseIcssPlugin,
    options
  );

  callback(null, `${importCode}${moduleCode}${exportCode}`);
}

从源码可以看到css-loader是一个normal loader,内部是同步返回postcss处理之后的内容

css module格式, export default的就是一个对象,对象内包含的就是我们的key还有样式的样式的新key

// Imports
import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from "../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/runtime/noSourceMaps.js";
import ___CSS_LOADER_API_IMPORT___ from "../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/runtime/api.js";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___);
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".DFDoHskQhc8WMFqQHxvM {\n  background-color: #ccc;\n}\n", ""]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
        "wrap": "DFDoHskQhc8WMFqQHxvM"
};
export default ___CSS_LOADER_EXPORT___;

css module格式,export default 就是一个数组

// Imports
import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from "../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/runtime/noSourceMaps.js";
import ___CSS_LOADER_API_IMPORT___ from "../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/runtime/api.js";
var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___);
// Module
___CSS_LOADER_EXPORT___.push([module.id, "body {\n  background-color: #ccc;\n}\n", ""]);
// Exports
export default ___CSS_LOADER_EXPORT___;

css-loader我们可以知道import './index.css' 经过css处理之后,相当于import './index.css.js'文件,通过export default的方式导出模块内容

style-loader@3.3.2

style-loader基础loader,专门用来处理开发环境css样式,将css-loader转化后的内容,通过style标签等的方式插入到html中,让最终的样式生效

内部处理流程: 进阶开发,跟我一起拿捏webpack loader原理

style-loader是一个picth loader,内部是同步返回非undefined的值,以use: ['style-loader', 'css-loader']为例,那么loader的执行顺序就是

- picth loader(style-loader)
	- 结束,因为style-loader在最左边

所以为了让样式生效,style picth返回的内容,如下

// 返回的内容内重新引入了css模块文件,并使用inline loader处理
import content, * as namedExport from "!!../node_modules/.pnpm/css-loader@6.7.4_webpack@5.74.0/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[2].use[1]!./index.css";
      
var options = {};
    
options.domAPI = domAPI;

// 对css loader处理后的内容通过,相应的api插入文档
var update = API(content, options);

export default ___CSS_LOADER_EXPORT___;

具体的过程如下所示 第一次处理index.csspicth style-loader => 返回非undefiend值,所以跳过了后面css-loader的执行 第二次处理index.css:因为第一次style-loader picth 返回的内容里面包含import content from !!.css-loader/dist/cjs.js!./index.css代码,所以会重新处理index.css模块,而inline-loader前面使用了!!,表示禁用rules中匹配到的的normal loaderpicth loader,所以只会命中css-loader

流程图如下所示 进阶开发,跟我一起拿捏webpack loader原理

webpackmoduleGraphaindex.css会出现有个module实例

  1. "./src/index.css模块内容包含style-loader返回的内容
  2. "./node_modules/css-loader/dist/cjs.js!./src/index.css" 包含css内容及css-loader提供的部分代码

css 模块最终经过了css-loader处理,同时又具有了style-loader插入html的能力

这就是picth loader设计的初衷,在不改变原有模块代码及css-loader的前提下,增强及拓展了css的能力

点击查看style-loader精简后的代码
const loaderAPI = () => {};

loaderAPI.pitch = function loader(request) {
  const options = this.getOptions(_options.default);
  const injectType = options.injectType || "styleTag";

  const insertType = typeof options.insert === "function" ? "function" : options.insert && _path.default.isAbsolute(options.insert) ? "module-path" : "selector";
  const styleTagTransformType = typeof options.styleTagTransform === "function" ? "function" : options.styleTagTransform && _path.default.isAbsolute(options.styleTagTransform) ? "module-path" : "default";

  switch (injectType) {
    case "styleTag":
    case "autoStyleTag":
    case "singletonStyleTag":
    default:
      {
        const isSingleton = injectType === "singletonStyleTag";
        const isAuto = injectType === "autoStyleTag";
        const hmrCode = this.hot ? (0, _utils.getStyleHmrCode)(esModule, this, request, false) : "";
        return `
      ${(0, _utils.getImportStyleContentCode)(esModule, this, request)}
      ${esModule ? "" : `content = content.__esModule ? content.default : content;`}

var options = ${JSON.stringify(runtimeOptions)};

var update = API(content, options);

${hmrCode}

${(0, _utils.getExportStyleCode)(esModule, this, request)}
`;}
  }
};

var _default = loaderAPI;
exports.default = _default;


function getImportStyleContentCode(esModule, loaderContext, request) {
  // 使用行内loader处理源样式模块,!!禁用rules中的所有loader
  const modulePath = stringifyRequest(loaderContext, `!!${request}`);
  return esModule ? `import content, * as namedExport from ${modulePath};` : `var content = require(${modulePath});`;
}

MiniCssExtractPlugin.loader@2.7.5

从上面use: ['style-loader', 'css-loader']可以看到css 模块已经可以被webpack正确处理了,但是这样处理有一个问题,样式文件是以style标签或者link标签插入的,没有生成单独的css文件,而在实际生产环境考虑到网络、缓存等问题,会将css内容提取到一个或者几个文件内,那么这个时候style-loader就做不到了,需要使用到另外一个loader,即MiniCssExtractPlugin.loader

关于MiniCssExtractPlugin.loader的原理可以查看面试官:生产环境构建时为什么要提取css文件?

file-loader@6.2.0

file-loader webpack5之前常用的loader,专门用来处理image等静态资源文件,现在webpack5以内置imagefont等静态资源的处理能力,所以使用没那么频繁,但是file-loader的内部实现我们还是可以进行了解

内部处理流程如下图所示 进阶开发,跟我一起拿捏webpack loader原理

点击查看file-loader精简后的代码
export default function loader(content) {
  const options = getOptions(this);

  const context = options.context || this.rootContext;
  const name = options.name || '[contenthash].[ext]';

  const url = interpolateName(this, name, {
    context,
    content,
    regExp: options.regExp,
  });

  let outputPath = url;

  let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;

  if (typeof options.emitFile === 'undefined' || options.emitFile) {
    const assetInfo = {};

    assetInfo.sourceFilename = normalizePath(
      path.relative(this.rootContext, this.resourcePath)
    );

    // 将传入的内容当成单独的文件输出,输出路径是outputPath,名称是sourceFilename
    this.emitFile(outputPath, content, null, assetInfo);
  }

  const esModule =
    typeof options.esModule !== 'undefined' ? options.esModule : true;

  // 将当前资源返回成js能够识别的模块
  return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`;
}

// 当前loader接受的source 接受buffer类型,而不是string类型
export const raw = true;

从源码可以看到file-loader是一个normal loader,内部是同步返回file loader处理之后的内容,但是使用了raw来表示当前loader接受的内容,必须是buffer类型

cache-loader@4.1.0

cache-loader webpack5之前常用的loader,专门用来处理缓存,现在webpack5已经内置了更完善的缓存策略,所以cache-loader使用的比较少了,但是cache-loader的实现,还是还是可以借鉴,因为cache-loader即即提供了normal loader又提供了picth loader

内部处理流程:

  • 使用picth loader读缓存,如果有缓存,则直接返回缓存内容,跳过后续loader的执行,如果没有缓存,则继续执行后续的loader
  • 使用normal loader存缓存,只要在picth loader的时候没有读取到缓存,那么就一定会执行到cache-loadernormal loader,然后在normal loader的时候将module的依赖及处理当前moduleloader文件路径存起来,方便缓存复用的时候判断缓存是否有效
  • 通过module的依赖文件及处理当前moduleloader文件最后的修改时间是否一致,一致则缓存有效,不一致,则缓存失效
  • 因为涉及到文件的读写,所以pitch loadernormal loader都是异步loader
  • cache-loader因为是通过文件的最后修改时间来判断缓存是否有效,对于ci场景每次都是重写拉文件,导致最后修改时间变化,所以需要修改下cache-loader通过文件md5的方式来判断缓存是否失效

流程图如下所示 进阶开发,跟我一起拿捏webpack loader原理

点击查看cache-loader精简后的代码
function loader(...args) {
  const options = Object.assign({}, defaults, getOptions(this));

  // 获取innercallback具柄
  const callback = this.async();
  
  const { data } = this;
  
  // 获取依赖项,及处理该依赖的loader
  const dependencies = this.getDependencies().concat(
    this.loaders.map((l) => l.path)
  );
  
  // 获取动态加载的依赖项,及处理该依赖的loader
  const contextDependencies = this.getContextDependencies();

  const FS = this.fs || fs;
  const toDepDetails = (dep, mapCallback) => {
    // 获取文件的状态
    FS.stat(dep, (err, stats) => {

      // 获取文件的修改时间
      const mtime = stats.mtime.getTime();

      mapCallback(null, {
        path: pathWithCacheContext(options.cacheContext, dep),
        mtime,
      });
    });
  };

  async.parallel(
    [
      (cb) => async.mapLimit(dependencies, 20, toDepDetails, cb),
      (cb) => async.mapLimit(contextDependencies, 20, toDepDetails, cb),
    ],
    (err, taskResults) => {
      const [deps, contextDeps] = taskResults;
      // 将当前module的dependencies、contextDependencies及处理当前module的loader存起来
      writeFn(
        data.cacheKey,
        {
          remainingRequest: pathWithCacheContext(
            options.cacheContext,
            data.remainingRequest
          ),
          dependencies: deps,
          contextDependencies: contextDeps,
          result: args,
        },
        () => {
          // ignore errors here
          callback(null, ...args);
        }
      );
    }
  );
}

function pitch(remainingRequest, prevRequest, dataInput) {
  const options = Object.assign({}, defaults, getOptions(this));

  const callback = this.async();
  const data = dataInput;

  data.remainingRequest = remainingRequest;
  data.cacheKey = cacheKeyFn(options, data.remainingRequest);

  // 通过cachekey,获取cache-loader上一次的缓存
  readFn(data.cacheKey, (readErr, cacheData) => {

    const FS = this.fs || fs;
    async.each(
      cacheData.dependencies.concat(cacheData.contextDependencies),
      (dep, eachCallback) => {
        const contextDep = {
          ...dep,
          path: pathWithCacheContext(options.cacheContext, dep.path),
        };
      	// 依次判断之前cache-loader内存储的dependencies、contextDependencies及loader文件的最后修改时间
        FS.stat(contextDep.path, (statErr, stats) => {

          // 只要有一个依赖 or loader文件的最后修改时间变了,则不复用缓存
          if (compareFn(compStats, compDep) !== true) {
            eachCallback(true);
            return;
          }
          eachCallback();
        });
      },
      (err) => {
        // 复用缓存之后,将当前模块的dependencies、contextDependencies还原,方便webpack继续递归解析依赖树
        cacheData.dependencies.forEach((dep) =>
          this.addDependency(pathWithCacheContext(cacheContext, dep.path))
        );
        cacheData.contextDependencies.forEach((dep) =>
          this.addContextDependency(
            pathWithCacheContext(cacheContext, dep.path)
          )
        );
        callback(null, ...cacheData.result);
      }
    );
  });
}

const directories = new Set();

function cacheKey(options, request) {
  const { cacheIdentifier, cacheDirectory } = options;
  const hash = digest(`${cacheIdentifier}\n${request}`);

  return path.join(cacheDirectory, `${hash}.json`);
}

function compare(stats, dep) {
  return stats.mtime.getTime() === dep.mtime;
}

export const raw = true;
export { loader as default, pitch };

可以看到cache-loader既是normal loader也与picth loader,内部是异步返回内容,但是使用了raw来表示当前normal loader接受的内容,必须是buffer类型

总结

掌握以下知识点,不仅可以让我们熟练使用,还能够从原理层面了解webpack loader

loaders顺序是在webpack内部组合完成,loader按照postinlinenormalpre 的顺序进行排序组装,然后传给loader-runner执行

loader-runner封装成了一个单独的npm包,其内通过数组下标index来控制loader的执行顺序

异步loader与同步loader的区别就是,异步loader需要主动调用this.async,拿到一个innercallback,然后在异步调用完成之后调用innercallback的时机不同

picth loader阻断原理就是,每次判断picth loader的返回值是否为非undefined的值,如果是则index--, 然后直接跳过后续的pitch loader执行,直接进入执行前一个的normal loader逻辑

raw 属性只作用于normal loader,原理就是判断内容是否是buffer类型,如果不是则通过Buffer.fromstring转换成buffer类型

感谢各位看官老爷耐心看完,如果觉得对看官老爷有帮助,动动手指头点个👍吧!