【万字详解】如何在微信小程序的 Taro 框架中设置静态图片 assets/image 的 Base64 转换上限值
设置方法
mini
中提供了 imageUrlLoaderOption
和 postcss.url
。
其中:
config.limit
和 imageUrlLoaderOption.limit
服务于 Taro 的 MiniWebpackModule.js , 值的写法要 ()KB * 1024
。
config.maxSize
服务于 postcss-url 的 inline.js , 值的写法要 ()KB
。
关于为什么 limit
和 maxSize
的写法不同?
因为在源码层:
limit
在使用时是判断 limit
和 2 * 1024
的值: maxSize: options.limit || 2 * 1024
;
maxSize
在使用时是判断 maxSize
和 0
,再乘以 1024
: const maxSize = (options.maxSize || 0) * 1024;
;
所以在配置时要注意区分。
const config = {
// ...
mini: {
// ...
imageUrlLoaderOption: {
limit: num * 1024,
},
postcss: {
// ...
url: {
enable: true / false,
config: {
limit: num * 1024,
maxSize: num,
},
},
// ...
},
},
// ...
};
// ...
Base64 转换上限值分以下 12 种情况去配置:
url | config | imageUrlLoaderOption | 转成 Base64 的图片上限 |
url.enable 为 true | config.limit 和 config.maxSize 都存在 | 没有 imageUrlLoaderOption.limit | config.limit 和 maxSize的最大值 |
有 imageUrlLoaderOption.limit | config.limit、imageUrlLoaderOption.limit 、 maxSize的最大值 | ||
config.maxSize 不存在, config.limit 存在 | 没有 imageUrlLoaderOption.limit | 全部图片都被转成 Base64 | |
有 imageUrlLoaderOption.limit | 全部图片都被转成 Base64 | ||
config.limit 不存在, config.maxSize 存在 | 没有 imageUrlLoaderOption.limit | 当 maxSize > 10 ,以 maxSize 为主;否则小于 10KB 的图片被转成 Base64 | |
有 imageUrlLoaderOption.limit | imageUrlLoaderOption.limit 和 maxSize的最大值 | ||
config.limit 和 config.maxSize 都不存在 | 没有 imageUrlLoaderOption.limit | 全部图片都被转成 Base64 | |
有 imageUrlLoaderOption.limit | 全部图片都被转成 Base64 | ||
url.enable 为 false | - | 没有 imageUrlLoaderOption.limit | 2KB |
有 imageUrlLoaderOption.limit | imageUrlLoaderOption.limit | ||
不存在 url | - | 没有 imageUrlLoaderOption.limit | 全部图片都被转成 Base64 |
有 imageUrlLoaderOption.limit | 全部图片都被转成 Base64 |
最实用的配置:
不使用 postcss 插件,通过 Taro 提供的 imageUrlLoaderOption
来设置转换上限值
// ...
const config = {
// ...
mini: {
// ...
imageUrlLoaderOption: {
limit: -1,
},
postcss: {
// ...
url: {
enable: false,
},
// ...
},
},
// ...
};
// ...
关于 limit
config.limit
和 imageUrlLoaderOption.limit
的替换关系如图所示
源码解析
Taro 部分
class MiniWebpackModule
是 Taro 框架中用于处理和封装 Webpack 构建模块的类。它负责配置、加载和编译与各类文件格式相关的模块,通常在 Taro 生成项目构建配置时发挥作用。它的目标是将开发者的源代码转换成适用于小程序、 H5 、 React Native 等平台的最终代码。
getModules
getModules
方法的核心作用是配置和返回不同类型模块的处理规则,包括对 Sass 、 Scss 、 Less 、 Stylus 等样式文件的处理,并将其与 CSS 、 PostCSS 等工具结合起来,最终生成适合各种小程序平台的构建规则。
parsePostCSSOptions()
解析 PostCSS 的配置选项,返回 postcssOption
、postcssUrlOption
和 cssModuleOption
等。
通过 getDefaultPostcssConfig
方法获取 PostCSS 的默认配置,返回给 this.__postcssOption
进行保存。
调用 getImageRule
方法对 image
进行配置,并将配置存入 rule
对象中。
getModules() {
const { appPath, config, sourceRoot, fileType } = this.combination;
const { buildAdapter, sassLoaderOption, lessLoaderOption, stylusLoaderOption, designWidth, deviceRatio } = config;
const { postcssOption, postcssUrlOption, cssModuleOption } = this.parsePostCSSOptions();
this.__postcssOption = (0, postcss_mini_1.getDefaultPostcssConfig)({
designWidth,
deviceRatio,
postcssOption,
alias: config.alias,
});
const rule = {
image: this.getImageRule(postcssUrlOption)
};
return { rule };
}
parsePostCSSOptions
defaultUrlOption
确实默认启用 postcss-url,并设置 limit
为 10KB(10240 字节)。代码中 postcssOption.url
会通过 recursiveMerge
方法与 defaultUrlOption
合并,如果配置中提供了 postcss-url 的自定义配置,将会覆盖默认值。
postcssUrlOption
不会赋值,即 config/index.ts 中配置的 config
不会被使用或存储。
parsePostCSSOptions() {
const { postcss: postcssOption = {} } = this.combination.config;
const defaultUrlOption = {
enable: true,
config: {
limit: 10 * 1024 // limit 10k base on document
}
};
const urlOptions = (0, helper_1.recursiveMerge)({}, defaultUrlOption, postcssOption.url);
let postcssUrlOption = {};
if (urlOptions.enable) {
postcssUrlOption = urlOptions.config;
}
return {
postcssOption,
postcssUrlOption
};
}
getDefaultPostcssConfig
接收 postcssOption
,从中提取 url
配置,并通过 recursiveMerge
将其与 defaultUrlOption
合并为 urlOption
。如果 postcssOption.url
存在,就会用自定义配置来替代或合并默认配置。
const getDefaultPostcssConfig = function ({ postcssOption = {} }) {
const { url } = postcssOption,
options = __rest(postcssOption, ["url"]);
const urlOption = (0, helper_1.recursiveMerge)({}, defaultUrlOption, url);
return [
["postcss-url", urlOption, require("postcss-url")],
...Object.entries(options),
];
};
getImageRule
当调用 getImageRule
时,postcssOptions
和 imageUrlLoaderOption
合并生成新的 options
,并传递给 WebpackModule.getImageRule()
,limit
的优先级以 imageUrlLoaderOption.limit
为主导。
getImageRule(postcssOptions) {
const sourceRoot = this.combination.sourceRoot;
const { imageUrlLoaderOption } = this.combination.config;
const options = Object.assign({}, postcssOptions, imageUrlLoaderOption);
return WebpackModule_1.WebpackModule.getImageRule(sourceRoot, options);
}
WebpackModule_1.WebpackModule.getImageRule
如果 postcss.url.config.limit
没有设置,系统应有逻辑保证 limit
的默认值为 2KB。
static getImageRule(sourceRoot, options) {
return {
test: helper_1.REG_IMAGE,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: options.limit || 2 * 1024 // 2kb
}
},
generator: {
emit: options.emitFile || options.emit,
outputPath: options.outputPath,
publicPath: options.publicPath,
filename({ filename }) {
if ((0, shared_1.isFunction)(options.name))
return options.name(filename);
return options.name || filename.replace(sourceRoot + '/', '');
}
}
};
}
postcss-url 部分
plugin
options
是个对象,属性有 url
、 limit
、 maxSize
。 url
默认值是 inline
。
当 enable
设为 false
, options
为 {}
;设为 true
, options
为打印配置中所设的,可能有 url
、 limit
、 maxSize
。
styles.walkDecls((decl) =>{});
遍历 CSS 文件中的每个声明(decl)。对每个声明,调用 declProcessor
函数,并将结果(是个 Promise
) push
到 promises
数组中。返回的 Promise
通过 then
打印的结果,是一个数组,值为'url("OSS || Base64 || assets")'
。
const plugin = (options) => {
options = options || {};
return {
postcssPlugin: "postcss-url",
Once(styles, { result }) {
const promises = [];
const opts = result.opts;
const from = opts.from ? path.dirname(opts.from) : ".";
const to = opts.to ? path.dirname(opts.to) : from;
styles.walkDecls((decl) =>
promises.push(declProcessor(from, to, options, result, decl))
);
return Promise.all(promises);
},
};
};
declProcessor
获取 patten
,值存在 undefined
、/(url\(\s*['"]?)([^"')]+)(["']?\s*\))/g
还有其他。
如果 patten
是 undefined
,直接返回 Promise.resolve()
。这是为了在 pattern
不存在时直接短路,避免不必要的计算。
正常函数内部执行了
Promise.resolve()
后,后面的代码是可以继续执行的。
如果 patten
不是 undefined
,新建一个 promises
数组,用来存储所有异步操作的 Promise
。 decl.value.replace(pattern, (matched, before, url, after) => { ... })
使用 replace
函数遍历和处理 decl.value
中的每个匹配项。 replace
函数的第二个函数是个回调函数,最终的返回值是 matched
。
在回调函数中执行 replaceUrl
函数,返回一个为 Promise
的 newUrlPromise
,然后调用 then
方法获取 newUrlPromise
的值。
如果 newUrlPromise
的值是 undefined
,直接返回 matched
。这种情况会发生在没有转换成 Base64 编码的本地图片路径上。
declProcessor
最终返回的是 Promise.all(promises)
(是个 Promise
)。
const declProcessor = (from, to, options, result, decl) => {
const dir = { from, to, file: getDirDeclFile(decl) };
const pattern = getPattern(decl);
if (!pattern) return Promise.resolve();
const promises = [];
decl.value = decl.value.replace(pattern, (matched, before, url, after) => {
const newUrlPromise = replaceUrl(url, dir, options, result, decl);
promises.push(
newUrlPromise.then((newUrl) => {
if (!newUrl) return matched;
if (WITH_QUOTES.test(newUrl) && WITH_QUOTES.test(after)) {
before = before.slice(0, -1);
after = after.slice(1);
}
decl.value = decl.value.replace(matched, `${before}${newUrl}${after}`);
})
);
return matched;
});
return Promise.all(promises);
};
replaceUrl
asset
是一个对象,里面有 url
、 originUrl
、 pathname
、 absolutePath
、 relativePath
、 search
、 hash
。
当传入的是 OSS 路径或者 data:font/woff;base64
时, matchedOptions
是 undefined
,直接返回 Promise.resolve()
。
当传入的是 asset/images 下的图片时, matchedOptions
是 options
的值。会判断 matchedOptions
是不是个数组。如果是数组,则对数组里面的值一一执行 process
函数;不是数组,直接执行 process
函数。
process
函数里的 getUrlProcessor
函数会根据 url
的值判断走哪种类型的编译方法。
process
函数里的 wrapUrlProcessor
函数实现了对 urlProcessor
的“增强”,使其在处理 URL 的过程中可以记录警告信息和依赖关系。
replaceUrl
最终返回一个 Promise
,在 resultPromise.then(...)
的链式调用中, return newUrl
; 实际上是将 newUrl
封装在一个新的 Promise
中作为最终返回值,并且 Promise
的解析值是 newUrl
,可以是经过编码的 URL
、文件路径或 undefined
。
const replaceUrl = (url, dir, options, result, decl) => {
const asset = prepareAsset(url, dir, decl);
const matchedOptions = matchOptions(asset, options);
if (!matchedOptions) return Promise.resolve();
const process = (option) => {
const wrappedUrlProcessor = wrapUrlProcessor(
getUrlProcessor(option.url),
result,
decl
);
return wrappedUrlProcessor(asset, dir, option);
};
let resultPromise = Promise.resolve();
if (Array.isArray(matchedOptions)) {
for (let i = 0; i < matchedOptions.length; i++) {
resultPromise = resultPromise
.then(() => process(matchedOptions[i]))
.then((newUrl) => {
asset.url = newUrl;
return newUrl;
});
}
} else {
resultPromise = process(matchedOptions);
}
return resultPromise.then((newUrl) => {
asset.url = newUrl;
return newUrl;
});
};
getUrlProcessor
根据 url
的值判断走哪种 post-url 类型
function getUrlProcessor(optionUrl) {
const mode = getUrlProcessorType(optionUrl);
if (PROCESS_TYPES.indexOf(mode) === -1) {
throw new Error(`Unknown mode for postcss-url: ${mode}`);
}
return require(`../type/${mode}`);
}
wrapUrlProcessor
wrapUrlProcessor
实现了对 urlProcessor
的“增强”,使其在处理 URL 的过程中可以记录警告信息和依赖关系。
const wrapUrlProcessor = (urlProcessor, result, decl) => {
const warn = (message) => decl.warn(result, message);
const addDependency = (file) =>
result.messages.push({
type: "dependency",
file,
parent: getPathDeclFile(decl),
});
return (asset, dir, option) =>
urlProcessor(asset, dir, option, decl, warn, result, addDependency);
};
inline.js
根据 options
中的 maxSize
获取 maxSize
,所以配置表中传入的maxSize
不需要乘 1024
。
如果 maxSize
不是 0
,获取图片的 size
。如果图片的 size
大于 maxSize
,调用 processFallback
,按回退处理方式(如文件路径链接)返回;如果图片的 size
小于 maxSize
,调用 inlineProcess
编译成 Base64 。
module.exports = function (
asset,
dir,
options,
decl,
warn,
result,
addDependency
) {
return getFile(asset, options, dir, warn).then((file) => {
if (!file) return;
if (!file.mimeType) {
warn(`Unable to find asset mime-type for ${file.path}`);
return;
}
const maxSize = (options.maxSize || 0) * 1024;
if (maxSize) {
const size = Buffer.byteLength(file.contents);
if (size >= maxSize) {
return processFallback.apply(this, arguments);
}
}
return inlineProcess(file, asset, warn, addDependency, options);
});
};
processFallback
根据 options.fallback
的值进行调用,当前 options.fallback
是 undefined
,直接返回 Promise.resolve();
。
function processFallback(originUrl, dir, options) {
if (typeof options.fallback === "function") {
return options.fallback.apply(null, arguments);
}
switch (options.fallback) {
case "copy":
return processCopy.apply(null, arguments);
case "rebase":
return processRebase.apply(null, arguments);
default:
return Promise.resolve();
}
}
inlineProcess
该方法实现了将文件进行 Base64 转换,如果是 SVG 文件,则使用 encodeURIComponent
,否则使用 base64
编码。
const inlineProcess = (file, asset, warn, addDependency, options) => {
const isSvg = file.mimeType === "image/svg+xml";
const defaultEncodeType = isSvg ? "encodeURIComponent" : "base64";
const encodeType = options.encodeType || defaultEncodeType;
// Warn for svg with hashes/fragments
if (isSvg && asset.hash && !options.ignoreFragmentWarning) {
// eslint-disable-next-line max-len
warn(
`Image type is svg and link contains #. Postcss-url cant handle svg fragments. SVG file fully inlined. ${file.path}`
);
}
addDependency(file.path);
const optimizeSvgEncode = isSvg && options.optimizeSvgEncode;
const encodedStr = encodeFile(file, encodeType, optimizeSvgEncode);
const resultValue =
options.includeUriFragment && asset.hash
? encodedStr + asset.hash
: encodedStr;
// wrap url by quotes if percent-encoded svg
return isSvg && encodeType !== "base64" ? `"${resultValue}"` : resultValue;
};