Before understanding modularity and componentization, it is a good idea to understand what high cohesion and low coupling are. It helps you better understand modularity and componentization.

High cohesion, low coupling

High cohesion and low coupling are concepts in software engineering, which is an important index to judge the quality of code. High cohesion means that a function tries to do only one thing. Low coupling refers to the low degree of association between two modules.

It may be hard to understand just by looking at the text, but here’s a simple example.

// math.js
export function add(a, b) {
    return a + b
}

export function mul(a, b) {
    return a * b
}
Copy the code
// test.js
import { add, mul } from 'math'
add(1.2)
mul(1.2)
mul(add(1.2), add(1.2))
Copy the code

Math.js above is a good example of high cohesion, low coupling. The add() and mul() functions only do one thing, and there is no direct connection between them. If you want to link the two functions together, you can only do so by passing parameters and returning values.

If there are good examples, there are bad examples, and here’s another bad example.

/ / parent company
class Parent {
    getProfit(. subs) {
        let profit = 0
        subs.forEach(sub= > {
            profit += sub.revenue - sub.cost
        })

        return profit
    }
}

/ / subsidiary
class Sub {
    constructor(revenue, cost) {
        this.revenue = revenue
        this.cost = cost
    }
}

const p = new Parent()
const s1 = new Sub(100.10)
const s2 = new Sub(200.150)
console.log(p.getProfit(s1, s2)) / / 140
Copy the code

The code above is a bad example because the parent company directly manipulates its subsidiary’s data when calculating profits. Better still, the subsidiary returns the profits directly to the parent company, which then does a summary.

class Parent {
    getProfit(. subs) {
        let profit = 0
        subs.forEach(sub= > {
            profit += sub.getProfit()
        })

        return profit
    }
}

class Sub {
    constructor(revenue, cost) {
        this.revenue = revenue
        this.cost = cost
    }

    getProfit() {
        return this.revenue - this.cost
    }
}

const p = new Parent()
const s1 = new Sub(100.10)
const s2 = new Sub(200.150)
console.log(p.getProfit(s1, s2)) / / 140
Copy the code

This is much better. Subsidiaries have added a getProfit() method that the parent company calls directly when it does the aggregation.

Application of high cohesion and low coupling in business scenarios

Ideal is beautiful, reality is cruel. The previous example is a classic example of high cohesion and low coupling. But writing code in a business scenario is not that perfect, and many times a function will have to handle more than one logic.

Take, for example, user registration. Typically, registration binds a click-event callback function register() to the button, which handles the registration logic.

function register(data) {
    // 1. Verify that the user data is valid
    /** * Verify account * Verify password * verify SMS verification code * Verify ID card * Verify email address */
    // omit a bunch of if statements...

    // 2. If the user has uploaded a profile picture, the profile picture is converted to Base64 code and saved
    /** * Create a new FileReader object * to convert the image to base64 */
    // omit conversion code...

    // 3. Invoke the registration interface
    // omit registration code...
}
Copy the code

This example is a very common requirement for clicking a button to process multiple logic. As you can see from the code, the result is three functions coupled together.

For high cohesion and low coupling, a function should try to do only one thing. So we can separate out the other two functions of the function: validation and transformation, and encapsulate them into a single function.

function register(data) {
    // 1. Verify that the user data is valid
    verifyUserData()

    // 2. If the user has uploaded a profile picture, the profile picture is converted to Base64 code and saved
    toBase64()

    // 3. Invoke the registration interface
    // omit registration code...
}

function verifyUserData() {
    /** * Verify account * Verify password * verify SMS verification code * Verify ID card * Verify email address */
    // omit a bunch of if statements...
}

function toBase64() {
    /** * Create a new FileReader object * to convert the image to base64 */
    // omit conversion code...
}
Copy the code

After this modification, it is more in line with the requirements of high cohesion, low coupling. It is very convenient to modify, remove or add new functions in the future.

Modularization and componentization

modular

