Giter VIP home page Giter VIP logo

lzxhahaha.github.io-backup's Introduction

运行

首次运行

在文件中新建config.json,里面加上

{
  "salt": "盐"
}

不要将config.json上传到git仓库

然后执行

$ npm install
$ npm start

访问localhost:3096即可

Windows下脚本

babel编译脚本

可能需要在管理员权限的PowerShell下执行set-executionpolicy remotesigned

run.ps1

babel -d dist/ src/
cp ./src/public ./dist -r -force
cp ./src/views ./dist -r -force

*nix下脚本

首次建立记得执行chmod +x

更新脚本

update

$ forever stop ./dist/bin/www
$ git pull
$ npm install
$ ./run
$ forever start ./dist/bin/www

babel编译脚本

run

babel -d dist/ src/
cp -rf ./src/public ./dist
cp -rf ./src/views ./dist

lzxhahaha.github.io-backup's People

Contributors

lzxhahaha avatar

Watchers

 avatar  avatar

lzxhahaha.github.io-backup's Issues

Electron的IPC管理

Electon中的IPC

在Electron中,App的生命周期和页面渲染分别由主进程和渲染进程控制,想要通过页面操作主进程的一些函数并获取数据,就需要通过IPC进行通信。

Electron提供了ipcMainipcRenderer两个模块,分别用于主进程和渲染进程的事件监听与发送。在主进程使用ipcMain进行事件监听后,就可以在渲染进程中触发事件;同样,渲染进程中也可以监听主进程发来的事件。

下面是官方文档中的例子:

// 主进程中
const {ipcMain} = require('electron');
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg);  // prints "ping"
  event.sender.send('asynchronous-reply', 'pong');  // 异步返回
});

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg);  // prints "ping"
  event.returnValue = 'pong';  // 同步返回
});
// 渲染进程中
const {ipcRenderer} = require('electron');
// 获取同步返回值
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')); // prints "pong"

// 获取异步返回值
ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg); // prints "pong"
});
ipcRenderer.send('asynchronous-message', 'ping');

可以看出,Electron提供了同步和异步两种方式来获取数据,那么想象中的使用方式,就是对直接可以获取数据的函数使用,需要等待或者多步操作的就使用异步的形式来获取。然而事实上这么做却可能导致一些问题。

同步获取的问题

首先来看看同步消息的通信过程。

// 主进程
ipcMain.on('synchronous-message', (event, arg) => {
  // 各种操作...
  
  event.returnValue = 'pong';
});
// 渲染进程
const value = ipcRenderer.sendSync('synchronous-message', 'ping');

要获取同步消息的返回值,就必须在主进程中给event.returnValue赋值,这时渲染进程中的ipcRenderer.sendSync函数才会产生返回值,并结束函数。

但在returnValue被赋值之前,渲染进程就会被阻塞,整个UI都会卡在那。这就是同步获取数据最大的问题,一旦出现了任何意外导致returnValue赋值失败,就会将界面卡住,用户将完全无法操作。即使将sendSync放入Promis中,也无法阻止UI被阻塞的悲惨命运。所以说,Electron的同步消息函数是一个非常糟糕的API。

使用异步通信的问题

当然,完全可以全靠异步的形式进行通信。但同样也会出现一些问题。

首先,使用异步的通信时,需要在主进程和渲染进程中都要定义事件并监听,也就是说,一个事件需要有两个事件名才能完成一次操作(当然用同一个事件名也行,只是个人感觉这样没那么直观)。

异步通信的例子:

