聊天室Demo
实现一个最简单的聊天室,需求如下:
- 全局只有一个房间
- 支持查看历史
- 支持实时推送
用传统方式实现,一般是nginx/apache+php/nodejs+mysql/mongo+redis+websocket,对于一个经验丰富的后端开发来说,这很容易。但对于一个新手来说,这些技术点每个都要熟悉几天,满满的挫败感。就算可以跑起来,还有后面复杂的运维。
用bucky来实现这些就容易很多。bucky提供的基础设施,让开发者不用考虑nginx/apache+php/nodejs+redis+websocket,只要熟悉一点简单的mongo即可。整个后端,不用搭任何服务,不到40行代码搞定。前端实现也更简单同样不需要40行,还包括操作UI。
这里用到了bucky的两个功能
- GlobalEvent 实现跨设备推送,简单易用,不用关心网络拓扑结构
- Mongo资源 数据持久化存储
准备工作
- 登陆buckycloud.com后台创建一个新应用,起名叫minichat
创建项目
创建一个空目录:
mkdir minichat
在此目录执行bucky init -i,选择新建解决方案(solution):
◎ init bucky...
◎ install npm packages...
解决方案类型:
────────────────────
1. 示例
2. 新建解决方案
$请选择解决方案类型[1/2]:2
接着输入要创建的项目(project)相对子路径,此处我们创建一个服务端project:
◎ 添加新项目...
请输入项目相对子路径(i.e: src/test):src/server
接着选择项目类型,此处有三种选择,我们选择默认的第一种bucky项目(后台),后面两种后面再介绍,此处先忽略。
◎ 选择项目类型...
请选择项目类型:
────────────────────
1. bucky项目(后台)
2. HTML5(前端)
3. 微信小程序(前端)
$请输入序号[1/2/3]:1
接着在项目目录下创建新的XARPackage,输入包名,选择新建package:
◎ 添加新package到项目src....
$请输入package名字:minichat
选择package类型:
────────────────────
1. 新建package
2. 示例package
$请输入序号:1
因为要用到mongo资源,下面在配置中要显式指定
请问这个包需要限制在什么运行时(runtime)加载么?
────────────────────
1. 只允许前端(不能使用mysql/redis/mongo驱动)
2. 只允许后端(可以使用mysql/redis/mongo驱动)
3. 没有限制(不能使用mysql/redis/mongo驱动,一般是工具包)
$请输入序号:2
$请问这个包需要使用mysql资源么? [y/n]: n
$请问这个包需要使用redis资源么? [y/n]: n
$请问这个包需要使用mongo资源么? [y/n]: y
注意:如果有更多修改,请阅读文档并修改包目录下的config.json
位置:src/minichat/config.json
提示是否继续创建同项目下的package。目前,我们的服务端只需要创建一个package就够:
$继续添加package? [y/n]: n
提示是否继续创建其他项目。我们需要继续创建前端H5项目:
$继续添加项目? [y/n]: y
◎ 添加新项目...
请输入项目相对子路径(i.e: src/test):src/client
◎ 选择项目类型...
请选择项目类型:
────────────────────
1. bucky项目(后台)
2. HTML5(前端)
3. 微信小程序(前端)
$请输入序号[1/2/3]:2
[+]add: /test/minichat/src/client
[+]add: /test/minichat/src/client/.gitkeep
[+]add: /test/publish/minichat/src/client/css
[+]add: /test/minichat/src/client/img
[+]add: /test/minichat/src/client/index.html
[+]add: /test/minichat/src/client/js
[+]add: /test/minichat/src/client/css/main.css
[+]add: /test/minichat/src/client/img/bkg.jpg
[+]add: /test/minichat/src/client/js/app.js
[+]add: /test/minichat/src/client/js/main.js
[+]add: /test/minichat/src/client/js/h5_core.js
[+]add: /test/minichat/src/client/js/h5_ld_core.js
然后结束项目创建:
$继续添加项目? [y/n]: n
解决方案初始化成功,请尝试后续操作:
1. 使用下面的命令编译:
- bucky build
2. 使用下面的命令部署到巴克云
- bucky deploy
3. 配置konwledges.json并重置knowldege:
- bucky k -reset
4. 请按照文档添加包调用的测试代码(或参考test/目录下为示例package生成的测试代码):
5. 使用下面的命令运行测试代码或者本地调试:
- bucky run -main ${path_to_test_file}
- bucky debug -main ${path_to_test_file}
done.
─────────────
打开编辑器,可以看到到目前为止的目录结构:
├── dist
│ ├── bucky
│ ├── h5
│ └── wx
├── knowledges.json
├── solution.json
├── src
│ ├── client
│ │ ├── css
│ │ │ └── main.css
│ │ ├── img
│ │ │ └── bkg.jpg
│ │ ├── index.html
│ │ └── js
│ │ ├── app.js
│ │ ├── h5_core.js
│ │ ├── h5_ld_core.js
│ │ └── main.js
│ └── server
│ └── minichat
│ ├── config.json
│ ├── minichat.js
│ └── onload.js
└── test
└── minichat
服务端逻辑
功能实现
首先,修改knowledges.json,增加mongo instance配置 ,只有配置了instance在代码中才可以使用。
"global.mongo.instances": {
"type": 0,
"object": {
"chatroom": {}
}
}
其次,编辑src/server/minichat/minichat.js,添加如下的代码:
async function _fireMessage(username, message, time) {
const category = 'chatroom';
const em = getCurrentRuntime().getGlobalEventManager();
if (!em.queryExist(category)) {
await em.create(category);
}
await em.activeEvent(category, 'message', { username, message, time });
}
async function _getMongoDB() {
const runtime = getCurrentRuntime();
const mongoDriver = runtime.getDriver("bx.mongo.client");
let [err, mongo] = await mongoDriver.getMongoInstance("chatroom", runtime, true);
if (err !== 0) {
[err, mongo] = await mongoDriver.getMongoInstance("chatroom", runtime, false);
}
const [_, db] = await mongo.connect();
return db;
}
async function getMessageList() {
const db = await _getMongoDB();
const messages = await db.collection('message').find({}).toArray();
db.close();
return [messages];
}
async function putMessage(username, message) {
const time = Date.now();
const db = await _getMongoDB();
const ret = await db.collection('message').insertOne({username, message, time});
db.close();
await _fireMessage(username, message, time);
return [ret.result];
}
module.exports = {
putMessage,
getMessageList
};
简单介绍下实现,putMessage和getMessageList是客户端调用的接口。功能是发一条消息和获取聊天历史记录。
putMessage第一步获取mongo instance,将新消息插入到数据库中,然后fire一次有新消息的事件。getMessageList比较简单,就是把所有的历史记录查询返回。_getMongoDB获取mongo db对象,用于操作mongo。注意getMongoInstance的第一个参数要和knowledges.json中的配置一致_fireMessage将新消息推送到客户端
最后,执行构建(按需提示输入用户名和密码,注意选择app的时候选择前面在官网后台创建的minichat)和重置knowledge动作:
bucky build
bucky deploy
bucky k -reset
则服务端代码已经自动部署完毕,下面我们可以为该模块编写简单的单元测试。
单元测试
在test/minichat目录下新建一个test.js,添加测试逻辑:
// test.js
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
//
// global variables, received messges
//
let g_receivedMessages=[];
let g_showMessages = false;
/**
* do question
* @param {[type]} title [description]
* @return {[type]} [description]
*/
async function _question(title) {
return new Promise((resolve) => {
rl.question(title, (answer) => {
resolve(answer)
});
});
}
/**
* attach global event
* @return {[type]} [description]
*/
async function _attachEvent() {
const eventCategory = 'chatroom';
const eventManager = getCurrentRuntime().getGlobalEventManager();
// if global 'chatroom' event not exist, create a new one
if (!eventManager.queryExist(eventCategory)) {
await eventManager.create(eventCategory);
}
// attach to listen the 'chatroom' global event
eventManager.attach(eventCategory, 'message', ({ username, message, time }) =>{
let msg = `${new Date(time)} ${username}说:${message}`;
g_receivedMessages.push(msg);
});
}
/**
* put message and read message loop
* @param {[type]} minichat [description]
* @param {[type]} username [description]
* @return {[type]} [description]
*/
async function _processMessageLoop(minichat, username){
if(g_showMessages){
// read message
return new Promise((resolve, reject) => {
setTimeout(()=>{
for(let msg of g_receivedMessages){
console.log(msg);
}
// if receive messages not empty,
// switch to put message again
g_showMessages = g_receivedMessages.length===0;
g_receivedMessages = [];
resolve();
},1);
});
}else{
// put message
const message = await _question(`${username}:`);
await minichat.putMessage(username, message);
// switch to read message
g_showMessages = true;
if (message === 'bye'){
process.exit(0);
}
}
}
/**
* 1. input username
* 2. show message history
* 3. run process message loop
* @return {[type]} [description]
*/
async function _questionMessagesLoop(){
const [_, minichat] = await getCurrentRuntime().loadModule('minichat:minichat');
console.log({ minichat });
// input username
const [messages] = await minichat.getMessageList();
const username = await _question('请输入昵称:');
// show history
console.log('----------------');
console.log('message history:');
console.log('----------------');
messages.forEach(({ username, message, time }) =>{
console.log(`${new Date(time)} ${username}说:${message}`)}
);
// quesion loop
while(true){
await _processMessageLoop(minichat,username);
}
}
/**
* main funciton
* @return {[type]} [description]
*/
async function main() {
console.log('enter main...');
await _attachEvent();
await _questionMessagesLoop();
}
// start
main();
执行下面命令,看看是否正常工作,可以反复输入聊天内容。
bucky run -main test/minichat/test.js
前端代码
布局
打开src/client/index.html,在<body>标签内添加布局代码:
<div id="container">
<textarea id="chat-area" class="form-control"></textarea>
<div class="input-send">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">@</span>
<input type="text" class="form-control" id="nickname" placeholder="昵称" aria-describedby="basic-addon1">
</div>
<div class="input-group">
<input id="sendtxt" type="text" class="form-control" placeholder="聊天内容">
<span class="input-group-btn">
<button id="submit" class="btn btn-default" type="button">Send</button>
</span>
</div>
</div>
<!-- /input-group -->
</div>
打开src/client/css/main.css,添加如下的CSS代码:
.input-send {
display: flex;
flex-direction: column;
}
.input-send .input-group {
margin-top: 8px;
}
#chat-area {
flex: 1;
}
交互
打开src/client/js/main.js,添加页面交互代码:
function append(line) {
const textarea = $('#chat-area');
textarea.append(line + '\n');
if(textarea.length)
textarea.scrollTop(textarea[0].scrollHeight - textarea.height());
}
async function init() {
let app = new bucky.Application();
bucky.setCurrentApp(app);
const [err, metaInfo] = await app.init(appMetaInfo);
bucky.initCurrentRuntime(app);
}
async function _attachEvent() {
const eventCategory = 'chatroom';
const eventManager = bucky.getCurrentRuntime().getGlobalEventManager();
if (!eventManager.queryExist(eventCategory)) {
await eventManager.create(eventCategory);
}
eventManager.attach(eventCategory, 'message', ({username, message, time}) =>
append(`${moment(time).format('YYYY/MM/DD HH:mm:SS')} ${username} 说: ${message}`));
}
async function main() {
await init();
await _attachEvent();
const [_, minichat] = await bucky.getCurrentRuntime().loadModule('minichat:minichat');
const [messages] = await minichat.getMessageList();
messages.forEach(({ username, message, time }) =>
append(`${moment(time).format('YYYY/MM/DD HH:mm:SS')} ${username} 说: ${message}`));
$('#submit').click(async () => {
minichat.putMessage($('#nickname').val(), $('#sendtxt').val());
$('#sendtxt').val('');
});
}
main();
下面简单介绍下实现
main是入口函数,主要做了
- 初始化bucky
- 关注新消息事件
- 拉取历史消息并展示
- 发送按钮点击实现
这里可以看到调用了两个服务器端的导出函数putMessage和getMessageList。就像调用本地函数一样。框架自动完成RPC。(RPC的目标服务器是buckycloud.com,所以要求浏览器支持跨域CORS访问)接收服务器的推送也很简单,attach事件即可。
设置AppID
编辑src/client/js/app.js,修改${appid}为当前选择的应用的appid,或者可以通过再次执行构建命令自动设置:
bucky build
bucky deploy
测试
使用chrome打开src/client/index.html,即可看到聊天界面,输入昵称和信息,点击发送开始测试。
上面介绍的所有代码,都在sdk的demos/minichat里
关于async/await
代码中用了很多的async/await,这个是ES7标准引入的。作用是 使得异步操作变得更加方便
async/await 在node8、主流浏览器中都已经支持(不包括IE)。具体版本支持情况,可参考 async-functions
async/await 使用介绍可参考 MDN文档
bucky中使用async/await的注意事项
async function exportFunction() {
return [result];
}
module.exports = { exportFunction };
定义声明async类型的导出函数
const [result, err, errStack] = await targetModule.exportFunction();
使用async类型的导出函数。
- result:返回值
- err: RPC错误码
- errStack: 如果目标函数执行发生脚本错误,errStack会保存错误堆栈
如何部署
- 申请域名,并备案。
- 购买云主机,并将域名dns指向云主机外网ip
- 将html目录上传至服务器,/opt/www
- 切换到/opt/www,执行
[root@VM_116_61_centos www]# python -m SimpleHTTPServer 80
如果需要https,还需要购买ssl证书
为何bucky不提供静态页面托管服务
因为在国内,对于提供互联网服务的人或组织,需要实名和监管。