前端數值展現的思考與實踐

前端如何友好的展現數值?本文基於實踐總結了一些原則,介紹封裝的工具庫 Number Display ,並分析源碼的實現。前端

Number Display 有適用於 Web 的 JavaScript 版和適用於 Flutter 的 Dart 版。git

JavaScript 版github

Dart 版web

在前端開發中,數值展現是一個常見的需求。不一樣於統計或實驗報表對精確性和規範性的注重,前端展現數值時更注重用戶友好,讓人一眼能感知數字,而且保持頁面簡潔、整齊。正則表達式

不管是移動端的 App ,仍是管理後臺型的頁面,或者流行的數據大屏,如下一些需求是展現數值時經常遇到的,具備必定的共性:數組

  • 受佈局空間的限制,數值字符串不能超過某個長度
  • 不能出現 null 、 NaN 、 undefined 之類的狀況,異常時但願有 「--」 之類的佔位符
  • 數字過大時但願加上千位分隔符 (1,234,222) ,或用數值單位表示 (1.23k)
  • 不要出現科學計數法 (1.23e+4)
  • 小數末尾不要有一串 0

若是能有一個工具函數,只需簡單的配置,即可將輸入的數值轉換成符合要求的字符串就行了,特別是當長度有限時,能自動經過變換單位,壓縮小數位的方式確保字符串不溢出長度限制,好比:bash

-254623933.876  =>  -254.62M
-1.2345e+5  =>  -123,450
12.000  => 12 
NaN  =>  --
複製代碼

Number Display 就是爲這樣的需求而建立的:wordpress

rstStr = display(-254623933.876)    // result: -254.62M
複製代碼

使用

本着配置和使用分離的原則,Number Display 暴露名爲 createDisplay 的高階函數,經過傳入參數給 createDisplay 進行配置,定製實際組件中析數值的 display 函數:函數

Web 中使用:工具

import createDisplay from 'number-display';

const display = createDisplay({
  length: 8,
  decimal: 0,
});

<div>{display(data)}</div>
複製代碼

Flutter 中使用:

import 'package:number_display/number_display.dart';

final display = createDisplay(
  length: 8,
  decimal: 0,
);

print(display(data));
複製代碼

這樣使得實際組件中實用 display 的代碼更爲簡潔,且方便批量配置。值得注意的是,在 JavaScript 中配置項是以對象的方式傳入, Dart 中是以命名參數的方式傳入。

Number Display 的配置項,目前包括如下 5 個:

  • length :數值字符串的長度限制。爲確保任何數值均可以被壓縮,length 最好大於等於5,這樣最極端的狀況(-123000)也能被壓縮到 length 限制之內。
  • decimal :最大小數位數。當空間不夠時,實際小數位數會比這個值小,另外小數末尾的 0 會去掉.
  • placeholder :當值爲非法數字時輸出的佔位符。
  • allowText :若是輸入不是數字而是一段文本,是否原樣輸出。Dart 版無此參數。
  • comma :是否顯示逗號千位分隔符。千位分隔符的意義見 這篇博客

限長壓縮原則

數值在展現時,會受到組件尺寸的約束。

理論上組件設計尺寸時須考慮到值的範圍,但實際不可能溝通的這麼完美,保證數值不溢出組件的任務每每仍是落到開發。

數值的展現方式能夠變通,但絕對不能溢出組件以外,無疑是首要原則。這也正是 Number Display 最主要的功能。當長度不夠時,Number Display 按照如下優先級對數值進行壓縮:

  1. 去掉千位分隔符;
  2. 按剩餘空間減小小數位數;
  3. 採用數值單位 ( k, M, G, T, P ) 壓縮整數部分;
  4. 按以上步驟,任何數字均可以壓縮到5個字符之內,但若是設置的 length 小於5而且實際數值過長,將拋出異常;

代碼實現

Number Display 處理數值的原理最主要是利用正則表達式 ,過程分爲如下幾步:

1. 判斷合法性

對於 JavaScript ,因爲其類型的靈 (sui) 活 (yi) 性,實際開發中傳入的「數值」 type 每每既有多是 number ,也有多是 string :

if (
  (type !== 'number' && type !== 'string')
  || (type === 'number' && !Number.isFinite(value))
) {
  return placeholder;
}
複製代碼

而對於靜態類型的 Dart,限定輸入參數類型爲 num 則是比較常規的作法:

if (value == null || !value.isFinite) {
  return placeholder;
}
複製代碼

2. 截取符號、整數、小數部分

截取一個數值的符號、整數、小數三部分,是依靠字符 "-" 和 "." 進行拆分匹配。借鑑 Ant Design 的 Statistic 組件 源碼 ,咱們利用 JavaScript 中 String.match 函數一次匹配多個模式並返回對應數組的特性,只用一行代碼即可同時獲取三個部分:

const cells = value.match(/^(-?)(\d*)(\.(\d+))?$/);
複製代碼

固然 Dart 就沒有這種技巧了,三部分要分別匹配獲取:

final negative = RegExp(r'^-?').stringMatch(valueStr) ?? '';
final integer = RegExp(r'\d+').stringMatch(valueStr) ?? '';
final deciRaw = RegExp(r'(?<=\.)\d+$').stringMatch(valueStr) ?? '';
複製代碼

值得一提的是,Dart 語言的正則部分正在逐步完善中,我注意到最近的幾個版本更新(2.1-2.4)每次都有正則語法的改動。

3. 壓縮小數部分

壓縮首先會去掉千位分隔符,當空間還不夠時,就會按剩餘空間縮減少數部分,注意因爲小數點必須與小數部分共存亡,剩餘空間須要以 0 和 2 兩個邊界分段,同時在此過程當中順便用正則去除小數末尾的 0:

(如下代碼 JavaScript 和 Dart 基本相似,僅以 JavaScript 爲例)

let space = length - negative.length - int.length;
if (space >= 2) {
  deciShow = deci.slice(0, space - 1).replace(/0+$/, '');
  return `${negative}${int}${deciShow && '.'}${deciShow}`;
}
if (space >= 0) {
  return `${negative}${int}`;
}
複製代碼

4. 壓縮整數部分

若是徹底去掉了小數空間仍然不夠,就要將整數部分用 k 、 M 等數值單位進行壓縮了。以前咱們曾給整數部分加上過千位分隔符:

const localeInt = int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
複製代碼

這裏恰好能夠經過分隔段數求得數值單位:

const sections = localeInt.split(',');
const units = ['k', 'M', 'G', 'T', 'P'];
const unit = units[sections.length - 2];
複製代碼

最後和壓縮小數相似,咱們要再根據剩餘的空間肯定轉換數值單位後的小數位數:

space = length - negative.length - mainSection.length - 1;
if (space >= 2) {
  const tailShow = tailSection.slice(0, space - 1).replace(/0+$/, '');
  return `${negative}${mainSection}${tailShow && '.'}${tailShow}${unit}`;
}
  if (space >= 0) {
  return `${negative}${mainSection}${unit}`;
}
複製代碼

討論

數值展現是經常被人忽視的一個點,自己也確實沒有什麼高深的技術,但若是不思考周全,會帶來不少麻煩。

以上總結的需求原則儘可能作到共性,實際項目仍是會有特殊的需求,所以要儘可能作到可配置。Number Display 經歷過一次大版本的迭代,對可配置的參數進行了精簡,同時出於性能考慮用正則匹配替代了一些循環和判斷,但願有需求的讀者對功能上提出建議:

Issues (JavaScript)

Issues (Dart)

相關文章
相關標籤/搜索