For a better reading experience, view this on the original site.

为了获得更好的阅读体验,请在原始网站上查看。

I’ve been learning Golang and one of things I love so much about it is the fact that they have this.

我一直在学习Golang,而我最喜欢的一件事就是他们有这个。

This is especially useful when you’re halfway in a project and want to test something out but don’t want to go through the trouble of setting up another work directory for a small little test.

当您在项目的中途并且想要测试某些东西但又不想为另一个小的测试而设置另一个工作目录的麻烦时,这特别有用。

I’m also on that Leetcode Grind so this has been serving as my personal bug catcher. Also, I always thought it was kinda cool that companies would make use of collaborative code editing websites to conduct their interviews.

我也在Leetcode Grind上,所以它一直是我的个人错误捕捉器。 另外,我一直认为公司利用协作代码编辑网站进行采访是很酷的。

So I thought, why not try to make my own?

所以我想,为什么不尝试自己做?

问题 Problem

I started off giving myself some time to brainstorm for a solution and some of the things I came up with are as follows:

我开始给自己一些时间集思广益以寻求解决方案,我想到的一些事情如下:

1.在每次按键时发送整个文档 (1. Send the whole document on every keypress)

So obviously this wasn’t going to be my best idea but I’m not sure why I thought it would work under certain circumstances. The most obvious problem would be network delays causing each user to overwrite the other. Also, the amount of data to send over the network would begin to ramp up as the document gets bigger.

因此,显然这不是我的最佳主意,但是我不确定为什么我认为在某些情况下它会起作用。 最明显的问题是网络延迟,导致每个用户覆盖另一个用户。 另外,随着文档的增大,通过网络发送的数据量将开始增加。

2.发送信件以及插入位置 (2. Send the letter along with the position to insert)

This would fail since the positions change on every insert and the documents would get out of sync really quickly. This got me to the next one which I almost thought I had it.

这将失败,因为每个插入位置的位置都会发生变化,并且文档将很快变得不同步。 这使我进入了下一个我几乎以为自己拥有的地方。

3.按行分割文档,并按行插入 (3. Split the document by lines and handle inserts per line)

I honestly thought this would work albeit being very hard to implement UI-wise since I would have to account for different line inputs and displaying the text. I also quickly realise that I shifted my problem from character positions in #2 to line positions in this one.

老实说,尽管很难在UI上实现,但我必须这样做,因为我必须考虑到不同的行输入和显示文本。 我也很快意识到我将问题从#2的字符位置转移到了这一位置的行位置。

4.为每个字符附加一个唯一的递增计数器 (4. Attach a unique increasing counter for each character)

Looking back, this was probably my closest idea. I thought of either linearly increasing the counter by a certain factor to account for minor edits in the middle but I wasn’t really sure how I would handle cases where the difference in count was less than 1 (e.g. inserting between counts 1 and 2). If I could do this however, it was just a matter of sorting and inserting at the right locations.

回顾过去,这可能是我最接近的想法。 我考虑过将计数器线性增加某个因子以解决中间的少量修改,但我不确定如何处理计数差小于1的情况(例如在计数1和2之间插入) 。 但是,如果我能够做到这一点,那只是在正确的位置进行排序和插入的问题。

I wasn’t exactly working on this the whole time I was awake so at this point it was already a couple of days in and I decided to just Google it.

我整个清醒时都没有真正在做这项工作,所以到现在为止已经有几天了,所以我决定只使用Google。

解 Solution

I came across two ways collaborative text editing could be achieved.

我遇到了两种可以实现协作文本编辑的方式。

  1. Operational Transforms (OT) 运算转换(OT)
  2. Conflict-free Replicated Data Type (CRDT) 无冲突复制数据类型(CRDT)

I’m no expert so to give a very very very high-level overview:

我不是专家,所以给一个非常非常高层次的概述:

