A personal understanding of front-end componentization development

preface

My personal feeling about front-end componentization is that it’s very close and very far at the same time. This is because every day when we write code, we feel that our components are componentized. This is because everything before is an illusion without consideration of complex scenarios (performance, maintenance).

I always feel very strange about component design specification and related best practices is not a lot of people discuss, fewer articles can be found, and most of the article is basically useless, only told us to high cohesion and low coupling, etc., does not tell how to do and how to think componentized it.

Therefore, the following can only elaborate on my own understanding at present, and there may be wrong ideas. Because Vue2 is used for development at work, I use Vue2 as an example. However, this article does not cover the detailed usage of components, such as life cycles, event passing, etc., but only the design level.

Component types

Components can be roughly divided into basic components, intermediate components, and business components.

A base component is basically a component provided by the various UI component libraries. It is a component that can run independently of your system because you must use the data structure specified by these components.

Business components are components that are relevant to your business and are the cornerstone of the entire system. They are not easy to operate independently of the system because they rely only on specific data and state generated by the current business.

Intermediate components, as I call them, glue together multiple business components and interpret business logic in terms of the events and interfaces that business components expose.

The rest focuses on the design of business components and intermediate components.

What is componentization?

First of all, what do components consist of?

Component is the combination of data and process, that is, component = data + process.

Data is a set of data structures that a component uses for rendering.

A process is an aggregation of logic, consisting of component UI logic and business logic.

It may sound like an object-oriented idea, but components are more functional, with the same output for the same input, and easy to combine.

In my opinion, to achieve a component that is easy to change and compose, the following points are needed:

  • Single responsibility
    • This component focuses on only one function
  • Data ownership
    • Determine the data required by the component. Extraneous data is not allowed.
    • It works without relying on external data
  • Interface design
    • Provides a set of interfaces for manipulating (setting, getting) current data
  • Incident response
  • Easy to test
    • It is difficult for a component to test UI behavior and business logic at the same time, and it is important to ensure that business-related logic can be tested separately.

Discuss the direction

  • A way of thinking about componentization

  • How is data ascribed

  • The way components of different responsibility types are decoupled

  • Interface design

  • Component test

    In fact, there are many points that can be extended to discuss these five points, and only some important points will be discussed here.

    Due to limited space, this article only discusses the way of componentized thinking and how data is ascribed.

A way of thinking about componentization

How to think?

When we think about a feature, the normal way to think about it is from top to bottom, layer by layer, what each layer needs to do, and then implement it.

Must not be multiple hierarchy thinking at the same time, because when doing so, will inevitably data ownership (that is, the current components needed data) mess, once the data belongs in confusion, and interface will be messy, that event, the whole process is to break down when the end is what you are doing will keep modify, or can achieve same function, But subsequent changes can become difficult or cause performance problems.

When implementing a new feature, I am used to it is according to the distribution of prototype components first, and then the first not to think about the needs of each component of the data, but put them together first, and then began to think about this from a functional group a function of the input and output, what kind of events and the response to expose what interface to communicate with external components, Once you have this roughly determined, you can pull this component down into the current smallest unit component and implement it from there. Once you’ve implemented the current level, implement the next level. This allows you to focus on the thinking and design of the current component, and makes it easier to maintain a single responsibility for the component.

Split hierarchy is for better combination, so when designing components, we should avoid the design logic of parent and child components (granularity is an abstract function), but divide all functions and combine them with one or more intermediate components.

Abstract functional granularity refers to the fact that a component has an independent piece of functionality that has no relationship to other components

The parent-child component design refers to placing all the required states of the child components in the parent component, which causes the child component A to update the child component B when it is updated. Such A coupling design cannot exist in multiple abstract function granularity.

The component itself should have its own data, not just rely on the flow of props.

When you have a large number of components, you will find that they are no longer fixed in a component, even if the component itself is an intermediate component, it can be combined eventually, which is the purpose of componentization.

Business logic changes

There is also the question of how business logic should be handled. All business logic is nothing more than doing something to the data after a UI interaction. Therefore, the change of business logic must mean that the operation on the data is changed, and the data operation is changed, we must adjust the code, so the design of the component should ensure two points:

  • Data operations corresponding to business logic are invoked as services rather than embedded in components. Smaller ones can be maintained using a JS file exposed function, while larger ones can be packaged as a component to be called.

    • Too small: for example, we have a business multi-choice component TenantSet (checkbox). The database stores a value after bit operation, which needs to be converted into Array Array when used on the page. Therefore, [bit operation value to Array], [Array to bit operation value] these two functions are encapsulated in the JS file we maintain. The idea is that when other components need to rely on the converted value, they can use the function directly instead of relying on the TenantSet component.

    • Large: For example, if we have a business table component, TenantTable, which can be configured with buttons, each button can support different behaviors (such as export, bulk import, delete, jump, etc.), what should we do about these behaviors?

      • Write it directly in TenantTable
      • Use a JS file
      • Process with a behavior processing component

      If one day we need a Kanban component that displays the same behavior of the corresponding button as that supported by TenantTable, then I think the third option would be a better choice

  • The UI events of the component itself should be passed externally as much as possible, and no side effects should be done in the pass action functions that make the component difficult to use.

    The idea of reactive programming can also be a good solution to the problem of business logic changes, Vue3 core also provides a reactive API, but this is not the whole of reactive, this aspect of my own understanding is not too deep, I hope to be able to post an article in the future.

How to determine the attribution of data (attributes)?

