Несколько советов по упаковке компонентов Vue

Vue.js

Эта статья синхронизирована в личном блогеshymean.comвверх, добро пожаловать, чтобы следовать

Я давно пишу Vue, помимо регулярного развития бизнеса, мне также следует подумать и поразмышлять над правильным способом инкапсуляции компонентов. Взяв в качестве примера компонент всплывающего окна, одна из реализаций заключается в том, чтобы ввести компонент, который необходимо отображать во всплывающем окне, в требуемом шаблоне, а затем управлять компонентом всплывающего окна через переменную флага. Бизнес-код будет полон избыточной логики компонентов всплывающих окон, что очень неэлегантно.

В этой статье собраны некоторые советы по разработке компонентов Vue, включая множество примеров кода.

среда разработки

vue-cli3Он предоставляет очень удобную функцию, с помощью которой можно быстро написать несколько тестовых демонстраций, что является необходимой средой для разработки компонентов. Ниже приведены шаги установки

// 全局安装vue-cli3
npm install -g @vue/cli
vue -V // 查看版本是否为3.x

// 安装扩展,此后可以快速启动单个vue文件
npm install -g @vue/cli-service-global

// 快速启动demo文件
vue serve demo.vue

Если вам нужен scss, вам также нужно установить sass-loader в директорию и т.д.

Ниже приведены несколько проблем, с которыми вы можете столкнуться при использовании vue-cli3.Дополнительные руководства см. по адресу:Очень подробное руководство по Vue-cli3.0 [попробуйте прямо сейчас! ]

пользовательский файл записи

При необходимости (например, необходимость разработки мобильных компонентов) можно использоватьvue serveПри настройке файла записи html запишите его в корневой каталогindex.html, и убедитесь, что страница содержит#appДом может быть.

Внедрение общедоступных файлов миксов

пройти черезstyle-resources-loaderВнесите общие сочетания стилей и т. д. в каждый файл, см.автоматизированный импорт

Необходимо получить доступ к глобальному объекту Vue

В какой-то момент вам нужно запросить глобальный объект Vue, например, при разработке глобальных директив и плагинов.

import Vue from "vue"
import somePlugin from "../src/somePlugin"

Vue.use(somePlugin)

Приведенный выше способ записи не подойдет, т.к.vue serve xxx.vueЕго можно использовать только как решение для быстрого прототипирования, а используемый Vue — это не тот объект, который импортируется при импорте. Один обходной путь - указать вручнуюvue serveвходной файл

// index.js
import Vue from "../node_modules/vue/dist/vue.min"
import placeholder from "../src/placeholder/placeholder"

Vue.use(placeholder)

new Vue({
    el: "#app",
    template: ``,
    created(){},
})

Компонентная система Vue

API компонентов Vue в основном состоит из трех частей: prop, event, slot.

  • propsУказывает параметры, полученные компонентом, предпочтительно в виде объектов, чтобы для каждого атрибута можно было установить тип, значение по умолчанию или значение пользовательского атрибута проверки, а также вход можно было проверить по типу, валидатору и т. д. .
  • slotНекоторый контент или компоненты могут быть динамически вставлены в компоненты, что является важным способом реализации компонентов более высокого порядка; когда требуется несколько слотов, можно использовать именованные слоты.
  • eventЭто важный способ для дочерних компонентов передавать сообщения родительским компонентам.

Однонаправленный поток данных

Ссылаться на:Однонаправленный поток данных — официальная документация.

Обновления родительских свойств распространяются на дочерние компоненты, но не наоборот.

Односторонний поток данных — очень очевидная особенность компонентов Vue, и значение props не должно напрямую изменяться в дочерних компонентах.

  • Если переданный реквизит используется только для отображения и не предполагает модификации, его можно использовать прямо в шаблоне.
  • Если вам нужно преобразовать значение свойства, а затем отобразить его, вы должны использовать вычисляемое свойство
  • Если значение prop используется для инициализации, вы должны определить свойство данных дочернего компонента с prop в качестве его начального значения.

