javascript

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

  • суббота, 8 июля 2017 г. в 03:11:51
https://habrahabr.ru/post/332628/
  • Разработка веб-сайтов
  • JavaScript


Недавно решил разобраться с 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 шрифт + классы на основе названий файлов. Чем это можно делать?