nodejs后台babel在线热编译jsx

浏览器加载react/vue组件时,遇到es6转es5,jsx转js...时,一种方法是用webpack离线编译,一种方法是在后台用babel在线热编译(为了效率部署前可以预热)。

我比较喜欢在线热编译,好处是发布时快,不经过build直接源码发布,并可以避免忘记编译步骤导致bug。

为了提供效率,把热编译结果保存为文件缓存起来。先检查是否有编译后缓存文件且没有过期,有就直接读取,否者编译后再读取。

node.js代码。

let fs=require('fs');
let babel = require("@babel/core");
let babel_preset_env=require('@babel/preset-env');
let babel_preset_react=require("@babel/preset-react");

function transform(f,cb){
  let fc=f+'.js';
  function _trans(){
    fs.readFile(f,'utf8',function(err,code){
      if (err){cb(err)}
      else{
        let r=babel.transformSync(code,{presets:[babel_preset_env,babel_preset_react]});
        code=r.code;
        cb(null,code);
        fs.writeFile(fc,code,function(err){
        });
      }
    });
  }
  fs.lstat(fc,function(err,fcStat){
    if (err){
      _trans();
    }
    else{
      fs.lstat(f,function(err,fStat){
        if (err) cb(err);
        else{
          if (fcStat.mtimeMs<fStat.mtimeMs){
            _trans();
          }
          else{
            fs.readFile(f,'utf8',function(err,code){
              cb(err,code);
            });
          }
        }
      });
    }
  });
}
transform('s.jsx');

但在多并发时,问题来了:多个并发任务可能会同时都检查到缓存不存在,然后开始编译.......很浪费,其实只需要一个任务来编译,其它任务等待编译结束后再读取缓存。

其实有点复杂,涉及到文件锁机制,阿里“通义千问”建议用proper-lockfile,我没用。模拟一下:

/**
 * 测试多进程下,判断一个文件是否存在,不存在才生成内容创建文件
 * 难点:
 * (1)避免多个进程在生成文件内容。
 * (2)一个进程如何等待正在生成文件的进程生成完成再读取。
 * (3)等待的效率,降低cpu占用
 */
let path=require('path');
let fs=require('fs');
let fn=path.join(__dirname,'a.txt');

function rw5(task){
  let fnlock=fn+'.lock';
  //递归获取锁
  function lock(cb){
    fs.open(fnlock,'wx',function(err,fhandle){
      if (err){
        console.log(task,' locked,try again...');
        setTimeout(function(){
          lock(cb);
        },1);
      }
      else{
        fs.close(fhandle,function(err){
          if (err) console.log(err);
          console.log(task,' got lock');
          cb();
        });
      }
    });
  }
  function unlock(){
    fs.unlink(fnlock,function(err){if (err) console.log(err)});
  }
  function read(cb){
    fs.readFile(fn,'utf8',function(err,data){
      //读不到正在写入的内容
      if (err) console.log(err);
      else console.log(task,' readed:',data);
      if (cb) cb();
    });
  }
  function write(cb){
    let content='hello';
    //用setTimeout模拟一个长时间的content计算过程,比如babel转码
    setTimeout(function(){
      fs.writeFile(fn,content,function(err,data){
        console.log(task,' writed:',content);
        if (cb) cb();
      });
    }, 1000);
  }
  fs.access(fn,function(err,data){
    if (err){
      console.log(task,' not exists');
      lock(function(){
        fs.access(fn,function(err,data){
          if (err){
            console.log(task,' not exists');
            write(function(){
              unlock();
            });
          }
          else{
            read(function(){
              unlock();
            });
          }
        });
      });
    }
    else{
      read();
    }
  });
}
rw5(1);
rw5(2);

看到的运行记录,可能如下:

D:\work\Source\yujiang.Foil.Node\test\filerw>node multirw.js
1  not exists
2  not exists
1  got lock
2  locked,try again...
1  not exists
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
2  locked,try again...
1  writed: hello
2  locked,try again...
2  got lock
2  readed: hello

封装一下:

yjFile.js

let fs=require('fs');
let path=require('path');
let debug=[require('debug')('Foil:yjFile.js'),function(){}][1];

