5 приемов в помощь разработке на vue.js + vuex

  • Tutorial
Недавно решил разобраться с vue.js. Лучший способ изучить технологию — что-нибудь на ней написать. С этой целью был переписан мой старый планировщик маршрутов, и получился вот такой проект. Код получился достаточно большим для того, чтобы столкнуться с задачей масштабирования.

В этой статье приведу ряд приемов, которые, на мой взгляд, помогут в разработке любого крупного проекта. Этот материал для вас, если вы уже написали свой todo лист на vue.js+vuex, но еще не зарылись в крупное велосипедостроение.



1. Централизованная шина событий (Event Bus)


Любой проект на vue.js состоит из вложенных компонентов. Основной принцип — props down, events up. Подкомпонент получает от родителя данные, которые он не может менять, и список событий родителя, которые он может запустить.

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

Разберемся с событиями. Зачастую полезно иметь глобальный event emitter, с которым может общаться любой компонент независимо от иерархии. Его очень легко сделать, дополнительные библиотеки не нужны:

Object.defineProperty(Vue.prototype,"$bus",{
	get: function() {
		return this.$root.bus;
	}
});

new Vue({
	el: '#app',
	data: {
		bus: new Vue({}) // Here we bind our event bus to our $root Vue model.
	}
});

После этого в любом компоненте появляется доступ к this.$bus, можно подписываться на события через this.$bus.$on() и вызывать их через this.$bus.$emit(). Вот пример.

Очень важно понимать, что this.$bus — глобальный объект на все приложение. Если забывать отписываться, компоненты остаются в памяти этого объекта. Поэтому на каждый this.$bus.$on в mounted должен быть соответствующий this.$bus.$off в beforeDestroy. Например, так:

mounted: function() {
	this._someEvent = (..) => {
		..
	}
	this._otherEvent = (..) => {
		..
	}
	this.$bus.$on("someEvent",this._someEvent);
	this.$bus.$on("otherEvent",this._otherEvent);
},
beforeDestroy: function() {
	this._someEvent && this.$bus.$off("someEvent",this._someEvent);
	this._otherEvent && this.$bus.$off("otherEvent",this._otherEvent);
}

2. Централизованная шина промисов (Promises Bus)


Иногда в компоненте нужно инициализировать некую асинхронную штуку (например, инстанц google maps), к которой хочется обращаться из других компонентов. Для этого можно организовать объект, который будет хранить промисы. Например, такой. Как и в случае в event bus, не забываем удаляться при деинициализации компонента. И вообще, указанным выше способом к vue можно прицепить любой внешний объект с любой логикой.

3. Плоские структуры (flatten store)


В сложном проекте данные зачастую сильно вложены. Работать с такими данными неудобно как в vuex, так и в redux. Рекомендуется уменьшать вложенность, например, воспользовавшись утилитой normalizr. Утилита — это хорошо, но еще лучше понимать, что она делает. Я не сразу пришел к пониманию плоской структуры, для таких же типа себя рассмотрю подробный пример.

Имеем проекты, в каждом — массив слоев, в каждом слое — массив страниц: projects > layers > pages. Как организовать хранилище?

Первое, что приходит в голову — обычная вложенная структура:

projects: [{
	id: 1,
	layers: [{
		id: 1,
		pages: [{
			id: 1,
			name: "page1"
		},{
			id: 2,
			name: "page2"
		}]
	}]
}];

Такую структуру легко читать, легко бегать циклом foreach по проектам, рендерить подкомпоненты со списками слоев и так далее. Но предположим, что нужно поменять название страницы с id:1. Внутри некоторого маленького компонента, который отрисовывает страницу, вызывается $store.dispatch(«changePageName»,{id:1,name:«new name»}). Как найти место, где в этой глубоко вложенной структуре лежит нужный page с id:1? Пробегать по всему хранилищу? Не лучшее решение.

Можно указывать полный путь, типа

$store.dispatch("changePageName",{projectId:1,layerId:1,id:1,name:"new name"})

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

Вторая попытка, из sql:

projects: [{id:1}],
layers: [{id:1,projectId:1}],
pages: [{
	id: 1,
	name: "page1",
	layerId: 1,
	projectId: 1
},{
	id: 2,
	name: "page2",
	layerId: 1,
	projectId: 1
}]

Теперь данные легко менять. Но тяжело бегать. Чтобы вывести все страницы в одном слое, нужно пробежать по вообще всем страницам. Это может быть спрятано в getter-е, или в рендеринге шаблона, но пробежка все равно будет.

Третья попытка, подход normalizr:

projects: [{
	id: 1,
	layersIds: [1]
}],
layers: {
	1: {
		pagesIds: [1,2]
	}
},
pages: {
	1: {name:"page1"},
	2: {name:"page2"}
}