OT works by changing the remote operation before applying it on the local document. This method is, in a sense, more algorithmic to determine what change must be applied to the received operation before executing it. I haven’t read up much about this because I chose to go for the latter.

OT通过在将远程操作应用于本地文档之前更改远程操作来工作。 从某种意义上说,这种方法更具算法性,可以确定在执行接收到的操作之前必须对其进行哪些更改。 我还没有读过很多,因为我选择了后者。

CRDTs are just data structures that can be modified locally and have these updates propagated remotely without any conflict. There are many types of CRDTs and the one I chose to implement is the LSEQ (or at least my jumbled version of it). The final implementation mentioned in the paper is CRATE which is LSEQSplit, an extension of LSEQ. Their implementation can also be found here.

CRDT只是可以在本地修改的数据结构,并且可以将这些更新远程传播而没有任何冲突。 CRDT的类型很多,我选择实现的一种是LSEQ(或者至少是我的混杂版本)。 在论文中提到的最终实现是CRATE是LSEQSplit,LSEQ的延伸。 它们的实现也可以在这里找到。

序列 (LSEQ)

I’m not even going to pretend to fully understand the motivations and improvements of LSEQ compared to other CRDTs but I’m guessing it has something to do scalability of the size of identifiers allocated to each update. If you do want to know more, read the paper I linked to down below.

The general idea behind this is issuing a unique identifier to each character that you add in order to resolve conflicts in concurrent editing. This is done with the following elements.

其背后的总体思路是为您添加的每个字符提供唯一的标识符,以解决并发编辑中的冲突。 这是通过以下元素完成的。

Path

路径

The path is just a number indicating where the character should be. A character with a higher path will come after a character with a lower path.

路径只是一个数字,指示字符应该在哪里。 具有较高路径的字符将位于具有较低路径的字符之后。

Site

现场

The site is the unique value attached to each user, this will help to resolve conflicts when two paths are the same.

该站点是附加给每个用户的唯一值,当两个路径相同时,这将有助于解决冲突。

Count

计数

Each character will also have a number indicating the number of operations the user has already performed on the document

每个字符还将有一个数字,指示用户已对文档执行的操作数

These three form a triplet which is used to determine a character’s order in the document. The identifier for a character will contain a list of triplets. In the event that the first identifier is the same, the second identifier is compared to determine the order. To compare a triplet, the path is first compared, then the site and finally the count.

这三个构成一个三元组,用于确定文档中字符的顺序。 字符的标识符将包含三联列表。 在第一标识符相同的情况下,比较第二标识符以确定顺序。 要比较三元组,首先要比较路径,然后是站点,最后是计数。

Example: <Path, Site, Count>

示例:<路径,站点,计数>

Identifier for "A" -> [<4, 'User1', 1>] 
Identifier for "B" -> [<5, 'User1', 2>]
The order will be "AB" since the path for "B" is higher than the path for "A"Identifier for "A" -> [<5, 'User2', 2>]
Identifier for "B" -> [<5, 'User1', 1>]
The order will be "BA" since the site for "A" is greater (lexicographically) compare to "B"Identifier for "A" -> [<3, 'User2', 2>]
Identifier for "B" -> [<3, 'User1', 1>, <1, 'User2', 3>]
The order will be "BA" since the first identifier for "B" is less than the first identifier for "A". The second identifier is not compared since the first is not equal.

The paper outlines an algorithm to generate these identifiers given the identifier of the character before and after it so do have a look at the paper to learn how it is done.

本文概述了在给定字符前后标识符的情况下生成这些标识符的算法,因此请查看本文以了解如何完成。

For a TLDR:

对于TLDR:

  1. Compare the path of the first triplets of the characters before and after the position you wish to insert. 比较您要插入的位置之前和之后的字符的第一个三元组的路径。
  2. Obtain a number between the paths. 获取路径之间的数字。
  3. Copy all triplets from the previous identifier up until the level of the newly generated triplet. 从先前的标识符复制所有三胞胎,直到新生成的三元组的水平。
  4. Add the newly generated triplet to the list of copied triplets. 将新生成的三元组添加到复制的三元组列表中。
  5. Attach the character to insert with this list of triplets. 附加字符以插入此三元组列表。

