Writing in the front

When I first started my career, I was in a media Internet unit. Since I was a media unit, the online editing of copywriting naturally became an unavoidable obstacle

So rich text editors on the Web came into my world. Rich text was the front-end clear (a joke that means rich text is harder). I started out as a colored pen and it was a front-end black box.

At that time, our system was still using a large UEditor. Due to the special properties of the media industry, a large amount of video and audio disks and other content processing was needed in rich text, so we had to develop it again and add some custom functions.

So we did gross secondary development on an editor that wasn’t maintained. It works, but the user experience is pretty dumb

I spent two weeks researching mature editors on the market, such as draft. js, quill. js, and ProseMirror. Good upstarts, like TinyMCE and CKEditor, And our own open source wangEditor

While all of these editors work out of the box, they have a common problem: they are all English documents that are difficult to read, and many of the functions required by the business do not know how to implement.

Although wangEditor is a Chinese document, its orientation at that time was to be a simple and easy to use rich text, but its extensibility was limited and complex business scenes could not be met. For example, it needed to customize video uploading, and it could support video clip interception, picture cropping and other functions.

So we chose Tinymce. First of all, it has a lot of custom extension functions. Some people in the community also maintain this Chinese document and there are many users

What followed was a few years of endless struggle with rich text

Rich text in-depth understanding

As mentioned last time, although we chose Tinymce, it was in English all the time, so when we wanted to understand it from the inside out, we had no way to start. Besides, there were not a few lines of annotations in their code at that time, only some annotations in key places, which were still in English and difficult to understand.

Then I think of our domestic light, Wangeditor, because they have similar functions and principles. The differences are actually some differences in design ideas

Before we begin the journey of wangEditor, let’s be clear that rich text editors are editable thanks to the HTML contenteditable property that allows us to edit content within tags. This is fundamental to implementing a rich text

wangeditor

I’ve seen almost all of wangEditor since the third version, Witnessed his step by step from a JS to TS reconstruction, from the emphasis on extensibility to object-oriented and now popular functional community, from must be compatible with IE to abandon compatible WITH IE, from the traditional Webpack engineering construction to rollPU + Monorepo engineering construction. It can be said that the history of WangEditor is the history of the whole front-end field.

In addition, IN the V5 version, I abandoned the burden of history and started a new project, no longer carrying shackles to do the architecture design. It was refreshing for me to apply some mature schemes like VDOM.

Let’s focus on what I know about wangEditor V4 and V5 versions.

V4

When I first read V4, I was most happy that every method has comments, at least we know where to start, and then classic object-oriented design. This kind of design gives me a concrete picture in my mind of the concepts of object orientation and separation of attention.

Before we get started, what is object orientation

object-oriented

Object Oriented programming (OOP) is a design idea or architectural style

Its essence is that OOP should embody a network structure in which each node “Object” can communicate with other nodes only through “messages”. Each node has an internal hidden state that cannot be changed directly, but indirectly through messaging.

It is this network structure that enables object orientation to support large and complex systems, decoupling each function point. That’s why it’s so popular.

When we talk about object orientation, we have to mention encapsulation, inheritance and polymorphism

  • “Encapsulation” is to abstract out a logic/concept to achieve relative independence
  • “Inheritance” is the hope that the relationship between two objects to achieve code reuse
  • Polymorphism refers to having a group of objects express the same concept and exhibit different behaviors

Using these three points, we can deal with the design and implementation of large and complex systems, but of course it is not a panacea

Such as

I have a data, need to be constantly converted and cleaned into another format, then obviously object oriented is not suitable.

So in our rich text, object orientation obviously applies.

As shown above, these individual functions are actually nodes in a network structure, so wangEditor is object-oriented.

Analysis of design ideas

The design of WangEditor is very readable. The overall design architecture is to combine one function object after another into rich text functions by using the idea of object-oriented, mainly realizing the following function objects

  • Editor Object for the general Editor function
  • Command document. ExecCommand function encapsulation
  • Text some initialization of the edit area (including time binding, etc.)
  • Some initialization of the Menus bar
  • Change editor Change event related
  • History Indicates the History records
  • Encapsulation of Mutation MutationObserver
  • DomElement encapsulates DOM operations

It is through the combination of these functional objects that we present the rich text editor we see right out of the box

So borrowing the current thinking in our daily development, if there is a scenario that can use OOP, is it also possible to organize the functions of each part like him, through combination, inheritance and nesting to quickly build your application?

Ok, that’s it for v4, so let’s move on to our big show, V5

V5

Recently, I have been reading the source code of V5, and I have also organized and planned the mind map of the execution process of V5

If there are interested fellow passers-by, want to, you can shout in the comments section, leave contact information, I have sorted out, hands to.

