koa+socket.io尝试简单的web动作同步

动作同步

尝试用过browser-sync辅助开发的前端同学,大概都会感到神奇:在多个端打开网页,网页的动作却是完全同步的

当然我还没有看过源码,大概也知道是通过’websocket’实现信息同步。 今天在看书籍《跨终端Web》–徐凯 的时候,里面有一部分 web动作同步的代码演示。于是做了demo做练习。

准备工具

koa : @1.1.2 socket.io : @1.5.0

其中koa并没有什么特别意思,只是作为一个服务器存在,用express或者其他什么都可以。 socket.io是我们需要的通讯库

原理

1、 前端捕获正在发生的动作action,和触发动作的元素target,通过客户端socket传输到服务器socket; 2、 服务器socket接收到信息,再将信息广播到其他所有客户端socket; 3、 其他客户端socket接收到广播信息,使特定的元素target触发特定的动作action

服务端准备

服务器的准备很简单:搭起服务器,接入socket.io

1、 首先利用koa-generator搭起一个koa程序.

koa -e web-transcribe
cd web-transcribe && npm install

2、 接入socket.io

npm install --save socket.io

接着在./bin/下,新建一个js文件io-server.js

//io-server.js
var io=require('socket.io')();
exports.listen= function (_server) {
    return io.listen(_server);
};
io.on('connection', function (_socket) {
    console.log('connection:\t' + _socket.id );
    //接收客户端信息
    _socket.on('send', function (json) {
        //广播到其他客户端
        _socket.broadcast.emit('get',json);
    })
});

在’./bin/www’中加入io-server.js的引用

var app = require('../app');
var debug = require('debug')('demo:server');
var http = require('http');
//加下面这一句,接入io-server.js
var io = require('./io-server');

var port = normalizePort(process.env.PORT || '3000');

var server = http.createServer(app.callback());

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
//加下面这一句,使socket.io监听在server上
io.listen(server);
//...

客户端的准备

客户端的准备要比较多。

1、 准确捕获发生的动作和目标元素 2、 通过socket发送出去 3、 接受socket,并使目标元素促发动作。

1、 准确捕获发生的动作和目标元素

正在发生的动作相对比较好捕获,因为只是简单的实验,我只做了click动作的捕获。 如何确定正在发生动作的元素呢。 编写如下代码

//transcribe.js
//确定正在发生动作的元素
function getSelector (element) {
    var tagName = element.tagName.toLowerCase();
    //去空格
    function trim(string) {
        return string && string.replace(/^\s+|\s+$/g,"") || string;
    }

    //id绑定的,直接返回。
    if (element.id){
        return '#' + element.id;
    }
    //html
    if (element == document || element == document.documentElement ){
        return 'html';
    }
    //body
    if (element == document.body) {
        return 'html>' + tagName;
    }
    //无父级元素,则返回自己
    if (!element.parentNode) {
        return tagName;
    }

    //最后是有父类元素的情况下,确定target是同种兄弟元素的第几个,返回 parentNode > childNode 的精确形式
    var ix = 0,
        siblings = element.parentNode.childNodes,
        elementTagLength = 0,
        className = trim(element.className);

    //统计同种兄弟元素
    for (var i = 0, l = siblings.length; i < l; i++){
        if (className) {
            if(trim(siblings[i].className) === className){
                ++elementTagLength;
            }
        }
        else {
            if ( (siblings[i].nodeType == 1) && (siblings[i].tagName === element.tagName) ) {
                ++elementTagLength;
            }
        }
    }
    //确定target是父类元素下的第几个兄弟元素
    for (i = 0, l = siblings.length; i < l; i++){
        var sibling = siblings[i];
        if (element === sibling) {
            return arguments.callee(element.parentNode) + '>'
                + ( className
                    ? '.' + className.replace(/\s+/g,',')
                    : tagName)
                + ( (!ix && elementTagLength === 1)
                    ? ''
                    : ':nth-child(' + (ix + 1) + ')');
        }
        else if (sibling.nodeType == 1) {
            ix++;
        }
    }

};

最终返回的是id || class || targetName

我们需要做一些优化,筛选掉无用的事件,减少socket传输量。 在一个html的<head>内写入以下代码,改造addEventListener,为调用过addEventListener的元素加入标识。

<script>
  var __addEventListener = Element.prototype.addEventListener;
  Element.prototype.addEventListener = function (type, handler, capture) {
    if(!this['events']){
      this['events'] = {};
    }
    this['events'][type] = 1;
    return __addEventListener.apply(this, [type, handler, capture]);
  }
</script>

并且编写一个函数做辨识

//transcribe.js
var findHashEventsElements = function (ele, eventType) {
    if (!ele.tagName) return null;
    // 有events标识的 或者 html标签内绑定的 或者 js直接绑定的
    if ( (ele['events'] && ele['events'][eventType]) || ele.hasAttribute("on" + eventType) || ele['on' + eventType] ) {
        return ele;
    }
    else {
        if (ele.parentNode != null) {
        //可能有事件委托,追溯上一层
            return findHashEventsElements(ele.parentNode, eventType);
        }
        else {
            return null;
        }
    }
};