There is waaaaay more to this than what I’ve listed out. For example, the range of values for the path and the amount to increment for a new path are all controlled in the paper for best results. E.g. range of path values increment exponentially per level.

除了我列出的内容以外,还有更多其他内容。 例如,纸张中的路径值范围和增加的新路径量都已得到控制,以达到最佳效果。 例如,路径值的范围按级别呈指数增长。

This is how I’ve implemented my identifiers and triplets

这就是我实现标识符和三胞胎的方式

comparable.ts
comparable.ts
// Interface to implement a generic form of Skiplist (for testing)
export default interface Comparable {
value: any;
compare: (other: any) => number;
}
triplet.ts
triplet.ts
export default class Triplet {
path: number;
site: string;
count: number;
constructor(path: number, site: string, count: number) {
this.path = path;
this.site = site;
this.count = count;
}
compare(other: Triplet) {
if (this.path < other.path) return -1;
if (this.path > other.path) return 1;
if (this.site < other.site) return -1;
if (this.site > other.site) return 1;
if (this.count < other.count) return -1;
if (this.count > other.count) return 1;
return 0;
}
}
identifier.ts
identifier.ts
import Comparable from "./skiplist/comparable";
import Triplet from "./triplet";
export default class Identifier implements Comparable {
value: string;
triplets: Triplet[];
constructor(value: string, triplets: Triplet[]) {
this.value = value;
this.triplets = triplets;
}
compare(other: Identifier) {
let i = 0;
while (true) {
let t1 = this.triplets[i];
let t2 = other.triplets[i];
if (t1 === undefined && t2 === undefined) return 0;
if (t1 === undefined && t2 !== undefined) return -1;
if (t1 !== undefined && t2 === undefined) return 1;
let cmp = t1.compare(t2);
if (cmp !== 0) return cmp;
else i++;
}
}
}

存储这些标识符的数据结构 (Data structure to store these identifiers)

Now the paper definitely alludes to a tree-like structure and even the implementation of CRATE uses a tree. However, I was a little skeptical of the deletion process for a tree structure and whether or not there would be useless nodes hanging around so I decided to look up more implementations. I found a repository that implemented this in Rust and saw the words SkipList in the code somewhere.

现在,本文无疑暗示了一种树状结构,甚至CRATE的实现都使用了树。 但是,我对树形结构的删除过程以及是否有无用的节点悬空表示怀疑,因此我决定查找更多实现。 我找到了一个在Rust中实现此功能的存储库,并在代码中的某处看到了SkipList字样。

Now having just completed my university course on Data Structures & Algorithms, I thought it was a perfect time to put into practice what I learnt (although I would definitely regret this later).

现在,我刚刚完成了有关数据结构和算法的大学课程,我认为这是实践所学知识的绝佳时机(尽管我以后一定会后悔的)。

A skiplist is like a linked list but it has pointers that point to items on its left, right, top and bottom. There are multiple levels in a skiplist. The lowest level of the skiplist operates like how a normal linked list would and contains all elements of the list. The higher levels do not contain all elements. They act like express routes that allow you to skip elements. Skiplists are probabilistic data structures which give you O(n) search and insert for n elements.

跳过列表就像一个链接列表,但是它的指针指向其左侧,右侧,顶部和底部。 跳过列表中有多个级别。 跳过列表的最低级别的操作类似于普通链接列表的方式,并包含列表的所有元素。 较高级别并不包含所有元素。 它们的作用就像快递路线,可让您跳过元素。 跳过者是概率数据结构,可为您提供O(n)搜索并插入n个元素。

