前端工具链实战:构建可复用的金额大写转换NPM包(支持Vue 3/React 18)
前端工程化实战构建高可用金额大写转换NPM包Vue 3/React 18全适配在金融、财务类前端应用中金额大写转换是高频需求场景。传统解决方案往往停留在单个页面内嵌脚本或复制粘贴工具函数阶段缺乏工程化封装思维。本文将带你从零构建一个支持Tree Shaking、多框架适配、完整类型声明的现代化NPM包覆盖以下核心环节标准化项目架构设计符合开源包最佳实践的目录结构与配置核心算法优化处理边界情况的健壮金额转换逻辑多框架适配层Vue 3 Composition API与React 18 Hooks的差异化封装工程化配套设施Vite构建、Jest测试、ESLint校验的完整流水线1. 项目初始化与架构设计1.1 创建标准化NPM包mkdir rmb-uppercase cd rmb-uppercase npm init -y基础目录结构应遵循现代前端包规范├── src │ ├── core # 核心转换逻辑 │ ├── adapters # 框架适配层 │ └── index.ts # 主入口 ├── tests # 测试用例 ├── examples # 示例项目 │ ├── vue-demo │ └── react-demo ├── types # 类型声明 └── vite.config.ts # 构建配置关键配置文件示例// package.json { name: rmb-uppercase, version: 1.0.0, main: ./dist/index.umd.js, module: ./dist/index.es.js, types: ./types/index.d.ts, exports: { .: { import: ./dist/index.es.js, require: ./dist/index.umd.js }, ./vue: ./dist/adapters/vue.js, ./react: ./dist/adapters/react.js } }1.2 核心算法实现在src/core/converter.ts中实现健壮的转换逻辑const UNITS [, 拾, 佰, 仟, 万, 亿] const DIGITS [零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖] interface ConversionOptions { strict?: boolean // 是否严格校验输入 prefix?: string // 金额前缀如人民币 suffix?: string // 金额后缀如整 } export function toRMBUpper( value: number | string, options: ConversionOptions {} ): string { // 参数校验与标准化处理 if (typeof value string) { if (!/^-?\d(\.\d{0,2})?$/.test(value)) { if (options.strict) throw new Error(Invalid money format) return 无效金额 } value parseFloat(value) } // 整数部分处理 const integerPart Math.abs(Math.floor(value)) let result convertInteger(integerPart) // 小数部分处理 const decimalPart Math.round((Math.abs(value) - integerPart) * 100) if (decimalPart 0) { result DIGITS[Math.floor(decimalPart / 10)] 角 if (decimalPart % 10 ! 0) { result DIGITS[decimalPart % 10] 分 } } else { result 整 } // 符号与装饰处理 if (value 0) result 负${result} if (options.prefix) result options.prefix result return result }注意算法需特别处理零的多种出现场景如1001应转换为壹仟零壹元整而非壹仟零零壹元整2. 多框架适配层实现2.1 Vue 3 Composition API封装在src/adapters/vue.ts中import { toRMBUpper } from ../core/converter import type { App, Plugin } from vue declare module vue/runtime-core { interface ComponentCustomProperties { $rmb: typeof toRMBUpper } } export const VueRMBPlugin: Plugin { install(app: App) { app.config.globalProperties.$rmb toRMBUpper } } // 组合式函数版本 export function useRMB() { return { toUpper: toRMBUpper, format: (value: number, prefix ¥) ${prefix}${value.toFixed(2)} (${toRMBUpper(value)}) } }2.2 React 18 Hooks封装在src/adapters/react.ts中import { toRMBUpper } from ../core/converter import { useMemo } from react export function useRMBFormatter() { const formatter useMemo(() { return { toUpper: toRMBUpper, format: (value: number, options?: { prefix?: string }) { const prefix options?.prefix || ¥ return ${prefix}${value.toFixed(2)} (${toRMBUpper(value)}) } } }, []) return formatter } // 创建React上下文 import { createContext, useContext } from react const RMBContext createContextReturnTypetypeof useRMBFormatter(null!) export function RMBProvider({ children }: { children: React.ReactNode }) { const formatter useRMBFormatter() return RMBContext.Provider value{formatter}{children}/RMBContext.Provider } export function useRMB() { return useContext(RMBContext) }3. 工程化配套设施3.1 Vite打包配置// vite.config.ts import { defineConfig } from vite import dts from vite-plugin-dts export default defineConfig({ build: { lib: { entry: { index: src/index.ts, vue: src/adapters/vue.ts, react: src/adapters/react.ts }, formats: [es, umd], name: RMBUpperCase }, rollupOptions: { external: [vue, react], output: { globals: { vue: Vue, react: React } } } }, plugins: [dts()] })3.2 单元测试方案使用Jest配合Testing Library编写测试用例// tests/core/converter.test.ts import { toRMBUpper } from ../../src/core/converter describe(RMB Converter, () { test(basic conversion, () { expect(toRMBUpper(1234.56)).toBe(壹仟贰佰叁拾肆元伍角陆分) expect(toRMBUpper(-100.02)).toBe(负壹佰元零贰分) }) test(edge cases, () { expect(toRMBUpper(0)).toBe(零元整) expect(toRMBUpper(100000000)).toBe(壹亿元整) expect(toRMBUpper(abc, { strict: false })).toBe(无效金额) }) }) // tests/adapters/react.test.tsx import { renderHook } from testing-library/react-hooks import { useRMBFormatter } from ../../src/adapters/react test(React hook, () { const { result } renderHook(() useRMBFormatter()) expect(result.current.toUpper(123)).toBe(壹佰贰拾叁元整) })4. 文档与示例工程4.1 完整使用示例Vue 3示例script setup import { useRMB } from rmb-uppercase/vue const { toUpper, format } useRMB() const amount ref(8888.88) /script template div p原始值: {{ amount }}/p p大写形式: {{ toUpper(amount) }}/p p格式化: {{ format(amount) }}/p /div /templateReact 18示例import { useRMB } from rmb-uppercase/react function Invoice() { const { toUpper } useRMB() const [amount, setAmount] useState(2023.03) return ( div input typenumber value{amount} onChange{e setAmount(parseFloat(e.target.value))} / p结算金额: {toUpper(amount)}/p /div ) }4.2 性能优化建议记忆化处理对频繁转换的相同数值进行缓存const memo new Mapnumber, string() export function toRMBUpper(value: number): string { if (memo.has(value)) return memo.get(value)! const result //...转换逻辑 memo.set(value, result) return result }Web Worker支持// src/core/worker.ts self.addEventListener(message, (e) { const result toRMBUpper(e.data) self.postMessage(result) })Tree Shaking优化// 按需导入 import { toRMBUpper } from rmb-uppercase/core