webpack loader源码解析系列(三)-raw-loader、file-loader、url-loader

韦熙云
2023-12-01

raw-loader

  • 将匹配的文件输出成字符串,处理其中分隔符,因为浏览器中js表达式中不允许出现换行分隔符
import { getOptions } from 'loader-utils';
import { validate } from 'schema-utils';

import schema from './options.json';

export default function rawLoader(source) {
  const options = getOptions(this);

  validate(schema, options, {
    name: 'Raw Loader',
    baseDataPath: 'options',
  });

  const json = JSON.stringify(source)
    .replace(/\u2028/g, '\\u2028') // 行分隔符 => 行结束符
    .replace(/\u2029/g, '\\u2029');// 段落分隔符 => 行结束符	

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

  return `${esModule ? 'export default' : 'module.exports ='} ${json};`;
}

file-loader

  • 处理资源文件,将导入的结果变成url,并输出资源文件到打包后的文件中
    • 主要通过loaderUtils.interpolateName对文件重新命名、this.emitFile输出文件、exports.raw = raw 对于资源文件返回 buffer
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = loader;
exports.raw = void 0;

var _path = _interopRequireDefault(require("path"));

var _loaderUtils = require("loader-utils");

var _schemaUtils = require("schema-utils");

var _options = _interopRequireDefault(require("./options.json"));

