Author | Zheng Jiatao (Qunqing)

Source |ERDA official account

preface

In the previous article, we described the background of the story, and also briefly explained the components and protocol assumptions:

A rich common component library.


Second, component rendering ability, rendering business components into common components.


Protocol rendering capabilities to handle complex interactions.

And the benefits of this development model:

This is designed to significantly reduce front-end effort, especially in front and back end docking, and docking can even be considered “reversed” in two ways: the reversal of interface definition and the change in development timing.

If you’re not familiar with our design philosophy, you can read the previous post, “Are they crazy enough to write the” front end “in Go?”

In this post I’ll go into more detail about component and protocol rendering, and how you can use both to make the front end completely business-free.

In the end, of course, you’ll find that REST is not what matters, it’s the proper sharding of concerns that matters, and the framework is just a means of helping with sharding.

The component rendering

Specifically, how does the business logic accomplish for a common component?

For example, the following same Card component is composed and rendered with common elements:

cardComp:
  props:
    titleIcon: bug-icon
    title: Title
    subContent: Sub Content
    description: Description

However, with different props, different scenes can be rendered.

Scenario 1: Requirement cards

KanbancardComp: Props: TitleIcon: Requirement - Icon Title: A Simple Requirement Subcontent: Complete container expansion without dipping Description: The need to store and record the expansion changes of users is realized by calling the internal encapsulated K8S interface.

Scenario 2: Pack task cards

TaskcardComp: Props: TitleIcon: Flow-Task-Icon Title: BuildPack (Java) Subcontent: ✅ Success Description: time 02:09, begin at 10:21 am ...

For the back end, just follow the data definition of the generic component and implement the rendering method according to the rules of the component renderer (again, the back end doesn’t need to know what the UI looks like; the back end is always dealing with the data).

func Render(ctx Context, c *Comp) error {
  // 1. query db or internal service
  // 2. construct comp
  return nil
}

On the interaction side, we also need a common component that defines all operations, which can be thought of as the effect or result of the interaction. For example, query rendering is the most basic operation; For the requirement card, click to view the details, and the delete, edit and so on in the upper right corner are all actions:

However, at the level of common components, there is no need to be aware of the business, and what is defined is common operations such as click, menu-list, etc., and the specific business is realized by business components.

The interactions expressed by the front end at the rendering layer (such as hover, click, release, etc.) ultimately correspond to the actions defined by the generic component, which are a standard component render request. Think of it this way: assuming that the page is already presented to the user, the browser interaction events triggered by the user’s mouse (or perhaps the trackpad) are translated by the front-end “renderer” into operations for components, such as deletions, that trigger a rerender once performed.

The following pseudocode describes how the operation is rendered:

