손쉬운 협업 자바스크립트 스프레드시트 만들기

에서는 FortuneSheet 을 사용하여 javascript 스프레드시트를 만드는 방법을 소개했습니다. 이번에는 협업 기능을 활성화하는 방법을 보여드리겠습니다.



백엔드



주제에 들어가기 전에 가장 먼저 고려해야 할 사항은 백엔드 저장소입니다. 다른 사람이 페이지에 들어갈 때 최신 시트 상태를 볼 수 있도록 시트 데이터를 저장해야 하기 때문입니다.

Websocket은 클라이언트 간에 라이브 데이터를 교환하는 주요 전송 수단입니다. 여기서는 Express를 백엔드 서버로, MongoDB를 데이터베이스로 선택합니다. 익스프레스 서버의 주요 역할은 웹 소켓 연결을 관리하고 초기 데이터를 제공하며 증분op 메시지를 처리하는 것입니다.

이 데모에서는 컬렉션의 모든 문서를 데모 통합 문서의 시트로 사용합니다.

async function getData() {
  const db = client.db(dbName);
  return await db.collection(collectionName).find().toArray();
}


간단한 익스프레스 서버를 만드는 코드는 건너뛰고 핵심 코드에 집중하겠습니다. 걱정하지 마세요. 이 게시물의 끝에서 전체 코드를 찾을 수 있습니다.

그런 다음 websocket 메시지를 처리하기 위한 코드를 추가하고 브로드캐스팅을 위한 연결을 관리합니다.

const connections = {};

const broadcastToOthers = (selfId, data) => {
  Object.values(connections).forEach((ws) => {
    if (ws.id !== selfId) {
      ws.send(data);
    }
  });
};

const wss = new SocketServer({ server, path: "/ws" });

wss.on("connection", (ws) => {
  ws.id = uuid.v4();
  connections[ws.id] = ws;

  ws.on("message", async (data) => {
    const msg = JSON.parse(data.toString());
    if (msg.req === "getData") {
      ws.send(
        JSON.stringify({
          req: msg.req,
          data: await getData(),
        })
      );
    } else if (msg.req === "op") {
      await applyOp(client.db(dbName).collection(collectionName), msg.data);
      broadcastToOthers(ws.id, data.toString());
    }
  });

  ws.on("close", () => {
    delete connections[ws.id];
  });
});


여기서 applyOp 기능은 협업의 핵심입니다. 프론트엔드 라이브러리에서 보낸 Op 를 읽고 데이터베이스에 데이터 변형을 수행합니다.

처리 작업


OpImmer.js 의 패치에 의해 생성됩니다. 예를 들어 사용자가 셀 A2에서 셀 글꼴을 굵게 설정했을 때의 연산입니다.

[
    {
        "op": "replace",
        "index": "0",
        "path": ["data", 1, 0, "bl"],
        "value": 1
    }
]


이 작업을 MongoDB 업데이트 쿼리로 변환해야 합니다. 직접 변환하면 결과는 다음과 같습니다.

db.updateOne(
  { index: "0" },
  { $set: { "data.1.0.bl": 1 } }
);


그러나 저장소 크기를 고려하여 셀 데이터를 데이터베이스에 드물게 저장합니다. 즉, 전체 2차원 셀 배열을 저장하는 대신 값을 포함하는 셀의 1차원 배열을 저장합니다. 따라서 데이터베이스의 셀은 다음과 같은 형식입니다.

{
   r: number, // row index
   c: number, // column index
   v: any, // cell value
}


위의 업데이트 쿼리는 다음과 같습니다.

db.updateOne(
  { index: "0" },
  { $set: { "celldata.$[e].v.bl": 1 } },
  { arrayFilters: [{ "e.r": 1, "e.c": 0 }] }
);


시트의 다른 필드에 대한 업데이트도 비슷합니다.

이것이 우리 백엔드 서버의 전부입니다. 전체 코드는 https://github.com/ruilisi/fortune-sheet/tree/master/backend-demo을 참조하십시오.

프런트엔드



이제 프런트엔드 부분에 초점을 맞추겠습니다. 매우 간단합니다.

1단계, websocket 연결을 만듭니다.

const wsRef = useRef<WebSocket>();

useEffect(() => {
  const socket = new WebSocket("ws://localhost:8081/ws");
  wsRef.current = socket;

  socket.onopen = () => {
    socket.send(JSON.stringify({ req: "getData" }));
  };
}, []);


2단계, 로컬 변경에서 op를 보내고 다른 사람으로부터 op를 받아 통합 문서에 적용합니다.

// A ref of Workbook
const workbookRef = useRef<WorkbookInstance>(null);

// In useEffect
socket.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.req === "getData") {
    setData(msg.data);
  } else if (msg.req === "op") {
    workbookRef.current?.applyOp(msg.data);
  }
};

// Workbook declaration
<Workbook
  ref={workbookRef}
  onOp={(op) => socket.send(JSON.stringify({ req: "op", data: op }))} />


전체 코드는 https://github.com/ruilisi/fortune-sheet/blob/master/stories/Collabration.stories.tsx을 참조하십시오.

읽어 주셔서 감사합니다



저장소는 완전히 오픈 소스인 Github에서 호스팅됩니다. 도움이 되셨다면 별표를 주세요 😄. 피드백은 대단히 감사합니다!

좋은 웹페이지 즐겨찾기