《起跑吧,Opa》 -- 中译本 第六章 在线聊天应用

第六章 在线聊天应用


最简单的实时web应用的实例莫过于在线聊天应用了,类似于Facebook chat。既然实时性是我们即将制作的微型博客系统的一个特性,在此我们先描述一下其能够达到的效果。

我们的目标是开发如图6-1的应用,一个独立的聊天室。用户使用通用的web浏览器即可连接并进入房间并展开实时聊天。 为了简洁性我们省掉了注册环节改为给登陆用户一个随机聊天昵称。在第8章我们会讨论如何添加用户管理模块。

在web领域,实时指的是当数据发生变化时网页可以及时自动更新。在Web 2.0应用中常指实时显示用户的交互结果,意即,当某个用户做了一些操作后在另一个用户的页面上可以立即反映出来。

图6-1 聊天效果图

开始一个新项目
利用Opa的脚手架功能创建我们的项目结构如下:
Tokyo:~ henri$ opa create chat
创建结构如下(译者注:Opa版本不同会略有出入):
+- chat
| +- Makefile
| +- Makefile.common
| +- opa.conf
| +- resources
| | +- css
| | | +- style.css
| +- src
| | +- model.opa
| | +- view.opa
| | +- controller.opa
该项目结构包括:
• 一个Makefile文件(可以后续修改)
• 一个通用的Makefile.common文件(通常来说该文件不用修改)
• 一个配置文件opa.conf (其中罗列出了项目需要的所有代码文件及互相依赖关系,第七章我们会详解该文件)
• 一个CSS样式文件的例子style.css
• 资源文件,以及由经典的MVC模式组成的三个子文件夹:model, view, 和 controller, 嗯哼,标准的三层结构。
可用下记命令编译和运行:
Tokyo:~ henri$ cd chat; make run

视图:构建用户界面
让我们从用户界面开始(构建我们的聊天应用),如下:
(译者注:此处代码有问题,参数不够以及返回值不对,详参后续译者整理代码)
module View {
// View code goes here
function page_template(content) {
<div class="navbar navbar-inverse navbar-fixed-top">
<div class=navbar-inner>
<div class=container>
<a class=brand href="#">
Opa Chat
</a>
</div>
</div>
</div>
<div id=#main>
{content}
</div>
}
function default_page() {
content =
<div class="hero-unit">
Page content goes here...
</div>
page_template("Default page", content)
}
}
视图模块有两个函数:page_template函数,包含一个通用的页面模板;default_page函数, 调用page_template函数以构建页面。
对于聊天应用来说,你需要修改视图模块中的page_template 和 default_page 函数以获得所想要的用户界面。模板也提供了默认的样式文件(resources/css/style.css),你也许也想要修改它。

模型:应用逻辑
用户界面已经画好,下面是该添加应用逻辑了。应用逻辑在模型模块(src/model.opa)中添加,在那里,你将定义数据,修改数据以及存储数据。

一个聊天应用无非就是在用户之间传递信息,不过,具体传递何种"信息"却是有学问的。

本例中,一条信息包含两个字段:信息发出人员名称(字符串类型)和信息内容(也是字符串类型):
type message = {string author, string text}
现在你知道要传递何种格式的信息了,下一步需要考虑该如何将信息传递给不同的客户端(浏览器)。Opa提供三种方法用于客户端与服务器之间的通信:
• Session (单向异步通信)
• Cell (双向同步通信)
• Network (向多个观察者广播信息)

对聊天应用来说,相当数量的客户端会连接到聊天室,因此每当(某个人)发出消息后,其它人都应该同时收到,因此,选择Network:
private Network.network(message) room = Network.cloud("room")
上记代码创建了一个名为room的云网络(确保它在所有运行的实例之间被共享)。

同其它所有的Opa对象一样,network也有一个类型,类型为Network.network(message),意思是这是一个用来传递消息的网络对象。

该函数被定义为private,意味着该函数在模型层外部是不可访问的,只能通过模型层内部其它函数来调用它。这种(划分访问权限)的理念称之为封装或信息隐藏,对于编写良好设计架构的代码来说非常重要,我们会在后续"包"章节详细介绍。

