Bootstrap

【万字详解】如何在微信小程序的 Taro 框架中设置静态图片 assets/image 的 Base64 转换上限值

设置方法

mini 中提供了 imageUrlLoaderOptionpostcss.url

其中:
config.limitimageUrlLoaderOption.limit 服务于 Taro 的 MiniWebpackModule.js , 值的写法要 ()KB * 1024
config.maxSize 服务于 postcss-url 的 inline.js , 值的写法要 ()KB

关于为什么 limitmaxSize 的写法不同?
因为在源码层:
limit 在使用时是判断 limit2 * 1024 的值: maxSize: options.limit || 2 * 1024
maxSize 在使用时是判断 maxSize0 ,再乘以 1024const 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.limitimageUrlLoaderOption.limit 的替换关系如图所示

limit 的对决

源码解析

Taro 部分

class MiniWebpackModule 是 Taro 框架中用于处理和封装 Webpack 构建模块的类。它负责配置、加载和编译与各类文件格式相关的模块,通常在 Taro 生成项目构建配置时发挥作用。它的目标是将开发者的源代码转换成适用于小程序、 H5 、 React Native 等平台的最终代码。

getModules

getModules 方法的核心作用是配置和返回不同类型模块的处理规则,包括对 Sass 、 Scss 、 Less 、 Stylus 等样式文件的处理,并将其与 CSS 、 PostCSS 等工具结合起来,最终生成适合各种小程序平台的构建规则。

parsePostCSSOptions() 解析 PostCSS 的配置选项,返回 postcssOptionpostcssUrlOptioncssModuleOption 等。

通过 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 时,postcssOptionsimageUrlLoaderOption 合并生成新的 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 是个对象,属性有 urllimitmaxSizeurl 默认值是 inline

enable 设为 falseoptions{} ;设为 trueoptions 为打印配置中所设的,可能有 urllimitmaxSize

styles.walkDecls((decl) =>{}); 遍历 CSS 文件中的每个声明(decl)。对每个声明,调用 declProcessor 函数,并将结果(是个 Promisepushpromises 数组中。返回的 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 还有其他。

如果 pattenundefined ,直接返回 Promise.resolve() 。这是为了在 pattern 不存在时直接短路,避免不必要的计算。

正常函数内部执行了 Promise.resolve() 后,后面的代码是可以继续执行的。

如果 patten 不是 undefined ,新建一个 promises 数组,用来存储所有异步操作的 Promisedecl.value.replace(pattern, (matched, before, url, after) => { ... }) 使用 replace 函数遍历和处理 decl.value 中的每个匹配项。 replace 函数的第二个函数是个回调函数,最终的返回值是 matched

在回调函数中执行 replaceUrl 函数,返回一个为 PromisenewUrlPromise ,然后调用 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 是一个对象,里面有 urloriginUrlpathnameabsolutePathrelativePathsearchhash

当传入的是 OSS 路径或者 data:font/woff;base64 时, matchedOptionsundefined ,直接返回 Promise.resolve()
当传入的是 asset/images 下的图片时, matchedOptionsoptions 的值。会判断 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.fallbackundefined ,直接返回 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;
};
;