一、前言
初探在线文档门槛。
项目:
项目地址,运行:
$ npm install
$ npm start
> ot.js-demo@1.0.0 start ot.js-demo
> node server.js
listening on *:3000
先来看看效果:
可以看到基本可实现多人在线共同编辑。
通过
webosocket进行交互。
二、源码分析
项目结构图:
├─server.js # 服务端
├─index.html # 入口页面
├─package.json # 包依赖信息
├─node_modules # 打包后
└─ot.js # 目录
├─ajax-adapter.js #
├─client.js #
├─codemirror-adapter.js #
├─editor-client.js #
├─editor-socketio-server.js #
├─index.js #
├─selection.js #
├─server.js #
├─simple-text-operation.js #
├─socketio-adapter.js #
├─text-operation.js #
├─undo-manager.js #
└─wrapped-operation.js #
直接从以下文件入手:
server.jsindex.html
server.js 代码如下:
// 一个服务端框架罢了
var express = require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
// localhost:3000 -> index.html
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
// 对外提供访问
app.use('/ot.js', express.static('ot.js'));
app.use('/node_modules', express.static('node_modules'));
// 监听端口
http.listen(3000, function(){
console.log('listening on *:3000');
});
var EditorSocketIOServer = require('./ot.js/editor-socketio-server.js');
var server = new EditorSocketIOServer("", [], 1);
// socket 连接就调用
io.on('connection', function(socket) {
server.addClient(socket);
});
server.js 让人摸不着头脑的就是:
socket.io:一个面向实时web应用的JavaScript库,支持websocketEditorSocketIOServer:只是对socket的操作封装,暂且按下不表。
那么就先来了解下 socket.io,再来了解 EditorSocketIOServer。
(1)socket.io
直接从 官网 入手。
集成 socket.io 只需要两步:
- 服务端添加依赖
socket.io - 客户端在浏览器端使用
socket.io的client
简单介绍下:
- 服务端添加依赖
socket.io
添加依赖:
npm install socket.io
const express = require('express');
const app = express();
const http = require('http');
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', (socket) => {
console.log('a user connected');
});
server.listen(3000, () => {
console.log('listening on *:3000');
});
- 客户端在浏览器端使用
socket.io的client
在
index.html中添加对应js脚本
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>
1. 收发事件 Emitting events 和 socket.on()
通过
socket.io发送和接收事件和数据。
数据支持:
JSON- 二进制数据
接收需要绑定对应的事件类别
message-type。
举个栗子:chat message 就是对应事件类别
html中js脚本
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
var form = document.getElementById('form');
var input = document.getElementById('input');
form.addEventListener('submit', function(e) {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});
</script>
- 在
server端定义处理对应事件
io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
console.log('message: ' + msg);
});
});
2. 广播事件 emit
广播:将事件发送给任何人。
io.emit('some event', { someProperty: 'some value', otherProperty: 'other value' });
// This will emit the event to all connected sockets
举个栗子:
server端
io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
});
html中
<script>
var socket = io();
var messages = document.getElementById('messages');
var form = document.getElementById('form');
var input = document.getElementById('input');
form.addEventListener('submit', function(e) {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});
socket.on('chat message', function(msg) {
var item = document.createElement('li');
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
});
</script>
3. join 加入房间 & leave 离开
- 加入房间
socket.join(room)
io.on("connection", (socket) => {
console.log(socket.rooms); // Set { <socket.id> }
socket.join("room1");
console.log(socket.rooms); // Set { <socket.id>, "room1" }
});
- 离开房间
socket.leave(room)
io.on("connection", (socket) => {
socket.leave("room 237");
io.to("room 237").emit(`user ${socket.id} has left the room`);
});
4. broadcast 修饰事件发射
官方栗子如下:
io.on("connection", (socket) => {
// 除了发送方,其他人都能收到
socket.broadcast.emit("an event", { some: "data" });
});
那么来看下 ot demo 中:
socket.broadcast['in'](this.docId).emit('selection', clientId, selection);
- 这个
['in']官网也没有示例。黑魔法? - 暂且盲猜猜:
in就是字面意思,在对应房间(this.docId)进行广播
(2)EditorSocketIOServer
直接看
editor-socketio-server.js的源码。
看先引用部分:
// 引入依赖
// 1. Node.js 相关
// events 模块只提供了一个对象: events.EventEmitter。
// EventEmitter 的核心就是事件触发与事件监听器功能的封装。
var EventEmitter = require('events').EventEmitter;
// util 是一个Node.js 核心模块,提供常用函数的集合,用于弥补核心 JavaScript 的功能 过于精简的不足。
var util = require('util');
// 2. 下面这些都是 ot 相关的
var TextOperation = require('./text-operation');
var WrappedOperation = require('./wrapped-operation');
var Server = require('./server');
var Selection = require('./selection');
1. const util = require('util'); 使用
util是一个Node.js核心模块,提供常用函数的集合,用于弥补核心JavaScript的功能 过于精简的不足。
可看对应 文档
util.inherits(constructor, superConstructor)是一个实现对象间原型继承的函数。
JavaScript的面向对象特性是基于原型的,与常见的基于类的不同。JavaScript没有提供对象继承的语言级别特性,而是通过原型复制来实现的。
那么再看 editor-socketio-server.js 中有:
// 使 EditorSocketIOServer 继承 Server
util.inherits(EditorSocketIOServer, Server);
// 调用 extend 方法
extend(EditorSocketIOServer.prototype, EventEmitter.prototype);
function extend (target, source) {
// 遍历
for (var key in source) {
// 是否有对应的 key
// 感觉这块判断就多余了,直接赋值就行
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
2. 定义对象 EditorSocketIOServer
咋一看这代码一脸懵逼:这不是函数嘛?怎么搞的和对象一样。
function EditorSocketIOServer (document, operations, docId, mayWrite) {
// 1.
EventEmitter.call(this);
Server.call(this, document, operations);
this.users = {};
this.docId = docId;
// 2. 若 mayWrite 有值,则赋值 mayWrite
// 若 mayWrite 无值,则赋值 方法
this.mayWrite = mayWrite || function (_, cb) { cb(true); };
}
- 先来看下这个
call啥意思
答案:继承,调用
super(),调用父类构造方法,赋值父类属性 来个demo,解释下:demo来源
// 假如有个 EventEmitter 构造器方法
function EventEmitter(){
this.foo = 'foo';
this.bar = 'bar';
}
// Dog 构造器方法
function Dog(name) {
this.name = name;
EventEmitter.call(this);
}
// 创建 Dog
var newDog = new Dog('furball');
// 可以输出 狗名:name
newDog.name; //furball
// 这就获取父类 EventEmitter 属性
newDog.foo; //foo
newDoc.bar; //bar
- 再来看下
function (_, cb) { cb(true); }
旁白:额。。。
// cb 一种写代码风格
// 1. 直接风格写法
function getAvatar(user){
//...一些程式碼
return user
}
function display(avatar){
console.log(avatar)
}
const avatar = getAvatar('eddy')
display(avatar)
// 2. cb:CPS 风格写法
function getAvatar(user, cb){
//...一些程式碼
cb(user)
}
function display(avatar){
console.log(avatar)
}
getAvatar('eddy', display)
(3)index.html 入手
直接从源码入手:
<body>
<textarea id="note"></textarea>
<!-- init client -->
<script>
<!-- 从 socket.io 这里创建 -->
var socket = io()
<!-- 监听 doc的事件 -->
socket.on('doc', function(data) {
<!-- 创建编辑器 -->
var cm = CodeMirror.fromTextArea(document.getElementById('note'), {lineNumbers: true})
<!-- 设置数据。这里是全量数据?不是全量数据,那必定会指定位置吧! -->
cm.setValue(data.str)
<!-- 对 socket.io 的封装,处理一些事件 -->
var serverAdapter = new ot.SocketIOAdapter(socket)
<!-- 略 -->
var editorAdapter = new ot.CodeMirrorAdapter(cm)
<!-- 创建 editor-client -->
var client = new ot.EditorClient(data.revision, data.clients, serverAdapter, editorAdapter)
})
</script>
</body>
三、ot 算法
(1)可视化演示
根据 可视化文档。
网站中两个客户端分别为 Alice 和 Bob,这两者之间进行协同操作。
举个栗子,来看下 ot 算法如何处理:
Alice和Bob刚进入此文档,初始文档为Lorem ipsumAlice在语句末尾写入y,并发送给服务端Bob删除语句末尾的m,并发送给服务端Alice和Bob收到服务端消息
接收到数据后:
服务端接收操作请求数据先后:Alice(蓝色)、Bob (红色)。
Alice 操作过程:
- 发送操作事件
- 服务器接收事件,同步
Alice(蓝色)事件 - 服务器同步
Bob(红色)事件
Bob 操作过程:
- 发送操作事件
- 服务器同步
Alice(蓝色)事件 - 服务器同步
Bob(蓝色)事件
基于版本进行操作。
(2)源码解析
操作样例,如下:
// 定义两种操作
// 1. 先保留(移动光标),再插入
var operation0 = new ot.Operation()
.retain(11)
.insert(" dolor");
// 2. 先删除,再保留
var operation1 = new ot.Operation()
.delete("lorem ")
.retain(11);
// 初始数据
var str0 = "lorem ipsum";
// 应用操作
var str1 = operation0.apply(str0); // "lorem ipsum dolor"
var str2a = operation1.apply(str1); // "ipsum dolor"
ot 算法,主要操作有:
retain保持:移动光标到某个位置,输入正整数insert插入:在光标之后插入字符串delete删除:删除光标之后的字符串
交互中,服务端示例:
var server = new ot.Server("lorem ipsum");
server.broadcast = function (operation) {
// you have to broadcast the operation to all connected
// clients including the one that the operation came from
};
// when you receive an operation as a JSON string from one of the clients, do:
function onReceiveOperation (json) {
var operation = ot.Operation.fromJSON(JSON.parse(json));
}
交互中,客户端示例:
var client = new ot.Client(0); // the client joins at revision 0
client.applyOperation = function (operation) {
// apply the operation to the editor, e.g.
// operation.applyToCodeMirror(cm);
};
client.sendOperation = function (operation) {
// send the operation to the server, e.g. with ajax:
$.ajax({
url: '/operations',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(operation)
});
};
// 用户改变数据时
function onUserChange (change) {
var operation = client.createOperation(); // has the right revision number
// initialize operation here with for example operation.fromCodeMirrorChange
client.applyClient(operation);
}
// 当接收到服务端数据时
function onReceiveOperation (json) {
var operation = ot.Operation.fromJSON(JSON.parse(json));
client.applyServer(operation);
}
ot核心代码,如下:
// Call this method whenever you receive an operation from a client.
Server.prototype.receiveOperation = function (revision, operation) {
if (revision < 0 || this.operations.length < revision) {
throw new Error("operation revision not in history");
}
// Find all operations that the client didn't know of when it sent the
// operation ...
var concurrentOperations = this.operations.slice(revision);
// ... and transform the operation against all these operations ...
var transform = operation.constructor.transform;
for (var i = 0; i < concurrentOperations.length; i++) {
operation = transform(operation, concurrentOperations[i])[0];
}
// ... and apply that on the document.
this.document = operation.apply(this.document);
// Store operation in history.
this.operations.push(operation);
// It's the caller's responsibility to send the operation to all connected
// clients and an acknowledgement to the creator.
return operation;
};
如上,可分为三个步骤:
- 获取当前所有操作
- 转换:根据当前服务器的操作,对接收到的操作进行转换
- 存储操作