如何将格式化输出拆分为部分以进行样式化

使用 formatToParts() 访问格式化输出的各个组件以进行自定义样式

介绍

format() 方法在 JavaScript 格式化器中返回完整的字符串,例如 "$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:标识该部分的类型,例如 currency(货币)、month(月份)或 literal(文字)
  • 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);
// 输出: "$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);
// 输出: "<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);
// 输出: "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);
// 输出: "<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);
// 输出: "<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);
// 输出: "<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);
// 输出: "<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);
// 输出: "<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>"

这种模式将样式规则与格式化逻辑分离,使其更易于维护和重用。

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

部分数组会自动遵循特定语言环境的格式规则。不同的语言环境会以不同的顺序排列组件并使用不同的格式,但 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: "€" }
// ]

例如,德语格式会在数字后加上货币符号并留一个空格。分组分隔符是句号,小数分隔符是逗号。您的样式代码以相同的方式处理部分数组,而格式会根据语言环境自动调整。

创建无障碍的格式化显示

您可以使用 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} 美元">${formatted}</span>`;
}

console.log(formatAccessible(1234.56));
// 输出: "<span aria-label="1234.56 美元">$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()。这是大多数显示场景的常见情况。

当您需要以下功能时,请使用 formatToParts()

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

formatToParts() 方法的开销比 format() 略高,因为它创建的是对象数组而不是单个字符串。对于典型应用程序,这种差异可以忽略不计,但如果您每秒格式化数千个值,format() 的性能更好。

对于大多数应用程序,根据您的样式需求选择方法。如果不需要自定义输出,请使用 format();如果需要自定义样式或标记,请使用 formatToParts()

格式化器中的常见部分类型

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

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

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

  • 数字:currency(货币)、group(分组)、percentSign(百分号)、minusSign(负号)、plusSign(正号)、unit(单位)、compact(紧凑)、exponentInteger(指数整数)
  • 日期:weekday(星期)、era(纪元)、year(年份)、month(月份)、day(日期)、hour(小时)、minute(分钟)、second(秒)、dayPeriod(时间段)、timeZoneName(时区名称)
  • 列表:element(元素)
  • 相对时间:数字值显示为 integer,文本显示为 literal

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