如何将格式化输出拆分为可单独样式化的部分

使用 formatToParts() 方法访问格式化输出的各个组件,实现自定义样式

简介

JavaScript 格式化器上的 format() 方法会返回完整的字符串,如 "$1,234.56" 或 "2025年1月15日"。这种方式适合简单展示,但无法对各个部分进行不同的样式处理。例如,无法将货币符号加粗、将月份名称着色,或为特定组件应用自定义标记。

为了解决这个问题,JavaScript 提供了 formatToParts() 方法。它不会返回单一字符串,而是返回一个对象数组,每个对象代表格式化输出的一个部分。每个部分都有一个类型,如 currencymonthelement,以及包含实际字符串的值。你可以处理这些部分,实现自定义样式、构建复杂布局,或将格式化内容集成到丰富的用户界面中。

formatToParts() 方法可用于多个 Intl 格式化器,包括 NumberFormatDateTimeFormatListFormatRelativeTimeFormatDurationFormat。这使其成为 JavaScript 国际化格式化中的一致模式。

为什么格式化字符串难以直接样式化

当你收到类似 "$1,234.56" 这样的格式化字符串时,很难准确区分货币符号和数字的起止位置。不同的本地化环境会将符号放在不同位置,分隔符也可能不同。要可靠地解析这些字符串,需要复杂的逻辑,实际上是在重复 Intl API 已经实现的格式化规则。

假设你有一个仪表盘,需要将货币符号显示为不同颜色。如果只用 format(),你需要:

  1. 检测哪些字符是货币符号
  2. 处理符号与数字之间的空格
  3. 适应不同语言环境下符号的位置差异
  4. 仔细解析字符串,避免破坏数字本身

这种方法非常脆弱且容易出错。只要本地化格式规则发生变化,你的解析逻辑就会失效。

日期、列表和其他格式化输出也存在同样的问题。如果不重新实现每个语言环境的格式化规则,你无法可靠地解析格式化字符串以识别各个组成部分。

formatToParts() 方法通过分别提供各个组成部分,彻底解决了这个问题。你会收到结构化数据,能够准确知道每一部分的含义,无论使用哪种语言环境。

formatToParts 的工作原理

formatToParts() 方法的工作方式与 format() 完全相同,唯一的区别在于返回值。你可以用相同的选项创建格式化器,然后调用 formatToParts(),而不是 format()

该方法返回一个对象数组。每个对象包含两个属性:

  • type:标识该部分的含义,例如 currencymonthliteral
  • value:包含该部分的实际字符串

各部分的顺序与格式化输出中的顺序一致。你可以通过将所有 value 拼接起来进行验证,结果与调用 format() 得到的输出完全相同。

这种模式在所有支持 formatToParts() 的格式化器中都保持一致。具体的部分类型因格式化器而异,但结构始终相同。

将格式化数字拆分为各个部分

NumberFormat 格式化器提供 formatToParts(),用于分解格式化后的数字。此方法适用于基本数字、货币、百分比及其他数值样式。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = formatter.formatToParts(1234.56);
console.log(parts);

这将输出一个对象数组:

[
  { type: "currency", value: "$" },
  { type: "integer", value: "1" },
  { type: "group", value: "," },
  { type: "integer", value: "234" },
  { type: "decimal", value: "." },
  { type: "fraction", value: "56" }
]

每个对象都标识该部分的含义并提供其值。currency 类型表示货币符号。integer 类型表示整数位数字。group 类型表示千位分隔符。decimal 类型表示小数点。fraction 类型表示小数点后的数字。

你可以验证各部分与格式化输出是否一致:

const formatted = parts.map(part => part.value).join("");
console.log(formatted);
// Output: "$1,234.56"

将各部分拼接后,输出与调用 format() 的结果完全一致。

在格式化数字中为货币符号设置样式

formatToParts() 的主要用例是为不同组件应用不同样式。你可以处理 parts 数组,将特定类型包裹在 HTML 元素中。

将货币符号加粗:

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = formatter.formatToParts(1234.56);
const html = parts
  .map(part => {
    if (part.type === "currency") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>$</strong>1,234.56"

此方法适用于任何标记语言。你可以通过处理 parts 数组生成 HTML、JSX 或其他格式。

为小数部分设置不同样式:

const formatter = new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 2
});

