微博上看到有讨论cloud9的,抱着尝鲜的态度装了一下试试。
cloud9是一个web的IDE,后端基于node.js构建,前端基于html5打造,貌似很牛叉的样子。
我的服务器操作系统是ubuntu12.04,安装过程还算比较顺利。
第一步,准备环境。由于基于node.js,所以要先把node环境装好,当然黄金搭档npm也要装好。

sudo apt-get install python-software-properties
sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs npm

上面的步奏是先添加ubuntu的ppa源,然后更新源,再安装。我在这一步上耗费了大量时间,主要是国内连ppa源太慢了,蛋碎一地。

第二步:安装git和其他依赖包。由于是从github上clone源码出来的,所以需要先把git装好。谁让我的机器是裸机呢,连make都没装过。

sudo apt-get install git
sudo apt-get install libxml2-dev make

第三步,clone代码并安装。

npm install -g sm
git clone https://github.com/ajaxorg/cloud9.git cloud9
cd cloud9
sm install

这一步也很消耗时间,因为源码有差不多100M,从github上clone出来也挺慢的。

然后,就可以运行了,新建一个目录,便于指定为工作目录

bin/cloud9.sh -l 127.0.0.1 -w ~/workspace -s

-l指定的是监听的ip地址,如果想其他机器能够访问,这里不能指定成127.0.0.1,要指定成对外的地址,-w指定的就是工作目录了。

最后终于见到IDE长什么样了,UI设计还行,就是不太稳定,而且还有各种搞不懂,不明白它的“run”到底是一个什么机制,折腾了很久没run起来一个hello world,只有html倒是可以预览。

参考:
https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager
https://github.com/ajaxorg/cloud9

围脖上看到有人说AppJS,去官网围观了下,果然NB,这东西还真就能用JS写图形化本地程序。

传统的图形化本地程序(或者说客户端程序)一般是一个可执行文件,在windows上一般是个exe,执行之后有个图形界面供用户交互。能写图形化本地程序的语言比较多,C/C++、java、C#、python等等,可能有些程序需要图形库支持,比如QT、GTK之类。JS写图形化本地程序之前我还没有听说过,今天这个算是看了眼界了。

简单来说,AppJS把Chromium和Node.js结合在一起,使用了Node.js的本地化能力来进行本地逻辑处理,同时使用Chromium的图形化能力来展现图形化界面,把这两者串起来的就是js了。Node果然无比强大。

使用AppJS来写本地程序有以下几个好处:

1、像写网页一样写本地程序。因为AppJS的图形化用的是Chromium,本质上还是使用浏览器来展现,所以写这样的本地程序跟写网页区别不大。
2、你可以使用Chromium提供的很多牛X哄哄的特性:HTML5, CSS3, SVG, WebGL等
3、你可以使用Node提供的与系统有关的特性:文件系统访问、网络访问、进程管理等,你甚至可是让你的程序是个http服务器。
4、跨平台,目前AppJS支持Windows、Linux和Mac。

来看一下AppJS自带的一个例子吧,界面如图:

这样的界面是怎么做出来的呢,还是看看代码好了

<!doctype html>
<html>
<head>
<title>Hello World!</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>

仅仅是一个页面而已,当然还用了点CSS3

html{ 
width: 100%; 
height: 100%; 
overflow: hidden; 
background-image: url(rainbowsky.jpg); 
background-size: cover;
-webkit-box-sizing: border-box; 
}
body{ 
margin: 0; 
height: 100%; 
font-family: 
FixedWidth; 
color: #fff 
}
h1{ 
text-align: center; 
font-weight: 900; 
font-size: 120px; 
line-height: 1; 
text-shadow: 6px 3px 3px rgba(0,0,0,.7), 0 0 30px #aaccff;
position: absolute; 
margin: -1em 0 0 0; 
top: 50%; 
width: 100%; 
-webkit-text-stroke-width: 2px; 
-webkit-text-stroke-color: #000; 
}