Теперь все страницы слоя могут быть получены через тривиальный геттер

layerPages: (state,getters) => (layerId) => {
	const layer = state.layers[layerId];
	if (!layer || !layer.pagesIds || layer.pagesIds.length==0) return [];
	return layer.pagesIds.map(pageId => state.pages[pageId]);
}

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

4. Мутации не нужны


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

Но валидация далеко не всегда синхронна. Следовательно, по крайней мере часть валидационной логики будет находится не в мутациях, а в действиях (actions).

Предлагаю не разбивать логику, и хранить в actions вообще всю валидацию. Мутации становятся примитивными, состоят из элементарных присваиваний. Но тогда к ним нельзя обращаться напрямую из приложения. Т.е. мутации — некая утилитарная штука внутри хранилища, которая полезна разве что для vuex-дебаггера. Общение приложения с хранилищем происходит через исключительно действия. В моем приложении любое действие, даже синхронное, всегда возвращает промис. Мне кажется, что заведомо считать все действия асинхронными (и работать с ними как с промисами) проще, чем помнить что есть что.

5. Ограничение реактивности


Иногда бывает, что данные в хранилище не меняются. Например, это могут быть результаты поиска объектов на карте, запрошенные из внешнего api. Каждый результат — это сложный объект с множеством полей и методов. Нужно выводить список результатов. Нужна реактивность списка. Но данные внутри самих объектов постоянны, и незачем отслеживать изменение каждого свойства. Чтобы ограничить реактивность, можно использовать Object.freeze.

Но я предпочитаю более тупой метод: пусть state хранит только список id-шников, а сами результаты лежат рядом в массиве. Типа:

const results = {};
const state = {resultIds:[]};

const getters = {
	results: function(state) {
		return _.map(state.resultsIds,id => results[id]);
	}
}

const mutations = {
	updateResults: function(state,data) {
		const new = {};
		const newIds = [];
		data.forEach(r => {
			new[r.id] = r;
			newIds.push(r.id);
		});
		results = new;
		state.resultsIds = newIds;
	}
}

Вопросы


Кое-что у меня получилось не настолько красиво, как хотелось. Вот мои вопросы к сообществу:

— Как победить css анимации сложнее изменения opacity? Часто хочется анимировать появление какого-то блока неизвестных размеров, т.е. изменить его высоту с height: 0 до height: auto.
Это легко решается с javascript — просто оборачиваем в контейнер с overflow: hidden, смотрим высоту обернутого элемента и анимируем высоту контейнера. Это можно решить через css?

