
如何開發一個共編筆記的開發技術,淺談 CRDT、OT 的概念,分享當前生態系與資源 著重在多人即時共編時如何做到不衝突資料變更 最終開發一個可以部署的超簡單版本 notion 筆記

































1npm init -y
2npm install ws y-websocket yjs1import * as Y from "yjs";1import * as Y from "yjs";
2
3const doc = new Y.Doc();
41import * as Y from "yjs";
2import { WebsocketProvider } from "y-websocket";
3import ws from "ws";
4
5const doc = new Y.Doc();
6const wsProvider = new WebsocketProvider(
7 "ws://localhost:1234",
8 "my-roomname",
9 doc,
10 { WebSocketPolyfill: ws }
11);1npx create-react-app frontend
2cd frontend
3npm install yjs y-websocket1import React from "react";
2
3function App() {
4 return (
5 <div className="App">
6
7 </div>
8 );
9}
10
11export default App;
121import React, { useEffect } from "react";
2import * as Y from "yjs";
3import { WebsocketProvider } from "y-websocket";
4
5function App() {
6 useEffect(() => {
7 const ydoc = new Y.Doc();
8 const provider = new WebsocketProvider(
9 "ws://localhost:1234",
10 "my-roomname",
11 ydoc
12 );
13
14 return () => {
15 provider.disconnect();
16 };
17 }, []);
18
19 return (
20 <div className="App">
21
22 </div>
23 );
24}
25
26export default App;
271import React, { useEffect, useRef } from "react";
2import * as Y from "yjs";
3import { WebsocketProvider } from "y-websocket";
4
5function App() {
6 useEffect(() => {
7 const ydoc = new Y.Doc();
8 const provider = new WebsocketProvider(
9 "ws://localhost:1234",
10 "my-roomname",
11 ydoc
12 );
13
14 return () => {
15 provider.disconnect();
16 };
17 }, []);
18
19 const textareaRef = useRef(null);
20
21 return (
22 <div className="App">
23 <textarea
24 ref={textareaRef}
25 style={{ width: "100%", height: "90vh" }}
26 ></textarea>
27 </div>
28 );
29}
30
31export default App;
321import React, { useEffect, useRef } from "react";
2import * as Y from "yjs";
3import { WebsocketProvider } from "y-websocket";
4
5function App() {
6 useEffect(() => {
7 const ydoc = new Y.Doc();
8 const provider = new WebsocketProvider(
9 "ws://localhost:1234",
10 "my-roomname",
11 ydoc
12 );
13
14 const yText = ydoc.getText("textarea");
15
16 return () => {
17 provider.disconnect();
18 };
19 }, []);
20
21 const textareaRef = useRef(null);
22
23 return (
24 <div className="App">
25 <textarea
26 ref={textareaRef}
27 style={{ width: "100%", height: "90vh" }}
28 ></textarea>
29 </div>
30 );
31}
32
33export default App;
341import React, { useEffect, useRef } from "react";
2import * as Y from "yjs";
3import { WebsocketProvider } from "y-websocket";
4
5function App() {
6 const textareaRef = useRef(null);
7
8 useEffect(() => {
9 const ydoc = new Y.Doc();
10 const provider = new WebsocketProvider(
11 "ws://localhost:1234",
12 "my-roomname",
13 ydoc
14 );
15
16 const yText = ydoc.getText("textarea");
17
18 function handleInput(event) {
19 if (event.inputType === "insertText") {
20 const data = event.data || "";
21 const position = textareaRef.current.selectionStart - 1;
22 yText.insert(position, data);
23 textareaRef.current.value = yText.toString();
24 }
25 }
26
27 textareaRef.current.addEventListener("input", handleInput);
28
29 return () => {
30 provider.disconnect();
31 textareaRef.current.removeEventListener("input", handleInput);
32 };
33 }, []);
34
35 return (
36 <div className="App">
37 <textarea
38 ref={textareaRef}
39 style={{ width: "100%", height: "90vh" }}
40 ></textarea>
41 </div>
42 );
43}
44
45export default App;
461import React, { useEffect, useRef } from "react";
2import * as Y from "yjs";
3import { WebsocketProvider } from "y-websocket";
4
5function App() {
6 const textareaRef = useRef(null);
7
8 useEffect(() => {
9 const ydoc = new Y.Doc();
10 const provider = new WebsocketProvider(
11 "ws://localhost:1234",
12 "my-roomname",
13 ydoc
14 );
15
16
17 const yText = ydoc.getText("textarea");
18
19 yText.observe(() => {
20 const textarea = textareaRef.current;
21
22 const currentValue = textarea.value;
23 const newValue = yText.toString();
24
25 if (currentValue !== newValue) {
26 textarea.value = newValue;
27 }
28 });
29
30 function handleInput(event) {
31 if (event.inputType === "insertText") {
32 const data = event.data || "";
33 const position = textareaRef.current.selectionStart - 1;
34 yText.insert(position, data);
35 textareaRef.current.value = yText.toString();
36 }
37 }
38
39 textareaRef.current.addEventListener("input", handleInput);
40
41 return () => {
42 provider.disconnect();
43 textareaRef.current.removeEventListener("input", handleInput);
44 };
45 }, []);
46
47 return (
48 <div className="App">
49 <textarea
50 ref={textareaRef}
51 style={{ width: "100%", height: "90vh" }}
52 ></textarea>
53 </div>
54 );
55}
56
57export default App;
581import React, { useEffect, useRef } from "react";
2import * as Y from "yjs";
3import { WebsocketProvider } from "y-websocket";
4
5function App() {
6 const textareaRef = useRef(null);
7
8 useEffect(() => {
9 const ydoc = new Y.Doc();
10 const provider = new WebsocketProvider(
11 "ws://localhost:1234",
12 "my-roomname",
13 ydoc
14 );
15 const yText = ydoc.getText("textarea");
16
17 yText.observe(() => {
18 const textarea = textareaRef.current;
19
20 const currentValue = textarea.value;
21 const newValue = yText.toString();
22
23 if (currentValue !== newValue) {
24 textarea.value = newValue;
25 }
26 });
27
28 function handleInput(event) {
29 if (event.inputType === "insertText") {
30 const data = event.data || "";
31 const position = textareaRef.current.selectionStart - 1;
32 yText.insert(position, data);
33 textareaRef.current.value = yText.toString();
34 } else if (event.inputType === "insertLineBreak") {
35 const position = textareaRef.current.selectionStart;
36 yText.insert(position, "\n");
37 textareaRef.current.value = yText.toString();
38 } else if (event.inputType === "deleteContentBackward") {
39 const position = textareaRef.current.selectionStart;
40 yText.delete(position, 1);
41 textareaRef.current.value = yText.toString();
42 }
43 }
44
45 textareaRef.current.addEventListener("input", handleInput);
46
47 return () => {
48 provider.disconnect();
49 textareaRef.current.removeEventListener("input", handleInput);
50 };
51 }, []);
52
53 return (
54 <div className="App">
55 <textarea
56 ref={textareaRef}
57 style={{ width: "100%", height: "90vh" }}
58 ></textarea>
59 </div>
60 );
61}
62
63export default App;1import React, { useEffect, useRef } from "react";
2import * as Y from "yjs";
3import { WebsocketProvider } from "y-websocket";
4
5function App() {
6 const textareaRef = useRef(null);
7
8 useEffect(() => {
9 const ydoc = new Y.Doc();
10 const provider = new WebsocketProvider(
11 "ws://localhost:1234",
12 "my-roomname",
13 ydoc
14 );
15 const yText = ydoc.getText("textarea");
16
17 yText.observe(() => {
18 const textarea = textareaRef.current;
19 const cursorPosition = textarea.selectionStart;
20
21 const currentValue = textarea.value;
22 const newValue = yText.toString();
23
24 if (currentValue !== newValue) {
25 textarea.value = newValue;
26 textarea.setSelectionRange(cursorPosition, cursorPosition);
27 }
28 });
29
30 function handleInput(event) {
31 if (event.inputType === "insertText") {
32 const data = event.data || "";
33 const position = textareaRef.current.selectionStart - 1;
34 yText.insert(position, data);
35 textareaRef.current.value = yText.toString();
36 } else if (event.inputType === "insertLineBreak") {
37 const position = textareaRef.current.selectionStart;
38 yText.insert(position, "\n");
39 textareaRef.current.value = yText.toString();
40 } else if (event.inputType === "deleteContentBackward") {
41 const position = textareaRef.current.selectionStart;
42 yText.delete(position, 1);
43 textareaRef.current.value = yText.toString();
44 }
45 }
46
47 textareaRef.current.addEventListener("input", handleInput);
48
49 return () => {
50 provider.disconnect();
51 textareaRef.current.removeEventListener("input", handleInput);
52 };
53 }, []);
54
55 return (
56 <div className="App">
57 <textarea
58 ref={textareaRef}
59 style={{ width: "100%", height: "90vh" }}
60 ></textarea>
61 </div>
62 );
63}
64
65export default App;