Before formally introducing V5, we need to introduce another rich text editor, Slate

Slate

Slate is a fully customizable rich text editor framework.

The main things he does are these three things

  • It’s a “very lightweight” solution: Slate.js integrates almost nothing, but provides a plugin extension mechanism for developers to implement desired functionality. The flysize kernel makes it easy for readers to “get a feel” for editor design.
  • It’s “view-neutral” : Slate.js defines a set of data models that are separate from the UI implementation, and given that we’re not learning React or Vue all over again, it also lets us focus on editor model design without UI red tape.
  • It is designed for “collaborative editing” : Due to the limitations of network conditions, client hardware and application architecture, some early Web rich text editors did not take real-time collaboration into consideration. The model design of Slate.js is naturally compatible with collaborative editing. By learning Slate.js, we can also learn the basic working principle of multi-user collaboration documents.

Slat.js provides a view-independent core. In my opinion, slat.js provides an editor object that holds a lot of editor related functions. And you can extend the functionality of the current configuration object through plug-ins. This design is very different from a traditional editor, which is the root of the v5 version’s preference for it.

With its own plug-in mechanism, V5 is also a no-brainer in the design of plug-in extensions.

After studying its principle, suddenly realized that the top technology is so simple and direct!

// Suppose there is a configuration object
const edit = {
  edit: () = > {
    console.log(1)}},// Suppose you have a history plug-in
function History(e) {
  // Extend the current edit
  // execute from right to left
  e.edit = function () {
    const edit = e.edit
    console.log(0)
    edit()
  }
  return e
}
const Edit = History(edit)
Copy the code

The second benefit is that it separates the view from the model so that you can implement your own rendering of the view based on the model

This gives V5 the ability to implement its own view rendering based on the kernel it provides, creating an out-of-the-box editor

Ok, now I will formally study the interior of V5 with you. First, I will divide V5 into several steps

  • The use of the v5
  • V5’s engineering is relevant
  • Interior design of V5
  • V5 some reference points for our daily development

The use of the v5

V5 continues the good tradition of V4, and is also out of the box. We only need to initialize the current toolbar and edit area where we need to use it. Please refer to the specific initialization method in the documentation, and we will not repeat the details, here we will talk about the custom configuration items. In V5, we attach great importance to the user’s custom configuration. In the source code, it will merge the user’s configuration with the default configuration to generate the final configuration. Here I will introduce some points that we can configure

editorConfig

EditorConfig is the whole configuration that needs to be passed in and it basically has callback callback methods in it.

For example, onchange, onblur, onfocus, etc. If you want to change the default toolbar configuration, you need to pass in MENU_CONF

MENU_CONF

MENU_CONF is our user configuration place where we can merge the incoming user configuration with the default configuration

Looking forward to

There’s also the possibility of expanding things like custom plugins in the future, so there’s more room for customization

V5’s engineering is relevant

  • monorepo

In 2021, monorepo’s multi-package management style is popular today, and V5 follows the trend by splitting the entire editor into: Core module, editor module, basic-modules (default common menu), code-highlight, list-module, table-module, upload-image- Module (picture uploading), video-module (video uploading) and other modules

  • rollup

    The entire project is built using rollup, reducing the size of the code and increasing readability.

    Added two package types for UMD and ESModule

  • CSS preprocessor

    Less preprocessor solution, with SVG icon

  • Code specification

    Eslint +prettier

  • modular

    Developed in TypeScript, complete type definition files are provided

Interior design of V5

Based on Lerna, the overall design of splitting multiple packages and V5 has been summarized by the author. I will not repeat it, please refer to the picture

Here I will briefly outline what I think is good about design

1. Functional code style

From V4 to V5 there is a clear sense that functions are first-class citizens, which is consistent with good open source projects like VUe3. Reduce the side effects of reference types in your project by combining different functions

2. Distinct module division to achieve functional decoupling

Before I explain why I understand this design, what are the functions and functions of each module

core

The most important module in the editor is the Core module, which takes care of what SLATE can’t — the rendering of the view layer

The author has also summarized the core logic of core. If necessary, please refer to the picture

The main thing I want to talk about is why do YOU want to use VDOM for V5?

Let’s look at the logic of V4 before we understand the values

The main use of MutationObserver in V4 is to listen for changes to the DOM tree and trigger the editor

Let’s use shorthand code to describe the core design of V4