const parts = formatter.formatToParts(1234.5);
const html = parts
  .map(part => {
    if (part.type === "decimal" || part.type === "fraction") {
      return `<span class="text-gray-500">${part.value}</span>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "1,234<span class="text-gray-500">.50</span>"

这种模式在价格展示中很常见,小数部分通常显示为较小或较浅的样式。

拆分格式化日期为各部分

DateTimeFormat 格式化器提供 formatToParts(),用于分解格式化后的日期和时间。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
console.log(parts);

这将输出一个对象数组:

[
  { type: "month", value: "January" },
  { type: "literal", value: " " },
  { type: "day", value: "15" },
  { type: "literal", value: ", " },
  { type: "year", value: "2025" }
]

month 类型表示月份名称或数字。day 类型表示日期。year 类型表示年份。literal 类型表示格式化器插入的空格、标点或其他文本。

为格式化日期中的月份名称设置样式

你可以使用与数字相同的模式为日期组件应用自定义样式。

将月份名称加粗:

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
const html = parts
  .map(part => {
    if (part.type === "month") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>January</strong> 15, 2025"

为多个日期组件设置样式:

const formatter = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);

const html = parts
  .map(part => {
    switch (part.type) {
      case "weekday":
        return `<span class="font-bold">${part.value}</span>`;
      case "month":
        return `<span class="text-blue-600">${part.value}</span>`;
      case "year":
        return `<span class="text-gray-500">${part.value}</span>`;
      default:
        return part.value;
    }
  })
  .join("");

console.log(html);
// Output: "<span class="font-bold">Wednesday</span>, <span class="text-blue-600">January</span> 15, <span class="text-gray-500">2025</span>"

这种细粒度的控制可以对每个组件进行精确样式设置。

拆分格式化列表为各部分

ListFormat 格式化器提供 formatToParts(),用于分解格式化后的列表。

const formatter = new Intl.ListFormat("en-US", {
  style: "long",
  type: "conjunction"
});

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
console.log(parts);

这将输出一个对象数组:

[
  { type: "element", value: "apples" },
  { type: "literal", value: ", " },
  { type: "element", value: "oranges" },
  { type: "literal", value: ", and " },
  { type: "element", value: "bananas" }
]

element 类型表示列表中的每一项。literal 类型表示格式化器添加的分隔符和连接词。

分别为列表项设置样式

你可以使用相同的模式为列表元素应用自定义样式。

将列表项加粗:

const formatter = new Intl.ListFormat("en-US", {
  style: "long",
  type: "conjunction"
});

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
const html = parts
  .map(part => {
    if (part.type === "element") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>apples</strong>, <strong>oranges</strong>, and <strong>bananas</strong>"

为特定列表项设置样式:

const formatter = new Intl.ListFormat("en-US", {
  style: "long",
  type: "conjunction"
});

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);

let itemIndex = 0;
const html = parts
  .map(part => {
    if (part.type === "element") {
      const currentIndex = itemIndex++;
      if (currentIndex === 0) {
        return `<span class="text-green-600">${part.value}</span>`;
      }
      return part.value;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<span class="text-green-600">apples</span>, oranges, and bananas"

这种方法可以高亮特定项,同时保持正确的本地化格式。

拆分格式化相对时间为各部分

RelativeTimeFormat 格式化器提供 formatToParts(),用于分解相对时间表达式。

const formatter = new Intl.RelativeTimeFormat("en-US", {
  numeric: "auto"
});

const parts = formatter.formatToParts(-1, "day");
console.log(parts);

这将输出一个对象数组:

[
  { type: "literal", value: "yesterday" }
]

对于数值型相对时间:

const formatter = new Intl.RelativeTimeFormat("en-US", {
  numeric: "always"
});

const parts = formatter.formatToParts(-3, "day");
console.log(parts);
// [
//   { type: "integer", value: "3" },
//   { type: "literal", value: " days ago" }
// ]

integer 类型表示数值。literal 类型表示相对时间单位和方向。

拆分格式化时长为各部分

DurationFormat 格式化器提供 formatToParts(),用于分解格式化后的时长。

const formatter = new Intl.DurationFormat("en-US", {
  style: "long"
});

const parts = formatter.formatToParts({
  hours: 2,
  minutes: 30,
  seconds: 15
});
console.log(parts);

这将输出一个类似于以下内容的对象数组:

[
  { type: "integer", value: "2" },
  { type: "literal", value: " hours, " },
  { type: "integer", value: "30" },
  { type: "literal", value: " minutes, " },
  { type: "integer", value: "15" },
  { type: "literal", value: " seconds" }
]

integer 类型表示数值。literal 类型表示单位名称和分隔符。

从格式化部分构建 HTML

你可以创建一个可复用的函数,处理各部分并统一应用样式规则。

function formatWithStyles(parts, styleMap) {
  return parts
    .map(part => {
      const style = styleMap[part.type];
      if (style) {
        return `<span class="${style}">${part.value}</span>`;
      }
      return part.value;
    })
    .join("");
}

const numberFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = numberFormatter.formatToParts(1234.56);
const html = formatWithStyles(parts, {
  currency: "font-bold text-gray-700",
  integer: "text-2xl",
  fraction: "text-sm text-gray-500"
});

console.log(html);
// Output: "<span class="font-bold text-gray-700">$</span><span class="text-2xl">1</span>,<span class="text-2xl">234</span>.<span class="text-sm text-gray-500">56</span>"

这种模式将样式规则与格式化逻辑分离,便于维护和复用。

理解特定语言环境的部分顺序

parts 数组会自动遵循特定语言环境的格式规则。不同语言环境的组件顺序和格式各不相同,但 formatToParts() 会自动处理这些差异。

const usdFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

console.log(usdFormatter.formatToParts(1234.56));
// [
//   { type: "currency", value: "$" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

const eurFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR"
});

console.log(eurFormatter.formatToParts(1234.56));
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "." },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "," },
//   { type: "fraction", value: "56" },
//   { type: "literal", value: " " },
//   { type: "currency", value: "€" }
// ]

德语格式会在数字后加空格再加货币符号。分组分隔符为句点,小数分隔符为逗号。无论语言环境如何,你的样式代码都以相同方式处理 parts 数组,格式会自动适配。

创建无障碍格式化显示

你可以使用 formatToParts() 为格式化输出添加无障碍属性,帮助屏幕阅读器正确朗读数值。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

function formatAccessible(number) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  return `<span aria-label="${number} US dollars">${formatted}</span>`;
}

console.log(formatAccessible(1234.56));
// Output: "<span aria-label="1234.56 US dollars">$1,234.56</span>"

这样可以确保屏幕阅读器在适当的上下文中朗读格式化显示值和底层数值。

将 formatToParts 与框架组件结合使用

现代框架(如 React)可以利用 formatToParts() 高效构建组件。

function CurrencyDisplay({ value, locale, currency }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currency
  });

  const parts = formatter.formatToParts(value);

  return (
    <span className="currency-display">
      {parts.map((part, index) => {
        if (part.type === "currency") {
          return <strong key={index}>{part.value}</strong>;
        }
        if (part.type === "fraction" || part.type === "decimal") {
          return <span key={index} className="text-sm text-gray-500">{part.value}</span>;
        }
        return <span key={index}>{part.value}</span>;
      })}
    </span>
  );
}

该组件针对不同部分应用不同的样式,同时确保任何本地化和货币格式下都能保持正确的格式。

何时使用 formatToParts

当你只需要一个简单的格式化字符串且无需自定义时,请使用 format()。这适用于大多数展示场景。

当你需要:

  • 对格式化输出的不同部分应用不同样式
  • 使用格式化内容构建 HTML 或 JSX
  • 为特定组件添加属性或元数据
  • 将格式化输出集成到复杂布局中
  • 以编程方式处理格式化输出
  • 创建需要精细控制的自定义视觉设计

formatToParts() 方法相比 format() 有略高的开销,因为它会生成一个对象数组而不是单一字符串。对于典型应用来说,这种差异可以忽略,但如果你每秒需要格式化成千上万个值,format() 的性能会更好。

对于大多数应用,建议根据样式需求而非性能来选择。如果不需要自定义输出,请使用 format()。如需自定义样式或标记,则使用 formatToParts()

各格式化器通用的部分类型

不同的格式化器会生成不同的部分类型,但有些类型会在多个格式化器中出现:

  • literal:由格式化添加的空格、标点或其他文本。出现在日期、数字、列表和时长中。
  • integer:整数位数字。出现在数字、相对时间和时长中。
  • decimal:小数分隔符。出现在数字中。
  • fraction:小数位数字。出现在数字中。

特定于格式化器的类型包括:

  • 数字:currencygrouppercentSignminusSignplusSignunitcompactexponentInteger
  • 日期:weekdayerayearmonthdayhourminuteseconddayPeriodtimeZoneName
  • 列表:element
  • 相对时间:数值显示为 integer,文本显示为 literal

理解这些类型有助于你编写能够正确处理任何格式化器输出的样式代码。