# 混合Web应用实践 本项目实例代码: ## 设计 ### 项目诉求 输出: * 图表按时间展示在线状况及效率 * 接口、图片输出当前在线状态 输入: * WRescueTime 插件获取在线行为数据 ### 存储设计 数据库采用`MySQL`,缓存采用`Redis`。 #### 表结构 ```sql 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`: ```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`方式动态加载: ```js const ENV = process.env.NODE_ENV || 'dev'; module.exports = (config) => (() => require(`./${config}.${ENV}`))(); ``` 考虑到本项目已经在实施过程中,变更改动较大,未修改。 ### Model #### `crontab/crab.js`片段 业务中插入操作尽可能精简,参数最好统一,像这样的方式调用: ```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` 片段: ```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`。 ```js 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 模板引擎,可以替换一些简单参数: ```js 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` 更优美,使用起来也更方便。感兴趣的同学可以尝试一下: ## 测试 练手项目,测试阶段暂时忽略。有时间了再来补上。 ## 部署 ```bash pm2 start up.config.js ``` 注意 PM2 版本使用大于 2.4,Node 版本大于 7.6.0。 ### P.S. SSL 证书由 `CertBot` 生成。