一、实际业务场景考虑插件作用
在日常开发中,我经常面临着如下的问题:
如上图所示:
一个销售单总共有2个产品,一个单价为43.3111
,而另一个单价为52.1233
。假设这个销售单分为2个发货单发货。按照正常逻辑来说,这个销售单总价应是43.3111+52.1233=93.4344
,但是在F12控制台你会发现并非如此。
全部发货、部分发货、未发货
。但是你在控制台上也可以得到:
所以就算2个发货单全部发完,金额并不相等,所以也得不出全部发货这个状态。(以上仅为举例,数值可能有差,实际工作中极为常见)。你有可能会说JS真坑。但是在JAVA\PHP中同样有这样的问题。在JS高级教程中有这样一句话:
关于浮点数值计算会产生舍入误差的问题,有一点需要明确:这是使用基于IEEE754数值的浮点计算的通病,ECMAScript并非独此一家;其他使用相同数值格式的语言也存在这个问题。
那么如何解决呢?
现实中有很多种解决方法,最简单的方法无非是将进行计算的数值转化为整数再进行计算,因为整数形不会有舍入误差的问题。
function calcDecimalLength(decimal){//计算数值的方法
var splitArray=decimal.toString().split(".");
if(splitArray.length===1){
return 0;
}
return splitArray[1].length;
}
function calcMax(a,b){//计算相乘的倍数
let aLength=calcDecimalLength(a);
let bLength=calcDecimalLength(b);
let max=Math.pow(10,Math.max(aLength,bLength));
return max;
}
function addCalc(a,b){//加法计算
if(typeof a!=="number" || typeof b!=="number"){
return a+b;
}
let max=calcMax(a,b);
return (a*max+b*max)/max;
}
function minusCalc(a,b){//减法计算
if(typeof a!=="number" || typeof b!=="number"){
return a-b;
}
let max=calcMax(a,b);
return (a*max-b*max)/max;
}
function multCalc(a,b){//乘法计算
if(typeof a!=="number" || typeof b!=="number"){
return a*b;
}
let max=calcMax(a,b);
return ((a*max)*(b*max))/max;
}
function diviCalc(a,b){//除法计算
if(typeof a!=="number" || typeof b!=="number"){
return a/b;
}
let max=calcMax(a,b);
return ((a*max)/(b*max))/max;
}
只需要在精度计算的时候进行引入相应的函数。
//导出上面的函数
module.exports={
addCalc,
minusCalc,
multCalc,
diviCalc
}
//需要使用的文件引入
var {
addCalc,
minusCalc,
multCalc,
diviCalc
}=require("导入计算函数文件");
//精确计算
addCalc(43.3111+52.1233);//93.4344
但是你会不会觉得很麻烦呢,需要在每个文件引入这样一个函数。不说繁琐,而且也更容易出错。
二、babel相关概念概述
1.Babel运行阶段
首先来了解Babel转码的过程分三个阶段:分析(parse)、转换(transform)、生成(generate)。
其中,分析、生成阶段由Babel核心完成,而转换阶段,则由Babel插件完成,这也是本文的重点
分析(@babel/core核心处理,无需考虑)
Babel读入源代码,经过词法分析、语法分析后,生成抽象语法树(AST)。
parse(sourceCode) => AST
转换(自己写)
经过前一阶段的代码分析,Babel得到了AST。在原始AST的基础上,Babel通过插件,对其进行修改,比如新增、删除、修改后,得到新的AST。
transform(AST, BabelPlugins) => newAST
生成(@babel/core核心处理,无需考虑)
通过前一阶段的转换,Babel得到了新的AST,然后就可以逆向操作,生成新的代码。
generate(newAST) => newSourceCode
2.插件基础格式
//其中types和template为@babel/core中的相对应的解析包(@babel/template|@babel/types),也可以单独引入进行使用
export default function({ types: babelTypes,template:babelTemplate }) {
return {
visitor: {
Identifier(path, state) {},
ASTNodeTypeHere(path, state) {}
}
};
};
- babelType:类似lodash那样的工具集,主要用来操作AST节点,比如创建、校验、转变等。举例:判断某个节点是不是标识符(identifier)。
- path:AST中有很多节点,每个节点可能有不同的属性,并且节点之间可能存在关联。path是个对象,它代表了两个节点之间的关联。你可以在path上访问到节点的属性,也可以通过path来访问到关联的节点(比如父节点、兄弟节点等)
- state:代表了插件的状态,你可以通过state来访问插件的配置项。
- visitor:Babel采取递归的方式访问AST的每个节点,之所以叫做visitor,只是因为有个类似的设计模式叫做访问者模式,不用在意背后的细节。
- Identifier、ASTNodeTypeHere:AST的每个节点,都有对应的节点类型,比如标识符(Identifier)、函数声明(FunctionDeclaration)等,可以在visitor上声明同名的属性,当Babel遍历到相应类型的节点,属性对应的方法就会被调用,传入的参数就是path、state。
三、本例解析
打开这个AST解析网址。
{
"type": "Program",
"start": 0,
"end": 15,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 15,
"expression": {//加号表达式
"type": "BinaryExpression",
"start": 0,
"end": 15,
"left": {//加号左边
"type": "Literal",
"start": 0,
"end": 7,
"value": 43.3111,
"raw": "43.3111"
},
"operator": "+",
"right": {//加号右边
"type": "Literal",
"start": 8,
"end": 15,
"value": 52.1233,
"raw": "52.1233"
}
}
}
],
"sourceType": "module"
}
这次本例我们解析的是二进制表达式(BinaryExpression)。对于BinaryExpression,BabelType的定义如下:
defineType("BinaryExpression", {
builder: ["operator", "left", "right"],
fields: {
operator: {
validate: assertOneOf(...BINARY_OPERATORS),
},
left: {
validate: assertNodeType("Expression"),
},
right: {
validate: assertNodeType("Expression"),
},
},
visitor: ["left", "right"],
aliases: ["Binary", "Expression"],
});
BinaryExpression 主要是由 operator、left、right
组成的。对于本例子来说,operator 是对应的+号,left是对应的加号左边,right是对应的加号右边。
四、插件实战
1.运算函数编写
即上面的运算方法
function addCalc(a,b){
if(typeof a!=="number" || typeof b!=="number"){
return a+b;
}
let max=calcMax(a,b);
return (a*max+b*max)/max;
}
function minusCalc(a,b){
if(typeof a!=="number" || typeof b!=="number"){
return a-b;
}
let max=calcMax(a,b);
return (a*max-b*max)/max;
}
function multCalc(a,b){
if(typeof a!=="number" || typeof b!=="number"){
return a*b;
}
let max=calcMax(a,b);
return ((a*max)*(b*max))/max;
}
function diviCalc(a,b){
if(typeof a!=="number" || typeof b!=="number"){
return a/b;
}
let max=calcMax(a,b);
return ((a*max)/(b*max))/max;
}
function calcDecimalLength(decimal){
var splitArray=decimal.toString().split(".");
if(splitArray.length===1){
return 0;
}
return splitArray[1].length;
}
function calcMax(a,b){
let aLength=calcDecimalLength(a);
let bLength=calcDecimalLength(b);
let max=Math.pow(10,Math.max(aLength,bLength));
return max;
}
module.exports={
addCalc,
minusCalc,
multCalc,
diviCalc
}
2.插件核心部分讲解
babel的插件引入方式有两种:
- 通过.babelrc文件引入插件
- 通过babel-loader的options属性引入plugins
babel-plugin接受一个函数,函数接收一个babel参数,参数包含bable常用构造方法等属性,函数的返回结果必须是以下这样的对象:
{
visitor: {
//...
}
}
visitor是一个AST的一个遍历查找器,babel会尝试以深度优先遍历AST语法树,visitor里面的属性的key为需要操作的AST节点名如BinaryExpression等,value值可为一个函数或者对象,完整示例如下:
{
visitor: {
BinaryExpression: {
enter(path){
//doSomething
}
exit(path){
//doSomething
}
}
}
}
函数参数path包含了当前节点对象,以及常用节点遍历方法等属性。 babel遍历AST语法树是以深度优先,当遍历器遍历至某一个子叶节点(分支的最终端)的时候会进行回溯到祖先节点继续进行遍历操作,因此每个节点会被遍历到2次。当visitor的属性的值为函数的时候,该函数会在第一次进入该节点的时候执行,当值为对象的时候分别接收两个enter,exit属性(可选),分别在进入与回溯阶段执行。
在代码中需要被替换的代码块为a + b这样的类型,因此我们得知该类型的节点为BinaryExpression
,而我们需要把这个类型的节点替换成accAdd(a, b)
。
利用babel.template
可以方便的构建出你想要的任何节点。这个函数接收一个代码字符串参数,代码字符串中采用大写字符作为代码占位符,该函数返回一个替换函数,接收一个对象作为参数用于替换代码占位符。
var preOperationAST=template("FUN_NAME(ARGS)");//将0.1+0.2转化为addCalc的模板
在BinaryExpression
回溯阶段查找并替换
BinaryExpression:{
exit:function(path){
if(path.node.operator==="+"){
path.replaceWith(
preOperationAST({
FUN_NAME:t.identifier("addCalc"),
ARGS:[path.node.left,path.node.right]
}))
}
if(path.node.operator==="-"){
path.replaceWith(
preOperationAST({
FUN_NAME:t.identifier("minusCalc"),
ARGS:[path.node.left,path.node.right]
}))
}
if(path.node.operator==="*"){
path.replaceWith(
preOperationAST({
FUN_NAME:t.identifier("multCalc"),
ARGS:[path.node.left,path.node.right]
}))
}
if(path.node.operator==="/"){
path.replaceWith(
preOperationAST({
FUN_NAME:t.identifier("diviCalc"),
ARGS:[path.node.left,path.node.right]
}))
}
}
}
AST遍历完毕最后退出的节点肯定是Program
的exit
方法,因此可以在这个方法里面对计算方法进行引用。
//path.unshiftContainer的作用就是在当前语法树插入节点,即这段代码的意思大概就是
Program:{
exit:function(path){
path.unshiftContainer("body",requireAST({
PROPERTIES:t.ObjectPattern([
t.objectProperty(t.identifier("addCalc"),t.identifier("addCalc"), false, true),
t.objectProperty(t.identifier("minusCalc"),t.identifier("minusCalc"), false, true),
t.objectProperty(t.identifier("multCalc"),t.identifier("multCalc"), false, true),
t.objectProperty(t.identifier("diviCalc"),t.identifier("diviCalc"), false, true),
]),
SOURCE: t.stringLiteral("calc/calc.js")
}))
}
},
最后在搭建的简易webpack环境demo中进行测试demo地址,亲测有效。(如果不会搭建webpack开发环境可以看我的另一篇文章webpack环境搭建)
五、代码精简
虽然说上面的代码可行,但是你有没有觉得有很多重复性的代码呢:
if(path.node.operator==="+"){
...
}
if(path.node.operator==="-"){
...
}
if(path.node.operator==="*"){
...
}
if(path.node.operator==="/"){
...
}
上面的代码中,四册运算就要写四个if 那么如果有很多个运算公式呢?而且由于不知道到底运用了何种运算符,每个页面都会把所有函数全部都导入过来,所以我们可以在BinaryExpression
中将运算符保存起来,然后在Program
中根据保存的值将需要导入的函数导入进来,可以造成不必要的资源浪费。
BinaryExpression:{
exit:function(path){
var replaceOperator = pushCache(path.node.operator);
replaceOperator!=="none" && path.replaceWith(
preOperationAST({
FUN_NAME:t.identifier(replaceOperator),
ARGS:[path.node.left,path.node.right]
})
)
}
}
var needRequireCache = [];//需要引入的数组
function pushCache(operation){//将操作符传化为对应的函数并且将函数push进数组里面
var operationFun;
switch(operation){
case '+':
operationFun = 'addCalc';
break;
case '-':
operationFun = 'minusCalc';
break;
case '*':
operationFun = 'multCalc';
break;
case '/':
operationFun = 'diviCalc';
break;
default:
operationFun = 'none';
}
if(needRequireCache.indexOf(operationFun)>=0) return operationFun;
operationFun !== 'none' && needRequireCache.push(operationFun);
return operationFun;
}
function preObjectExpressAST(keys){//传入的keys为require的数组
var properties=keys.map(function(){
return t.objectProperty(t.identifier(key),t.identifier(key),false,true)
});
return t.ObjectPattern(properties);
}
Program:{//注意在引入需要的函数后需要将数组置空
exit:function(path){
path.unshiftContainer("body",requireAST({
PROPERTIES:preObjectExpressAST(needRequireCache),
SOURCE: t.stringLiteral("calc/calc.js")
}));
needRequireCache = [];
}
},
可见,代码数量减少了数行。
注意:Program中的路径需要是node-modules里面的包,在正式项目中需要替换。
六、发布到线上去
最高逼格当然是发布到线上去将包,具体可以看我掘金的另一篇文章npm上线发布。
新建文件夹并经过一系列操作,发布到线上已经。
亲测,正常安装在项目中可用!哈哈哈!大功告成!那么本篇文章就告一段落了!有不足的地方请大神多指教!