/**
 * 递归获取文件锁。
 * @param {object} ops 
 * @param {int} ops.fLock 锁定的文件名
 * @param {int} ops.tryLockInterval 尝试锁定文件的时间间隔,单位:毫秒,预设值:1。数字越大,cpu占用越少,但任务时间拖得越长。
 * @param {any} ops.taskID debug时日志显示的函数调用任务ID
 * @param {function} ops.cb 获取到锁后的回调函数
 */
function lock(ops){
  debug(ops.taskID,ops.fLock,'try lock...');
  fs.open(ops.fLock,'wx',function(err,fHandle){
    if (err){
      setTimeout(function(){
        lock(ops);
      },ops.tryLockInterval);
    }
    else{
      fs.close(fHandle,function(err){
        if (err) debug(ops.taskID,err);
        debug(ops.taskID,ops.fLock,'......got lock');
        ops.cb();
      });
    }
  });
}

function unlock(ops){
  fs.unlink(ops.fLock,function(err){if (err) debug(ops.taskID,err)});
  debug(ops.taskID,ops.fLock,'unlock');
}

/**
 * 热转换文件,有缓存文件且没有过期时,直接读取,否则转换后保存读取。
 * 考虑了异步并发的状况,转化动作只会有一个任务执行,其它等待。
 * @param {object} ops 
 * @param {string} ops.fSource 源文件
 * @param {string} ops.fTarget 转码后的文件
 * @param {function} ops.transform 转换函数:function(cb)。cb为转换后回调函数:function(err,data)。
 * @param {int} ops.tryLockInterval 尝试锁定文件的时间间隔,单位:毫秒,预设值:1。数字越大,cpu占用越少,但任务时间拖得越长。
 * @param {any} ops.taskID debug时日志显示的函数调用任务ID
 * @param {function} ops.cb 转换后的结果回调函数:function(err,data)。
 */
function hotTransform(ops){
  if (!ops) ops={};
  if (ops.tryLockInterval==undefined) ops.tryLockInterval=1;
  let fLock=ops.fSource+'.lock';

  function read(cb){
    fs.readFile(ops.fTarget,'utf8',function(err,data){
      if (err){
        debug(ops.taskID,err);
      }
      else{
        debug(ops.taskID,'readed.');
      }
      if (cb) cb();
      ops.cb(err,data);
    });
  }
  function write(data,cb){
    if (typeof data!=='string') data=JSON.stringify(data);
    fs.writeFile(ops.fTarget,data,function(err,data2){
      if (err){
        if (err.code=='ENOENT'){
          //目录不存在
          let dirTarget=path.dirname(ops.fTarget);
          let mode=['try','lock'][0];
          //用锁的模式虽然优雅,但效率要差很多
          if (mode=='try'){
            fs.mkdir(dirTarget,{recursive:true},function(err){
              if (err){
                if (err.code=='EEXIST'){
                  //目录已经存在,忽略错误
                  write(data,cb);
                }
                else if (cb) cb(err);
              }
              else{
                write(data,cb);
              }
            });
          }
          else{
            //在dirTarget从后往前找,找到一级存在的目录来放置锁文件
            let dirParts=dirTarget.split('\\');
            function findExistTargetDir(){
              if (dirParts.length>0){
                dirParts.splice(dirParts.length-1);
                let dir=dirParts.join('\\');
                fs.access(dir,function(err){
                  if (err){
                    findExistTargetDir();
                  }
                  else{
                    //dirLock不能用fSource的目录,可能全部文件都要放在同一个fTarget下,导致dirLock不同锁不起作用。
                    let dirLock=dir+'\\.lock';
                    lock({
                      fLock:dirLock,
                      taskID:ops.taskID,
                      cb(){
                        fs.access(dirTarget,function(err){
                          if (err){
                            fs.mkdir(dirTarget,{recursive:true},function(err){
                              unlock({
                                fLock:dirLock,
                                taskID:ops.taskID
                              });
                              if (err){
                                if (cb) cb(err);
                              }
                              else{
                                write(data,cb);
                              }
                            });
                          }
                          else{
                            unlock({
                              fLock:dirLock,
                              taskID:ops.taskID
                            });
                            write(data,cb);
                          }
                        });
                      }
                    });
                  }
                });
              }
              else{
                cb(new Error('dir any level not exists:'+dirTarget));
              }
            }
            findExistTargetDir();
          }
        }
        else{
          if (cb) cb();
          debug(ops.taskID,err);
        }
      }
      else{
        if (cb) cb();
        debug(ops.taskID,'========= writed.');
      }
    });
  }
  function trans(){
    function handleError(e){
      unlock({fLock,taskID:ops.taskID});
      debug(ops.taskID,e);
      ops.cb(e);
    }
    function _t(){
      ops.transform(function(err,data){
        if (err){
          handleError(err);
        }
        else{
          try{
            write(data,function(err){
              unlock({fLock,taskID:ops.taskID});
              //注意:ops.cb要放在这里,如果放在write外面,可能ops.cb通知任务结束,子进程马上被杀掉,unlock不会被执行到。
              ops.cb(err,data);
            });
          }
          catch(e){
            handleError(e);
          }
        }
      });
    }
    lock({
      fLock,
      cb(){
        //要加锁后,才能读写ops.fTarget文件。要保证能解锁。
        try{
          fs.lstat(ops.fTarget,function(err,targetStat){
            try{
              if (err){
                debug(ops.taskID,'locked,not exists');
                _t();
              }
              else{
                fs.lstat(ops.fSource,function(err,sourceStat){
                  if (err) handleError(err);
                  else{
                    if (targetStat.birthtimeMs<sourceStat.mtimeMs){
                      debug(ops.taskID,'locked,expired');
                      _t();
                    }
                    else read(function(){
                      unlock({fLock,taskID:ops.taskID});
                    });
                  }
                });
              }
            }
            catch(e){
              handleError(e);
            }
          });
        }
        catch(e){
          handleError(e);
        }
      }
    });
  }
  //为了效率:先判断缓存文件是否可用,可用直接用,否则要进入trans转码(转码锁文件效率很低)
  fs.lstat(ops.fTarget,function(err,targetStat){
    if (err){
      debug(ops.taskID,'not exists');
      trans();
    }
    else{
      fs.lstat(ops.fSource,function(err,sourceStat){
        //并发时,这时可能ops.fTarget已经更新了,无所谓,只是进入trans再加锁读取。
        if (err){
          ops.cb(err);
        }
        else{
          if (targetStat.mtimeMs<sourceStat.mtimeMs){
            trans();
          }
          else read();
        }
      });
    }
  });
}
module.exports.lock=lock;
module.exports.unlock=unlock;
module.exports.hotTransform=hotTransform;

