Помогите вам начать работу с фронтенд-инжинирингом (3): фронтенд-компонентизация

внешний интерфейс JavaScript

Прежде чем разбираться в модуляризации и компонентизации, лучше понять, что такое высокая связность и низкая связанность. Это может помочь вам лучше понять модульность и компонентизацию.

Высокая сплоченность, низкая связанность

Высокая связность и низкая связанность — это концепции разработки программного обеспечения, и это важный показатель для оценки качества кода. Высокая связность означает, что функция максимально выполняет только одну функцию. Низкая связь означает, что степень связи между двумя модулями низкая.

Это может быть нелегко понять, просто взглянув на текст. Давайте рассмотрим простой пример.

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

export function mul(a, b) {
    return a * b
}
// test.js
import { add, mul } from 'math'
add(1, 2)
mul(1, 2)
mul(add(1, 2), add(1, 2))

надmath.jsЭто типичный пример высокой сплоченности и низкой связанности.add(),mul()Функция делает только одну вещь, и между ними нет прямой связи. Если вы хотите связать эти две функции вместе, это может быть достигнуто только путем передачи параметров и возврата значений.

Теперь, когда есть хорошие примеры, есть и плохие примеры, и вот еще один плохой пример.

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

        return profit
    }
}

// 子公司
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

Приведенный выше код является плохим примером, поскольку материнская компания напрямую манипулирует данными дочерней компании при расчете прибыли. Еще лучше, чтобы дочерняя компания напрямую возвращала прибыль материнской компании, которая затем производила агрегирование.

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

Гораздо лучше изменить этот способ, и дочерняя компания добавилаgetProfit()метод, материнская компания напрямую вызывает этот метод при выполнении агрегации.

Применение высокой связности и низкой связанности в бизнес-сценариях

Идеал прекрасен, реальность жестока. Только что приведенный пример является классическим примером высокой сплоченности и низкой связанности. Однако настолько идеально написать код в бизнес-сценарии невозможно: во многих случаях функция должна обрабатывать несколько логик.

Например, регистрация пользователей. Общая регистрация привяжет функцию обратного вызова события клика к кнопке.register(), который обрабатывает логику регистрации.

function register(data) {
    // 1. 验证用户数据是否合法
    /**
    * 验证账号
    * 验证密码
    * 验证短信验证码
    * 验证身份证
    * 验证邮箱
    */
    // 省略一大堆串 if 判断语句...

    // 2. 如果用户上传了头像,则将用户头像转成 base64 码保存
    /**
    * 新建 FileReader 对象
    * 将图片转换成 base64 码
    */
    // 省略转换代码...

    // 3. 调用注册接口
    // 省略注册代码...
}

Этот пример относится к очень распространенной потребности щелкнуть кнопку для обработки нескольких логических операций. Из кода также видно, что в результате написания этой статьи три функции связаны друг с другом.

В соответствии с требованиями высокой связности и низкой связанности функция должна пытаться делать только одну вещь. Таким образом, мы можем выделить две другие функции в функции: проверку и преобразование по отдельности и инкапсулировать их в функцию.

function register(data) {
    // 1. 验证用户数据是否合法
    verifyUserData()

    // 2. 如果用户上传了头像,则将用户头像转成 base64 码保存
    toBase64()

    // 3. 调用注册接口
    // 省略注册代码...
}

function verifyUserData() {
    /**
    * 验证账号
    * 验证密码
    * 验证短信验证码
    * 验证身份证
    * 验证邮箱
    */
    // 省略一大堆串 if 判断语句...
}

function toBase64() {
    /**
    * 新建 FileReader 对象
    * 将图片转换成 base64 码
    */
    // 省略转换代码...
}

После этой модификации он больше соответствует требованиям высокой когезии и низкой связи. Это очень удобно, даже если вы захотите изменить или удалить или добавить новые функции в будущем.

модульный, компонентный

модульный

Модульность заключается в том, чтобы рассматривать каждый файл как модуль, а их области изолированы друг от друга и не мешают друг другу. Модуль — это функция, и их можно использовать многократно. Кроме того, модульная конструкция также отражает идею «разделяй и властвуй». Что такое разделяй и властвуй?Википедияопределяется следующим образом:

Буквальная интерпретация - «разделяй и властвуй», что означает разделение сложной проблемы на две или более идентичных или похожих подзадач до тех пор, пока окончательная подзадача не будет решена просто и напрямую, а решение исходной проблемы не будет сочетание решений подзадач.

С точки зрения внешнего интерфейса отдельные файлы JavaScript и файлы CSS считаются модулем.

Напримерmath.jsфайл, представляющий собой математический модуль, содержащий функции, связанные с математическими операциями:

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

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

export function abs() { ... }
...

Одинbutton.cssфайл, содержащий стили, связанные с кнопками:

/* 按钮样式 */
button {
    ...
}

составной

Так что же такое компонентизация? Мы можем думать о компонентах как о компонентах пользовательского интерфейса на странице, а страница может состоять из многих компонентов. Например, страница системы фонового управления может содержатьHeader,Sidebar,Mainи другие компоненты.

Компонент также содержитtemplate(html),script,styleтри части, из которыхscript,styleМожет состоять из одного или нескольких модулей.

Как видно из приведенного выше рисунка, страницу можно разложить на компоненты, а каждый компонент можно разложить на модули, что полностью воплощает идею разделяй и властвуй (если вы забыли определение разделяй и властвуй, прочтите это снова).

Видно, что страница стала контейнером, а компонент — базовым элементом контейнера. Компоненты можно свободно переключать и повторно использовать несколько раз.Чтобы изменить страницу, вам нужно всего лишь изменить соответствующие компоненты, что значительно повышает эффективность разработки.