逻辑部分,代码就是Node了,不过例子里也没什么复杂逻辑,就是把界面显示出来而已

var app = module.exports = require('appjs');

app.serveFilesFrom(__dirname + '/content');

var window = app.createWindow({
  width  : 640,
  height : 460,
  icons  : __dirname + '/content/icons',
/***************************** defaults ********************************
* url            : 'http://appjs', // serve static file root and routers
* autoResize     : false,          // resizes in response to html content
* showChrome     : true,           // show border and title bar
* resizable      : false,          // control if users can resize window
* disableSecurity: true,           // allow cross origin requests
* opacity        : 1,              // flat percent opacity for window
* alpha          : false,          // per-pixel alpha blended (Win & Mac)
* fullscreen     : false,          // client area covers whole screen
* left           : -1,             // centered by default
* top            : -1,             // centered by default
*************************************************************************/
});

window.on('create', function(){
  console.log("Window Created");
  this.frame.show();
  this.frame.center();
});

window.on('ready', function(){
  console.log("Window Ready");
  this.require = require;
  this.process = process;
  this.module = module;
  this.console.log('process', process);
});

window.on('close', function(){
  console.log("Window Closed");
});

AppJS托管在github上,有兴趣的去围观吧 https://github.com/appjs/appjs

已经把这事搁置很久了,前一段时间主要是毕业的事情比较多,现在终于能有一点时间继续写了。好了,进入正题,本节的重点在于WebSocket。
由于各大浏览器对WebSocket的支持各不相同,所以直接使用浏览器提供的API会比较复杂。Socket.IO对这些API做了封装,并且还使用其他方式对不支持WebSocket的浏览器做了支持,使用Socket.IO会减轻很多工作量。
使用Socket.IO只需要在express里安装Socket.IO模块,同时在页面中加入/socket.io/socket.io.js的引用,服务端包含socket.io包即可。服务端代码如下:

var express = require('express')
  , routes = require('./routes');

var app = module.exports = express.createServer();
var io = require('socket.io').listen(app);
// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.set('view options',{layout:false});
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

// Routes

app.get('/', routes.index);
var conns = {};
var userNames = {};
var sids = {};
var onLineList = [];
io.sockets.on('connection',function(socket){
	var sid=socket.id;
	conns[sid]=socket;
    socket.on('submitUserName',function(userName, fn) {
        if (userNames[userName]) {
            fn("have");
        } else {
            for (var i = onLineList.length - 1; i >= 0; i--) {
                var tmpSid = userNames[onLineList[i]]["sid"];
                conns[tmpSid].emit('addNewUserToList', userName);
            }
            var userInfo = {};
            userInfo["sid"] = sid;
            userNames[userName] = userInfo;
            sids[sid] = userName;
            onLineList.push(userName);
            fn("not_have");

        }
    });
    socket.on('getOnlineList', function(userName, fn){
        fn(onLineList);
    });
    socket.on('setCompetitor', function(competitor, fn){
        var myName = sids[socket.id];
        for (var i = onLineList.length -1; i >= 0; i--) {
            if (onLineList[i] == myName) {
                onLineList.splice(i, 1);
            }
        }
        for (var i = onLineList.length -1; i >= 0; i--) {
            if (onLineList[i] == competitor) {
                onLineList.splice(i, 1);
            }
        }
        var deletedUser = [myName, competitor];
        for (var i = onLineList.length -1; i >= 0; i--) {
            var tmpSid = userNames[onLineList[i]]["sid"];
            conns[tmpSid].emit('deleteUserFromList', deletedUser);
        }
        if (!userNames[myName]["competitor"] && !userNames[competitor]["competitor"]) {
            userNames[myName]["competitor"] = competitor;
            userNames[competitor]["competitor"] = myName;
            var comSid = userNames[competitor]["sid"];
            conns[comSid].emit('beginChess', 'begin');
            fn("success");
            socket.on('goChess', function(data){
				conns[comSid].emit('comGoChess', data);
            });
            conns[comSid].on('goChess', function(data){
                socket.emit('comGoChess', data);
            });
        } else {
            fn("fail");
        }
    });
});
app.listen(3000, function(){
  console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});

