混合Web应用实践

本项目实例代码: https://github.com/js-cool/up.js.cool

设计

项目诉求

输出:

  • 图表按时间展示在线状况及效率
  • 接口、图片输出当前在线状态

输入:

  • WRescueTime 插件获取在线行为数据

存储设计

数据库采用MySQL,缓存采用Redis

表结构

CREATE TABLE `data` (
  `user` char(16) NOT NULL DEFAULT '' COMMENT '用户',
  `active` int(3) unsigned NOT NULL COMMENT '活跃时间(秒)',
  `efficiency` decimal(5,2) NOT NULL COMMENT '效率(%)',
  `date` int(10) unsigned NOT NULL COMMENT '数据时间(转时间戳)',
  KEY `whereorder` (`user`,`date`),
  KEY `date` (`date`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

缓存结构

up:data:username
up:latest:username

编码

初始化项目

yarn init
yarn add --dev eslint eslint-config-airbnb eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-import

配置ESLint

开发环境配置

config/index.js

const ENV = process.env.NODE_ENV || 'dev';

const users = require(`./users.${ENV}`);
const {redis, mysql, cdn} = require(`./server.${ENV}`);

module.exports = {
  cdn,
  users,
  redis,
  mysql
};

优化

可以用lazyload方式动态加载:

const ENV = process.env.NODE_ENV || 'dev';
module.exports = (config) => (() => require(`./${config}.${ENV}`))();

考虑到本项目已经在实施过程中,变更改动较大,未修改。

Model

crontab/crab.js片段

业务中插入操作尽可能精简,参数最好统一,像这样的方式调用:

data.rows.forEach(async (item) => {
  if (operator) {
    // 插入数据
    await dataAdd(user, item);
  } else if (item[0] === last[0]) {
    operator = true;
    if (item[1] !== last[1]) {
      // 更新最后一条数据
      await dataUpdate(user, item);
    }
  }
});

对应 Model 实现代码

model/data.js 片段:

const { pool, format } = require('@dwing/mysql');
const { mysql: mysqlOptions } = require('../config');
const { isEmpty } = require('../lib');

const DB = mysqlOptions.database;
const TABLENAME = `${DB}.data`;

exports.dataAdd = async (user, [date, active, , , efficiency]) => {
  const mysql = await pool(mysqlOptions);
  const sql = format('INSERT INTO ?? (user,active,efficiency,date) VALUES (?,?,?,?)',
    [TABLENAME, user, active, efficiency, parseInt(new Date(date) / 1000, 10)]);
  const result = await mysql.query(sql);
  mysql.release();
  return isEmpty(result) ? -1 : result.affectedRows;
};

exports.dataUpdate = async (user, [date, active,,, efficiency]) => {
  const mysql = await pool(mysqlOptions);
  const sql = format('UPDATE ?? SET active = ?, efficiency = ? WHERE user = ? AND date = ?',
    [TABLENAME, active, efficiency, user, parseInt(new Date(date) / 1000, 10)]);
  const result = await mysql.query(sql);
  mysql.release();
  return isEmpty(result) ? -1 : result.affectedRows;
};

这里主要用的是结构赋值新特性。

计划任务

采用 Later.js,类似于 Crontab

const later = require('later');
const {users} = require('../config');
const {random} = require('../lib');
const {lastClear, historyClear} = require('../model/data');
const crab = require('./crab');
const updateCertbot = require('./certbot');

users.forEach(async (x) => {
  // 每分钟抓取用户数据
  await crab(x);
  later.setInterval(async () => {
    await crab(x);
  }, later.parse.recur().every(random(50, 70)).second());
});

// 每天 0:00 清除计时器
later.setInterval(lastClear, later.parse.cron('0 0 */1 * * ?'));

// 每天 1:00 清除30天前历史数据
later.setInterval(historyClear, later.parse.cron('0 1 */1 * * ?'));

// 每周一 2:00 更新 certbot 证书
later.setInterval(updateCertbot, later.parse.cron('0 2 * * 1 ?'));

待填的坑

数据采集

从上文计划任务中即可看出,每个用户都会随机产生一条任务,由于用户是写在配置文件中的固定的,所以一旦想要改为动态的(比如开放注册),这套体系就不能支持了。

所以需要一个更好的手段进行数据采集。

欢迎提 ISSUE 发表自己的看法和建议。

服务器渲染

项目里写了一个简单的 HTML 模板引擎,可以替换一些简单参数:

const path = require('path');
const {readFileSync} = require('fs');
const {cdn} = require('../../config');

module.exports = (view, params = {}) => {
  let html = readFileSync(path.join(__dirname, `${view}.html`), 'utf8').replace(/{{cdn}}/g, cdn);
  Object.keys(params).forEach((key) => {
    html = html.replace(new RegExp(`{{${key}}}`, 'g'), params[key]);
  });
  return html;
};

其中用到了 readFileSync,该操作可能会在 I/O 密集发生阻塞。并且每个请求均会产生 IO 操作,可以从很多方面进行进一步优化。

部分优化建议:

  1. 可以进行内存缓存(仅适用该项目,因为只有一个页面,根据实际项目情况考虑)
  2. 可以通过反向代理直接访问静态 HTML 文件,参数通过异步请求带入

路由配置

koa-router 还是 koa-route? 这是个好问题。

该项目中使用的是koa-route,原因是当时并不知道有好多种路由中间件,这个是从官方仓库中发现的。

比较了一下源码,个人感觉 koa-router 更优美,使用起来也更方便。感兴趣的同学可以尝试一下: https://github.com/alexmingoia/koa-router

测试

练手项目,测试阶段暂时忽略。有时间了再来补上。

部署

pm2 start up.config.js

注意 PM2 版本使用大于 2.4,Node 版本大于 7.6.0。

P.S.

SSL 证书由 CertBot 生成。