寫了個插件,一口氣解決項目中所有精度丟失問題!
前言
大家好,我是林三心,用最通俗易懂的話講最難的知識點是我的座右銘,基礎是進階的前提是我的初心。
JS 繞不開的精度丟失問題
在 javascript 中,當我們進行運算時
0.1 + 0.2
你覺得輸出是 0.3 嗎?顯然不是的,由于 javascript 存在精度丟失問題,導致了輸出的不是你預期的
圖片
image.png
至于為什么會精度丟失呢?我之前出過一篇文章專門講了這個原因你知道 0.1+0.2 !==0.3是進制問題,但你講不出個所以然,是吧???,感興趣的朋友可以看看,由于這不是本文的重點,所以我在這就不過多講解~
解決精度丟失的方案?
我會選擇使用 decimal.js 這個庫,文檔在 文檔,他的基本使用如下:
// 先安裝
npm install decimal.js
// 后使用
const Decimal = require('decimal.js');
new Decimal(0.1).add(0.2) // 加法 輸出 0.3
new Decimal(0.1).sub(0.2) // 減法
new Decimal(0.1).mul(0.2) // 乘法
new Decimal(0.1).div(0.2) // 除法
使用 decimal.js進行運算,能解決精度丟失的問題~
不想手動!想自動!
很煩??!
當我們擁有了decimal.js之后,每當我們進行運算的時候,就必須引入它進行使用,每一個頁面都得重復這一操作,于是萌生了一個想法——我想自動!不想手動!
思路
那要怎么才能自動呢?由于前段時間群里很多人說想學習寫 babel 插件,所以剛好,針對這個需求,我可以實現一個 babel 插件,它的功能是:將項目中 0.1 + 0.2 這種表達式,轉換為 new Decimal(0.1).add(0.2)
0.1 + 0.2
// 轉換為
new Decimal(0.1).add(0.2)
這樣就能一次性把項目中的精度丟失問題解決了~
開發 babel 插件
前置準備
涉及到三個問題:
- webpack 和 rollup 如何選擇
- rollup 打包環境的搭建
- 如何發布到 npm 上
這三個問題具體我在上一篇文章【如何使用Rollup開發一個npm包并發布】里有提到過了,在本文我就不過多講解
搭建一個 Rollup 打包環境
新建一個 babel-plugin-sx-accuracy文件夾,用來開發 babel 插件
名字可以自己取,但是為了規范,最好是 babel-plugin- 開頭
接著進入 babel-plugin-sx-accuracy 文件夾,輸入
npm init
npm i rollup @rollup/plugin-babel -D
npm i decimal.js -S
package.json 中的內容為:
"name": "babel-plugin-sx-accuracy",
"version": "1.0.20",
"description": "",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "rollup -c"
},
"files": [
"dist/*",
"src/*"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-babel": "^6.0.3",
"rollup": "^3.26.2"
},
"dependencies": {
"decimal.js": "^10.4.3"
}
}
然后在根目錄下新建 rollup.config.js 文件,用來配置 rollup 打包
// rollup.config.js
import babel from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: {
file: 'dist/index.js',
format: 'cjs',
},
plugins: [
babel({
babelHelpers: 'bundled',
}),
],
};
最后新建 src/index.js,我們的插件邏輯就寫在這里
圖片
什么是抽象語法樹(AST)?
我們可以借助一個網站,來一睹抽象語法樹的真容~ https://astexplorer.net/
圖片
這里我們可以記住幾個點
- 每一個代碼片段都有屬于自己的節點類型
- 代碼最外層的節點類型為 Program
- 像 0.1+0.2 這種表達式,節點類型為 BinaryExpression
- BinaryExpression節點里會有幾個重要的東西
operaor:運算符號
left:左邊的數字
right:右邊的數字
其實抽象語法樹的節點類型有很多種,我列舉一些:
- 標識符(Identifier):表示變量、函數名等標識符的節點
- 字面量(Literal):表示字面量值,如字符串、數字、布爾值等
- 表達式語句(ExpressionStatement):表示包含表達式的語句節點
- 賦值表達式(AssignmentExpression):表示賦值操作的表達式節點,如 x = 5
- 二元表達式(BinaryExpression):表示包含二元操作符的表達式節點,如 x + y
- 一元表達式(UnaryExpression):表示包含一元操作符的表達式節點,如 -x
- 函數聲明(FunctionDeclaration):表示函數聲明的節點,包括函數名、參數和函數體
- 變量聲明(VariableDeclaration):表示變量聲明的節點,包含變量名和可選的初始值
- 條件語句(IfStatement):表示 If 條件語句的節點,包括條件表達式、if 分支和可選的 else 分支
- 循環語句(WhileStatement、ForStatement):表示循環語句的節點,分別代表 While 循環和 For 循環
- 對象字面量(ObjectLiteral):表示對象字面量的節點,包含對象屬性和屬性值
- 數組字面量(ArrayLiteral):表示數組字面量的節點,包含數組元素
- 函數調用(CallExpression):表示函數調用的節點,包含調用的函數名和參數列表
- 返回語句(ReturnStatement):表示返回語句的節點,包含返回的表達式
當然大家現階段不需要去記,大家只需要記得這兩個類型就行了:
- 代碼最外層的節點類型為 Program
- 像 0.1+0.2 這種表達式,節點類型為 BinaryExpression
其實,我們平時在 webpack 開發時會接觸到一系列的插件,他們的功能比如有
- 去除 console.log
- 壓縮代碼
- 去除注釋
其實他們的原理整體上都是一致的,分為三步:
- 第一步:將代碼轉換成抽象語法樹
- 第二步:使用 babel 為我們提供的方法,對語法樹進行增刪改查
- 第三步:將處理后的語法樹重新轉換成代碼
而我們將要開發的插件,也是用到這個過程,但是第一步和第三步我們不需要管,我們只需要完成第二步中的增刪改查操作即可~
注意點:在第二步中,babel 會對抽象語法樹進行深度遍歷,遍歷到目標節點后,又會重新回到上層節點去重新遍歷下一個目標節點,所以一個節點會被遍歷兩次,一來一回 進去是 enter 回去是 exit
圖片
插件基本代碼結構
下文使用 AST 來表達抽象語法樹
export default function ({ template: template, types: t }) {
return {
visitor: {
Program: {
exit: function (path) {
}
},
BinaryExpression: {
exit: function (path) {
}
}
}
}
}
開發一個 babel 插件,文件必須默認返回一個函數,接收一個對象參數,里面有個屬性我們需要用到
- template: 是@babel/template的一個方法,他能使用模板的方式生成AST節點
函數內部的東西,我們也介紹下
- vistor: 你可以理解為修改AST節點的入口
- Program、BinaryExpression: 你需要修改的AST節點類型
- exit: 就是剛剛說的 一來一回 中的,回
- path: 就是被遍歷到的AST節點對象
插件完全實現
// 定義構造函數的名稱常量
const DECIMAL_FUN_NAME = 'Decimal'
// 運算符號映射 decimal.js 的四個方法
const OPERATIONS_MAP = {
'+': 'add',
'-': 'sub',
'*': 'mul',
'/': 'div'
}
// 運算符號數組
const OPERATIONS = Object.keys(OPERATIONS_MAP)
export default function ({ template: template }) {
// require decimal.js 的節點模板
const requireDecimalTemp = template(`const ${DECIMAL_FUN_NAME}=require('decimal.js')`);
// 將運算表達式轉換為decimal函數的節點模板
const operationTemp = template(`new ${DECIMAL_FUN_NAME}(LEFT).OPERATION(RIGHT).toNumber()`);
return {
visitor: {
Program: {
exit: function (path) {
// 調用方法,往子節點body
// 中插入 const Decimal = require('decimal.js')
// 表達式
path.unshiftContainer("body",
requireDecimalTemp())
}
},
BinaryExpression: {
exit: function (path) {
const operator = path.node.operator;
if (OPERATIONS.includes(operator)) {
// 調用方法替換節點
path.replaceWith(
// 傳入 operator left right
operationTemp({
LEFT: path.node.left,
RIGHT: path.node.right,
OPERATION: OPERATIONS_MAP[operator]
})
)
}
}
}
}
}
}
打包 & 發布 NPM
當開發完成后,我們先 npm run build進行打包
然后運行 npm publish 發布到 NPM 上
圖片
項目使用
首先安裝 babel-plugin-sx-accuracy
npm i babel-plugin-sx-accuracy
只需要在項目中的 .babelrc 或者 babel.config.js 中加入 babel-plugin-sx-accuracy即可
{
"presets": ["@babel/preset-env"],
"plugins": ["babel-plugin-sx-accuracy"]
}
我們來試試,一開始代碼是
console.log(0.1 + 0.2)
console.log(0.3 - 0.1)
console.log(0.2 * 0.1)
console.log(0.3 / 0.1)
打包后我們看看產物,并且輸出的也都是沒有精度丟失的結果?。。?/p>
圖片