из источника/src/core/vdom/create-component.jsа также/src/core/vdom/helpers/extract-props.jsКак видите, при обработке значения реквизита сначала из

function extractPropsFromVNodeData(){
  const res = {}
  const { attrs, props } = data
  // 执行浅拷贝
  checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false)

  return res
}

Изменение свойств в дочернем компоненте не изменит родительский компонент, потому чтоextractPropsFromVNodeDataчерезмелкая копияПередать атрибуты реквизиту.

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

связь между компонентами

Здесь вы можете обратиться к:Связь компонента Vue полностью раскрыта, написано более развернуто

  • Отношения между родительским и дочерним компонентами можно резюмировать какреквизит передается вниз, событие события передается вверх
  • Передача данных между компонентами-предками и потомками (через несколько поколений) может быть выполнена с использованиемобеспечить и ввестиреализовать

Кроме того, если вам необходимо обмениваться данными между компонентами или родственными компонентами, вы можете реализовать это через eventBus или vuex.

«Обход» однонаправленного потока данных

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

В соответствии с односторонним потоком данных компонента и механизмом передачи событий дочерний компонент должен уведомить родительский компонент через событие и изменить исходные данные реквизита в родительском компоненте, чтобы завершить обновление статуса. Сценарий модификации данных родительского компонента в дочернем компоненте также относительно распространен в бизнесе, так как же можно «обойти» ограничение одностороннего потока данных?

повышение статуса

Вы можете обратиться к улучшению состояния React и напрямую передать логику обработки данных родительского элемента в дочерний компонент через реквизиты.Дочернему компоненту нужно только отображать данные и монтировать события.

<template>
    <div class="counter">
        <div class="counter_btn" @click="onMinus">-</div>
        <div class="counter_val">{{value}}</div>
        <div class="counter_btn" @click="onPlus">+</div>
    </div>
</template>

<script>
    export default {
        props: {
            value: {
                type: Number,
                default: 0
            },
            onMinus: Function,
            onPlus: Function
        },
    };
</script>

Затем передайте обработчик событий при вызове

<template>
    <div>
        <counter :value="counter2Val" :on-minus="minusVal" :on-plus="plusVal"></counter>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                counter2Val: 0,
            }
        },
        methods: {
            minusVal(){
                this.counter2Val--
            },
            plusVal(){
                this.counter2Val++
            }
        }
    }
</script>

Очевидно, поскольку в каждом родительском компоненте нужно реализоватьon-minusа такжеon-plus, поэтому усиление состояния не решает проблему в корне.

синтаксический сахар v-модели

Vue имеет встроенныйv-modelДиректива v-model — это синтаксический сахар, который можно разобрать на props:value и events:input. То есть, пока компонент предоставляет свойство с именем value и пользовательское событие с именем input, если эти два условия выполняются, пользователь может использовать его в пользовательском компоненте.v-model

<template>
  <div>
    <button @click="changeValue(-1)">-1</button>
    <span>{{currentVal}}</span>
    <button @click="changeValue(1)">+1</button>
  </div>
</template>

<script>
export default {
  props: {
    value: {
      type: Number // 定义value属性
    }
  },
  data() {
    return {
      currentVal: this.value
    };
  },
  methods: {
    changeVal(val) {
      this.currentVal += parseInt(val);
      this.$emit("input", this.currentVal); // 定义input事件
    }
  }
};
</script>

Тогда вам нужно только передать команду v-model при вызове

<counter v-model="counerVal"/>

С помощью v-model очень удобно синхронизировать данные родительского компонента в дочернем компоненте. В версиях после 2.2 есть возможность настроитьv-modelНазвание опоры и события директивы, см.элемент конфигурации модели

export default {
    model: {
        prop: 'value',
        event: 'input'
    },
    // ...
 }

Получить ссылку на экземпляр компонента