测试一下:

testCase_hotTransform.js

let path=require('path');
let fs=require('fs');
let yjFile=require('../../src/yjFile.js');

const CNST_taskCount=5;
let endTaskCount=0;
let fSource=path.join(__dirname,'s.txt');
let ops={
  fSource,
  fTarget:path.join(__dirname,'dist/t.txt'),
  transform(cb){
    fs.readFile(fSource,'utf8',function(err,data){
      data=data+',tranformed.';
      cb(err,data);
    });
  },
  cb(err,data){
    let t2=new Date().getTime();
    console.log('-------',this.taskID,t2-this.startTime+'ms',err,data);
    endTaskCount++;
    if (endTaskCount==CNST_taskCount){
      if (process.send){
        process.send({cmd:'end'});
      }
    }
  }
}

function start(taskID,waitTime){
  //增加延迟,让子进程的任务和主进程的任务尽量能同时并发执行,增加并发碰撞机会。
  //至少主进程内的2个任务是并发的,子进程内的2个任务是并发的。
  setTimeout(function(){
    for(let i=0;i<CNST_taskCount;i++){
      let ops1=Object.assign({taskID:taskID+i},ops);
      ops1.startTime=new Date().getTime();
      //console.log(ops1);
      yjFile.hotTransform(ops1);
    }
  },waitTime);
}

process.on("message",function({cmd,data}){
  switch (cmd){
    case 'start':
      start(data.taskID,0);
      break;
  }
});

module.exports.start=start;
module.exports.taskCount=CNST_taskCount;

run.js

