搭建简易画板(三)

本文作者为奇舞团前端开发工程师

前面搭建了一个单人可用的简易画板,这次我们实现一个多人协作画板。代码库地址

一 基于websocket实现的多人协作

主要用到的技术是 websocket 。因为websocket采取的方式是让所有客户端连接服务端,服务器将不同客户端发送给自己的消息进行转发或者广播。使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在开发方面,WebSocket API 也十分简单,我们只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息。

首先,我们创建客户端代码 client.ts。

 
 
export default class websocketManager{
static getInstance(): websocketManager {
if (websocketManager.instance == null) {
websocketManager.instance = new websocketManager();
}
return websocketManager.instance;
}
private static instance: any = null;
websocket = new WebSocket('ws://localhost:3000');
// 创建websocket链接
createWebSocket(drawFunc) {
this.websocket.onopen = function() {
console.log('开始链接')
}
this.websocket.onerror = (err) => {
console.log('websocket错误 ' + err)
// 需要重连
}
this.websocket.onclose = (err) => {
console.log('websocket 关闭 ' + err.reason)
}
this.websocket.onmessage = (event) => {
console.log('接收服务端返回的信息')
}
}
// 关闭websocket
closeWebSocket() {
this.websocket && this.websocket.close()
}
}

其次,我们基于nodejs建立下服务端代码 server.ts,用的 ws 这个库。

 
 
// 导入WebSocket模块:
const serverWebSocket = require('ws');
// 实例化:
const wss = new serverWebSocket.Server({
//端口号
port: 3000
});
wss.on('connection', function (ws) {
console.log('服务端连接');
ws.on('message', function (message) {
ws.send(message, (err) => {
if (err) {
console.log(`连接错误: ${err}`);
}
});
})
});

运行下node ./webSocket/server.ts, 可以看到控制台和终端都响应了连接。

dd989aced22c2b9a84534c7f28b10d94.png

eef24180ffc9c87dbc1e6d5be70b2e0e.png

客户端new了一个websocket对象后,会向服务器对应端口发起一个get请求。这里绑定的是3000端口,默认情况下,websocket使用80端口。后续客户端和服务端会在这个预定的端口上进行通信。

请求报文中的 upgrade 字段 是告诉服务端需要将通信协议切换到websocket,如果服务端支持websocket协议,那么它就会将请求报文中的Sec-WebSocket-Key解析出来,然后进行相应的拼接加密编码,将最后的结果作为 Sec-WebSocket-Accept 的值返回给客户端,并将自己的通信协议切换到websocket,返回状态码101。

以上过程都是利用http通信完成的,称之为websocket协议握手。握手之后,客户端和服务端就建立了websocket连接,以后的通信走的都是websocket协议。

“Sec-WebSocket-Key”是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept”应答,否则客户端会抛出“Error during WebSocket handshake”错误,并关闭连接。

建立连接之后,我们就可以进行数据传输了,websocket提供两种数据传输:文本数据和二进制数据。

回归到画板,我们开多个窗口来模拟下,每个链接后面我们拼一个用户id。然后一个用户在画的时候,每当笔抬起,就发送一次send请求,修改下绘制函数,这样其他用户能实时看到。服务端新增一个保存当前房间的大对象,里面是各个用户id下的绘图数据。

 
 