При разработке компонентов очень полезным методом является получение экземпляра компонента. компоненты могут быть$refs,$parents,$childrenПолучить ссылку на экземпляр виртуальной машины другими способами

  • $refsПросто добавьте атрибут ref к компоненту (или dom)

  • $parentsПолучите узел родительского компонента, смонтированный дочерним компонентом

  • $children, получить все дочерние узлы компонента

Эти интерфейсы возвращают все вноды, через которые можно пройти.vnode.componentInstanceПолучите соответствующий экземпляр компонента, а затем напрямую вызовите метод компонента или доступ к данным. Хотя этот метод несколько противоречит концепции проектирования компонентов и увеличивает стоимость связи между компонентами, реализация кода будет более лаконичной.

компонент проверки формы

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

Ниже приведен пример компонента формы, который демонстрирует проверку формы путем получения ссылки на компонент.

Сначала определите, как будет использоваться компонент,

  • xm-formперениматьmodelа такжеruleдва реквизита
    • modelПредставляет объект данных, привязанный к форме, и этот объект является последней отправленной формой.
    • ruleУказывает политику правила проверки, которую можно использовать при проверке формы.async-validatorплагин
  • xm-form-itemполученоpropАтрибут, соответствующий значению ключа модели и правила компонента формы, по ключу данные формы берутся из модели, а правило валидации берется из правила.

Вот пример кода с использованием

<template>
    <div class="page">
        <xm-form :model="form" :rule="rule" ref="baseForm">
            <xm-form-item label="姓名" prop="name">
                <input v-model="form.name"/>
            </xm-form-item>
            <xm-form-item label="邮箱" prop="email">
                <input v-model="form.email"/>
            </xm-form-item>
            <xm-form-item>
                <button @click="submit">提交</button>
            </xm-form-item>
        </xm-form>
    </div>
</template>

<script>
    import xmForm from "../src/form/form"
    import xmFormItem from "../src/form/form-item"

    export default {
        components: {
            xmForm,
            xmFormItem,
        },
        data() {
            return {
                form: {
                    name: "",
                    email: ""
                },
                rule: {
                    name: [
                        {required: true, message: '用户名不能为空', trigger: 'blur'}
                    ],
                    email: [
                        {required: true, message: '邮箱不能为空', trigger: 'blur'},
                        {type: 'email', message: '邮箱格式不正确', trigger: 'blur'}
                    ],
                }
            }
        },
        methods: {
            submit() {
                // 调用form组件的validate方法
                this.$refs.baseForm.validate().then(res => {
                    console.log(res)
                }).catch(e => {
                    console.log(e)
                })
            }
        }
    }
</script>

Далее Давайте реализуем компонент элемента формы, основной функцией которого является размещение элементов формы и отображения сообщений об ошибках

<template>
    <label class="form-item">
        <div class="form-item_label">{{label}}</div>
        <div class="form-item_mn">
            <slot></slot>
        </div>
        <div class="form-item_error" v-if="errorMsg">{{errorMsg}}</div>
    </label>
</template>
<script>
    export default {
        name: "form-item",
        props: {
            label: String,
            prop: String
        },
        data() {
            return {
                errorMsg: ""
            }
        },
        methods: {
            showError(msg) {
                this.errorMsg = msg
            }
        }
    }
</script>

Затем реализуем компонент формы

  • пройти черезcalcFormItemsполучить каждыйxm-form-item, который хранится в formItems
  • Предоставьте интерфейс проверки, вызовите AsyncValidator внутренне и просмотрите каждый элемент формы в formItems в соответствии с результатом.propАтрибут для обработки соответствующей информации об ошибке
<template>
    <div class="form">
        <slot></slot>
    </div>
</template>