2、 通过socket发送正在发生的动作和目标元素

正式做click动作的全局时间监听

//transcribe.js
//启动socket连接
var socket = io();
document.addEventListener('click', function (e) {
    if (e.button === 0) {
        var target = e.target,
            enable = findHashEventsElements(target, 'click');

        if(enable){
        //发送socket信息哦
            socket.emit('send', {
                action: 'click',
                target: getSelector(target),
                time: +new Date()
            })
        }
    }
},true);

3、 接受socket,并使目标元素促发目标动作。

新建一个文件trigger.js,写入促发代码

//trigger.js
socket.on('connect', function () {
    console.log('connect');
    socket.on('get', function (data) {
        console.log("trigger\t"+data.target+"\t"+data.action);
        document.querySelector(data.target).dispatchEvent(new Event(data.action));
    })
});

之前这里,我使用了JQuerytrigger:

$(data.target).trigger(data.action)

但是这样会造成页面间的重复触发。 就比如 a.html触发了 click; b.html页面收到指令,也触发 click,结果这个click又重新发送socket到b.html; b.html收到指令,又触发click,结果这个click又重新发送socket到a.html; 这样反复,永不停歇。 看来JQuerytrigger是直接操作元素触发的,于是改用原生的dispatchEvent。完成

效果图

合并源代码到io.js. 编写简单的html,完成一个demo

<!DOCTYPE html>
<html>
  <head>
    <script>
      var __addEventListener = Element.prototype.addEventListener;
      Element.prototype.addEventListener = function (type, handler, capture) {
        if(!this['events']){
          this['events'] = {};
        }
        this['events'][type] = 1;
        return __addEventListener.apply(this, [type, handler, capture]);
      }
    </script>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
    <button id="btn1">blue</button>
    <button onClick="body_color('red')">red</button>
    <button class="reset_btn">reset</button>
    <script>
      document.getElementById('btn1').addEventListener('click', function () {
        body_color('blue')
      })
      document.querySelector(".reset_btn").onclick= function () {
        body_color('none');
      }
      function body_color(color) {
          document.body.style['background'] = color;
      }
    </script>
    <script src="/javascripts/io.js"></script>
  </body>
</html>

源代码

结尾

现在只是最简单的click传输。 之后完善代码,就可以捕获其他动作。 完善服务器,做成代码植入的形式。 玩起来还是乐趣无穷的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java成神之路

js_调试_01_14 个你可能不知道的 JavaScript 调试技巧

了解你的工具在完成任务时有很重要的意义。 尽管 JavaScript 是出了名的难以调试,但是如果你掌握了一些小技巧,错误和 bug 解决起来就会快多了。

12830
来自专栏落影的专栏

iOS开发笔记(六)

前言 专注、坚持,是优良的品格。 正文 1、cell和cell.contentView 的区别 在给UITableViewCell添加视图的时候,我们有以下两种...

34350
来自专栏极乐技术社区

用 RxJS、RxWX 编写微信小程序

RxJS RxJS是微软推出的ReactiveX系列,符合纯函数特点的第三方开源库有非常著名underscore和lodash,以及更加强大的RxJS。它可以用...

47380
来自专栏GIS讲堂

lzugis——Arcgis Server for JavaScript API之POI

POI(Point Of Interest),感兴趣点,其实呢,严格意义上说应该不是POI,但是单位就这样叫了,我也就这样叫了,其实现的功能大致是这样的:用过百...

11720
来自专栏数据库

基于关系型数据库的App Inventor网络应用(3)

第三节 初识Node-RED 开发环境简介 如图8所示,整个浏览器窗口被划分为四个部分: (1) 顶部黑色通栏,左侧显示Node-RED的LOGO,右侧显著位置...

35470
来自专栏Flutter入门

Flutter入门三部曲(1) - 基础认识

image.png 看到整体的架构图,它是由dart完成上层的framework,然后由通过skia来完成图形的绘制。

1.6K60
来自专栏java思维导图

Intellij IDEA神器那些让人爱不释手的14种小技巧,统统告诉你!

来源:https://blog.csdn.net/linsongbin1/article/details/80560332

10450
来自专栏ThoughtWorks

你不知道的高性能JAVASCRIPT | TW洞见

想必大家都知道,JavaScrip是全栈开发语言,浏览器,手机,服务器端都可以看到JS的身影。 本文会分享一些高效的JavaScript的最佳实践,提高大家对...

35350
来自专栏Flutter入门

Flutter入门三部曲(1) - 基础认识

看到整体的架构图,它是由dart完成上层的framework,然后由通过skia来完成图形的绘制。

16900
来自专栏web前端教室

1228-redux学习笔记(摘录) | WEB前端零基础课

今天的WEB零基础课的内容是Redux,它的思路比较“拧”,不怎么好理解,算是react里的一个难点。讲完之后,同学们纷纷表示没听懂, 这个东西只靠听,肯定是搞...

201100

扫码关注云+社区

领取腾讯云代金券