currency-formatter vs currency.js vs dinero.js vs numeral
前端货币处理与格式化库的技术选型指南
currency-formattercurrency.jsdinero.jsnumeral类似的npm包:

前端货币处理与格式化库的技术选型指南

currency-formattercurrency.jsdinero.jsnumeral 都是用于处理数字格式化和货币计算的 JavaScript 库,但它们的侧重点截然不同。numeral 是一个通用的数字格式化工具,支持多种格式但已停止维护;currency-formatter 专注于根据 ISO 标准快速生成货币字符串;currency.js 旨在解决浮点数精度问题,提供轻量级的货币计算能力;而 dinero.js 则是一个功能完备的不可变货币对象库,专为复杂的金融逻辑和高精度计算设计。开发者需要根据项目是仅需展示格式化、需要简单计算,还是需要严格的金融级精度和领域模型来选择相应的工具。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
currency-formatter0212-155 年前MIT
currency.js03,380-605 年前MIT
dinero.js06,793858 kB246 个月前MIT
numeral09,703-3459 年前MIT

前端货币处理深度对比:从格式化到金融级精度

在前端开发中,处理货币不仅仅是加个美元符号那么简单。我们面临着三个核心挑战:本地化格式化(不同国家的符号和分隔符)、浮点数精度陷阱(JavaScript 原生 Number 类型的致命缺陷)以及业务逻辑复杂性(舍入规则、货币分配)。

currency-formattercurrency.jsdinero.jsnumeral 代表了四种不同的解决思路。本文将深入剖析它们的内部机制,通过代码实战帮你做出正确的架构决策。

⚠️ 重要警示:关于 numeral 的状态

在深入技术细节前,必须明确一点:numeral 已不再维护。它的最后一次更新远在多年前,社区已普遍转向其他方案。虽然它的 API 设计曾经非常优雅,支持极其丰富的格式化模板,但在现代技术栈中引入一个“死库”会带来安全隐患和构建工具兼容性问题。

// numeral (已过时,不推荐在新项目使用)
// 这种写法在现代打包工具中可能需要额外的 shim 配置
var numeral = require('numeral');
numeral(1000.234).format('$0,0.00'); // 输出:$1,000.23

建议:如果你的项目仅需格式化,请迁移至 currency-formatter 或原生 Intl.NumberFormat;若需计算,请选择 currency.jsdinero.js

🎨 场景一:纯展示格式化 (Formatting Only)

当你的需求只是将后端传来的数字(通常是整数分或浮点数)展示给用户看时,性能和解耦是关键。

currency-formatter:专注格式的轻量选手

currency-formatter 不做任何计算,它只做一件事:把数字变成字符串。它依赖 ISO 4217 标准数据,能正确处理各种奇怪的货币符号位置和小数位数(如日元没有小数位)。

import { format } from 'currency-formatter';

// 自动处理日元无小数位
format(5000, { code: 'JPY' }); 
// 输出:"¥5,000"

// 自定义欧元格式
format(1234.56, { code: 'EUR', locale: 'de-DE' }); 
// 输出:"1.234,56 €" (符合德国习惯)

优势:体积极小,无副作用,专门解决 Intl.NumberFormat 在某些旧环境或特定配置下不够直观的问题。 劣势:无法处理计算,输入必须是数字,如果传入浮点数进行复杂运算后再格式化,精度问题依然存在。

numeral (历史参考)

作为对比,看看 numeral 曾经如何处理。它的模板系统非常强大,但过于灵活导致容易出错。

// numeral (已过时)
numeral(1234567).format('0,0.00 $'); // 完全自定义模板
// 输出:"1,234,567.00 $"

对比结论:在现代项目中,currency-formatternumeral 更安全、更专注。如果只需要格式化,甚至可以直接使用浏览器原生的 Intl.NumberFormat,无需额外依赖。

🧮 场景二:轻量级计算 (Lightweight Calculation)

当需要在购物车、表单预览等场景进行简单的加减乘除,且必须避免 0.1 + 0.2 = 0.30000000000000004 这种尴尬时。

currency.js:以开发者体验为导向

currency.js 的核心思想是“让货币像数字一样好用,但没有精度问题”。它在内部将金额转换为整数(分)进行计算,但在 API 层面暴露为浮点数对象,支持链式调用。