<script>
    import AsyncValidator from 'async-validator';

    export default {
        name: "xm-form",
        props: {
            model: {
                type: Object
            },
            rule: {
                type: Object,
                default: {}
            }
        },
        data() {
            return {
                formItems: []
            }
        },
        mounted() {
            this.calcFormItems()
        },
        updated() {
            this.calcFormItems()
        },
        methods: {
            calcFormItems() {
                // 获取form-item的引用
                if (this.$slots.default) {
                    let children = this.$slots.default.filter(vnode => {
                        return vnode.tag &&
                            vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'form-item'
                    }).map(({componentInstance}) => componentInstance)

                    if (!(children.length === this.formItems.length && children.every((pane, index) => pane === this.formItems[index]))) {
                        this.formItems = children
                    }
                }
            },
            validate() {
                let validator = new AsyncValidator(this.rule);

                let isSuccess = true

                let findErrorByProp = (errors, prop) => {
                    return errors.find((error) => {
                        return error.field === prop
                    }) || ""
                }

                validator.validate(this.model, (errors, fields) => {
                    this.formItems.forEach(formItem => {
                        let prop = formItem.prop
                        let error = findErrorByProp(errors || [], prop)
                        if (error) {
                            isSuccess = false
                        }

                        formItem.showError(error && error.message || "")
                    })
                });

                return Promise.resolve(isSuccess)
            }
        }
    }
</script>

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

Инкапсулировать компоненты API

Некоторые компоненты, такие как окна подсказок, всплывающие окна и т. д., больше подходят для отдельных вызовов API, например

