深入源码分析 file-loader 哈希生成规则

我们知道,在 Webpack 中有三种哈希:

  • hash:一次 compilation 总体的哈希,只要有一个文件修改,整个哈希就会发生变化
  • chunk-hash:根据 chunk 生成的哈希,同一个 chunk 中所有文件的哈希相同
  • content-hash:根据文件内容生成的哈希

在 Vue-cli 默认 Webpack 配置中,对 JS 启用 chunk-hash,CSS 启用 content-hash,而图片和字体文件则是 hash。这样就产生一个问题,修改 JS 代码后,图片和字体的哈希是否会发生变化?

这个问题看起来有点中二,如果修改 JS 代码,导致图片、字体的哈希改变了,显然非常不合理。。但还是抱着好奇的心态去看了源码

file-loader 源码简化之后如下:

// 这里的 loader-utils 是 Webpack 暴露给 loader 的 API
import { getOptions, interpolateName } from 'loader-utils';

export default function loader(content) {
  // getOptions 方法用于获取 loader 的配置
  const options = getOptions(this);
  // 这里的 name 选项是配置中传递的
  const name = options.name || '[contenthash].[ext]';
  // interpolateName 方法可以根据 name 和 content 内容生成哈希
  // 可以保证文件内容没有发生变化的时候,文件名中的 [hash] 字段不变
  const url = interpolateName(this, name, { content });
  // 拼接文件路径
  // 这里的 __webpack_public_path__ 是 Webpack 提供的运行时的全局变量,即 publicPath
  let publicPath = `__webpack_public_path__ + ${JSON.stringify(url)}`;
  // emitFile 是 Loader Context 中的 API,告诉 Webpack 创建一个文件
  // 这样 Webpack 就会在 dist 目录下创建一个对应的文件
  this.emitFile(url, content);
  const esModule =
    typeof options.esModule !== 'undefined' ? options.esModule : true;
  // 返回一个字符串形式的 JS 模块,显然是在浏览器端执行的
  return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`;
}

// 记得加上这个,默认情况下 Webpack 会把文件内容当做 utf8 字符串处理
// 而图片是二进制的,当做 utf8 会导致图片格式错误
export const raw = true;

上面的代码完全可以正常运行。我们可以看到,file-loader 其实就做了三件事:

  • 根据给定的文件名配置和文件内容,生成带有哈希的文件路径;
  • 根据生成的文件路径,创建一个文件;
  • 最后返回一个字符串形式的 JS 模块,加载这个模块,就可以得到文件路径;

关于 Loader Context,应该有不少小伙伴都知道,例如可以使用 this.callback() 返回多个结果,使用 this.async() 指定异步 loader。这里用到的 this.emitFile() 也是 Loader Context 上的方法,用于创建一个文件。

https://webpack.docschina.org/api/loaders/#thisemitfile

但是可能大家对 loader-utils 了解得比较少,这同样也是 Webpack 暴露给 Loader 的 API,只不过这个是通过第三方库的形式引入的。

这里提一下,loader-utils 中的 getOptions 方法在 v3.2.0 中已经被移除了,Webpack5 可以从 Loader Context 的 this.getOptions 方法获取。这相当于是一个破坏性更新,file-loader 中之所以还能使用,是因为依赖版本锁定为 "loader-utils": "^2.0.0",也就是范围在 >=2.0.0 <3.0.0

从上面的代码中可以看出,生成哈希相关逻辑都在 interpolateName 这个方法里面,部分源码如下:

function interpolateName(loaderContext, name, options = {}) {
	let filename = name || "[hash].[ext]";
	const content = options.content;
	// ...
	let url = filename;
	if (content) {
	    // Match hash template
	    url = url
	      // `hash` and `contenthash` are same in `loader-utils` context
	      // let's keep `hash` for backward compatibility
	      .replace(
	        /\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi,
	        (all, hashType, digestType, maxLength) =>
	          getHashDigest(content, hashType, digestType, parseInt(maxLength, 10))
	      );
    }
    // ...
}

看这边用到了 getHashDigest 方法,我们可以不用关心内部实现。源码中的正则表达式,我们使用 "[hash].[ext]" 进行实验,发现能拿到 hashcontenthash 配置信息的,只有第一个参数 all,其他都是 undefined
请添加图片描述
然而在源码中这个 all 根本就没有传给 getHashDigest 方法,传递的参数中唯一有用的参数就是 content,也就是文件内容。因此我们可以得出结论,文件名中无论配置 hash 或者 contenthash,都是等价的,实际上都是 contenthash,都是根据文件本身的内容生成的,与 Webpack 的构建过程无关。

参考

webpack 源码解析:file-loader 和 url-loader

https://github.com/webpack-contrib/file-loader

https://github.com/webpack/loader-utils

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值