The key to the skiplist is maintaining a sorted order. I will go through briefly on some of the operations and caveats of a skiplist but this will surely not be a comprehensive explanation.

跳过列表的关键是保持排序顺序 。 我将简要介绍一个跳过列表的一些操作和注意事项,但这肯定不是一个全面的解释。

Finding an element

寻找元素

These are the general steps to take for finding an element in a skiplist that is sorted in an ascending order.

这些是在按升序排序的跳转列表中查找元素所需采取的一般步骤。

  1. Start from the highest level of the head nodes. 从头节点的最高级别开始。
  2. Compare the element on your right. If the element is smaller or equal, move to it. If it is equal, you have found the element. Otherwise, go down. 比较右边的元素。 如果元素小于或等于,则移至该元素。 如果相等,则找到元素。 否则,下去。
  3. If there are no more nodes below you and the element on the right is still larger. The element does not exist. 如果您下面没有更多的节点,并且右侧的元素仍然更大。 元素不存在。

Suppose we want to find the number 5 in the picture above. These are the steps we would take. Note that this is a skiplist in ascending order.

假设我们要在上图中找到数字5。 这些是我们将要采取的步骤。 请注意,这是一个按升序排列的跳过列表

The levels will be labelled 0,1,2,3 with 0 being the lowest level that contains all the elements.

级别将被标记为0、1、2、3,其中0是包含所有元素的最低级别。

  1. Start at level 3 of the head nodes. 从头节点的第3层开始。
  2. Compare element on the right which is 1. Since 1 is smaller than 5, we move to it and are currently at level 3 with value 1. 比较右边的元素1。由于1小于5,我们将其移至当前位置3,值为1。
  3. Compare element on the right again, this time it is NIL. Thus we move down to level 2 with value 1. 再次比较右边的元素,这次是NIL。 因此,我们向下移动到值为1的2级。
  4. Compare the element on the right again which is 4. Since 4 is smaller than 5, we move to it and are currently at level 2 with value 4. 再次比较右边的元素4。由于4小于5,我们将其移到当前值2的第2级。
  5. Compare the element on the right which is 6. This is larger than our element so we move down to level 1 with value 4. 比较右边的元素6。该元素比我们的元素大,因此我们向下移动到值为1的级别1。
  6. Compare the element on the right which is still 6. This is still larger and we move down again to level 0 with value 4. 比较右边的元素仍然是6。这个元素仍然更大,我们再次向下移动到值为0的级别0。
  7. Compare the element on the right which is 5. Move to it and you have found the element. 比较右边的元素5。移动到该元素上,您已经找到了该元素。

Inserting an element

插入元素

  1. Find the element 查找元素
  2. At the final position in step 1, insert the element between the current element you are on and the larger element on the right. 在步骤1的最终位置,将元素插入到当前使用的元素和右侧的较大元素之间。
  3. Get a random number from [0,1]. If the number if 1, create a new node for the element and attach the new node to the original node’s top. Similarly, the new node’s bottom will point towards the original node. Repeat step 3 for the new node. If the number is 0, you have finished inserting the element 从[0,1]获取一个随机数。 如果数字为1,则为该元素创建一个新节点,并将新节点附加到原始节点的顶部。 同样,新节点的底部将指向原始节点。 对新节点重复步骤3。 如果数字为0,则说明您已完成元素的插入

The interesting part is the randomization of “promoting” an element to higher levels. There is a 50% chance that an element will move on to level 1, 25% chance to move on to level 1 and 2 and so on.

有趣的部分是将元素“提升”到更高级别的随机化。 元素有50%的机会继续前进到第1级,有25%的机会继续前进到第1级和第2级,依此类推。

Deleting an element

删除元素

  1. Find the element 查找元素
  2. Delete the element and all levels it has 删除元素及其具有的所有级别
  3. Reattach the left of the element to its right 将元素的左侧重新附加到其右侧