import currency from 'currency.js';

const price = currency(10.50);
const tax = currency(2.10);

// 链式计算,自动处理精度
const total = price.add(tax).multiply(2).subtract(1);

console.log(total.value); // 获取原始数字 (24.00)
console.log(total.format()); // 直接格式化 "$24.00"

技术细节

  • 内部存储:使用整数运算避免浮点误差。
  • API 风格:类似 jQuery 的链式调用,非常直观。
  • 容错性:构造函数能自动处理字符串输入 currency("10.50")

适用场景:电商购物车、简单的费用计算器、SaaS 订阅价格预览。 局限性:不支持复杂的金融规则(如银行家舍入法的高级配置),也不支持货币兑换逻辑。

🏦 场景三:金融级精度与领域模型 (Financial Grade)

当你的应用涉及账务系统、多方金额分配、复杂的税务逻辑或需要严格审计时,简单的库已经不够用了。你需要的是一个“领域模型”。

dinero.js:不可变的货币对象

dinero.js 不仅仅是一个工具库,它是一个完整的货币领域模型。它强制你使用“分”或最小货币单位作为整数输入,从根本上杜绝了浮点数。它的所有操作都是不可变的(Immutability),即每次操作都会返回一个新的 Dinero 对象,而不是修改原值。

import Dinero from 'dinero.js';

// 必须使用整数(美分)初始化,杜绝浮点歧义
const price = Dinero({ amount: 1050, currency: 'USD' }); // $10.50
const tax = Dinero({ amount: 210, currency: 'USD' });    // $2.10

// 计算返回新对象,原对象不变
const total = price.add(tax);

// 复杂的舍入控制
const halfDollar = Dinero({ amount: 1235, currency: 'USD' });
const rounded = halfDollar.toUnit('dollar', { round: 'HALF_UP' });

// 货币分配(解决分钱除不尽的问题)
// 将 $10.03 分配给 3 个人,不会出现丢失 1 分钱的情况
const [alice, bob, charlie] = Dinero({ amount: 1003, currency: 'USD' }).allocate([33, 33, 34]);

核心优势

  1. 安全性:强制整数输入,API 层面无法创建不精确的货币对象。
  2. 不可变性:在复杂的状态管理(如 Redux)中非常安全,不会出现意外修改。
  3. 高级功能:内置 allocate(分配)、compare(比较)、hasSameCurrency(同币种校验)等金融专用方法。
  4. 链式与函数式:支持完美的函数式编程风格。

代价

  • 学习曲线:需要理解“最小货币单位”概念,不能直接传 10.5
  • 体积:比 currency.js 大得多。
  • 互操作性:与后端交互时,需要频繁地在“整数分”和“浮点元”之间转换(建议后端也统一返回分)。

🥊 深度对比:核心差异点

1. 数据输入与精度控制

这是最本质的区别。currency.js 试图对你输入的浮点数进行“补救”,而 dinero.js 直接禁止你输入浮点数。

// currency.js: 尝试修复浮点数,但输入仍是浮点
const c1 = currency(0.1).add(0.2); 
// 内部转换:10 分 + 20 分 = 30 分 -> 0.3 (成功)

// dinero.js: 强制整数,从源头消灭问题
const d1 = Dinero({ amount: 10, currency: 'USD' }).add(
  Dinero({ amount: 20, currency: 'USD' })
);
// 必须显式构造,无法误传 0.1

分析:在简单场景下,currency.js 的 DX(开发体验)更好;但在涉及大量数据导入或用户自由输入的场景,dinero.js 的严格模式能防止更多潜在 Bug。

2. 货币分配算法 (Allocation)

在金融场景中,将总金额按比例分配给多方时,常因除不尽导致“丢失几分钱”。这是检验库专业度的试金石。

// dinero.js: 内置专业的分配算法
const amounts = Dinero({ amount: 100, currency: 'USD' }).allocate([50, 50]);
// 结果:[50, 50]

const amounts2 = Dinero({ amount: 101, currency: 'USD' }).allocate([50, 50]);
// 结果:[51, 50] (自动处理余数,保证总和不变)

// currency.js: 无此功能,需手动实现
// 开发者必须自己写逻辑处理余数,极易出错
const total = 101;
const part1 = Math.floor(total * 0.5);
const part2 = total - part1; // 手动补丁