// client.ts add
this.websocket.onmessage = (event) => {
drawFunc(JSON.parse(event.data))
}
sendMessage(value) {
this.websocket.send(`${JSON.stringify(value)}`)
}
// pointer.ts add
function drawAll(userPathData) {
let canvasDom: any = document.getElementById('drawCanvas');
let curCtx = canvasDom!.getContext('2d');
let rect = canvasDom!.getBoundingClientRect();
curCtx.clearRect(rect.x, rect.y, rect.width, rect.height);
for (const key in userPathData) {
if (Object.prototype.hasOwnProperty.call(userPathData, key)) {
const pathData = userPathData[key];
pathData.map(item => {
if (Object.prototype.toString.call(item) === '[object Array]') {
item.map(info => draw(info, curCtx))
} else {
flowDraw(item, curCtx)
}
})
}
}
}
// 撤销函数更改
function undo() {
pathData.pop();
websocketManager.getInstance().sendMessage(pathData)
// let canvasDom: any = document.getElementById('drawCanvas');
// let curCtx = canvasDom!.getContext('2d');
// let rect = canvasDom!.getBoundingClientRect();
// curCtx.clearRect(rect.x, rect.y, rect.width, rect.height);
// pathData.map(item => {
// if (Object.prototype.toString.call(item) === '[object Array]') {
// item.map(info => draw(info, curCtx))
// } else {
// flowDraw(item, curCtx)
// }
// })
}
// 监听鼠标放开函数中取消绘制函数
function handleMouseMove(event) {
if (mouseButtonDown && !config.flowType) {
let singleData = {beginX: lastPt.x, beginY: lastPt.y, lastX: event.pageX, lastY: event.pageY, strokeStyle: config.strokeStyle, lineWidth: config.lineWidth, drawType: config.drawType, flowType: config.flowType};
singlePathData.push(singleData)
// draw(singleData)
lastPt = {
x: event.pageX,
y: event.pageY
}
}
if (mouseButtonDown && config.flowType) {
let flowPathData = {beginX: flowLastPt.x, beginY: flowLastPt.y, lastX: event.pageX, lastY: event.pageY, strokeStyle: config.strokeStyle, lineWidth: config.lineWidth, drawType: config.drawType, flowType: config.flowType};
tempDomDraw(flowPathData)
}
}
// useEffect函数中增加websocket逻辑
useEffect(() => {
...
websocketManager.getInstance().createWebSocket(drawAll);
return () => {
websocketManager.getInstance().closeWebSocket()
}
}, [])
// server.ts add
let allData = {};
wss.on('connection', function (ws) {
console.log('服务端连接');
ws.on('message', function (message) {
const {userID, pathData} = JSON.parse(message);
allData[userID] = pathData;
ws.send(JSON.stringify(allData), (err) => {
if (err) {
console.log(`连接错误: ${err}`);
}
});
})
});

66aa8c536b2a5fc7a6e09061a4c1c14f.gif

二、基于share实现的文件共享

Navigator.share() 方法通过调用本机的共享机制。是 Web Share API 的一部分。不过这是一个实验中的功能,浏览器兼容性不是很好。语法很简单。

 
 
/*
data可用选项包括:
url: 要共享的 URL( USVString )
text: 要共享的文本( USVString )
title: 要共享的标题( USVString)
files: 要共享的文件(“FrozenArray”)
*/
const data = {
title: document.title,
text: '简易画板分享链接',
url: document.location.href
}
const sharePromise = window.navigator.share(data);

9b0134cc09687067e20f3d1951aad68e.png

404f00fecbf7b3f0886ff4579bf3826a.png

接下来我们共享下图片。share API 的files参数需要接收 file 格式的数组,其次生成file的构造函数方法,它接收的是UTF-8 格式的编码(一种针对Unicode的可变长度字符编码)格式的数组。但canvas.toDataURL("image/png") 生成的是Data URL,由四个部分组成:前缀 (data:)、指示数据类型的 MIME 类型、Base64编码标记以及数据本身。所以我们需要将base64编码的数据转化成UTF-8再转化成file。

data URL 也是 URL,网上有base64转化成UTF-8用的是decodeURI。但亲测这个方法会报错,原因是canvas生成的data URI相对来说很长,又经过了base64的编码,一些换行符、制表符、空格的格式化会有问题。

 
 
/* file 构造函数
`bits: 一个包含ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的 Array
*/
let file = new window.File([bits], filename[, options]);
 
 
// 生成canvas图片地址
let imgUrl:any = canvas.toDataURL("image/png");
// data:image/png;base64 字符需要单独提出来
var arr = imgUrl.split(',');
// 拿到图片格式 image/png
var mime = arr[0].match(/:(.*?);/)[1];
var suffix = mime.split('/')[1];
// 解析后面的文件流
var bstr = atob(arr[1]);
var n = bstr.length;
// 初始化Uint8Array数组
var u8arr = new Uint8Array(n);
while(n--) {
// 对应位置放上相应字符的unicode编码
u8arr[n] = bstr.charCodeAt(n);
}

a07c113de1f0e5ef99c8a0501bc3d3e0.png我们把得到的file文件读取成路径形式,和咱们一开始的toDataURL得到的路径是一样的。

 
 
var reader = new FileReader();
reader.readAsDataURL(imgFile);
reader.onload = function() {
console.log(this.result == imgUrl) // true
}
 
 
navigator.share({
files: [imgFile]
})
.then(() => {
console.log('Share was successful.')
})
.catch((error) => console.log('Sharing failed', error));

739ed2dea2b842dfe16235db85afccf0.png

5ccebab122583fed80a8000c38c069fc.png

参考资料

MDN websocket

ws

MDN share

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

5564a302f7f38b477e3dd76335f5beaa.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值