import MessageBox from '@/components/MessageBox.vue'
MessageBox.toast('hello)

Как добиться такого компонента, который не нужно встраивать в шаблон вручную? Оказывается, помимо монтирования компонентов путем встраивания компонентов в шаблон в дочерние элементы, Vue также предоставляет метод ручного монтирования компонентов.$mount

let component = new MessageBox().$mount()
document.getElementById('app').appendChild(component.$el)

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

компонент всплывающего сообщения

Компонент сообщения – это компонент, предназначенный для рисования и отображения подсказок на странице. Ниже приведена простая реализация.

<template>
    <div class="alert">
        <div class="alert-main" v-for="item in notices" :key="item.name">
            <div class="alert-content">{{ item.content }}</div>
        </div>
    </div>
</template>

<script>
    let seed = 0;

    function getUuid() {
        return 'alert_' + (seed++);
    }

    export default {
        data() {
            return {
                notices: []
            }
        },
        methods: {
            add(notice) {
                const name = getUuid();

                let _notice = Object.assign({
                    name: name
                }, notice);

                this.notices.push(_notice);

                // 定时移除,单位:秒
                const duration = notice.duration;
                setTimeout(() => {
                    this.remove(name);
                }, duration * 1000);
            },
            remove(name) {
                const notices = this.notices;

                for (let i = 0; i < notices.length; i++) {
                    if (notices[i].name === name) {
                        this.notices.splice(i, 1);
                        break;
                    }
                }
            }
        }
    }
</script>

Реализуем логику монтирования компонента сообщения на страницу, и выложим интерфейс для отображения сообщения внешнему миру

// alert.js
import Vue from 'vue';

// 具体的组件
import Alert from './alert.vue';
Alert.newInstance = properties => {
    const props = properties || {};
	// 实例化一个组件,然后挂载到body上
    const Instance = new Vue({
        data: props,
        render (h) {
            return h(Alert, {
                props: props
            });
        }
    });
    const component = Instance.$mount();
    document.body.appendChild(component.$el);
	// 通过闭包维护alert组件的引用
    const alert = Instance.$children[0];
    return {
        // Alert组件对外暴露的两个方法
        add (noticeProps) {
            alert.add(noticeProps);
        },
        remove (name) {
            alert.remove(name);
        }
    }
};

// 提示单例
let messageInstance;
function getMessageInstance () {
    messageInstance = messageInstance || Alert.newInstance();
    return messageInstance;
}
function notice({ duration = 1.5, content = '' }) {
    // 等待接口调用的时候再实例化组件,避免进入页面就直接挂载到body上
    let instance = getMessageInstance();
    instance.add({
        content: content,
        duration: duration
    });
}

// 对外暴露的方法
export default {
    info (options) {
        return notice(options);
    }
}

Затем вы можете использовать API для вызова всплывающего компонента

import alert from './alert.js'
// 直接使用
alert.info({content: '消息提示', duration: 2})
// 或者挂载到Vue原型上
Vue.prototype.$Alert = alert
// 然后在组件中使用
this.$Alert.info({content: '消息提示', duration: 2})

компоненты более высокого порядка

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

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

Компоненты высокого порядка в React — относительно часто используемый пакет компонентов, как реализовать компоненты высокого порядка в VUE?

В функции рендеринга компонента вам нужно только вернуть тип данных vNode.Если вы сделаете некоторую обработку заранее в функции рендеринга и вернетеthis.$slots.default[0]Соответствующий vnode может реализовывать компоненты более высокого порядка.

Встроенная поддержка активности

Vue имеет встроенный компонент более высокого порядкаkeep-alive,Проверитьисходный кодМожно обнаружить, что принцип реализации заключается в достижении сохраняемости компонентов путем поддержания кеша и возврата кэшированного vnode в соответствии с ключом в функции рендеринга.

throttle

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

Во-первых, давайте посмотрим, как используется компонент Throttle, получая два реквизита

  • timeУказывает интервал времени для дросселирования
  • eventsУказывает имя обрабатываемого события, несколько событий разделяются запятыми.

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

 <template>
    <div>
        <Throttle :time="1000" events="click">
            <button @click="clickBtn">click {{count}}</button>
        </Throttle>
    </div>
</template>

Ниже приведена конкретная реализация.Основная функция реализации компонентов более высокого порядка заключается в обработке vnode в текущем слоте в функции рендеринга.

const throttle = function (fn, wait = 50, ctx) {
    let timer
    let lastCall = 0
    return function (...params) {
        const now = new Date().getTime()
        if (now - lastCall < wait) return
        lastCall = now
        fn.apply(ctx, params)
    }
}

export default {
    name: 'throttle',
    abstract: true,
    props: {
        time: Number,
        events: String,
    },
    created() {
        this.eventKeys = this.events.split(',')
        this.originMap = {}
        this.throttledMap = {}
    },
    // render函数直接返回slot的vnode,避免外层添加包裹元素
    render(h) {
        const vnode = this.$slots.default[0]
        this.eventKeys.forEach((key) => {
            const target = vnode.data.on[key]
            if (target === this.originMap[key] && this.throttledMap[key]) {
                vnode.data.on[key] = this.throttledMap[key]
            } else if (target) {
                // 将原本的事件处理函数替换成throttle节流后的处理函数
                this.originMap[key] = target
                this.throttledMap[key] = throttle(target, this.time, vnode)
                vnode.data.on[key] = this.throttledMap[key]
            }
        })
        return vnode
    },
}

Мы можем дополнительно инкапсулировать и реализовать компонент Debounce с помощью функции debounce.Можно видеть, что роль компонентов более высокого порядка существует для улучшения компонента. Для других приложений компонентов более высокого порядка вы можете обратиться кПрименение HOC (компонента высшего порядка) в Vue.

резюме

В этой статье обобщаются несколько методов реализации компонентов Vue.

  • На примере компонента счетчика показано, как синхронизировать родительские и дочерние компоненты с помощью синтаксического сахара v-model.
  • Взяв в качестве примера компонент проверки формы, он показывает метод инкапсуляции компонента путем получения экземпляра подкомпонента.
  • На примере компонента глобального всплывающего окна показано, как вручную монтировать и монтировать компонент для инкапсуляции компонента API.
  • Взяв в качестве примера компонент дроссельной заслонки, он показывает способ реализации компонентов более высокого порядка в vue.

После понимания API Vue относительно легко понять вышеупомянутые концепции, инкапсуляция компонентов, помимо владения API, больше связана с основами JavaScript. Начать работу с Vue очень просто, но написание элегантного кода Vue также требует больших знаний.