代码前半部分都是Express框架的设置部分,然后io.sockets.on();部分开始监听来自浏览器端的WebSocket连接。连接建立之后,会将该连接存起来,并在该连接上设置一些可能的监听事件。

socket.on('submitUserName',function(userName, fn){});
//用户提交自己的名字时触发的事件

socket.on('getOnlineList', function(userName, fn){});
//用户获取在线等待配对用户列表时触发的事件

socket.on('setCompetitor', function(competitor, fn){});
//用户选中一个对手之后设置对手的事件

浏览器端在触发事件时会发送一些数据给服务端,比如socket.on(‘submitUserName’,function(userName, fn){})中,userName是浏览器端发送过来的用户名,而fn则是浏览器端定义的一个函数回调函数,当服务端完成某些操作后,会调用fn函数,相当于浏览器端得到了服务端的响应。当然,这里也可以不设置fn,只发送数据。关于socket.io的具体情况可以查看官方文档,地址:http://socket.io/#how-to-use
浏览器端会根据用户的一些操作触发一些服务端事件,部分代码如下:

$(function(){
    $("#chessCanvas").hide();
    $("#PiecesCanvas").hide();
    $("#onlinelist").hide();
    socket = io.connect();
    window.submitUserName = function(){
        var userName = $("#username").val();
        if (userName) {
            socket.emit('submitUserName', userName, function(data){
                if (data == "have") {
                    alert("用户名已经被使用,请重新取一个更酷的名字吧!");
                    $("#username").val("");
                    $("#username").focus();
                } else {
                    socket.emit('getOnlineList', userName, function(list){
                        $.each(list, function(i, item){
                            if (item == userName){
                                return true;
                            }
                            var str = '<div id="' + item + '"><input type="radio" name="onlineuser" value="'
                                + item + '" /><span>' + item + '</span></div>';
                            $("#namelist").append(str);
                        });
                        $("#login").hide();
                        $("#onlinelist").show();
                    });
                }
            });
            socket.on('addNewUserToList', function(data){
                var str = '<div id="' + data + '"><input type="radio" name="onlineuser" value="'
                    + data + '" /><span>' + data + '</span></div>';
                $("#namelist").append(str);
            });
            socket.on('deleteUserFromList', function(data) {
                $.each(data, function(i, item) {
                    $("#namelist > #" + item).remove();
                });
            });
            socket.on('beginChess', function(data) {
                showChess('red', 'wait', socket);
            });
        } else {
            alert("请输入用户名!");
        }

    };
    window.beginChess = function(){
        var competitor = $("input[name='onlineuser']:checked").val();
        if (competitor) {
            socket.emit('setCompetitor', competitor, function(data){
                if (data == "success") {
                    showChess('black', 'idle', socket);
                } else {
                    //未配对成功的情况
                }
            });
        } else {
            alert("请选择一个对手!");
        }
    };
    function showChess(ourColor, status, sock) {
        $("#onlinelist").hide();
        $("#chessCanvas").show();
        $("#PiecesCanvas").show();
        window.chss = chess("chessCanvas", "PiecesCanvas", sock);
        window.chss.ourColor = ourColor;
        window.chss.status = status;
        window.chss.initPieces();
    }

});

当用户输入完用户名之后,点击登录,就会使用socket.emit(‘submitUserName’, userName, function(data){})触发服务端响应。同时,浏览器端也会监听一些服务端事件,例如socket.on(‘beginChess’, function(data) {})就监听了服务端发起的棋局开始事件。这就是WebSocket的强大之处,浏览器可以被动接收服务端发送的消息,换个角度说,这一刻的浏览器就是C/S结构中的服务端,而服务器则是C/S结构中的客户端。当棋局开始后,浏览器使用如下代码监听服务器发送过来的对方走子情况,并在本地执行。