— Ищу нормальный способ работы с иконками в webpack, пока безуспешно (поэтому продолжаю пользоваться fontello). Нравятся иконки whhg. Вытащил svg, разбил на файлы. Хочу выбрать несколько файлов и автоматически собирать в inline шрифт + классы на основе названий файлов. Чем это можно делать?
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 20
  • +1
    Часто хочется анимировать появление какого-то блока неизвестных размеров, т.е. изменить его высоту с height: 0 до height: auto.

    Анимируй не height, а max-height. Главное чтоб значение max-height было больше, чем элемент должен достигать. Тем самым установив элементу height: auto; max-height: 0; анимируешь уже только max-height.
    <div id="menu">
        <a>hover me</a>
        <ul id="list">
            <!-- Create a bunch, or not a bunch, of li's to see the timing. -->
            <li>item</li>
            <li>item</li>
            <li>item</li>
            <li>item</li>
            <li>item</li>
        </ul>
    </div>
    

    #menu #list {
        max-height: 0;
        transition: max-height 0.15s ease-out;
        overflow: hidden;
        background: #d5d5d5;
    }
    
    #menu:hover #list {
        max-height: 500px;
        transition: max-height 0.25s ease-in;
    }
    
    


    Живой пример: jsfiddle
    • +1
      Вредный совет, высота неизвестна, а вы на глаз предлагаете подбирать.
      • 0
        Во первых не на глаз, а выставить максимально возможную для элемента, которую он не должен достигать. Во вторых это единственное решение, которое есть. В третьих, результат работы полностью удовлетворительный.
        • +1
          1. У вас 1000 компонентов плавно изменяющих высоту, в разных местах приложения. Откуда вы узнаете максимально возможную высоту?
          2. Блок с высотой 500px будет анимироваться 200мс, а с высотой 100px уже 40мс.
          Единственно правильное решение — это мерить элемент javascript'ом.
          • 0
            Речь шла исключительно о решении на CSS. Оно есть, оно не идеальное, есть подводные камни, но тем не менее, факт его существования и возможность его применения отрицать нельзя. И не надо приплетать хитровымудренные примеры, когда оно не может быть применено, для каждого решения есть своя область применения.
      • 0
        По хорошему, если вдруг очень нужно анимировать что-то кроме transform или opacity, например высоту как в данном случае, что влечёт перерасчёт макета страницы, то лучше использовать методику First Last Invert Play с помощью js. Немного мудрённо, но для для перформанса хорошо.
        • –1

          Это решение нечестное. Нужно заранее в CSS выставить max-height. Это получится только для фиксированных выпадающих меню, а у автора в проекте это используется для динамического списка точек. Придется ставить заведомо большее значение, что-то вроде 10000px.


          А если вы выставите такое значение в своем демо, то заметите, что анимация выпадения сильно ускорилась. Это происходит потому, что браузер рассчитывает кадры, исходя из высоты от 0 до 10000, хотя реальная высота блока будет намного меньше.


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


          А метод с max-height сгодится лишь для фиксированных блоков из 5 элементов, или прототипов, которые все равно придется переделывать нормально.

        • +1

          По анимации, могу посоветовать вот эту часть (5) еще есть остальные 4 части

          • 0

            Спасибо, познавательно. Особенно в части мутаций.


            Я использую webpack-svgstore-plugin.
            Также можно подключить директиву


            <svg width="12" height="12" fill="#ccc9c6" v-svg="'mono-info'" />,
            # где `v-svg` — путь к `id` в спрайте
            • 0
              Есть ли разница, где отписываться: в beforeDestroy или destroyed?
              У меня тоже были проблемы с памятью, но после добавления отписок в destroyed от части утечек памяти избавился.
              • 0
                Имхо, если у тебя листенер через addEventListener привязан к ДОМу — отписывайся в beforeDestroy.
                В destroyed ДОМ не доступен:

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


                Если не привязан — хоть там, хоть там.
                • 0
                  Нет, у меня там общая шина, не привязанная к DOM.
                  Спасибо, теперь понятно, в чем отличие.
              • +1
                Можно глупый вопрос? Почему event bus создается как пустой объект Vue. Во-первых, вроде как в приложении уже есть инстанс Vue. Почему нельзя использовать его? Во-вторых, с точки зрения дизайна это вообще ни разу не очевидно и выглядит как какой-то левый хак. И в целом, насколько легковесна такая операция? Потому что вот пример плагина, который берет и создает собственный even bus, хотя мог бы использовать тот, который уже есть в приложении.
                • 0
                  Это рекомендация из официальной документации для распространения. Ранее (v1) был кроме emit() был еще dispatch(), который бросал эвент вверх по дереву компонентов.
                • 0

                  Первый совет напомнил повсеместное использование rootScope в angular 1 приложении, что как по мне антипаттерн, от которого в последствии избавились.

                  • 0
                    На мой взгляд, между глобальными переменными и глобальной шиной событий есть большая разница. По сути, все redux-mobx-vuex это попытки более безопасного использования глобальных переменных. Как только приложение разбивается на локальные компоненты, над ними всегда висит что-то глобальное, что их связывает. EventEmitter — еще один способ связи, довольно безопасный.
                  • 0
                    Все события на странице так или иначе приводят к изменению данных. Зачем городить глобальный Event Bus и следить за утечками, если есть vuex и actions, а компоненты уже реактивно отреагируют на изменившиеся данные?
                    • 0
                      Наверное, в vuex возможно хранить вообще все переменные приложения, но, на мой взгляд, это — неоптимальный вариант. У меня в vuex хранится только критический минимум важных данных, а компоненты имеют свои собственные локальные данные и события. Они более независимы, лучше живут сами по себе.
                      Можно же вообще все в одном компоненте написать.
                      Простой пример — вызов модального окошка с настройками, которое вызывается из компонента каждого элемента списка и еще откуда-нибудь.
                      • 0
                        Не надо в vuex хранить всё. Но если компонентам нужна связь длиннее, чем родитель-потомок, то есть смысл задуматься о вынесении общего состояния в vuex (не обязательно всё в одном хранилище держать, там система модулей есть).
                        А модальное окошко я бы через плагин сделал, добавляющий всем компонентам метод для показа окошка (можно возвращать промис с результатами работы окошка).
                    • 0
                      Часто хочется анимировать появление какого-то блока неизвестных размеров, т.е. изменить его высоту с height: 0 до height: auto.

                      Вопрос не имеет отношение в Vue, это независимая css проблема.
                      Анимировать max-height невкусно, ибо (1) изчезает плавность, если взять с большим запасом, или (2) можно «подрезать» текст, если высота окажется больше значения max-height. Лично мне нравится в подобных случаях анимировать line-height+opacity, получается прикольно и вполне плавно.

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.