url-loader
使用过webpack的开发者,基本上都听说或者用过url-loader。
a loader for webpack which transforms files into base64 URIs.
url-loader 允许你有条件地将文件转换为内联的 base64(减少小文件的 HTTP 请求数),如果文件大于该阈值,会自动的交给 file-loader 处理。
配置项
在看源码前,我个人比较倾向于先仔细阅读使用文档,文档的使用和配置往往可以帮助理解源码阅读。
![](https://img.haomeiwen.com/i18646161/2c9ab6bbca387b1c.png)
options总共有6个配置属性,一般使用都只是配置limit:
- limit: 小于limit值会被url-loader转换,默认是base64
- mimetype: 被转换文件的mimetype,默认取文件扩展名
- encoding: 默认base64
- generator:默认转成base64:xxxx,开发者可以通过generator自己实现转换
- fallback: 如果文件大于limit,把文件交给fallback 这个loader,默认是file-loader
- esModule:是否es module
源码分析
如果只看url-loader的核心代码(loader函数),代码只有60行左右。
其中的 export const raw = true
是告诉 webpack 该 loader 获取的 content 是 buffer
类型
( loader 第一个值的类型是 JavaScript 代码字符串或者 buffer,开发者自行决定使用哪种类型):
export default function loader(content) {
// Loader Options
const options = getOptions(this) || {};
validate(schema, options, {
name: 'URL Loader',
baseDataPath: 'options',
});
// No limit or within the specified limit
if (shouldTransform(options.limit, content.length)) {
const { resourcePath } = this;
const mimetype = getMimetype(options.mimetype, resourcePath);
const encoding = getEncoding(options.encoding);
const encodedData = getEncodedData(
options.generator,
mimetype,
encoding,
content,
resourcePath
);
const esModule =
typeof options.esModule !== 'undefined' ? options.esModule : true;
return `${
esModule ? 'export default' : 'module.exports ='
} ${JSON.stringify(encodedData)}`;
}
// Normalize the fallback.
const {
loader: fallbackLoader,
options: fallbackOptions,
} = normalizeFallback(options.fallback, options);
// Require the fallback.
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.
const fallbackLoaderContext = Object.assign({}, this, {
query: fallbackOptions,
});
return fallback.call(fallbackLoaderContext, content);
}
// Loader Mode
export const raw = true;
获取options以及校验options
webpack
官方文档中的 编写一个loader 一文介绍了编写loader需要使用到的两个工具库:loader-utils
和 schema-utils
。
充分利用 loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项。schema-utils 包配合
loader-utils
,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验。
const options = getOptions(this) || {};
validate(schema, options, {
name: 'URL Loader',
baseDataPath: 'options',
});
validate
需要一个 schema 来对获取到的 options 进行校验,url-loader 的 schema 如下图:
![](https://img.haomeiwen.com/i18646161/ed96937712314fcb.png)
判断文件和limit的大小关系
content 是 Buffer
类型,buf.length 返回 buf 中的字节数,单位和 limit
一致。
if (shouldTransform(options.limit, content.length)) {
// ....
}
在 shouldTransform
中,对 limit
执行3种类型判断 :
- 如果是
true
或者false
,直接return limit
- 如果是字符串,转成数字,再和
limit
比较 - 如果是数字,直接和
limit
比较 - 不符合上述3种判断,
return true
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;
}
转化文件
shouldTransform
返回 true后,需要把当前文件转化成Data URLs (默认base64):
const { resourcePath } = this;
const mimetype = getMimetype(options.mimetype, resourcePath);
const encoding = getEncoding(options.encoding);
const encodedData = getEncodedData(
options.generator,
mimetype,
encoding,
content,
resourcePath
);
const esModule =
typeof options.esModule !== 'undefined' ? options.esModule : true;
return `${
esModule ? 'export default' : 'module.exports ='
} ${JSON.stringify(encodedData)}`;
其中 this.resourcePath
是文件的绝对路径,通过 getMimetype
得到文件最终生成的 mimeType
,通过 getEncoding
得到编码方式。
mimetype
首先通过 path.extname
获取当前扩展名,再通过 mime-types
把扩展名包装成对应的mimeType
:
function getMimetype(mimetype, resourcePath) {
if (typeof mimetype === 'boolean') {
if (mimetype) {
const resolvedMimeType = mime.contentType(path.extname(resourcePath));
if (!resolvedMimeType) {
return '';
}
return resolvedMimeType.replace(/;\s+charset/i, ';charset');
}
return '';
}
if (typeof mimetype === 'string') {
return mimetype;
}
const resolvedMimeType = mime.contentType(path.extname(resourcePath));
if (!resolvedMimeType) {
return '';
}
return resolvedMimeType.replace(/;\s+charset/i, ';charset');
}
那下面这段代码有什么用处呢?
resolvedMimeType.replace(/;\s+charset/i, ';charset')
原因是 mime-types
的 contentType
返回值 ; charset
中间是有空格的:
![](https://img.haomeiwen.com/i18646161/ee55b01b951446f7.png)
而在Data URL 的书写格式中,mediatype ;charset
是无空格的:
![](https://img.haomeiwen.com/i18646161/9fb5f928d1f83953.png)
因此需要做一步替换去除空格。
如上文配置项部分所说,encoding
默认是base64,开发者可以自行决定是否覆盖:
function getEncoding(encoding) {
if (typeof encoding === 'boolean') {
return encoding ? 'base64' : '';
}
if (typeof encoding === 'string') {
return encoding;
}
return 'base64';
}
最后,根据 getMimeType
、 getEncoding
的返回值,对文件内容进行拼接转化。
如果存在 options.generator
,则执行 options.generator
对内容做转化。
如果不存在,则按照 Data URL
格式拼接文件:
data:[<mediatype>][;base64],<data>
function getEncodedData(generator, mimetype, encoding, content, resourcePath) {
if (generator) {
return generator(content, mimetype, encoding, resourcePath);
}
return `data:${mimetype}${encoding ? `;${encoding}` : ''},${content.toString(
encoding || undefined
)}`;
}
对于不符合shouldTransform的文件,则继续往下执行。
执行fallback loader
首先获取 fallback loader 和 options :
const {
loader: fallbackLoader,
options: fallbackOptions,
} = normalizeFallback(options.fallback, options);
url-loader
默认使用 file-loader
作为处理 >limit 文件 的 loader。
- 如果开发者没有配置
options.fallback
,就直接使用url-loader
的 options 作为file-loader
的options。 - 如果开发者配置了
options.fallback
- 如果 fallback 类型是 string,loader 名称和 options 通过?隔开
- 如果 fallback 是 object,loader 名称和 options 分别为 fallback 的属性(这种写法在
url-loader
的文档没有介绍)
normalizeFallback如下:
export default function normalizeFallback(fallback, originalOptions) {
let loader = 'file-loader';
let options = {};
if (typeof fallback === 'string') {
loader = fallback;
const index = fallback.indexOf('?');
if (index >= 0) {
loader = fallback.substr(0, index);
options = loaderUtils.parseQuery(fallback.substr(index));
}
}
if (fallback !== null && typeof fallback === 'object') {
({ loader, options } = fallback);
}
options = Object.assign({}, originalOptions, options);
delete options.fallback;
return { loader, options };
}
引入 loader,然后执行 loader 并返回结果:
// Require the fallback.
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.
const fallbackLoaderContext = Object.assign({}, this, {
query: fallbackOptions,
});
return fallback.call(fallbackLoaderContext, content);
流程图
![](https://img.haomeiwen.com/i18646161/7408ec1530c93583.png)
file-loader
Instructs webpack to emit the required object as file and to return its public URL
file-loader 可以指定要放置资源文件的位置,以及如何使用哈希等命名以获得更好的缓存。这意味着可以通过工程化方式就近管理项目中的图片文件,不用担心部署时 URL 的问题。
配置项
![](https://img.haomeiwen.com/i18646161/523f96568e3bc4e4.png)
源码分析
获取options以及根据schema校验options:
const options = getOptions(this);
validate(schema, options, {
name: 'File Loader',
baseDataPath: 'options',
});
获取 context 及生成文件名称。使用loader-utils提供的interpolateName获取文件的hash值,并生成唯一的文件名:
const context = options.context || this.rootContext;
const name = options.name || '[contenthash].[ext]';
const url = interpolateName(this, name, {
context,
content,
regExp: options.regExp,
});
其中 interpolateName
的定义如下:
interpolatename(loadercontext, name, options)
// loadercontext 为 loader 的上下文对象,name 为文件名称模板,options 为配置对象
根据配置的 outputPath 和 publicPath 生成最终的 outputPath 和 publicPath,如果想要在dev和prod环境写不同的值,就可以把outputPath和publicPath写成函数形式:
// 处理outputPath
let outputPath = url;
if (options.outputPath) {
if (typeof options.outputPath === 'function') {
outputPath = options.outputPath(url, this.resourcePath, context);
} else {
outputPath = path.posix.join(options.outputPath, url);
}
}
// 处理publicPath
let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;
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。如果没有配置emitFile或者配置了emitFile,最后会执行this.emitFile在outputPath生成一个文件:
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);
}
const isImmutable = /\[([^:\]]+:)?(hash|contenthash)(:[^\]]+)?]/gi.test(
normalizedName
);
if (isImmutable === true) {
// 指定 asset 是否可以长期缓存的标志位(包括哈希值)
assetInfo.immutable = true;
}
}
assetInfo.sourceFilename = normalizePath(
path.relative(this.rootContext, this.resourcePath)
);
this.emitFile(outputPath, content, null, assetInfo);
}
在webpack官方文档里emitFile只有3个参数:
emitFile(name: string, content: Buffer|string, sourceMap: {...})
关于emitFile第四个参数是在file-loader里的issue上看到的。
导出最终路径:
const esModule =
typeof options.esModule !== 'undefined' ? options.esModule : true;
return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`;
总结
file-loader
可能会生成文件(emitFile),返回的是文件的路径;
url-loader
小于limit的文件返回的是Data URL字符串,其他文件返回执行 fallback loader 的结果;
url-loader
和 file-loader
的唯一联系就是 url-loader
把 file-loader
作为默认的 fallback loader,而我们也经常在项目的 url-loader
配置写 file-loader
的配置:
{
limit: CDN_THRESHOLD,
publicPath: `http://${genIp()}:9081/`,
name: '[path]/[name].[ext]',
}
其中的 publicPath
和 name
都属于 file-loader
的 options。
网友评论