Func Render(CTX Context, c *Comp, ops string) error {if ops! = "view" { doOps() } // continue render (aka re-render) return nil }

Is there something missing? Yes, the back end can’t conjure up a card out of thin air. Components must be rendered with input, either directly or indirectly from the user. For example, when a user says, “I want to see a requirement card with id=42,” this is a direct input, typically represented in the URL. The other case is an indirect input: “I want to see all the requirement cards with status = DONE,” so for a particular requirement card, the required ID is obtained from another component, the requirement list.

How this data is bound between components will be explained in more detail in the following section (protocol rendering). For now, just know that for the rendering of a single component (i.e., a business component), we have regulated the developer to define only the input necessary for rendering the component. This is an attractive approach, effectively high cohesion and low coupling through parameter masking of external logic.

Of course, where there is an input, there is an output (remember that data binding certainly binds the output of one component to the input of another). Of course interacting with its stateful nature (explained in more detail in protocol rendering), we end up having the inputs and outputs combined in a single state, again the example of the requirement card:

KanbancardComp: Props: TitleIcon: Requirement - Icon Title: A Simple Requirement Subcontent: Complete container expansion without dipping Description: The need to store and record the expansion changes of users is realized by calling the internal encapsulated K8S interface. state: ticketId: 42

The last big image summarizes the component rendering process:

Agreement apply colours to a drawing

Here we need to extend a practical problem, using a Web UI example: When a user accesses a page, the page does not have just one component. For example, an event Kanban page has components such as filters, Kanban corridors, event cards, type switchers, and so on.

Also, there is a headache: the components are clearly interconnected. For example, the filter condition of the filter controls the list result of the Kanban passage. In traditional Web development, these linkages must be implemented by the front-end code. But if the front end is to implement these linkages, it obviously requires deep understanding and participation in the business, which is against our whole design approach.

Here we need to be clear: in a real scenario, there is no way to standardize the structure of a single component and then completely separate the front and back ends. In other words, simply moving the definition of the structure from the back end to the front end is only half the battle: decoupling the front and back ends at the static level.

The other half, which requires us to link the components together, the actions on the components, the actions that lead to renderings, etc., can also be properly handled by the renderer, i.e. decoupling the front and back ends at the dynamic level.

When we talk about component rendering we deliberately leave it in suspense: in order to maintain high cohesion and low coupling of the component, we parameterize all input required by the component, and we call the input and output parameters together “state”. Then how to connect parameters and states together to complete the logic of the whole page?

If you think about it, it’s easy. We need a protocol to define these dependencies and how they are passed, as shown in the following form.

Protocol. Yaml:

// Component: KanBancardComp: State: // TicEtid:?? operations: click: reload: true ticketDetailDrawerComp: state: visible: false // ticketId: ?? // Rendering process: __Trigger__: kanbanCardComp: operations: click: set ticketDetailDrawerComp.state.visible = true ticketDetailDrawerComp: operations: close: set ticketDetailDrawerComp.state.visible = false __Default__: kanbanCardComp: state: ticketId: {{ url.path.2 }} ticketDetailDrawerComp: state: ticketId: {{ kanbanCardComp.state.ticketId }}

When rendering a protocol, __Trigger__ is executed first. Rendering an action type temporarily changes the state of some of the components. Secondly, __Default__ is executed to perform data binding between components. Finally, the rendering of the individual business components is done, which was explained in detail in the first article.

Ultimately, however, we need to render this protocol to the front end, because rendering is just procedural data and ultimately needs to be converted to mundane values. In this case, the protocol ends up rendering (assuming the user clicks on the card) as:

Component: KanbancardComp: props: // Backend component renders concrete data based on ticKETID =42 Description: It is necessary to store and record the user's expansion changes, which can be achieved by calling the internal encapsulated K8S interface. State: ticketId: 42 operations: click: Reload: true TicketDetailDrawerComp: props: // state: visible: true ticketId: 42 operations: close: reload: true

It is worth emphasizing that the front end does not need to know about the interaction between components. All of the linkage is done through renderings. This means that each operation will cause the protocol to be re-rendered. Internally, it is the implementation of the operation (such as deletion, update), that is to call the determined interface to perform the operation, and then re-render the scene.

To put it simply, every time an operation occurs in the front end, as long as I tell the back end what I have done (operation), the back end immediately refreshes the page after performing the operation. Of course, the actual process is a little complicated.

As you can see from the above diagram, each operation is very “short-sighted”, especially since the front end only needs to “tell” what the back end is doing and nothing else needs to be known. Then one might ask: What if some operation needs to pass data? For example, in the traditional docking approach, if a resource is to be deleted, the front end must pass in the ID of the backend resource. That brings us to one feature that protocols must have: state.

RESTful APIs are stateless, but the business logic needs to be sequential, and therefore requires state. Traditionally, this state is maintained by the front end, especially the SPA, which maintains all the state in memory.

For example, for an editing form, after opening the form first, the front end needs to call the back-end interface to pass the resource ID to get data, and copy the data into the form for rendering; When the Save button click is triggered, the current value in the form needs to be obtained and the back-end Save interface is called for saving.

As we know, the maintenance of state breaks down when the current end doesn’t care about the business. This state must sink to the same level as the render, specifically the protocol render level (because the component monomer is deliberately designed to be cohesive and stateless).



So how do we do that? It’s also very simple, we know the fact that we have to render before we do something (that is, we can’t click on a page until we visit it). We only need to predict all the data needed for subsequent operations in advance during rendering and build it into the protocol in advance. The front end in the execution of the operation, the protocol and the operation of the object and other information can be reported. When the component renderer receives the protocol, it will have all the required parameters (because I prepared them for myself), and then it will start the next prediction and re-render the protocol for the front end to render the interface.

In the following example, you can see that when the user enters the first page (currentPageNo = 1), we already expect the user to go to the next page, and we already have the parameters required for that action (pageNo = 2) in the protocol; Then the user performs an operation on the component paginationBar, next, and when the operation is processed, the required data can be obtained.

components:
  paginationBar:
    state:
      currentPageNo: 1
    operations:
      next:
        reload: true
        meta:
          pageNo: 2

So-called “has long been thought that” it is easy, because each business component will define the business component implementation in the common component of the operation, we ask for when define these operations, must want to define these operations must be outside the incoming parameters (the main reason why the outside world, because of some business parameters inside the component can dispose of, without having to rely on external components, For example, state or props data is sufficient).

Finally, in terms of presentation, the hierarchical relationship between components should be supplemented to form a tree-shaped relationship. In order to layout, some “meaningless” components such as Container and LrContainer should be filled:



However, these are static data that can be put directly into the protocol without rendering:

hierarchy:
  root: ticketManage
  structure:
    ticketManage:
      - head
      - ticketKanban
    head:
      left: ticketFilter
      right: ticketViewGroup

components:
  ticketManage:
    type: Container
  head:
    type: LRContainer
  ...

That’s all for now

We achieved complete front-end separation through component rendering, protocol rendering, and a common component library. However, we have found in practice that there are many times when a complete separation of front and back ends is difficult, which is why we would think of the protocol as hosting the scene rather than the page.

If there is a complete back-to-back separation, then the entire page or even the entire website should be a protocol, because there are business implications to jumping out of the protocol or switching between pages. But the reality is that if there are too many components in a protocol that need to be orchestrated, the complexity of the choreography is cumbersome for the developer, and the loss of complexity completely overwhelms the benefits of complete backend separation.

As a practical matter, we should practice a “separation of concerns” rather than a complete “separation of front and back”. When designing components and protocols, we always ask ourselves:

  • What does the front end care about?
  • What does the back end care about?
  • What should the framework/protocol focus on?

In the end, our framework chooses a form that coexists with the traditional docking mode, and can operate with each other in a friendly way.

For example, when the front end presents a component, it can choose to “secretly” call some RESTful APIs to complete certain tasks, or “piece together” multiple protocols in a page for linkage, and so on.

We also found that the logic of the front-end presentation layer becomes very simple (with a limited number of components) when a large amount of business logic can sink from the front end to the back end. We had unexpected multi-terminal support, such as a rendering layer for the CLI, a rendering layer for the IDE plug-in, and so on.

Of course, we haven’t done it yet, but if you’re smart, it won’t be hard to do it

At present, all the code of Erda is open source, sincerely hope you can participate in it!

  • Erda Github address:https://github.com/erda-project/erda
  • Erda Cloud Website:https://www.erda.cloud/