你需要两个函数:一个用来将消息广播到所有客户端;另一个用来注册回调功能(回调功能意指当消息到达时会被自动触发的处理单元):
function broadcast(message) {
Network.broadcast(message, room);
}
function register_message_callback(callback) {
Network.add_callback(callback, room);
}
上面两个函数都是简单调用了Network模块的核心功能。

最后,你需要一个给聊天用户取昵称的函数,之前我们提到过,简简单单滴给个随机名字就好:
function new_author() {
Random.string(8);
}

模型层完整的代码如下:
type message = { string author
, string text
}
module Model {
private Network.network(message) room = Network.cloud("room")
exposed function broadcast(message) {
Network.broadcast(message, room);
}
function register_message_callback(callback) {
Network.add_callback(callback, room);
}
function new_author() {
Random.string(8);
}
}
注意,broadcast函数被定义为exposed,这是一个Opa关键字,意思是这个函数可以被客户端代码直接调用。

关联模型和视图
是时候将模型层和视图层关联起来了。
其实蛮简单,就是在视图层直接调用模型层函数即可。且看下面的视图层代码:
function default_page() {
author = Model.new_author();
page_template("Opa chat", (chat_html(author)))
}

接下来你将学习如何做下面两件事:
• 显示到达的消息
• 当前用户输入消息后广播该消息