//监听服务端发送过来的对方棋子移动的事件
me.socket.on('comGoChess', function(movement) {
    var lineX = 10 - movement.toLineX;
    var lineY = 11 - movement.toLineY;
    var comPiece = movement.piece;
    me.pieceMoveTo('opposite', comPiece, lineX, lineY);
    me.status = 'idle';
});

当然,在本地走子之后,也会将走子信息发送给服务器,服务器再发送到对方。本子走子信息通过如下代码向服务端发送。

var movement = {};
movement["piece"] = me.activePiece;
movement["toLineX"] = lineX;
movement["toLineY"] = lineY;
me.socket.emit('goChess', movement);

到这里,仅仅实现了配对,同步走子的功能。判断输赢和结束棋局重来棋局等功能还未实现。最后,再放截图一张。

本节的主要工作是给棋子添加鼠标事件,实现选中棋子、走子以及吃子等功能,同时将服务器端的基础搭建好。

棋盘初始化完成之后,棋盘背景、棋子位置都已经绘制完毕。这时候就需要给上层的Canvas添加鼠标事件的侦听:

me.piecesCanvas.addEventListener("click",me.onClick,false);

当鼠标在棋盘区域进行单击事件的时候,就会触发me.onClick函数的执行,下面就看一下me.onClick的定义

me.onClick = function(e) {
    var x = e.clientX - me.startX - me.piecesCanvas.offsetLeft;
    var y = e.clientY - me.startY - me.piecesCanvas.offsetTop;
    var lineX = x / me.xStep;
    var lineY = y / me.yStep;
    lineX = parseInt(lineX + 1.5);
    lineY = parseInt(lineY + 1.5);
    var pX = (lineX - 1) * me.xStep;
    var pY = (lineY - 1) * me.yStep;
    var dist = Math.sqrt((x - pX) * (x - pX) + (y - pY) * (y - pY));
    var r = me.getPiecesR();
    if (dist < r) {
        if (me.status == "idle") {
            var selectPoint = me.board[lineY][lineX];
            if (selectPoint == "") {
                return;
            } else {
                var selectArr = selectPoint.split("-");
                var position = selectArr[0];
                var pieceStr = selectArr[1];
                if (position == "our") {
                    me.activePiece = pieceStr;
                    me.piecesList["our"][pieceStr].active = true;
                    me.status = "active";
                    me.reDrawPieces();
                }
            }
        } else if (me.status == "active") {
            var selectPoint = me.board[lineY][lineX];

            if (selectPoint == "") {
                me.pieceMoveTo("our", me.activePiece, lineX, lineY);
                me.piecesList["our"][me.activePiece].active = false;
                me.activePiece = "";
                me.status = "idle";
                me.reDrawPieces();
            } else {
                var selectArr = selectPoint.split("-");
                var position = selectArr[0];
                var pieceStr = selectArr[1];
                if (position == "opposite") {
                    me.pieceMoveTo("our", me.activePiece, lineX, lineY);
                    me.piecesList["our"][me.activePiece].active = false;
                    me.activePiece = "";
                    me.status = "wait";
                    me.reDrawPieces();
                } else if (position == "our") {
                    me.piecesList["our"][me.activePiece].active = false;
                    me.piecesList["our"][pieceStr].active = true;
                    me.activePiece = pieceStr;
                    me.reDrawPieces();
                }
            }

        }
    }
};

这里主要将棋局分成三个状态,idle、active和wait。在idle状态,我方可以选子,一旦选定一个棋子,状态就变成active状态,被选中棋子的active属性就变成true。active状态下,继续侦听鼠标事件,当鼠标选再次单击时判断单击的位置。如果仍然选中了我方棋子,那么就更改被选中的棋子,就是换一个棋子走。如果选中了空白棋子,就走到该位置,棋局状态变为wait状态。如果选中的是对方棋子,那么吃掉对方棋子,棋局状态变成wait状态。这里注意两点:一是还没有考虑不同棋子的走子规则,马走日字象走田嘛,这个以后添加。二是wait状态之后就需要和服务器通讯了,现在也还没有实现。