结论:只要涉及“分账”、“佣金计算”、“税费分摊”,dinero.js 是唯一可靠的选择。

3. 可变性 (Mutability)

// currency.js: 可变操作 (部分方法) 或 返回新对象混合
// 需注意链式调用是否改变了原引用,文档虽声称返回新对象,但内部状态管理较简单
const cart = currency(100);
const updated = cart.add(10); 

// dinero.js: 严格不可变
const original = Dinero({ amount: 100, currency: 'USD' });
const modified = original.add(10);

console.log(original.getAmount()); // 依然是 100,绝对安全

在 React 或 Vue 等响应式框架中,不可变数据能显著减少调试难度,确保状态流转清晰。

📊 选型决策矩阵

特性currency-formattercurrency.jsdinero.jsnumeral
主要用途纯格式化展示轻量级计算 + 格式化金融级计算 + 领域模型通用格式化 (已过时)
精度处理无 (依赖输入)内部转整数,对外屏蔽强制整数输入,严格无 (浮点数)
API 风格函数式链式 (jQuery 风)链式 (函数式风)链式
不可变性N/A部分严格不可变
高级功能多 Locale 支持基础四则运算分配、比较、兑换、舍入策略丰富但过时
维护状态✅ 活跃✅ 活跃✅ 活跃 (v2 重构中)停止维护
推荐场景商品列表、报表购物车、表单账务系统、支付核心无 (请避免)

💡 架构师建议

  1. 对于绝大多数内容型网站或简单电商: 组合使用 原生 Intl.NumberFormat (用于格式化) + 简单的整数运算 (后端返回分,前端直接加减)。如果不想处理整数转换,使用 currency.js 是最快的落地方案,它能以最小的成本解决精度痛点。

  2. 对于 Fintech、SaaS 计费、复杂 ERP 系统: 不要犹豫,直接上 dinero.js。虽然前期需要适应“整数输入”和“不可变对象”的模式,但它提供的 allocate 方法和严格的类型约束,会在项目后期节省大量的调试时间和资金风险。它的代码即文档,明确表达了货币的领域逻辑。

  3. 关于 numeral 的迁移: 如果老项目中使用了 numeral,建议制定计划逐步替换。格式化逻辑可替换为 currency-formatter 或原生 API;计算逻辑必须重构为 currency.jsdinero.js,否则精度 Bug 随时可能爆发。

最后总结

  • 只要展示?选 currency-formatter
  • 简单算算?选 currency.js
  • 涉及钱袋子核心逻辑?选 dinero.js
  • numeral?让它留在历史里吧。

如何选择: currency-formatter vs currency.js vs dinero.js vs numeral

  • currency-formatter:

    如果你的需求仅仅是将数字转换为符合特定地区标准的货币字符串(如 '$1,234.56'),而不涉及复杂的数学运算,currency-formatter 是最佳选择。它轻量且专注于格式化,API 简单直接,适合用于电商列表页、报表展示等只读场景。但请注意,它不负责解决浮点数精度问题,计算逻辑需自行处理。

  • currency.js:

    当你需要在浏览器端进行简单的货币加减乘除运算,且希望避免 JavaScript 原生浮点数精度错误(如 0.1 + 0.2 !== 0.3)时,应选择 currency.js。它提供了类似 jQuery 的链式调用 API,上手成本低,适合中小型项目或原型开发。不过,它的功能集相对基础,不适合处理复杂的税务计算或货币兑换逻辑。

  • dinero.js:

    对于涉及核心金融逻辑、高并发交易或对数据一致性要求极高的企业级应用,必须选择 dinero.js。它采用不可变数据模式,提供丰富的领域方法(如分配、比较、舍入),并严格遵循金融计算标准。虽然学习曲线较陡且包体积较大,但它能从根本上杜绝精度丢失和状态突变带来的 bug,是构建可靠财务系统的首选。

  • numeral:

    仅建议在维护旧项目时使用 numeral,新项目中应避免引入。虽然它支持非常灵活的数字格式化(包括百分比、字节、时间等),但该库已长期停止维护,存在未修复的 bug 且不支持现代 ES 模块标准。如果需要通用数字格式化,建议寻找更新的替代方案或自行编写格式化函数。

currency-formatter的README