Deleting is pretty simple where you remove the element and make sure the list is still linked by reconnecting the ends.

删除非常简单,只需删除元素并通过重新连接两端来确保列表仍处于链接状态。

Minor change

微小的变化

In the LSEQ algorithm, we would need to insert and delete elements at various positions. To account for this, we just have to add another field to our elements that indicate how many elements you will skip. Higher levels will have a skip field with larger numbers and the lowest level will only have skip values of 1.

在LSEQ算法中,我们需要在各个位置插入和删除元素。 为了解决这个问题,我们只需要在元素中添加另一个字段即可指示您将跳过多少个元素。 较高的级别将具有较大数字的跳过字段,而最低的级别将仅具有1的跳过值。

After inserting / deleting an element from the skiplist, I also return the position of that particular element in the list. This will be useful in correcting the position of the cursor later on.

在插入/删除跳过列表中的元素之后,我还返回该特定元素在列表中的位置。 以后在校正光标位置时将很有用。

skiplist/comparable.tsskiplist/comparable.ts
// Interface to make skiplist and box type generic
export default interface Comparable {
value: any;
compare: (other: any) => number;
}
skiplist/box.tsskiplist/box.ts
// Box type to wrap an object with pointers for skip list
export default class Box<T extends Comparable> {
item: T;
level: number;
skipped: number = 1;
top: Box<T> | null = null;
right: Box<T> | null = null;
left: Box<T> | null = null;
bottom: Box<T> | null = null;
head: boolean;
static Head(level: number);
constructor(item: T, level: number, head: boolean = false);
compare(other: Box<T>): number;
toString(): string;
}
skiplist/index.tsskiplist/index.ts
// Functions supported by Skiplist
export default class SkipList<T extends Comparable> {
head: Box<T>[];
highestLevel: number;
// Initialize default values
constructor() {
this.head = [];
this.head[0] = Box.Head(0);
this.highestLevel = 0;
}
// Finds the Box at the position that we want to insert a new
// character in. This is needed for the LSEQ path generation algorithm
atPosition(position: number): Box<T>;
// Find a particular box with T.
// First return value indicates if the item exists
// Second return value gives the corresponding box or the one right before where box would have been placed
// Third return value returns the position of the box
find(box: Box<T>): [boolean, Box<T>, number];
// Inserts an item T into the skiplist and returns
// the position it is inserted
// -1 is returned if item is not found
insert(item: T): number;
// Deletes the item T in the skiplist and returns
// the position of the deleted item
// -1 is returned if item is not found
delete(item: T): number;
// Rolls a random float from [0,1] to determine if the box should be
// promoted to the level in the argument.This function calls itself
// again with a higher level if the current function succeeds in promoting
shouldPromote(prev: Box<T>, level: number, position: number);
// When an item is inserted / deleted, all higher level nodes on its
// right must be updated with a new "skipped" value
propagate(node: Box<T> | null, inc: number);
// Returns all items in a sorted array
get values(): T[];
}
带有Golang的gRPC gRPC with Golang

This is my first ever project with Golang and you should probably not take this code as reference for anything that you will ever do. For this project, the code was referenced off Tech School which has a really great playlist showcasing the different features of gRPC as well as its portability to different languages.

这是我有史以来第一个关于Golang的项目,您可能不应该将此代码作为以后做任何事情的参考。 对于该项目,该代码是从Tech School引用的,该学院的播放列表非常好,展示了gRPC的不同功能以及可移植到不同语言的能力。

What I loved about Golang is how easy it is to learn. There are only a few unique features like goroutines, implementing interfaces and channels which can easily be picked up after looking at a few examples (which there are a ton of). The only difficulty I face so far is learning to write idiomatic Go code but I guess that would probably hold true for many languages. The standard library in Golang is also extremely useful. It comes with everything you need to get a webserver up and running without any third-party dependencies. A bunch of my favorite projects are also implemented in Golang (e.g. Docker, Kubernetes).