var _utils = require("./utils");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function loader(content) {
  const options = (0, _loaderUtils.getOptions)(this);
  (0, _schemaUtils.validate)(_options.default, options, {
    name: 'File Loader',
    baseDataPath: 'options'
  });
  // 没传递上下文就使用项目的根目录
  const context = options.context || this.rootContext;
  // 如果没有指定名称,则根据资源文件等内容hash和后缀生成新的文件名
  const name = options.name || '[contenthash].[ext]';
  // 通过指定名称、占位符等为文件重新生成一个名称
  const url = (0, _loaderUtils.interpolateName)(this, name, {
    context,
    content,
    regExp: options.regExp
  });
  let outputPath = url;

  if (options.outputPath) {
    if (typeof options.outputPath === 'function') {
      outputPath = options.outputPath(url, this.resourcePath, context);
    } else {
      // 拼接开发者传入的 outputPath
      outputPath = _path.default.posix.join(options.outputPath, url);
    }
  }
  // __webpack_public_path__ 等同于 webpack.config.js 中设置的 output.publicPath
  let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;
  
  // 如果指定了 publicPath,则以开发者传入的为准
  if (options.publicPath) {
    if (typeof options.publicPath === 'function') {
      publicPath = options.publicPath(url, this.resourcePath, context);
    } else {
      publicPath = `${options.publicPath.endsWith('/') ? options.publicPath : `${options.publicPath}/`}${url}`;
    }

    publicPath = JSON.stringify(publicPath);
  }

  if (options.postTransformPublicPath) {
    publicPath = options.postTransformPublicPath(publicPath);
  }
  
  // 开发者通过 emitFile 选项配置是否生成文件
  if (typeof options.emitFile === 'undefined' || options.emitFile) {
    const assetInfo = {};

    if (typeof name === 'string') {
      let normalizedName = name;
      const idx = normalizedName.indexOf('?');

      if (idx >= 0) {
        normalizedName = normalizedName.substr(0, idx);
      }
      
      // 判断文件是否是不可变的,如果包含[hash]或[contenthash]表明不可变
      const isImmutable = /\[([^:\]]+:)?(hash|contenthash)(:[^\]]+)?]/gi.test(normalizedName);

      if (isImmutable === true) {
        assetInfo.immutable = true;
      }
    }
    
    // 获取资源相对项目目录的相对路径:_path.default.relative(this.rootContext, this.resourcePath 
    // normalizePath作用是规范斜杠为'/'
    // sourceFilename:src/assets/1.png
    assetInfo.sourceFilename = (0, _utils.normalizePath)(_path.default.relative(this.rootContext, this.resourcePath));

    // outputPath:"6f25203d91f0bae2bb1f6d99c8eb6cab.png"
    this.emitFile(outputPath, content, null, assetInfo);
  }
  
  // 根据开发者指定是否是esm
  const esModule = typeof options.esModule !== 'undefined' ? options.esModule : true;

  // 返回资源url: "__webpack_public_path__ + \"6f25203d91f0bae2bb1f6d99c8eb6cab.png\""
  return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`;
}

const raw = true;
exports.raw = raw;  // 对于资源文件返回 buffer

url-loader

  • 当资源文件大小小于指定 limit 时,进行 base64 压缩,通过 Buffer.toString(“base64”) 实现
  • 当资源文件大小大于限制时,直接 require(“file-loader”),传入资源相关参数,调用 file-loader 函数返回结果

var _path = _interopRequireDefault(require("path"));

var _loaderUtils = require("loader-utils");

var _schemaUtils = require("schema-utils");

var _mimeTypes = _interopRequireDefault(require("mime-types"));

var _normalizeFallback = _interopRequireDefault(require("./utils/normalizeFallback"));

var _options = _interopRequireDefault(require("./options.json"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function loader(content) {
  // Loader Options
  const options = (0, _loaderUtils.getOptions)(this) || {};
  (0, _schemaUtils.validate)(_options.default, options, {
    name: 'URL Loader',
    baseDataPath: 'options'
  }); // No limit or within the specified limit

  // 通过 content.length 获取到资源文件的大小,小于指定 limit 需要转换成 base64 编码
  if (shouldTransform(options.limit, content.length)) {
    const {
      resourcePath
    } = this;
    // 根据资源文件后缀名返回mine-type
    const mimetype = getMimetype(options.mimetype, resourcePath);
    // options.encoding 指定资源文件需要被内联的方式,默认为 base64
    const encoding = getEncoding(options.encoding);
    // 如果内容不是 buffer,则转换成 buffer
    if (typeof content === 'string') {
      // eslint-disable-next-line no-param-reassign
      content = Buffer.from(content);
    }
    
    // 获取编码后的内容 'data:image/png;base64,xxx'
    const encodedData = getEncodedData(options.generator, mimetype, encoding, content, resourcePath);
    const esModule = typeof options.esModule !== 'undefined' ? options.esModule : true;
    // 将 base64 内容导出
    return `${esModule ? 'export default' : 'module.exports ='} ${JSON.stringify(encodedData)}`;
  } // Normalize the fallback.

  
  // 当资源文件没有超过大小限制时,通过传入一个自定义 loader 去处理,默认为 file-loader
  // 将传入的自定义 loader,变成 {loader,options} 这种格式返回, options 为传递给 loader 的参
  const {
    loader: fallbackLoader,
    options: fallbackOptions
  } = (0, _normalizeFallback.default)(options.fallback, options); // Require the fallback.
  // eslint-disable-next-line global-require, import/no-dynamic-require
  
  // 导入 loader ,这里为 file-loader
  const fallback = require(fallbackLoader); // Call the fallback, passing a copy of the loader context. The copy has the query replaced. This way, the fallback
  // loader receives the query which was intended for it instead of the query which was intended for url-loader.

  // 传递给 file-loader 的 this
  const fallbackLoaderContext = Object.assign({}, this, {
    query: fallbackOptions
  });
  // 返回 file-loader 的调用结果
  return fallback.call(fallbackLoaderContext, content);
} // Loader Mode

const raw = true;
exports.raw = raw;

shouldTransform

  • 判断是否需要编码成 base64
function shouldTransform(limit, size) {
  if (typeof limit === 'boolean') {
    return limit;
  }

  if (typeof limit === 'string') {
    return size <= parseInt(limit, 10);
  }

  if (typeof limit === 'number') {
    return size <= limit;
  }

  return true;
}

getMimetype

  • 通过 mime-type 库根据文件后缀名去获取对应的 mine-type
// 根据资源文件后缀名返回 mine-type
function getMimetype(mimetype, resourcePath) {
  if (typeof mimetype === 'boolean') {
    if (mimetype) {
      // 根据 mimeTypes 第三方库,根据资源后缀名返回 mine-type
      const resolvedMimeType = _mimeTypes.default.contentType(_path.default.extname(resourcePath));

      if (!resolvedMimeType) {
        return '';
      }
      // 去掉 charset 之前的空白、缩进等
      return resolvedMimeType.replace(/;\s+charset/i, ';charset');
    }

    return '';
  }
  // 如果开发者指定了,那么直接返回
  if (typeof mimetype === 'string') {
    return mimetype;
  }

  // 'image/png'
  const resolvedMimeType = _mimeTypes.default.contentType(_path.default.extname(resourcePath));

  if (!resolvedMimeType) {
    return '';
  }

  return resolvedMimeType.replace(/;\s+charset/i, ';charset');
}

getEncoding

  • 获取编码方式
function getEncoding(encoding) {
  if (typeof encoding === 'boolean') {
    return encoding ? 'base64' : '';
  }

  if (typeof encoding === 'string') {
    return encoding;
  }

  return 'base64';
}

getEncodedData

  • 将资源 buffer 转换成 base64
function getEncodedData(generator, mimetype, encoding, content, resourcePath) {
  // 如果自定义了生成函数
  if (generator) {
    return generator(content, mimetype, encoding, resourcePath);
  }
// 通过 buffer.toString("base64") 将内容编码成base64
  return `data:${mimetype}${encoding ? `;${encoding}` : ''},${content.toString( // eslint-disable-next-line no-undefined
  encoding || undefined)}`;
}
 类似资料: