聊天室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. 配置konwledges.json并重置knowldege:
   - bucky k -reset
3. 请按照文档添加包调用的测试代码(或参考test/目录下为示例package生成的测试代码):
4. 使用下面的命令运行测试代码或者本地调试:
   - 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 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是入口函数,主要做了

  1. 初始化bucky
  2. 关注新消息事件
  3. 拉取历史消息并展示
  4. 发送按钮点击实现

这里可以看到调用了两个服务器端的导出函数putMessage和getMessageList。就像调用本地函数一样。框架自动完成RPC。(RPC的目标服务器是buckycloud.com,所以要求浏览器支持跨域CORS访问)接收服务器的推送也很简单,attach事件即可。

设置AppID

编辑src/client/js/app.js,修改${appid}为当前选择的应用的appid,或者可以通过再次执行构建命令自动设置:

bucky build

测试

使用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会保存错误堆栈

如何部署

  1. 申请域名,并备案。
  2. 购买云主机,并将域名dns指向云主机外网ip
  3. 将html目录上传至服务器,/opt/www
  4. 切换到/opt/www,执行
[root@VM_116_61_centos www]# python -m SimpleHTTPServer 80

如果需要https,还需要购买ssl证书


为何bucky不提供静态页面托管服务

因为在国内,对于提供互联网服务的人或组织,需要实名和监管。

results matching ""

    No results matching ""