Currency Formatter

Build Status

A simple Javascript utility that helps you to display currency properly

STOP! You probably don't need this library

TL;DR: This library was created a long time ago. You should use Internationalization API instead.

Please don't add another dependency which you don't need. All modern browsers (and node.js) have this functionality built-in and do a much better job at formatting currencies. e.g. #57

Example:

new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(100000000)
// => "$100,000,000.00"

new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(100000000)
// => "€100,000,000.00"

new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'USD' }).format(100000000)
// => "100.000.000,00 $"

new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(100000000)
// => "100.000.000,00 €"

new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(100000000)
// => "100 000 000,00 €"

With that being said use this library if you need:

  • Your version of node.js doesn't come with the full-icu. See: #72 and #19214
  • Support old browsers.
  • Consistent formatting across all browsers.
  • You don't like the Intl APIs
  • ???

Install

npm install currency-formatter --save

Basic Usage

By specifying the currency code

var currencyFormatter = require('currency-formatter');

currencyFormatter.format(1000000, { code: 'USD' });
// => '$1,000,000.00'

currencyFormatter.format(1000000, { code: 'GBP' });
// => '£1,000,000.00'

currencyFormatter.format(1000000, { code: 'EUR' });
// => '1 000 000,00 €'

Or by specifying the locale

var currencyFormatter = require('currency-formatter');

currencyFormatter.format(1000000, { locale: 'en-US' });
// => '$1,000,000.00'

currencyFormatter.format(1000000, { locale: 'en-GB' });
// => '£1,000,000.00'

currencyFormatter.format(1000000, { locale: 'GB' });
// => '£1,000,000.00'

currencyFormatter.format(1000000, { locale: 'de-DE' });
// => '1.000.000,00 €'

currencyFormatter.format(1000000, { locale: 'nl-NL' });
// => '€1.000.000,00'

You can also get the currency information.

var currencyFormatter = require('currency-formatter');

currencyFormatter.findCurrency('USD');
// returns:
// {
//   code: 'USD',
//   symbol: '$',
//   thousandsSeparator: ',',
//   decimalSeparator: '.',
//   symbolOnLeft: true,
//   spaceBetweenAmountAndSymbol: false,
//   decimalDigits: 2
// }

Parse the number of a monetary value


currencyFormatter.unformat('$10.5', { code: 'USD' })
// => 10.5

currencyFormatter.unformat('$1,000,000', { code: 'USD' })
// => 1000000

currencyFormatter.unformat('10,5 €', { code: 'EUR' })
// => 10.5

currencyFormatter.unformat('1 000 000,00 €', { code: 'EUR' })
// => 1000000

currencyFormatter.unformat('1.000,99', { locale: 'de-DE' })
// => 1000.99

currencyFormatter.unformat('10\'000 CHF', { code: 'CHF' })
// => 10000

currencyFormatter.unformat('10.00 CHF', { code: 'CHF' })
// => 10

currencyFormatter.unformat('10,00 CHF', { code: 'CHF' })
// => 1000

Advanced Usage

Currency Formatter uses accounting library under the hood, and you can use its options to override the default behavior.

var currencyFormatter = require('currency-formatter');
currencyFormatter.format(1000000, {
  symbol: '@',
  decimal: '*',
  thousand: '^',
  precision: 1,
  format: '%v %s' // %s is the symbol and %v is the value
});

// => '1^000^000*0 @'

// Different formatting for positive and negative values
currencyFormatter.format(-10, {
  format: {
    pos: '%s%v' // %s is the symbol and %v is the value
    neg: '(%s%v)',
    zero: '%s%v'
  }
});

// => ($10)

You could also get a list of all the currencies here using one of the following:

var currencies = require('currency-formatter/currencies');
// OR
var currencyFormatter = require('currency-formatter');
var currencies = currencyFormatter.currencies;

Or the currencies in hashmap shape:

var currencies = require('currency-formatter/currencies.json');
// Result:
// {
//  "USD": {
//    "code": "USD",
//    "symbol": "$",
//    "thousandsSeparator": ",",
//    "decimalSeparator": ".",
//    "symbolOnLeft": true,
//    "spaceBetweenAmountAndSymbol": false,
//    "decimalDigits": 2
//  },
//  ...more currencies
// }

License

MIT