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