class Mutation { constructor(fn) { this.callback = mutations => { fn(mutations, This)} this.observer = new MutationObserver(this.callback)} class Change extends MutationObserver by inheriting the pair Mutation { constructor() { super((mutations, Observer) => {// trigger change this.save()})} private save() {this.emit()} public emit() {// Trigger some editor functions and callback}}Copy the code

In the above code, we found that the whole V4 has its own set of rendering rules, and without the whole concept of a model, all of its operations are deeply bound to the current rich text and cannot be removed

Since V5 is based on SLATE, it perfectly inherits the advantages of SLATE. By separating the model from the view, it can choose the existing efficient view renderer to render the view at will. In V5, snBBDOM of the same style as VUE2 is used

Back to our question. I suspect (probably incorrectly) that SNbbDOM is used in V5 for two reasons

1. Based on SLATE, I can get the data model of SLATE, use the existing renderer to render DOM at the lowest cost, and modify the Vdome to render the view by operating menu and other functions

Model-view separation is a trend and a higher abstraction that makes code architecture clearer and easier to understand.

basic modules

This module is actually a summary of some common menu functions, such as text size, color, setting title and so on

The content of this section is actually loaded in the form of plug-ins. This mechanism itself refers to the greatly enhanced extensibility of V5

Its principle is very simple. It mainly uses a global loader to mount the functions of the plug-in to the current editor instance, see the code

Function registerModule(modele) {const {setHtml} = modele arr_list.push (setHtml)} Let modele = {setHtml(vDOM) {// Suppose to insert HTML logic, call vDOM render DOM}, ForEach (setHtml => {setHtml(vDOM)})Copy the code

Code-highlight, list-module, table-module, upload-image-module, video-module

These modules are similar to Basic, except that they are more complex and rely on many plug-ins as separate modules

editor

This module is simply an external factory that encapsulates core and modules and exposes them to the user

Use beforeInput

** beforeInput ** indicates that editable DOM is triggered before being modified

Although this event, there are compatibility problems, but, in the VUE have abandoned IE today, compatibility problems or problems?

Why discard MutationObserver in favor of beforeInput?

Before we understand that, let’s talk about the history of listening to DOM changes

Evolution history of DOM variation technical solutions

Early pages did not provide support for listening, so polling was the only way to see if the DOM had changed, such as using setTimeout or setInterval to periodically check if the DOM had changed.

Until the introduction of Mutation Events in 2000, Mutation events adopted the design mode of the observer, which immediately triggered the corresponding Event when the DOM changed, which was a synchronous callback.

The use of Mutation events solves the real-time problem because the JavaScript interface is invoked as soon as the DOM changes. But it’s this real time that causes serious performance problems, because every time the DOM changes, the rendering engine calls JavaScript, which incurs a significant performance overhead.

To address the performance problems of Mutation events due to synchronous calls to JavaScript, starting with DOM4, it is recommended to use MutationObserver instead of Mutation events. The MutationObserver API can be used to monitor DOM changes, including attribute changes, node additions and subtractions, content changes, and so on.

MutationObserver changes the response function to an asynchronous invocation, so that instead of triggering the asynchronous invocation with each DOM change, the asynchronous invocation is triggered once after several DOM changes, and a data structure is used to record all DOM changes during that time. This allows frequent DOM manipulation without much impact on performance.

Ok, let’s get back to the reason

I think there are two reasons:

  • 1. Although MutationObserver is excellent, if its monitoring data changes, it needs to be compared, and the latest change value cannot be obtained, and many useless nodes will be generated, which is not suitable for SLATE’s system
  • Active operation menu to modify the style without listening, directly change SLATE’s data model

Here’s an example:

<div contenteditable ID ="input"> <div>sdfasdf</div> </div> // Select the node to watch for changes const targetNode = document.getElementById('input'); Const config = {subtree: true, childList: true, Attributes: true, attributeOldValue: const config = {subtree: true, childList: true, attributeOldValue: true, characterData: true, characterDataOldValue: true, }; Const callback = function(mutationsList, const callback = function(mutationsList, const callback); observer) { // Use traditional 'for loops' for IE 11 debugger console.log(mutationsList) }; Const Observer = new MutationObserver(callback); Observe.observe (targetNode, config);Copy the code

This code listens for changes to the value of the input, and it returns

As with the above data structure, if you type a character on the same line, you can’t get the direct value, while beforeInput gets the current changed value and knows the type of the current input through inputType

 <div contenteditable id="input">
      <div>sdfasdf</div>
    </div>   
const input = document.querySelector('#input');
    const log = document.getElementById('values');
    function blod() {
      input.childNodes[1].style.color = 'red'
    }
    input.addEventListener('beforeinput', updateValue);
    function updateValue(e) {
      console.log(e)
    }
Copy the code

The code is shown above, and e is printed as follows

After listening for events prior to input, we can call different SLATE apis to change SLATE’s internal data model, trigger callback functions, and trigger PATH to render the DOM

Let’s reproduce the process with a small example

Var obj = [{type: "header1", children: [{type: 'span', children: [ { text: }}]}] function createEditor(option) {const {data, Function mountElement(vnodes, container) {vnodes.forEach((VNodeChild) => {const {type, text } = VNodeChild; if (text) { container.innerHTML = text } else { if (type === 'header1') { var el = VNodeChild.el = Idocument. The createElement method (" h1 "); } if (type === 'span') { var el = VNodeChild.el = document.createElement('span'); } mountElement(VNodeChild.children, el); container.appendChild(el); }}); } const editor = { mount() { const container = document.querySelector(selector) mountElement(data, container) container.addEventListener('beforeinput', this.changeViewState) }, ChangeViewState (e) {console.log(e) // Call SLATE's API to change the data model this.updateView()}, updateView(this, Editor) {// Then update the view with vDOM}} return {... editor, children: data } } createEditor({ data: obj, selector: '#input' }).mount()Copy the code

If you look at the code above, you’ll see that high-end ingredients are often the easiest to cook

V5 some reference points for our daily development

The last part is actually the essence of what we should focus on. All the research is for the purpose of applying what we learn. Next, we will focus on programming. Architecture, engineering. Absorb them separately, these are also the three qualities a reliable programmer must have.

programming

Programming is all about how elegantly we write a piece of code

For example, look at the source code comrades, will find that the source code with a large number of weakMap to save some state, not because weakMap is the new syntax of ES6, but he maintains a weak reference to the object referenced by the key name, if this object is not used, can be GC, so his appearance must solve a practical problem.

In a section such as the source code to register the event, place the time name and the method corresponding to the event in an object like this

const eventConf = {
  beforeinput: handleBeforeInput,
  blur: handleOnBlur,
  focus: handleOnFocus,
  click: handleOnClick,
  compositionstart: handleCompositionStart,
  compositionend: handleCompositionEnd,
  compositionupdate: handleCompositionUpdate,
  keydown: handleOnKeydown,
  keypress: handleKeypress,
  copy: handleOnCopy,
  cut: handleOnCut,
  paste: handleOnPaste,
  dragover: handleOnDragover,
  dragstart: handleOnDragstart,
  dragend: handleOnDragend,
  drop: handleOnDrop,
}
Copy the code

At registration time, we don’t need a binding, just a wrapper method

forEach(eventHandlerConf, (fn, eventType) => {
      $textArea.on(eventType, event => {
        fn(event, this, editor)
      })
    })
Copy the code

Not only is the code concise, but the directory is clear, and in fact, there are a lot of things in the code like closures,

The use of higher-order functions will not be described here

Architectural aspects

The so-called architecture aspect is to make the code layered, with clear structure, decoupling function

Architectural aspects, in fact, is actually need strength, for example, on how to design mentioned in the text, view update, how to design model synchronization, plug-in mechanism how to design, how to open the closed, so that using design patterns to clever to solve the problem, how to determine the appropriate programming thought, he needs to be groped for a long time to form, still need to see the source code,

Here, for example, is the plug-in mechanism of V5. As mentioned above, the plug-in mechanism follows the open and closed principle, which greatly increases the scalability of the system and the decoupling of functions

engineering

The so-called engineering aspect is to build the current project into a convenient development project through the tool chain (Webpack, Babel, etc.).

In V5, the learn +rollup scheme is actually the current engineering mainstream, and the specific configuration still needs to read the documents of each tool chain

But what we can learn from V5 is that it provides a lot of file solutions, which we can refer to in the project, for example; The style solution, the SVG solution, the TS solution, the build production package solution, it can actually be used as a reference for our current project. Not when you’re building an engineering project from scratch.

Editor selection is recommended

Having said that, but getting back to the point, how should we choose an editor in our own project?

Because most people don’t study the source code, he doesn’t care about the underlying implementation logic

1. First of all, if your business is extremely complex and needs to customize a lot of custom functions, then SLATE is undoubtedly the first choice, but the premise is that you have to implement the View layer by yourself, and have this development ability

2, if the project is small and beautiful, only need to use the common functions of rich text, do not need to customize, then in fact, the range of options is relatively broad, basically now all the out-of-the-box rich text on the market are suitable for you, for exampleDraft.js .Prosemirror .Quill,TinyMCE,CKEditor, can be satisfied

3. If you want clarity, wangEditor and TinyMce are fine, but ckeditor is not recommended

4. If you want a feature-rich, readable document out of the box, uEditor or TinyMac will do, but take precedence over the latter, which is unmaintainable and too heavy

5. If you are a Source code enthusiast in China, wangEditor V4 and V5 are well worth researching, and of course, vue….. is the best one to look into