In a word: everything that involves rendering itself is in this component, and everything that does not involve rendering itself is not in this component.

example

Scene:

We wrapped a table component to display some data, similar to the el-Table, and when the list cell content was too small, we wanted the mouse to pop up a tooltip to display the complete information.

The code is as follows:

<template> <div class="table-container"> <t-header/> <t-body @mouseenter="handleMouseEnter"/> <custom-tooltip :tooltip-content="tooltipContent" /> </div> </template> <script> import CustomTooltip from './CustomTooltip' // CustomTooltip import THeader from './THeader' import TBody from './TBody' export default { components: { CustomTooltip, THeader, TBody }, data() { return { tooltipContent: '' } } methods: { handleMouseEnter(tooltipContent) { this.tooltipContent = tooltipContent } } } </script>Copy the code

Take a second and think about what the problem is with this.

Moving from the mouse to displaying the ToolTip, the T-body component emits the MouseEnter event, sets the TooltipContent in the handleMouseEnter event, and the CustomToolTip component displays it as if nothing was wrong.

The problem is that after changing tooltipContent, the entire Table component needs to be re-rendered.

The key point is: tooltipContent has changed, should I rerender the entire table?

Obviously not, so this property should be maintained by the CustomToolTip component itself.

So how do you do that?

<template> <div class="table-container"> <t-header/> <t-body @mouseenter="handleMouseEnter"/> <custom-tooltip ref="customTooltip" :tooltip-content="tooltipContent" /> </div> </template> <script> import CustomTooltip from './CustomTooltip' // Custom prompt import THeader from './THeader' import TBody from './TBody' export default {components: { CustomTooltip, THeader, TBody }, data() { return { tooltipContent: '' } } methods: { handleMouseEnter(tooltipContent) { this.$refs.customTooltip.setContent(tooltipContent) } } } </script>Copy the code

Why do we take this example? Because of this problem with ElementUI’s Table component, our project is a PaaS project with a lot of configuration items, so there are a lot of processing functions, and when the mouse moves over the table, it’s obvious that it’s stucking because it triggers multiple renders, causing a lot of function execution. Just before dealing with memory leaks, optimization of el-table, read the source code, so this way to solve the problem.

So here comes another question: How can props and data be distinguished?

The first step is to determine the purpose of the component, which is generally divided into two categories, as follows

  • Display class: Receives data and performs display operations without modifying data. Such as display features like error logging or to-do lists.
  • Edit class: Data needs to be modified based on input. Like filling out some forms or something.

For presentation-class components, data properties that do not involve changes during subsequent execution can be put into props because there are no rendering performance issues involved and it is easier to write.

For editing class components, you must have your own data structure in data.

Part of the reason is that updates to props cause rendering to happen over and over again, affecting its parent and sibling components.

The other part is that the data structure of the component is not clear. If it depends too much on external input, the capability of the component will become fuzzy in a series of subsequent changes, and the encapsulation can not be guaranteed. Therefore, there is a hard and fast requirement for such components that they can still be used without relying on external data. If the external wants to manipulate the data of the component, it must be done through the interface exposed by the component itself. Encapsulation must be ensured.

There is no absolute rule here. For example, use more $refs to call components and less props. You need to make a choice between performance and convenience, and it’s not always a trade-off. This can be more troublesome for small projects, but the benefits for large projects can be huge.

Extending from this component, let’s write a business table component that does the following

  • Request data based on the interface
  • Handle various actions when a user clicks on a table (such as editing, creating, exporting, and so on)
<template> <div class="template-table"> <custom-table ref="table" :get-list="getList" :button-config="buttonConfig" @row-button-click="handleRowButtonClick" /> <action-handler ref="actionHandler" /> </div> </template> <script> import CustomTable from '@/ Components /CustomTable' // Table component import ActionHandler from '@/ Components /ActionHnadle' // Process action component Export default {components: {CustomTable, ActionHandler} props: {// getList: {type: Function, default: () => alert('no getList method, how run? ButtonConfig: {type: Object, default: () => {name: 'edit', action: 'row.edit()' } } }, methods: { handleRowButtonClick(row, button) { this.$refs.actionHandler.handlerSingleAction(row, button) } } } </script>Copy the code

First of all, the business table component is divided into two blocks. One is the table component, which is used to render data and has the function of filtering, paging and so on. One is a behavior processing component that handles the behavior of table buttons, such as imports, exports, and so on.

Properties such as getList and buttonConfig, which are unlikely to change, are passed in via props.

After clicking the button on the table, it could be export, import, edit, or delete, which contains a lot of state, so we aggregate the state into the ActionHandler component and then use the interface provided by ActionHandler to handle the corresponding behavior.

There may be a large number of behavior processing components in ActionHandler, but since only one behavior can be executed at a time, we don’t need to break it down any more and just condense the state into that component.

By writing this way, the two components can be used separately. For example, instead of displaying a table, I can display a kanban or other card layout. As long as the input is the same, I can use this component to process the corresponding behavior.

Custom-table can be split into filters, table, pagination, sort, etc. Again, these components work even if they are not in custom-table.

If a component does more than one thing, it’s time to consider breaking it up. This is what the core business component must do, because most of the change will be done in these components.

Another point about rendering is that while the framework provides ways to optimize rendering overhead, such as virtual DOM and diff algorithms, its purpose is to reduce overhead in one necessary update, not multiple unnecessary renders.

If you have a different opinion or a good one, please feel free to comment or like in the comments section to let more people see it, thank you!