1.技术栈说明
- 载体:此部分的代码在React Next都可以轻松迁移转换。
- 表格插件:利用konva实现,协作提示相关使用多层layer来展示。
- 服务端:Nest.js 也集合成了socket.io 喜欢依赖注入的方式来书写.
2.实现的效果 多人协作
-
腾讯文档的:
-
自己实现的
3.进入正题 前端部分
- 通过上面的截图,我们分析出有用的信息是些什么呢。要表示一个用户的状态,需要些什么数据
export interface User {
// 连接时间
loginTime: string;
// 唯一id
userID: string;
// 分配一个标识color
color: number;
// 用户选择的 cells
cells: [number, number, number, number]
// editing 是否正在编辑
editing : boolean;
// 用户名称
name : string
}
- 有了描述,就需要渲染。这里我们采用konva画布来实现。
单个用户的大概就这样。看过我上篇文章的应该知道。我们还需要一个manager来管理 前端部分的 增删改查。我就简要的实现一个更新吧。待会儿协作通知的实现需要用到这个class来更新通知每一个range。
3.React dom结构搭建.
export function Container(){
return (
<LayerContianerProvider>
<SocketProvider>
<SmartSheet />
</SocketProvider>
</LayerContianerProvider>
)
}
import { useUnmount } from 'ahooks'
import { createContext, PropsWithChildren } from 'react'
import { SocketClient } from './socket'
import { useSmartSheetInstance } from '../smartsheet';
import { RangeManager } from '@/views/konvaCanvas';
const SocketClientContext = createContext<SocketClient>({} as SocketClient);
export function SocketProvider({ children }: PropsWithChildren) {
const core = useSmartSheetInstance();
const userManager = useState(() => new RangeManager())[0];
const [store] = useState(() => new SocketClient(userManager, core));
useUnmount(() => {
store.destory();
})
return <SocketClientContext.Provider value={store}>{children}</SocketClientContext.Provider>
}
export function useScoket() {
return useContext(SocketClientContext)
}
着重关注 SocketClient即可 与后台打交道。前端的代码就到这里了,等下连接socket的时候再对照讲下具体的。
4。后台nest部分( 需要对nest熟悉 )
- 使用redis来缓存房间协作的信息。为了适配有的用单节点 | 分布式环境 | 哨兵模式。这里我们需要实现一下 WebSocketAdapter。
来看看协作我们需要怎么定义数据结构 待会就操作redis中的数据即可。
注册一个自定义的redis模块和服务。
这个服务其实就是处理房间 用户 分配颜色的一个管理器。
- SocketRedisRooms => 增删改查房间 和 管理 用户 | 颜色
- Colors => 增删 用户颜色
- Users => 增删 用户 和 用户的集合
其实无非就是对redis hash Set数据的增删改查罢了。
前端携带握手参数 发起socket连接。
后台在中间件中验证握手参数并处理 协作房间的初始化操作。主要就是生成房间,生成一个用户,生成color。
中间件验证完成 就要去处理socket网关服务。
然后前端处理接收事件
来前端部分正式连接一下试试。顺便看看nest服务操作redis成功没。
能成功连接并写入缓存数据了。此时需要与表格插件接洽了。将用户操作表格的事件与socket对应起来。我表格插件会向外界传递用户的操作事件,我只需要监听一下selectionMove事件即可,然后当前socket端传递到服务端,有服务端去更新当前用户的选择区域并广播给其他房间内的用户。
export class SocketClient {
private socket!: Socket;
private readonly userid = localStorage.getItem('userid') || Math.random() * 1000 + '';
public users = observable.map<string, User>();
constructor(
private readonly rangeManager: RangeManager,
// 表格插件
private readonly smartSheetCore: any
) {
// 只观察 users
makeObservable(this, { users: observable });
// 监听选择区域
this.smartSheetCore.on('selectionMove', (event: { cells: any }) => {
if (!this.socket) {
return;
}
// 触发的太频繁了 为了减轻redis服务器的压力 最好加个节流
this.socket.emit(SocketEventKey.smartsheet, {
cells: event.cells,
event: SmartSheetEventKey.selection,
sheetId: '123'
})
})
}
服务端只需要处理 更新redis缓存和广播即可。
其它客户端接收到变更 需要更新用户在界面上的选择区域。
const { socket } = this;
// 连接
socket
.on('connect', () => {
// 自己加入房间 并收到 已在房间内的所有协作用户信息
socket.on(SocketEventKey.room, (users: User[]) => this.addUserRange(users));
// 新加入协作房间的用户
socket.on(SocketEventKey.joined, (user) => {
message.info(user.userID + '加入了房间!')
this.addUserRange(user);
});
// 表格操作
socket.on(SocketEventKey.smartsheet, (event: Events) => {
switch (event.event) {
// 用户选择区域变更 (新增 | 修改)
case SmartSheetEventKey.selection:
const userGroup = this.smartSheetCore.find(`${this.userid}-selection`);
if (!userGroup) {
// this.rangeManager.createSelectionRange();
return;
}
// 构建一个user
this.rangeManager.updateRange(this.userid, event.cells as any);
break;
// 修改单元格内容
case SmartSheetEventKey.changCellText:
break;
// 修改列宽
case SmartSheetEventKey.changeColumnWidth:
break;
}
})
})
// 连接失败
.on('error', (error) => {
console.log(' error', error)
})
// 断开连接
.on('disconnect', (reason: string) => {
console.log('断开连接')
})
// 中间件验证失败
.on('connect_error', (error) => {
console.log('中间件验证失败', error.message)
})
rangeManager的作用就在这里体现了,它接管了数据去更新konva的Group的具体界面表现。这里只是简单的讲解了一下使用。
如果某个用户主动离开房间呢? 我们也无非就是广播房间内的其他用户和更新redis的缓存 将这个用户删除即可.
到目前为止,多人协作就完成了。
当然了 协作的问题还需要很多问题去处理
- 协作冲突的问题( 多个用户编辑同一单元格 )数据如何取舍整合?
- 某些用户协作过程中 网络中断 | 网络延迟过高。恢复后如何保持操作同步和数据同步? 这些问题就不展开了,我只是带大家入个门。
简单回答一下