On 2014年12月14日周日 上午10:53 chenlin rao <rao.chenlin@gmail.com> wrote:
-- --# Mojolicious + SocketIO + AngularJS今天本来是准备随便写点跟 Web 开发相关的话题,毕竟今年 YAPC 上最大的声音就是 SaywerX 的["CGI.pm must DIE!"](www.youtube.com/watch?v=tu6_3fZbWYw)。正在犹豫是介绍 [Mojolicious](https://metacpan.org/pod/Mojolicious) 还是 [PocketIO](https://metacpan.org/pod/PocketIO) 模块的时候,偶然在 gist 上看到一个单文件程序,结合了 Mojolicious、socket.io 和 angular.js 三大框架,简直就是任性。那么好,今天就拿这个做例子说一说好了:## Mojolicious::Lite 的 DSL因为是单文件程序,所以用的是 Mojolicious::Lite,这个提供了一些很方便的类 sinatra 的 DSL。常见的情况是 `get '/' => sub($c) { $c->render('index') }` 这样。如果一个 url 路径同时可能有 GET 或者 POST 请求,那么就用 `any` 指令。这里就是:use Mojolicious::Lite;use Protocol::SocketIO::Message;use Protocol::SocketIO::Handshake;use Class::Date;any '/' => 'index';any '/view1' => 'index';any '/view2' => 'index';any '/partials/:name' => sub {shift->render( $self->param('name') );};在 url 路径这里可以做捕获,方便在控制器函数里处理。这里用到了 `/socket.io/` 这个路径。这是 socket.io 协议规定的固定路径。any '/socket.io/:id/' => sub {渲染方法支持很多种方式。默认写法的话,会自动去找同名的 `.html.ep` 模板来渲染 —— 这种情况更简写的方式就是前面已经看到的 `any '/' => 'index'`,其实就是去找 `index.html.ep` 文件。如果写 API ,很多时候返回的并不是 HTML 内容,Mojolicious 也支持渲染其他格式的响应。最常见的是 `->render(json => $ref)` 这样。不过这里 socket.io 因为有单独的协议,所以是用 Protocol::SocketIO(这个包就是出自原先我打算讲的 PocketIO 模块) 来单独生成响应。shift->render( text=> Protocol::SocketIO::Handshake->new(session_id => 1234567890,heartbeat_timeout => 10,close_timeout => 15,transports => [qw/websocket xhr-polling/]) );};普通的 web 方法在 Mojolicious 里大致就是这样。下面进入更高级的异步交互环节了!我们这里示例的是一个每秒自动更新时间的页面。所以 Mojolicious 要定时执行。`Mojo::IOLoop->delay` 是个完全值得单独讲一次的好东西:my $clients = {};my $delay = Mojo::IOLoop->delay;Mojo::IOLoop->recurring( 1 => sub {for my $id( keys %$clients ) {# send name$clients->{$id}->send( Protocol::SocketIO::Message->new( type => 'event', data=>{ name=>'send:name', args=>[{ name=>'Jamie '.$id }] }) );# send time$clients->{$id}->send( Protocol::SocketIO::Message->new( type => 'event', data=>{ name=>'send:time', args=>[{ time=>''.Class::Date->now }] }) );}});Mojolicious 本身直接指示 websocket 协议。所以用起来 DSL 跟做 GET/POST 是一样的。而控制器函数里用法也跟 socket.io 的写法有些类似。采用 `$self->on(message => sub {})`。在控制器里,Mojolicious 允许直接操作整个请求响应的事务主体,也就是代码中的 `$self->tx` 。在异步处理的时候,肯定会需要直接操作 tx,这里作为一个纯粹的定时器,就只需要 send 了:websocket '/socket.io/:id/websocket/:oid' => sub {my $self = shift;app->log->debug(sprintf 'Client connected: %s', $self->tx);my $id = sprintf "%s", $self->tx;$clients->{$id} = $self->tx;$self->tx->send( Protocol::SocketIO::Message->new( type => 'connect') );$self->tx->send( Protocol::SocketIO::Message->new( type => 'event', data=>{ name=>'send:name', args=>[{ name=>'Jamie starting...' }] }) );$self->on(message => sub {my ($self, $msg) = @_;# no messages are being sent for now});$self->on(finish => sub {app->log->debug('Client disconnected');delete $clients->{$id};});};最后,调用 start 方法开始运行服务器:app->start;## AngularJS 示例Mojolicious::Lite 支持在 Perl 的 `__DATA__` 里直接写页面内容,每个页面以 @@开头命名即可:__DATA__@@index.html.ep### angular 的模板和变量绑定angular 深度的改造了 HTML 的样式和书写方式,在前端的层面上提供 MVC 功能。在使用 `ng-app` 标记整个页面归属的具体 angular 应用后,可以利用 `ng-controller` 指令将一个 div 关联到一个 angular 的控制器上。在这个 div 内,可以通过 `{{ }}` 语法加载控制器函数里的变量,可以渲染带有 `ng-view` 指令的 div 作为 HTML 内容展示。<!DOCTYPE html><html ng-app="myApp"><head><meta charset="utf8"><base href="/"><title>Angular Socket.io Seed App</title><link rel="stylesheet" href="app.css"></head><body><div ng-controller="AppCtrl"><h2>Helloo {{name}}</h2><ul class="menu"><li><a href="view1">view1</a></li><li><a href="view2">view2</a></li></ul><div ng-view></div><div>Angular Socket.io seed app: v<span app-version></span></div></div><script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script><script src="socket.js"> </script><script src="app.js"> </script></body></html>@@partial1.html.ep<p>This is the partial for view 1.</p><p>The current time is {{time}}</p>@@partial2.html.ep<p>This is the partial for view 2.</p><p>Showing of 'interpolate' filter:{{ 'Current version is v%VERSION%.' | interpolate }}</p>### angular 的应用模块上面页面部分就完成了。下面就开始在 js 中完成这个 angular 应用。我们前面已经看到,这个页面归属的应用名字叫 **myApp**。下面是应用的代码:@@app.js'use strict';// Declare app level module which depends on filters, and servicesvar app = angular.module('myApp', ['myApp.filters', 'myApp.services', 'myApp.directives']).config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {$routeProvider.when('/view1', {templateUrl: 'partials/partial1', controller: MyCtrl1});$routeProvider.when('/view2', {templateUrl: 'partials/partial2', controller: MyCtrl2});$routeProvider.otherwise({redirectTo: '/view1'});$locationProvider.html5Mode(true);}]);非常清晰,实现这个 myApp,加载了对应的 filters、services、directives 模块,然后定义了两个路由规则,分别指向两个不同的模板和控制器。### angular 的控制器angular 的控制器,最主要的作用就是处理应用作用域 `$scope` 与其他各种数据的关联。在这个示例里,就是把前面模板里的变量,跟 socket.io 从服务器拿到的数据关联在一起:function AppCtrl($scope, socket) {socket.on('send:name', function (data) {$scope.name = data.name;});}function MyCtrl1($scope, socket) {socket.on('send:time', function (data) {$scope.time = data.time;});}MyCtrl1.$inject = ['$scope', 'socket'];function MyCtrl2() {}MyCtrl2.$inject = [];### angular 的指令angular 的指令的作用,就是实际操作、修改变更应用中的数据,包括可能页面元素的变化等等。一般的 Web 开发中,这个事情是交给 jQuery 来操作 DOM 的。而在 angular 里。编写成指令,可以直接写成 HTML 元素的属性,看起来非常清爽。比如这个示例中就是生成了一个 `appVersion` 指令,在前面的 HTML 里,用在了一个 span 元素上。angular.module('myApp.directives', []).directive('appVersion', ['version', function(version) {return function(scope, elm, attrs) {elm.text(version);};}]);### angular 的过滤器angular 的过滤器,可以利用管道的方式帮助模板中的变量达到更好的渲染效果,默认提供有 date、json、limitTo、orderBy、number、lowercase、uppercase 等几个过滤器。也可以自己写新的过滤器。过滤器函数很简单,传一个参数返回一个结果即可:angular.module('myApp.filters', []).filter('interpolate', ['version', function(version) {return function(text) {return String(text).replace(/\%VERSION\%/mg, version);}}]);### angular 的服务和工厂augular 利用服务(service)或者工厂(factory)的方式来提供整个应用里,不同路由或者说控制器之间共用的单例变量。这二者的不同在于:service 其实就是一种不导入其他变量的简单 factory 的简写。比如下面这段代码是原程序中用 factory 写的,虽然名叫 myApp.services:// Demonstrate how to register services// In this case it is a simple value service.angular.module('myApp.services', []).value('version', '0.1').factory('socket', function ($rootScope) {var socket = io.connect();return {on: function (eventName, callback) {socket.on(eventName, function () {var args = arguments;$rootScope.$apply(function () {callback.apply(socket, args);});});},emit: function (eventName, data, callback) {socket.emit(eventName, data, function () {var args = arguments;$rootScope.$apply(function () {if (callback) {callback.apply(socket, args);}});})}};});可以看到就没有导入其他东西,所以可以写成服务,代码应该是下面这样:angular.module('myApp.services', []).value('version', '0.1').service('socket', function ($rootScope) {var socket = io.connect();this.on = function (eventName, callback) {...};this.emit = function (eventName, data, callback) {...};});## socket.io 接口上面 angular 服务里,就演示了 socket.io 的客户端用法。可以看到,其实客户端的接口跟服务器端是一一对应的。示例程序再往下就是纯粹的js和css文件,这就不再继续了。完整代码源地址见:<https://gist.github.com/rodrigolive/5546320>## 警告socket.io 的客户端 js 在本例中还是用的去年的 0.90 的版本。最近半年 socket.io 项目发生重大改变,从 1.0 开始,有了自己专门的协议分析库 engine.io,整个 url 路径和 handshake 编解码方式都不太一样了。在 Perl 方面,PocketIO 作者没时间跟进这个变化,所以 Protocol::SocketIO 还是只能支持 0.90 的版本。
您收到此邮件是因为您订阅了Google网上论坛上的"PerlChina Mongers 讨论组"群组。
要退订此群组并停止接收此群组的电子邮件,请发送电子邮件到perlchina+unsubscribe@googlegroups.com。
要发帖到此群组,请发送电子邮件至perlchina@googlegroups.com。
访问此群组:http://groups.google.com/group/perlchina。
要查看更多选项,请访问https://groups.google.com/d/optout。
您收到此邮件是因为您订阅了Google网上论坛上的"PerlChina Mongers 讨论组"群组。
要退订此群组并停止接收此群组的电子邮件,请发送电子邮件到perlchina+unsubscribe@googlegroups.com。
要发帖到此群组,请发送电子邮件至perlchina@googlegroups.com。
访问此群组:http://groups.google.com/group/perlchina。
要查看更多选项,请访问https://groups.google.com/d/optout。
没有评论:
发表评论