我最喜欢Golang的地方是学习起来很容易。 只有很少的独特功能,例如goroutines,实现接口和通道,这些功能可以在查看一些示例(数量众多)后轻松了解。 到目前为止,我面临的唯一困难是学习编写惯用的Go代码,但是我想这可能对许多语言都适用。 Golang中的标准库也非常有用 。 它具有启动和运行网络服务器所需的一切,而没有任何第三方依赖性。 我最喜欢的一堆项目也在Golang中实现(例如Docker,Kubernetes)。

gRPC, incase you haven’t heard of it, is a remote procedure call framework that is run over HTTP/2. This was great for my project needs since gRPC over HTTP/2 supports streaming of data compared to a traditional REST API. I could have used websockets here, and probably should have, but I’ve already experimented with websockets in socket.io so I wanted to try and learn something new.

gRPC(如果您还没有听说过的话)是一个通过HTTP / 2运行的远程过程调用框架。 与传统的REST API相比,这非常适合我的项目需求,因为基于HTTP / 2的gRPC支持数据流传输 。 我本可以在这里使用websockets,也许应该使用,但是我已经在socket.io中尝试过websockets,所以我想尝试学习一些新知识。

战略 (Strategy)

The code is available so I’ll just give a quick run-through of my strategy.

该代码可用,因此我将快速介绍一下我的策略。

go run [roomID].gogo run [roomID].go

This was the core logic of the service and I ran into a TON of bugs just because of my unfamiliarity with Golang. For example, I locked the mutex before calling a nested function call which required access to the same lock which just hangs the execution.

这是服务的核心逻辑,我遇到了错误,只是因为我不熟悉Golang的一 。 例如,我在调用嵌套函数调用之前锁定了互斥锁,该嵌套函数调用需要访问只是挂起执行的同一锁。

Also, I didn’t realise that there would be additional dependencies required for my React application to communicate with the gRPC server. grpc-web was required to enable invoking gRPC services from modern browsers and even then, only some of the operations were supported like unary / server-side streaming. This does not work on ALL browsers too. grpc-web also relies on websockets to support streaming so I guess I would have been better off just implementing my server with websockets in the first place. However, I can see how this might be helpful for services which were already implemented in gRPC.

另外,我没有意识到我的React应用程序与gRPC服务器进行通信还需要其他依赖项。 必须启用grpc-web才能从现代浏览器中调用gRPC服务,即使这样,也仅支持某些操作,例如一元/服务器端流。 这也不适用于所有浏览器。 grpc-web还依靠websockets支持流传输,因此我想我最好只使用websockets实现服务器。 但是,我可以看到这对于已经在gRPC中实现的服务有何帮助。

用React创建代码编辑器 Creating the code editor with React

I didn’t want to add a whole ton of dependencies like routing so I just used a simple state change for the pages.

我不想添加诸如路由之类的全部依赖关系,因此我只对页面使用了简单的状态更改。

1.创建/加入页面 (1. Create / Join Page)

The create room button simply generates a random 6 character string and requests the server to create a room with that code. This code will be used to group users together. If a successful response is received from the server, the room is created and the application automatically sends another request to connect to the room.

创建房间按钮仅生成一个随机的6个字符串,并请求服务器使用该代码创建一个房间。 此代码将用于将用户分组在一起。 如果从服务器收到成功的响应,则会创建会议室,应用程序会自动发送另一个请求以连接到会议室。

The join room button takes the room code that the user can input in the text field and sends the same connect request. The connect function contains a callback that will handle messages that it receives from other users. The handling of the responses are omitted here but you can refer to the source code to find out more.

2.文字编辑器 (2. Text Editor)

onKeyDown
onKeyDown
textareatextareavaluedefaultValue
textareatextareavaluedefaultValue
学习要点 Learning points

1.选择适合工作的工具 (1. Choose the right tool for the job)