Modularization is to regard a file as a module, and the scope between them is isolated from each other. A module is a function that can be reused many times. In addition, the modular design also reflects the idea of divide and conquer. What is divide and conquer? Wikipedia defines it as follows:

The literal interpretation is “divide and conquer”, which is to divide a complex problem into two or more identical or similar sub-problems until the sub-problems can be solved simply and directly, and the solution of the original problem is the combination of the solutions of the sub-problems.

From the front end, a single JavaScript file or CSS file is a module.

For example, a math.js file is a math module that contains functions related to math operations:

// math.js
export function add(a, b) {
    return a + b
}

export function mul(a, b) {
    return a * b
}

export function abs() {... }...Copy the code

A button.css file containing button-related styles:

/* Button style */
button{... }Copy the code

componentization

So what is componentization? We can think of components as UI components in a page, and a page can be made up of many components. For example, a background management system page may contain Header, Sidebar, Main, and other components.

A component contains template(HTML), script, and style, in which script and style can be composed of one or more modules.

As you can see from the figure above, a page can be broken down into components, and each component can be broken down into modules, which is a good example of the idea of divide-and-conquer (if you forget the definition of divide-and-conquer, go back and look again).

Thus, the page becomes a container, and components are the basic elements of this container. Components can be freely switched between components, multiple reuse, modify the page only need to modify the corresponding component, greatly improving the development efficiency.

Ideally, a page element is made up entirely of components, so that the front end only needs to write some interaction logic code. Although this situation is difficult to achieve completely, but we should try to do in this direction, and strive to achieve comprehensive componentization.

Web Components

Thanks to technological advances, there are currently three major frameworks in building tools (e.g. Webpack, Vite…). Can be very good with the combination of componentization. Vue, for example, uses a *. Vue file to write template, script, and style together. A *. Vue file is a component.

<template>
    <div>
        {{ msg }}
    </div>
</template>

<script>
export default {
    data() {
        return {
            msg: 'Hello World! '}}}</script>

<style>
body {
    font-size: 14px;
}
</style>
Copy the code

Can you componentize without using frameworks and build tools?

The answer is yes, componentization is the future of the front-end, and Web Components is the componentization standard that browsers support natively. Using the Web Components API, browsers can implement componentization without introducing third-party code.

In actual combat

Now let’s create a Web Components button component that will pop up with a message Hello World! . Click here to see the DEMO.

Custom Elements

The browser provides a customElements.define() method that allows usto define a custom element and its behavior and then use it in the page.

class CustomButton extends HTMLElement {
    constructor() {
        // The super method must be called first
        super(a)// The function code of the element is written here
        const templateContent = document.getElementById('custom-button').content
        const shadowRoot = this.attachShadow({ mode: 'open' })

        shadowRoot.appendChild(templateContent.cloneNode(true))

        shadowRoot.querySelector('button').onclick = () = > {
            alert('Hello World! ')}}connectedCallback() {
        console.log('connected')
    }
}

customElements.define('custom-button', CustomButton)
Copy the code

The above code registers a new element using the customElements.define() method and passes it the element name custom-button, the class CustomButton that specifies the element’s functionality. We can then use this in the page:

<custom-button></custom-button>
Copy the code

This custom element inherits from HTMLElement (the HTMLElement interface represents all HTML elements), indicating that the custom element has the characteristics of an HTML element.

use<template>Sets the custom element content

<template id="custom-button">
    <button>Custom button</button>
    <style>
        button {
            display: inline-block;
            line-height: 1;
            white-space: nowrap;
            cursor: pointer;
            text-align: center;
            box-sizing: border-box;
            outline: none;
            margin: 0;
            transition:.1s;
            font-weight: 500;
            padding: 12px 20px;
            font-size: 14px;
            border-radius: 4px;
            color: #fff;
            background-color: #409eff;
            border-color: #409eff;
            border: 0;
        }

        button:active {
            background: #3a8ee6;
            border-color: #3a8ee6;
            color: #fff;
        }
      </style>
</template>
Copy the code

As you can see from the code above, we set the content and the style for this custom element, in the

Shadow DOM