另外,服务器端使用express来处理web请求,同时使用socket.io来进行websocket通讯。服务端代码大致如下:

var express = require('express')
  , routes = require('./routes');

var app = module.exports = express.createServer();
var io = require('socket.io').listen(app);
// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', 'ejs');
  app.set('view options',{layout:false});
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

// Routes

app.get('/', routes.index);
var conns={};
io.sockets.on('connection',function(socket){
	//do something
});
app.listen(3000, function(){
  console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
});

最近时间太紧,以后慢慢写。

本节主要是使用Canvas绘制棋盘和棋子,并确定前端的基本程序结构。
由于刚刚开始学习使用html5,所以程序写的比较挫,各位看客轻拍。
首先,写象棋的基本骨架,html页面

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
 "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 <meta http-equiv="Content-Language" content="zh-cn" />
 <title>象棋示例</title>
 <link charset="utf-8" rel="stylesheet" href="style.css" type="text/css" />
 <script type="text/javascript" src="chess.js" charset="utf-8"></script>
</head>
<body>
<canvas id="chessCanvas" width="600" height="600">
 您的浏览器不支持canvas,推荐使用chrome!
</canvas>
<canvas id="PiecesCanvas" width="600" height="600"></canvas>
</body>
</html>

这里定义了两个Canvas,chessCanvas当棋盘使用,PiecesCanvas用来绘制棋子。棋局开始时先在chessCanvas上绘制棋盘,然后这个Canvas一直保持不变,作为棋子Canvas的背景,PiecesCanvas叠加在棋盘Canvas之上,每次棋局发生改变只需要重绘piecesCanvas即可,不需要重绘棋盘,降低了重绘代价。
使用简单的CSS来将两个Canvas叠加:

#chessCanvas {
 position: absolute;
 left: 0px;
 top: 0px;
 z-index: 0;
}
#PiecesCanvas {
 position: absolute;
 left: 0px;
 top: 0px;
 z-index: 9999;
}

然后需要在js中定义象棋需要使用的一些对象和变量

var me = {};
//棋盘Canvas
me.chessCanvas=document.getElementById(chessId);
me.cxt=me.chessCanvas.getContext("2d");

//棋盘高度
me.boardHight = me.chessCanvas.height;
//棋盘宽度
me.boardWidth = me.chessCanvas.width;

//棋子Canvas
me.piecesCanvas=document.getElementById(piecesId);
//保证棋子Canvas的高度宽度与棋盘相同
me.piecesCanvas.height = me.boardHight;
me.piecesCanvas.width = me.boardWidth;
me.piececxt=me.piecesCanvas.getContext("2d");

//设置棋子中字体
me.piececxt.font = 'bold 30px 宋体';
//棋子文字中线垂直对齐
me.piececxt.textBaseline = 'middle';
//棋子文字水平居中对齐
me.piececxt.textAlign = "center";

me.startX = 0;//棋盘从Canvas的左起始点
me.startY = 0;//棋盘从Canvas的上起始点
me.xStep = 0;//棋盘格子横向间距
me.yStep = 0;//棋盘格子垂直间距
me.piecesList = {};//棋子列表

另外,还定义了一些动作函数:

 /*
 *绘制棋盘,确定棋盘左上角起点位置以及横向纵向间距
 */
 me.drawChessBoard = function() {};

 /*
 * 绘制单个棋子
 */
 me.drawPiece = function(word, x, y, color) {};

 /*
 * 清空棋盘
 */
 me.erasePieces = function() {};

 /*
 * 初始化棋盘
 */
 me.initPieces = function() {};

 /*
 * 重绘棋局中所有活着的棋子
 */
 me.reDrawPieces = function() {};

目前还只是把棋盘和棋子绘制了出来,没有和鼠标事件关联,也没有和服务器通讯,这些后面慢慢做。
最后,放一张截图。

棋盘