I chose React solely out of familiarity and undertaking this project has just made me realise how unfamiliar I am with React. I have a lot to learn regarding some of the internals on how React handles state and variables that are not handled by state. The whole project would have been much smoother had I gone with pure javascript instead of the React framework.

我完全是出于不熟悉而选择React,而从事这个项目使我意识到我对React并不熟悉。 关于React如何处理状态以及状态未处理的变量的一些内部知识,我有很多东西要学习。 如果我使用纯JavaScript而不是React框架,整个项目将会更加顺利。

grpc-web
grpc-web

2.编写测试 (2. Write your tests)

I cannot stress the importance of tests enough. Throughout the course of this project, I wrote a few tests here and there and they have been so useful in verifying that my code works as expected. Halfway through, I got lazy and stopped writing these tests. This was not a good idea and the bugs just kept piling on and on while manually testing each time took me ages.

我不能足够强调测试的重要性。 在该项目的整个过程中,我到处都写了一些测试,它们对于验证我的代码是否按预期工作非常有用。 中途,我变得懒惰并停止编写这些测试。 这不是一个好主意,并且每次手动测试时,这些漏洞不断堆积,使我花了很长时间。

skipped
skippedskipped

3.更早开始 (3. Start earlier)

This was my first project and I really didn’t think I was going to pull it off. I read the paper on LSEQs so many times and got so confused on how I was going to implement this. I spent a lot of time thinking about various implementations but just starting on writing code helps so much. I had to refactor my implementation of the internal data structure for the LSEQ algorithm so many times. I actually started with a tree-like diagram before I implemented a skip list but going through the process of writing all this code helped me to form a better idea of what I needed to change in the next one.

这是我的第一个项目,我真的不认为我会实现它。 我读了很多关于LSEQ的论文,对如何实现它感到困惑。 我花了很多时间思考各种实现,但是从编写代码开始才有很大帮助。 我不得不为LSEQ算法重构内部数据结构的实现很多次。 实际上,在实现跳过列表之前,我实际上是从一个树状图开始的,但是经历编写所有这些代码的过程有助于我更好地了解下一个需要更改的内容。

I’m not saying you should dive in without formulating any sort of plan but if you’re a newbie like me, there’s only so much that you can plan for before you are just stuck on some details that you wouldn’t know due to the lack of experience. My takeaway for this point would be to not worry if your project is going to be a failure or success and just start on it.

我并不是说您应该在不制定任何计划的情况下潜入,但是如果您是像我这样的新手,那么您只能计划很多事情,然后再继续停留一些您可能不知道的细节缺乏经验。 关于这一点,我的要点是不必担心您的项目是失败还是成功,而只是从头开始。

下一步是什么 What’s Next

For the current state of my project, I would give myself a pass for implementing the algorithm but in terms of the execution such as creating the actual application for a use case, it’s definitely a fail. The site is really slow at some points like running the file with the text editor being out of sync at times due to React’s virtual DOM.

对于项目的当前状态,我会给自己一个实现算法的通道,但是就执行(例如为用例创建实际应用程序)而言,这肯定是失败的。 由于React的虚拟DOM有时使网站运行时文本编辑器不同步,该站点确实有些缓慢。

  1. Rewrite the server with websocket 用websocket重写服务器
  2. Learn more about React / Just use Javascript 了解有关React的更多信息/仅使用Javascript
  3. Hot reloading with Go Go热装
  4. Test cases 测试用例

With school starting in a few days, I probably won’t have enough time to consistently work on the project but if I came back to it, I would definitely have to revamp the entire thing from scratch.

随着几天的上学时间,我可能没有足够的时间来持续进行该项目,但是如果我回到项目上,我肯定会从头开始对整个事情进行改进。

结束 End

I hope you learnt something regarding how to create your own collaborative editing tool. Thanks for reading!

希望您学到了有关如何创建自己的协作编辑工具的知识。 谢谢阅读!