Идеальная ситуация заключается в том, что элемент страницы полностью состоит из компонентов, так что внешнему интерфейсу нужно только написать некоторый интерактивный логический код. Хотя полностью осознать эту ситуацию сложно, мы должны приложить все усилия, чтобы сделать это в этом направлении и стремиться к полной компонентизации.

Web Components

Благодаря развитию технологий три основных фреймворка могут быть хорошо разделены на компоненты с помощью инструментов построения (таких как webpack, vite...). Например, Vue, используйте*.vueфайл можетtemplate,script,styleнаписано вместе, а*.vueФайл является компонентом.

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

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

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

Можно ли реализовать компонентизацию без использования фреймворков и инструментов сборки?

Ответ — да, компонентизация — это будущее направление развития интерфейса.Web ComponentsЭто стандарт компонентизации, изначально поддерживаемый браузером. Используя API веб-компонентов, браузеры могут реализовывать компонентизацию без добавления стороннего кода.

настоящий бой

Теперь давайте создадим компонент кнопки веб-компонентов, нажмите на него, появится сообщение.Hello World!.кликните сюдаВы можете увидеть эффект DEMO.

Пользовательские элементы

Браузер предоставляетcustomElements.define()метод, который позволяет нам определить пользовательский элемент и его поведение, а затем использовать его на странице.

class CustomButton extends HTMLElement {
    constructor() {
        // 必须首先调用 super方法 
        super()

        // 元素的功能代码写在这里
        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)

В приведенном выше коде используетсяcustomElements.define()метод регистрирует новый элемент, передавая ему имя элементаcustom-button, класс, определяющий функцию элементаCustomButton. Затем мы можем использовать его на странице следующим образом:

<custom-button></custom-button>

Этот пользовательский элемент наследуется отHTMLElement(Интерфейс HTMLElement представляет все элементы HTML), указывая на то, что этот пользовательский элемент имеет атрибуты элемента HTML.

использовать<template>Установить содержимое пользовательского элемента

<template id="custom-button">
    <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>

Как видно из приведенного выше кода, мы устанавливаем содержимое для этого пользовательского элемента.<button>自定义按钮</button>как и стили, стили размещаются в<style>в этикетке. Можно сказать<template>На самом деле это HTML-шаблон.

Тень ДОМ

Имя, содержимое и стиль пользовательского элемента заданы, и теперь последний шаг — смонтировать содержимое и стиль пользовательского элемента.

// 元素的功能代码写在这里
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!')
}

Функциональный код элемента имеетattachShadow()метод, его роль заключается в подключении теневого DOM к пользовательскому элементу. DOM Мы знаем, что это значит, что означает элементы страницы. Что значит «тень»? «Тень» означает, что функциональность DOM, прикрепленная к пользовательскому элементу, является частной и не конфликтует с другими элементами страницы.

attachShadow()Метод имеет еще один параметрmode, который имеет два значения:

  1. openДелегат может получить доступ к теневой модели DOM извне.
  2. closedУказывает, что теневой DOM недоступен извне.
// open,返回 shadowRoot
document.querySelector('custom-button').shadowRoot
// closed,返回 null
document.querySelector('custom-button').shadowRoot

Жизненный цикл

Пользовательские элементы имеют четыре жизненных цикла:

  1. connectedCallback: вызывается при первом подключении пользовательского элемента к DOM документа.
  2. disconnectedCallback: вызывается, когда пользовательский элемент отключается от DOM документа.
  3. adoptedCallback: вызывается, когда пользовательский элемент перемещается в новый документ.
  4. attributeChangedCallback: вызывается при добавлении, удалении или изменении свойства пользовательского элемента.

Жизненный цикл будет автоматически вызывать соответствующую функцию обратного вызова при его срабатывании, например, в этом примере он установленconnectedCallback()крюк.

Наконец, полный код прилагается:

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

    <template id="custom-button">
        <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() {
                // 必须首先调用 super方法 
                super()

                // 元素的功能代码写在这里
                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>

резюме

Студенты, которые использовали Vue, могут обнаружить, что стандарт веб-компонентов очень похож на Vue. Я предполагаю, что Vue ссылался на веб-компоненты, когда он был разработан (личное предположение, не проверено).

Если вы хотите узнать больше о веб-компонентах, см.Документация MDN.

использованная литература

Позвольте вам начать работу с фронтенд-инжинирингомПолнотекстовый каталог:

  1. Выбор технологии: как сделать выбор технологии?
  2. Единообразные нормы: как сформулировать нормы и использовать инструменты для обеспечения их строгого соблюдения?
  3. Компонентизация интерфейса: что такое модульность и компонентизация?
  4. Тестирование: как писать модульные тесты и E2E (сквозные) тесты?
  5. Инструменты сборки: что такое инструменты сборки? Каковы особенности и преимущества?
  6. Автоматическое развертывание: как использовать Jenkins, Github Actions для автоматизации развертывания проектов?
  7. Интерфейсный мониторинг: объясните принцип внешнего мониторинга и как использовать sentry для мониторинга проекта.
  8. Оптимизация производительности (1): как определить производительность веб-сайта? Каковы некоторые практические правила оптимизации производительности?
  9. Оптимизация производительности (2): как определить производительность сайта? Каковы некоторые практические правила оптимизации производительности?
  10. Рефакторинг: зачем делать рефакторинг? Какие существуют методы рефакторинга?
  11. Микросервисы: что такое микросервисы? Как построить проект микросервиса?
  12. Serverless: что такое Serverless и как использовать Serverless?