With the name, content, and style of the custom element set, the last step is to mount the content and style onto the custom element.

// The function code of the element is written here
const templateContent = document.getElementById('custom-button').content
const shadowRoot = this.attachShadow({ mode: 'open' })

shadowRoot.appendChild(templateContent.cloneNode(true))

shadowRoot.querySelector('button').onclick = () = > {
    alert('Hello World! ')}Copy the code

The element has a attachShadow() method in the function code, which attaches the shadow DOM to the custom element. DOM, we know what it means, is a page element. So what does “shadow” mean? “Shadow” means that DOM functionality attached to a custom element is private and does not conflict with other elements on the page.

The attachShadow() method also has a mode parameter, which has two values:

  1. openRepresents that the shadow DOM can be accessed externally.
  2. closedThe shadow DOM cannot be accessed externally.
// open, return to shadowRoot
document.querySelector('custom-button').shadowRoot
// closed, returns null
document.querySelector('custom-button').shadowRoot
Copy the code

The life cycle

Custom elements have four life cycles:

  1. connectedCallback: called when the custom element is first connected to the document DOM.
  2. disconnectedCallback: called when a custom element is disconnected from the document DOM.
  3. adoptedCallback: called when a custom element is moved to a new document.
  4. attributeChangedCallback: called when an attribute of a custom element is added, removed, or changed.

The lifecycle automatically invokes the corresponding callback function when triggered, such as the connectedCallback() hook in this example.

Finally, attach the complete code:

<! DOCTYPEhtml>
<html>
<head>
    <meta charset="utf-8">
    <title>Web Components</title>
</head>
<body>
    <custom-button></custom-button>

    <template id="custom-button">
        <button>Custom button</button>
        <style>
            button {
                display: inline-block;
                line-height: 1;
                white-space: nowrap;
                cursor: pointer;
                text-align: center;
                box-sizing: border-box;
                outline: none;
                margin: 0;
                transition:.1s;
                font-weight: 500;
                padding: 12px 20px;
                font-size: 14px;
                border-radius: 4px;
                color: #fff;
                background-color: #409eff;
                border-color: #409eff;
                border: 0;
            }

            button:active {
                background: #3a8ee6;
                border-color: #3a8ee6;
                color: #fff;
            }
          </style>
    </template>

    <script>
        class CustomButton extends HTMLElement {
            constructor() {
                // The super method must be called first
                super(a)// The function code of the element is written here
                const templateContent = document.getElementById('custom-button').content
                const shadowRoot = this.attachShadow({ mode: 'open' })

                shadowRoot.appendChild(templateContent.cloneNode(true))

                shadowRoot.querySelector('button').onclick = () = > {
                    alert('Hello World! ')}}connectedCallback() {
                console.log('connected')
            }
        }

        customElements.define('custom-button', CustomButton)
    </script>
</body>
</html>
Copy the code

summary

Those of you who have used Vue may notice that the Web Components standard is very similar to Vue. I assume that Vue was designed with reference to Web Components.

If you want to learn more about Web Components, refer to the MDN documentation.

The resources

  • Front End Engineering – Basics
  • Web Components

Get you started with front-end engineering

  1. Technology selection: How to do the technology selection?
  2. Uniform specifications: How do you create specifications and use tools to ensure that they are strictly followed?
  3. Front-end componentization: What is modularization and componentization?
  4. Testing: How do I write unit tests and E2E (end-to-end) tests?
  5. Build tools: What are the build tools? What are the features and advantages?
  6. Automated Deployment: How to automate deployment projects with Jenkins, Github Actions?
  7. Front-end monitoring: explain the principle of front-end monitoring and how to use Sentry to monitor the project.
  8. Performance Optimization (I) : How to detect website performance? What are some useful performance tuning rules?
  9. Performance Optimization (2) : How to detect website performance? What are some useful performance tuning rules?
  10. Refactoring: Why do refactoring? What are the techniques for refactoring?
  11. Microservices: What are microservices? How to set up a microservice project?
  12. Severless: What is Severless? How to use Severless?