#说# 到WebSocket想比大家不会陌生,如果用一句话概括,那就是:

WebSocket protocol是HTML5一种新的协议,它实现了浏览器与服务器全双工通信。

相比较传统那些服务器,WebSocket的推送技术简直好了太多。

有了它,我们可以挥手向comet、长轮询这些技术说再见了,庆幸我们生活在拥有HTML5的时代。

得益于WebSocket,“移动+”在它的基础上实现了协同开发功能。

下面将分三个部分讲解“移动+”的协同开发功能:协同开发功能介绍,移动+协同原码解析,开发中踩过的坑。

1 协同开发功能介绍

“移动+”支持多人同时开发一个app,那么就涉及锁、合并、同步的问题。

还有一个最常见的问题是,如果1个以上的开发人员同时开发一个页面,那么可能其中一个人的修改会被另一个人的修改冲掉。

因此,我们提出了使用websocket来解决协同开发引发的这一系列问题。

功能目标

1、当1个以上用户同时开发同一个应用时,以页面为单位实现锁定,只允许1个用户编辑一个页面。其它用户被通知页面被锁定,并禁止编辑该页面。

2、当正在编辑的用户完成编辑保存时,通知其它用户页面开放 ,并更新被修改过的页面。

实现效果

页面没有编辑操作时,所有用户界面均无变化

当其中1个用户对页面进行操作时,其它用户的相应界面被锁定

当前用户操作完成保存后,其它用户的相应界面被修改

2 移动+协同功能原码解析

移动+ webSocket 后台是基于node开发的,主要使用ws的模块。

安装 :npm install ws

官方示例代码

启动一个WebSocket服务

var WebSocketServer = require('ws').Server
, wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
	ws.on('message', function incoming(message) {
		console.log('received: %s', message);
});
ws.send('something');
});

前端Client js代码

var WebSocket = require('ws');var ws = new WebSocket('ws://www.host.com/path');

ws.on('open', function open() {
	ws.send('something');
});

ws.on('message', function(data, flags) {
		// flags.binary will be set if a binary data is received.
		// flags.masked will be set if the data was masked.
});

ws模块使用很简单,启动服务设下message监听数据即可,主要的逻辑就是在message是对消息的封装、解包。

“移动+”coffee后台代码

ws-server.js 文件websocket 服务关键代码

引入ws模块

ws = require('ws')
WsServer = ws.Server

定义SocketServer 服务类:以server服务类为启动服务,这样可以统一http端口,避免前端跨域问题,path配置主要是生产中nginx代理拦截作用。

verifyClient 连接验证回调。

SocketServer = (server) ->
	_this = this
	this.wss = new WsServer
		server: server
		clientTracking: true
		path: '/ws'
		# 连接验证
  		verifyClient: (info) ->
    			# params = _this.getParams info.req.url
    			return true

connection连接成功事件中监听message,判断数据流根据操作标识operate处理client连接。

数据流结构是自定义的,具体可以根据前端业务定义,这里主要定义了operate的四种状态:login,lock,look,close。

四种状态对应用户对应用户操作流程,具体看下面的流程图:

this.wss.on 'connection', (ws) ->
    ws.on 'message', (data) ->
      return unless this.upgradeReq

      _data = JSON.parse data
      # 操作流
      switch _data.operate
        when "login"
          list = {}
          return unless list
          # 发送协作清单
          this.send JSON.stringify list

        when "lock"
          this.info = _data
          _this.broadcast (JSON.stringify _data), this.app_id, this.user_id

        when "look"
          this.info = _data
          _this.broadcast (JSON.stringify _data), this.app_id, this.user_id

        when "close"
          _this.broadcast (JSON.stringify _data), this.app_id, this.user_id

    # 连接关闭
    ws.on 'close', (code, message) ->
      _data = this.info
      return unless _data

      _data.operate = 'close'
      _this.broadcast (JSON.stringify _data), this.app_id, this.user_id

    # 连接错误
    ws.on 'error', (code, message) ->
      _data = this.info
      return unless _data
      _data = JSON.parse _data
      _data.operate = 'close'
      _this.broadcast (JSON.stringify _data), this.app_id, this.user_id

  return this
# 广播信息
SocketServer.prototype.broadcast = (data) ->
  this.wss.clients.forEach (client) =>
    client.send(data)

启动node http服务使用http server启动ws

 WsService = require 'ws-service'
 http = require 'http'
 express = require 'express'

 app = express()
 server = http.createServer(app)
 server.listen process.env.PORT || 3000
 new WsService(server)

ws-client.js 关键代码对应服务端数据包

function _init() {
  var host = window.document.location.host;
  this.ws = new WebSocket('ws://'+host+'/ws?id=xx&uid=xx');
  this.ws.onmessage = this.onmessage.bind(this);

  this.ws.onclose = function() {
    return null;
    console.info('WsClient 关闭....');
    this.intervalId = setInterval(function() {
      // this.ws.Reconnect();
      //console.info("WsClient 尝试重连");
    }.bind(this), 5000)
  }.bind(this);

  this.ws.onopen = function() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = 0;
    }
    // 发送登录
    this.send({operate: "login"});
  }.bind(this);
  this.ws.onerror = function(err) {
    console.error(err);
  }
 }

function onmessage(event) {
  var data = event.data;
  if (!data)
    return ;
  data = JSON.parse(data);
  switch(data.operate){
    case "setcookie":
      setCookie(data.key, data.value, 24*60);
      break;
    case "close”:
      var _temp = _.findWhere(this.listData.items, {page_id: data.page_id, user_id: data.user_id});
      this.listData.items.splice(_index,1);
      break;
    case "look":
    case "lock":
      // 更新or新增用户状态
			var _temp = _.findWhere(this.listData.items, {page_id: data.page_id, user_id: data.user_id});
      if (_temp){
         _temp.operate = data.operate;
      } else {
        this.listData.items.push(data);
      }
      break;
    case "unlock":
      this.operate = "look"
      var _page = this.props.app.pages.findWhere({id: data.page_id});
      break;
    case "list”:
      //下拉列表
      this.listData = data;
      this.checkPageStateSend();
      break;
  }
}

function send(data) {
		if (!this.ws) return;
		this.ws.readyState == this.ws.OPEN && this.ws.send(JSON.stringify(data));
}

在整个流程中,主要逻辑都是在message监听中完成。ws-server负责管理所有协同数据的分发,ws-client负责当前用户的数据封装和解包服务推送的指令更新界面状态。

3 踩过的坑

跨域

这个问题估计做web前端开发都会遇到过,这是常见却经常踩的坑啊。

刚开始websockt服务是独立端口的,虽然是同一个域名下,但是不同的端口也是跨域;在移动+的协同开发中结合express框架,启动服务的时候统一在一个端口解决。

location /ws {
          proxy_pass http://appbricks;
          proxy_redirect off;

          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "upgrade";
  }

部署生产nginx代理问题

部署生产nginx代理问题,nginx5.x以后就支持,网上搜一下配置解决,主要是启动统一拦截(/ws)匹配还是花了点时间,主要是ws模块二级目录pathg添加之后连接不成功,可能是与跨域问题滚在一起了,最后解决跨域之后二级目录就可以了。