显示新消息
为了显示消息,你需要写一个函数接收消息参数并更新用户界面:
function user_update(message msg) {
line = <div class="row-fluid line">
<div class="span1 userpic">
<img src="/resources/img/default-user.jpg" alt="User"/>
</div>
<div class="span2 user">{msg.author}:</>
<div class="span9 message">{msg.text}</>
</div>;
#conversation =+ line;
Dom.scroll_to_bottom(#conversation);
}

广播当前用户的消息
当用户输入新消息后需要将该消息发送给其它人:
function broadcast(author) {
text = Dom.get_value(#entry);
Model.broadcast(~{author, text});
Dom.clear_value(#entry);
}

连接一切
(译者注:让我想起了微信的"使命")
现在,所有的代码片段都已齐全,是全体整合的时候了。你需要做2件事:确保当用户输入新消息后该消息被广播出去;确保各客户端接收到新消息后将该消息正确显示在页面上。
你将使用事件处理器/监听器来创建之间的连接。

处理事件
介于之前在"事件处理"章节你已经学习过事件处理,在本例中事件的处理套路对你来说应该是驾轻就熟了。

来,给chat_html函数加点料。首先给函数传递一个代表当前用户名参数author,接下来添加下面3个事件处理:
• 给conversation元素添加onready事件,该事件在页面装载完毕后被触发,该事件会调用模型层的register_message_callback 函数并且传递user_update 作为回调函数,该回调函数在每次收到新消息后被触发执行。
• 给输入框添加onnewline事件,这样当当用户输入完毕按下回车键时就会将当前输入的消息广播到其它用户那里。
• 给Post按钮添加onclick事件以允许用户点击按钮发送并广播消息。
完整代码如下:
function chat_html(author) {
<div id=#conversation
onready={function(_) { Model.register_message_callback(user_update)}} />
<div id=#footer class="navbar navbar-fixed-bottom">
<div class=container>
<div class=input-append>
<input id=#entry class=input-xxlarge type=text
onnewline={function(_) { broadcast(author) }}>
<button class="btn btn-primary" type=button
οnclick={function(_) { broadcast(author) }}>Post</>
</div>
</div>
</div>
}
最后一步,编译并运行:
Tokyo:~ henri$ make run



译者注:整理代码如下
---------------
view.opa
---------------
module View {

function page_template(title, content) {
html =
<div class="navbar navbar-inverse navbar-fixed-top">
<div class=navbar-inner>
<div class=container>
<a class=brand href="./index.html">Opa Chat</>
</div>
</div>
</div>
<div id=#main>
{content}
</div>

Resource.page(title, html);
}

function default_page() {
author = Model.new_author();
page_template("Opa chat", (chat_html(author)))
}

function user_update(message msg) {
line = <div class="row-fluid line">
<div class="span1 userpic">
<img src="/resources/img/default-user.jpg" alt="User"/>
</div>
<div class="span2 user">{msg.author}:</>
<div class="span9 message">{msg.text}</>
</div>;

#conversation =+ line;

Dom.scroll_to_bottom(#conversation);
}

function broadcast(author) {
text = Dom.get_value(#entry);
Model.broadcast(~{author, text});
Dom.clear_value(#entry);
}

function chat_html(author) {
<div id=#conversation onready={function(_) { Model.register_message_callback(user_update)}} />
<div id=#footer class="navbar navbar-fixed-bottom">
<div class=container>
<div class=input-append>
<input id=#entry class=input-xxlarge type=text onnewline={function(_) { broadcast(author) }}>
<button class="btn btn-primary" type=button οnclick={function(_) { broadcast(author) }}>Post</>
</div>
</div>
</div>
}
}


---------------
model.opa
---------------
type message = {
string author
, string text
}

module Model {
private Network.network(message) room = Network.cloud("room")

exposed function broadcast(message) {
Network.broadcast(message, room);
}

function register_message_callback(callback) {
Network.add_callback(callback, room);
}

function new_author() {
Random.string(8);
}
}


---------------
controller.opa
---------------
module Controller {

dispatcher = {
parser {
case (.*) : View.default_page()
}
}

}

resources = @static_resource_directory("resources")

Server.start(Server.http, [
{ register:
[ { doctype: { html5 } },
{ js: [ ] },
{ css: [ "/resources/css/style.css"] }
]
},
{ ~resources },
{ custom: Controller.dispatcher }
])



理解网络
此处将阐述Opa中网络的低层机制。正如之前我们所说,网络基于一个称之为session的低层对象,Opa中session是一系列状态和并发性的集合:
• 构造一个新session需要2个参数,session的初始状态和消息处理器,构造结束后会返回一个与session相连的通道(channel)。session一旦被创建将在一定时间内长久存在,而channel对象这可以被再次分发甚至复制。
• 可通过channel发送一个消息,该消息会被传递给消息处理器,后者访问session并处理消息,甚至可以更改session状态。
下图表示了session的通用功能。


图6-2. Opa中sessions的信息流

为了创建一个新session你需要编写如下类似代码:
chan = Session.make(msg_handler, initial_state)
上记chan对象就是一个channel,通过它可与session通信。
initial_state参数标识session的初始状态, msg_handler参数用来处理发送给session的消息。
请注意此处我们是在服务器端创建的session,那么该session被保存在服务器端;反之,如果我们在客户端创建session,那么该session则被保存在客户端。
消息处理器是一个函数,具有如下2个参数:
• session的当前状态
• 接收到的消息
消息以Session.instruction进行响应,其类型如下:
type Session.instruction('state) = {'state set} or {unchanged} or {stop}
该类型有下列3种用法:
• {set: value}, 给当前session设置一个状态
• {unchanged}, 不改变session状态
• {stop}, 终止当前session,任何发送到该session的消息会被忽略
来,试试。在一个服务器端函数中做如下调用:
Session.send(msg1, chan)
这会通过通道异步发送一则消息给session,后者接收到消息后调用相关消息处理器msg_handler(initial_state, msg1)。
如果消息处理器以{set: state1}来响应,那么state1将变成session的新状态。
接下来,程序运行期间,客户端1发出了消息发送指令:
Session.send(msg2, chan)
客户端2也发出了消息发送指令:
Session.send(msg3, chan)
那么,客户端2实际上将会接收到2则消息并且分别调用msg_handler(state1, msg2) 和msg_handler(state2, msg3)。
多亏了Opa透明的的客户端/服务器通信机制,虽然session通道存在于服务器端,但通过将通道作为参数传递,客户端也得以可以使用它。就是这么简单!
你可以在实时web应用中直接调用Opa的高级别网络模块,同时,低级别的session在很多情况下也是非常有用的。


更多内容请关注博客专栏:

http://blog.csdn.net/qq_27056755

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值