let path=require('path');
let {fork} = require('child_process');
let testProcess = fork(path.join(__dirname,'testCase_hotTransform.js'));
testProcess.send({cmd:'start',data:{taskID:1}},function(err,data){
  let test=require('./testCase_hotTransform.js');
  //增加延迟,让子进程的任务和主进程的任务大概能“同时”进行,才能真正模拟并发。
  //否则,总是主进程发起的任务先执行完。
  test.start(test.taskCount+1,100);
});
testProcess.on("message",function({cmd,data}){
  switch (cmd){
    case 'end':
      testProcess.kill();
      break;
  }
});

主进程开启5个并发,并fork一个子进程,子进程里面也开启5个并发。

先随便建立一个s.txt文件,node run.js执行,可能看到的结果:

D:\work\Source\yujiang.Foil.Node\test\filerw>node run.js
6 not exists
6 try lock...
7 not exists
7 try lock...
8 not exists
8 try lock...
9 not exists
9 try lock...
10 not exists
10 try lock...
6 ......got lock
7 try lock...
8 try lock...
9 try lock...
10 try lock...
6 locked,not exists
1 not exists
7 try lock...
8 try lock...
9 try lock...
1 try lock...
10 try lock...
2 not exists
2 try lock...
7 try lock...
8 try lock...
9 try lock...
3 not exists
3 try lock...
4 not exists
10 try lock...
4 try lock...
7 try lock...
5 not exists
8 try lock...
5 try lock...
9 try lock...
1 try lock...
2 try lock...
10 try lock...
7 try lock...
3 try lock...
8 try lock...
4 try lock...
9 try lock...
6 unlock
5 try lock...
------- 6 56ms undefined source,tranformed.
6 ========= writed: source,tranformed.
1 try lock...
10 try lock...
7 try lock...
2 try lock...
3 try lock...
4 try lock...
5 ......got lock
8 try lock...
1 try lock...
9 try lock...
2 try lock...
10 try lock...
3 try lock...
7 try lock...
4 try lock...
1 try lock...
2 try lock...
3 try lock...
4 try lock...
5 readed: source,tranformed.
5 unlock
------- 5 52ms null source,tranformed.
1 try lock...
8 try lock...
2 try lock...
3 try lock...
9 try lock...
4 try lock...
10 try lock...
2 try lock...
7 try lock...
1 ......got lock
3 try lock...
4 try lock...
1 readed: source,tranformed.
1 unlock
------- 1 72ms null source,tranformed.
2 try lock...
3 try lock...
4 try lock...
8 try lock...
9 try lock...
10 try lock...
7 try lock...
3 try lock...
2 ......got lock
8 try lock...
9 try lock...
4 try lock...
10 try lock...
3 try lock...
7 try lock...
4 try lock...
8 try lock...
3 try lock...
9 try lock...
10 try lock...
2 readed: source,tranformed.
7 try lock...
2 unlock
------- 2 83ms null source,tranformed.
4 try lock...
3 try lock...
4 ......got lock
8 try lock...
9 try lock...
10 try lock...
3 try lock...
7 try lock...
8 try lock...
4 readed: source,tranformed.
4 unlock
------- 4 93ms null source,tranformed.
3 try lock...
3 ......got lock
3 readed: source,tranformed.
3 unlock
------- 3 98ms null source,tranformed.
9 try lock...
10 try lock...
7 try lock...
8 try lock...
9 ......got lock
10 try lock...
7 try lock...
8 try lock...
10 try lock...
7 try lock...
8 try lock...
9 readed: source,tranformed.
9 unlock
------- 9 131ms null source,tranformed.
10 try lock...
7 try lock...
8 try lock...
10 ......got lock
7 try lock...
8 try lock...
7 try lock...
10 readed: source,tranformed.
10 unlock
------- 10 142ms null source,tranformed.
8 try lock...
7 try lock...
8 ......got lock
7 try lock...
8 readed: source,tranformed.
8 unlock
------- 8 148ms null source,tranformed.
7 try lock...
7 ......got lock
7 readed: source,tranformed.
7 unlock
------- 7 157ms null source,tranformed.

从结果看,10个并发任务,各自消耗的时间为:

------- 6 56ms
------- 5 52ms
------- 1 72ms
------- 2 83ms
------- 4 93ms
------- 3 98ms
------- 9 131ms
------- 10 142ms
------- 8 148ms
------- 7 157ms

第一个完成的任务是6号,会面等待的任务,本应该马上获得数据结束,但是花了太多的时间在try lock上。如何再提高效率?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火星牛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值