// 主进程
ipcMain.on('event', (e, data) => e.sender.send('event-response', data));
// 渲染进程
const foo = (data) => new Promise((resolve) => {
  ipcRenderer.send('event', data);
  ipcRenderer.on('event-response', (e, data) => resolve(data));
}

定义事件名只是个小问题,完全可以通过代码规范的约定来解决。实际上这样的方式可能还会出现一个问题,就是一个事件可能会同时有多个监听者,这是就无法确认每一次触发的事件应该只交给哪个监听器进行处理,并且每次有响应时,渲染进程中的每个监听器都会被触发。

定义协议

为了解决请求和响应匹配的问题,就需要定义一个简单的通信协议。

首先可以将通信分为两种方式,一种是单次请求并获取相应的,一种是需要持续坚挺的。

单次响应

对于单次响应的通信,可以确定一个请求对应一个响应。当响应到达的时候,也只触发对应的那一个监听器,这就需要在通信中增加一个唯一标识符。

在请求时,先在渲染进程计算出一个id,并以{id, data}的形式,将事件event发送给主进程,主进程收到后记录下id,然后在处理完成后,触发一个${event}_res_${id}的事件,客户端单次监听这个事件并处理。

// 主进程
const listen = (eventName, handler) => {
  ipcMain.on(eventName, async (e, request) => {
    const { id, data } = request;
    const response = { code: 200 };
    try {
      response.data = await handler(data);
    } catch (err) {
      response.code = err.code || 500;
      response.data = { message: err.message || 'Main process error.' };
    }
    e.sender.send(`${eventName}_res_${id}`, response);
  });
};
// 渲染进程
const sendEvent = (eventName, options = {}) => {
  const { data } = options;

  const id = uuid.v1();
  const responseEvent = `${eventName}_res_${id}`;

  return new Promise((resolve, reject) => {
    // 这里使用 once 监听,响应到达后就销毁
    ipcRenderer.once(responseEvent, (event, response) => {
      if (response.code === 200) {
        resolve(response.data);
      } else {
        reject(response.data);
      }
    });
    // 监听建立之后再发送事件,稳一点
    ipcRenderer.send(eventName, { id, data });
  });
};

整个流程看起来就是这样的

-------Render-------------------------Main-------
         |                              |
listen() |                              |
         |-- emit(event, {id, data}) -->|
         |                              | handle event
         |<---- emit(resEvent, data) ---|
         |                              |

这样,一次IPC通信在外面看上去就像一个异步网络请求一样了。这里还可以约定一些数据格式或者事件名格式之类的,都是小事,这里不讨论。

如果再进行其他一次封装和判断,就可以在不同环境使用不同的函数,来轻松的在Electron和Web环境下进行切换。

持续响应

需要持续监听的事件有点类似WebSocket,流程大概就是,渲染进程通知主进程开始任务,并持续返回带有特定id的事件,渲染进程进行监听。在停止时,清理两个线程中的监听器。

看上去就像这样:

--------Render----------------------------Main--------
          |                                 |
          |-- connect(event, {id, data}) -->|
listeners |                                 | register
          |------------- start ------------>|
          |                                 | start handler
          |<----- emit(idEvent, data) ------|
          |<-----          ...         -----|
          |<----- emit(idEvent, data) ------|
          |                                 |
          |------------- stop ------------->|
          |                                 | stop handler
          |---------- disconnect ---------->|
   clean  |                                 | clean
          |                                 |

首先渲染进程发送一个connect事件告知主进程事件和id,并对预计的事件进行监听;主进程在记录id,并监听${event}_${id}_start${event}_${id}_stop${event}_${id}_disconnect事件,给渲染进程一些操作空间。当收到开始事件时,就执行处理函数,并持续触发响应事件;收到结束事件就停止操作;断开连接时就清理监听器。

以下为主要部分的代码:

// 主进程
export default class LongEventServer {
  static listen(eventName, initHandler) {
    return new Promise((resolve) => {
      ipcMain.on(eventName, (e, args) => {
        const {id, data} = args;
        const server = new LongEventServer({
          eventName, id, data,
          sender: e.sender
        });
        initHandler(server);
        ipcMain.on(server._getEventName('start'), () => server.onStart(server.startData));
        ipcMain.on(server._getEventName('stop'), () => server.onStop());
        ipcMain.on(server._getEventName('disconnect'), () => {
          ['start', 'stop', 'disconnect'].forEach(el => ipcMain.removeAllListeners(server._getEventName(el)));
          server.onDisconnect();
        });
        resolve(server);
      })
    });
  }
  
  _getEventName(event) {
    return `${this.eventName}_${this.id}_${event}`;
  }
  
  // constructor/emit etc.
}
// 渲染进程
class LongEventClient {
  constructor(id, eventName, data) {
    this.id = id;
    this.eventName = eventName;
    this.startData = data;
  }

  _getEventName(event) {
    return `${this.eventName}_${this.id}_${event}`;
  }
  
  // other function like on(event) etc.
}

const sendLongEvent = (eventName, options = {}) => {
  const { data } = options;
  const id = uuid.v1();
  const client =  new LongEventClient(id, eventName, data);
  client.connect();
  return client;
};

// client.on('xxx') ...
// client.start()

这样就能愉快的进行异步通信了。

实现

主线程中的实现
渲染进程中的实现

分布式服务定时任务执行策略

背景

服务器信息

多台互通的服务器,服务器之间连接到相同的Mongodb;Mongodb的版本极低,完全没有事物等不存在的东西。

每台服务器上都部署了一个相同的eggjs应用,代码完全一致。

没有Redis。

应用信息

egg有一个master进程多个worker进程。master进程作为守护进程,不承担任何业务逻辑,只负责worker之间的通信,以及在worker挂掉的时候fork一个新的worker;worker进程用来处理业务以及定时任务,当到达定时任务的执行时间的时候,master会随机通知一个worker去执行。

需求背景

需要在每天一定时间执行一个任务,获取远端数据,更新到自己服务的数据库中。该任务一天只能执行一次,并且由于芒果们都不知道事物是什么,所以需要在失败的时候提供重入策略。

对于应用来说,任务是写在代码中的,也就是说,当到达执行时间的时候,每台服务器上的应用都会尝试去执行这个定时任务。

任务列表

  1. 确保只有一台机器执行定时任务
  2. 确保任务只执行一次
  3. 确保中途出现异常(代码错误,服务器挂掉等)情况下有办法继续上次任务
  4. 超时检查

解决思路

总的来说,一个定时任务的流程就是

  1. 争夺全局锁
  2. 建立文件锁
  3. 执行任务,记录每一步操作及数据
  4. 执行完毕
  5. 释放文件锁
  6. 释放全局锁

单台机器执行

可以通过建立全局锁来控制多台服务器只执行一次。由于没有Redis,并且考虑到对执行结果做记录,所有就直接使用MongoDB来做全局锁。

// SyncLock 表定义
{
    // 加锁日期 YYYY-MM-DD
    date: {
        type: String,
        index: true,
        unique: true
    },
    // 1: 执行中;
    // 2: 执行失败;
    // 10: 执行完成;
    status: {
        type: Number,
        default: 1
    },
    // 重试次数
    retryTimes: {
        type: Number,
        default: 0
    },
    // 执行任务机器的hostname
    hostname: String
}

MongoDB自身带有document级别的写锁,因此在date字段上建立了唯一索引之后,当几台机器同时执行定时任务的时候,就可以保证只有一台机器能写入数据。

单次执行

文件锁

为了保证任务不会重复执行,必须考虑到任务可能出现在执行过程中中止的情况。这里的中止说的是进程停止的意外情况,例如进程意外被kill、服务器突然自杀、蓝翔高级干员挖断了电线等外部因素,而不是由代码抛出的异常。

egg在到达定时任务执行的时间时,会随机通知一个worker去执行任务(也可以配置全部worker执行,这是题外话),但worker的稳定性相对来说不是很可靠,所以还需要一个方法,来确认执行任务的worker进程是否存活。这就可以引入文件锁来判断。

npm官方提供了一个叫lockfile的包来提供文件锁。这个包的设计**是,通过创建一个文件来作为锁,当文件存在的时候就说明锁存在;当主动释放锁,或是加锁的进程挂掉时,这个文件就会被删除。但这个文件本身是不带锁的,也就是说可以被其他进程编辑,官方的说法是用来防止不可预料的情况导致死锁,这种情况理论上不存在,所以也可以不考虑。

超时检查

由于egg没有提供worker启动的监听函数或事件,所以就需要在执行当前任务之后的一段时间内,再执行一个检查操作,时间根据当前任务的预估执行耗时定。检查任务执行时,会先去检查全局锁的状态。如果锁未被释放,就判定为执行超时,然后由执行任务的机器做下一步检查。

首先,就需要判断文件锁是否还存在,如果还存在,那么说明任务有可能还在执行,就让他接着思考人生,过段时间再来看看(同时可以分析一下执行任务的日志,判断是在哪一步了,为什么超时);如果文件锁已经被释放,那么就说明任务可能失败,无法正确的释放全局锁,需要根据log进行重新执行。

上面提到lockfile这个包有可能在进程结束时因为未知原因未能及时清理锁,应对这种情况,可以在任务执行过程中,通过一定的策略定期(不一定是定时)向锁文件中写入时间,来表明自己真的还活着

任务重入

在任务意外中止时有两种解决办法,一个是自己实现回滚,还原到执行前的状态,另一种就是从上一次中断的地方继续执行。相比之下还是第二种方法实现起来简单一点,代价也相对较小。

实现重入,可以将任务设计成一个具有多个入口的状态机,也就是将一个任务分为多个具有原子性的步骤。在任务开始前就生成一个描述文件,,每次执行成功后在描述文件中记录当前状态;在特定状态下可以记录一些参数和网络请求返回的数据,保证重新执行时数据一致。

最坑的一点是,MongoDB只具有document级别的原子性,也就是说insertMany这个函数是个坑爹玩意,需要增加一定的校验。

在记录日志时,可以考虑适当的划分任务的粒度,适当的重置描述文件,防止体积过大,增加重新执行的成本。

任务自动重新执行只是为了减少人力运维成本,应对外部的异常情况。如果出现代码错误或是数据错误等开发时未考虑到的情况,还是需要有一定的方式进行通知,然后去debug

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.