看新年晚会的时候,发现最大的乐趣就是微信上墙了,但是量大了要等好久才能看见自己发的,为什么不能是弹幕的形式呢? 发现在GitHub上开源了一个JS弹幕模块核心CommentCoreLibrary,慢慢开始学习Node.js的一套。原来是比较做后台开发的,也是第一次做这样的分享,请大家多多指教啦……
一、Express
Express是Node.js最流行的一款web框架,小而灵活。Node.js和npm的安装配置可以参考这里。
可以通过npm安装Express(参考),也可以使用Express application generator快速产生一个Express样例(参考)。
对于Express初学者,用Express application generator生成样例更有利于快速上手。因此就以此为例:
# install Express application generator
$ npm install express-generator -g
# create an Express app named danmaku
$ express danmaku
# install dependencies
$ cd danmaku
$ npm install
# run the app on Windows
$ set DEBUG=danmaku & node .\bin\www
# or
$ npm start
关于set DEBUG=danmaku
可以见此文。
可以用npm start
启动服务器是因为在packege.json中有了这么一段:
"scripts": {
"start": "node ./bin/www"
}
Express的4比之3,把服务器配置和服务器启动做了分离,原来都在app.js里,现在将启动代码放到了www中。 现在,浏览一遍这个Express样例,对这框架就可以知道个大概了。
- bin:存放启动项目的脚本文件
- node_modules:存放所有的项目依赖库
- public:静态文件(css、js、img等)
- routes:路由文件(MVC中的C,controller)
- views:页面文件(jade或ejs模板)
- package.json:项目依赖配置及开发者信息
- app.js:应用核心配置文件
更多参考:
二、路由
将实时弹幕系统实际上是分为三个角色:
- 服务端:监听客户端连接、弹幕事件等并响应。
- 发射客户端:由用户发射弹幕。以emitCtrl.js作为emit页面的controller。
- 屏幕客户端:接收弹幕并显示。以indexCtrl.js作为index页面的controller。
添加 routes/indexCtrl.js
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index',{title:"danmaku"});
});
module.exports = router;
添加 routes/emitCtrl.js
var express = require('express');
var router = express.Router();
/* GET emit page. */
router.get('/', function(req, res, next) {
res.render('emit');
});
module.exports = router;
修改 app.js
var indexCtrl = require('./routes/indexCtrl');
var emitCtrl = require('./routes/emitCtrl');
...
app.use('/', indexCtrl);
app.use('/emit', emitCtrl);
启动后可查看到index页面。
在后面还会对emitCtrl.js增加弹幕配置的文件config.json的读取。
三、屏幕客户端
1. 静态
CommentCoreLibrary是GitHub上开源的JS弹幕模块核心,提供从基本骨架到高级弹幕的支持。
考虑到实际,感觉并不应该引入外部库。如果作为外部库用,需要
$ npm install comment-core-library --save
使用时(去除public)
<link rel="stylesheet" href="/node_modules/comment-core-library/build/style.css" />
<script src="/node_modules/comment-core-library/build/CommentCoreLibrary.js"></script>
另外CommentCoreLibrary模块也有点笨重。
所以换种方式,将CommentCoreLibrary.js放入public/javascripts,style.css放入public/stylesheets中。
添加views/index.jade
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
link(rel='stylesheet', href='/stylesheets/index.css')
script(src='/javascripts/CommentCoreLibrary.js')
body
#my-player.abp(style='width:100%; height:600px; background:#000;')
#my-comment-stage.container
ul#messages
script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.js')
script(src='/javascripts/index.js')
添加public/stylesheets/index.css
* { margin: 0; padding: 0; box-sizing: border-box; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
body {
margin:0px;
padding:0px;
font-family: "Segoe UI", "Microsoft Yahei", sans-serif;
}
添加public/javascripts/index.js
window.addEventListener('load', function () {
// 在窗体载入完毕后再绑定
var CM = new CommentManager($('#my-comment-stage'));
CM.init();
// 先启用弹幕播放(之后可以停止)
CM.start();
// 开放 CM 对象到全局这样就可以在 console 终端里操控
window.CM = CM;
});
然后在Console里怒射一弹:
var danmaku = {
"mode": 1,
"text": "hello world",
"stime": 0,
"size": 25,
"color": 0xff00ff,
"dur": 10000
};
CM.send(danmaku);
不过这其实根本没用上服务器,也就是静态网页一样的效果。
2. 动态(服务端)
动态是实现一个真正的“屏幕客户端”,监听等待“显示弹幕”的事件,并实时显示。
在CommentCoreLibrary的Doc中有一段:
实时弹幕也需要后端服务器的支持。实时弹幕可以采取Polling(定时读取)或者 Push Notify(监听等待)两个主动和被动模式实现。
WebSocket
是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。既然是双向通信,就意味着服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。知乎上关于WebSocket的科普。
Socket.IO
是一个开源的WebSocket库,它通过Node.js实现WebSocket服务端,同时也提供客户端JS库。Socket.IO支持以事件为基础的实时双向通讯,它可以工作在任何平台、浏览器或移动设备。
Socket.IO支持4种协议:WebSocket、htmlfile、xhr-polling、jsonp-polling,它会自动根据浏览器选择适合的通讯方式,从而让开发者可以聚焦到功能的实现而不是平台的兼容性,同时具有不错的稳定性和性能。
p.s. 实际过程中踩到了phpwebsocket的坑。
用npm导入Socket.IO:
npm install socket.io --save
修改www(Express4从app.js里把启动分出来了)
// Create socket.io
var io = require('socket.io')(server);
...
// Wait for socket event
io.on('connection', function(socket){
console.log('a user connected');
socket.on('disconnect', function(){
console.log('user disconnected');
});
socket.on('danmaku send', function(msg){
console.log('message: ' + msg);
io.emit('danmaku show', msg);
});
});
修改index.jade
script(src='http://cdn.bootcss.com/socket.io/1.3.2/socket.io.js')
script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.js')
script(src='/javascripts/index.js')
修改index.js
window.addEventListener('load', function () {
// 在窗体载入完毕后再绑定
var CM = new CommentManager($('#my-comment-stage'));
CM.init();
// 先启用弹幕播放(之后可以停止)
CM.start();
// 开放 CM 对象到全局这样就可以在 console 终端里操控
window.CM = CM;
var socket = io();
socket.on('danmaku show', function (msg) {
console.log(msg);
$('#messages').append($('<li>').text(msg));
var danmaku = JSON.parse(msg);
CM.send(danmaku);
});
});
这样就由服务端监听了“connection”、“disconnect”和“danmaku send”三个事件,特别是在收到“danmaku send”时会发送“danmaku show”事件。而屏幕客户端监听“danmaku show”事件,并把传递来的弹幕显示出来。
启动后打开index,确实能看到"connection"事件执行的提示。
四、发射客户端
发射客户端发送“danmaku send”事件及弹幕给服务端。
除此之外,在CommentCoreLibrary里可以对弹幕属性进行设置,比如文字大小、模式、颜色,将它们的可选值写成配置文件,并设定默认值。
添加public/jsons/config.json
{"sizes":[{"size":12,"title":"非常小"},{"size":16,"title":"较小"},{"size":18,"title":"小"},{"size":25,"title":"中"},{"size":36,"title":"大"},{"size":45,"title":"较大"},{"size":64,"title":"非常大"}],
"modes":[{"mode":1,"title":"顶端滚动"},{"mode":2,"title":"底端滚动"},{"mode":5,"title":"顶端渐隐"},{"mode":4,"title":"底端渐隐"},{"mode":6,"title":"逆向滚动"}],
"colors":[{"color":"000000","title":"黑色"},{"color":"C0C0C0","title":"灰色"},{"color":"ffffff","title":"白色"},{"color":"ff0000","title":"红色"},{"color":"00ff00","title":"绿色"},{"color":"0000ff","title":"蓝色"},{"color":"ffff00","title":"黄色"},{"color":"00ffff","title":"墨绿"},{"color":"ff00ff","title":"洋红"}],
"inits":{"size":3,"mode":0,"color":4}}
修改emitCtrl.js,读取配置
var fs = require('fs');
...
/* GET emit page. */
router.get('/', function (req, res, next) {
var config = JSON.parse(fs.readFileSync(__dirname + './../public/jsons/config.json'));
res.render('emit', { title: 'Emitter', sizes: config.sizes, modes: config.modes, colors: config.colors, inits: config.inits});
});
添加views/emit.jade
doctype html
html
head
title= title
meta(name='viewport', content='width=device-width, initial-scale=1,maximum-scale=1')
link(rel='stylesheet',href='http://cdn.bootcss.com/jquery-mobile/1.4.3/jquery.mobile.css')
script(src='http://cdn.bootcss.com/socket.io/1.3.2/socket.io.js')
script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.min.js')
script(src='http://cdn.bootcss.com/jquery-mobile/1.4.3/jquery.mobile.js')
body
div(data-role='page')
div(data-role='content')
div.ui-grid-b
a#size.ui-btn.ui-btn-inline.ui-block-a(href='#popupMenu_font', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-size= sizes[inits.size].size )= sizes[inits.size].title
#popupMenu_font(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;')
ul(data-role='listview')
each val, index in sizes
li
a(data-rel='back',danmaku-size=val.size)= val.title
a#mode.ui-btn.ui-btn-inline.ui-block-b(href='#popupMenu_mode', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-mode= modes[inits.mode].mode )= modes[inits.mode].title
#popupMenu_mode(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;')
ul(data-role='listview')
each val, index in modes
li
a(data-rel='back',danmaku-mode=val.mode)= val.title
a#color.ui-btn.ui-btn-inline.ui-block-c(href='#popupMenu_color', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-color= colors[inits.color].color )= colors[inits.color].title
#popupMenu_color(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;')
.ui-grid-b
- var i=0;
each val, index in colors
case i++%3
when 0: a.ui-block-a(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title
when 1: a.ui-block-b(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title
when 2: a.ui-block-c(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title
textarea#msg(placeholder='来一发弹幕~')
button#btnSend 发射
script(src='/javascripts/emit.js')
添加public/javascripts/emit.js
var socket = io();
$('#popupMenu_font a').click(function(e){
$('#size').text($(e.target).text()).attr("danmaku-size",$(e.target).attr("danmaku-size"));
});
$('#popupMenu_mode a').click(function(e){
$('#mode').text($(e.target).text()).attr("danmaku-mode",$(e.target).attr("danmaku-mode"));
});
$('#popupMenu_color a').click(function(e){
$('#color').text($(e.target).text()).attr("danmaku-color",$(e.target).attr("danmaku-color"));
});
$('#btnSend').click(function(e){
e.preventDefault();
var danmaku = {
"mode": Number($("#mode").attr("danmaku-mode")),
"text": $('#msg').val(),
"stime":0,
"size": Number($("#size").attr("danmaku-size")),
"color":parseInt($("#color").attr("danmaku-color"),16),
"dur":10000
};
var msg=JSON.stringify(danmaku);
console.log(msg);
socket.emit('danmaku send',msg);
$('#msg').val("");
});
最后整个效果就是这样啦~ 源码在此:https://github.com/cstackess/danmaku