-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathguide.txt
622 lines (472 loc) · 30.5 KB
/
guide.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
********** 1-2 **********
1.
Устанавливаем Vue CLI
npm install -g @vue/cli
2.
Запускаем менеджер проектов
vue ui
В настройках пресета выбираем
- Babel
- Progressive Web App (PWA) Support
- Router
- Vuex
- CSS Pre-processors
- Linter / Formatter
- Использовать файл
history mode нужен для использования путей через /
Sass/SCSS (with node-sass)
3.
В VS Code необходимо установить расширение Vetur для работы с Vue
4.
Устанавливаем библиотеку MaterializeCSS
npm install materialize-css@next
Подключаем библиотеку внутри App.vue
Знак ~ ведёт к папке node_modules
@import '~materialize-css/dist/css/materialize.min.css';
Импортируем index.css из папки assets
@import 'assets/index.css';
при использовании алиаса @ в пути выдавал ошибку
Подключаем файл js из библиотеки MaterializeCSS внутри main.js
import 'materialize-css/dist/js/materialize.min'
Подключаем иконки в файле public/index.html
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
5.
EditorConfig позволяет поддерживать единый стиль кода проекта
независимо от редактора
Создаём файл .editorconfig
Устанавливаем расширение для редактора EditorConfig for VS Code
6.
Создаём папку src/layouts
В ней хранятся слои (базовые элементы окна для разных страниц)
Создаём страницы внутри папки src/views
Настраиваем роуты в файле src/router/index.js
В роутах можно использовать алиас @ в путях
Добавляем свойство meta, чтобы привязать страницы к нужным слоям
Внутри App.vue мы сможем получить данные из meta свойства
7.
Настраиваем рендеринг внутри App.vue
<component :is="layout"> рендерит слой
<router-view/> рендерит страницу (view) по текущему роуту
:is="layout" относится к методу computed.layout()
Делаем import слоёв (алиасы работают) и регистрируем их в components
********** 3 **********
8.
Выносим элементы слоёв в отдельные компоненты
Внутри папки components/app создаём компоненты Navbar и Sidebar
Данные компоненты необходимо зарегистрировать и прописать в MainLayout
Вставляем компоненты внутрь слоя
<Navbar />
<Sidebar />
9.
В компоненте Sidebar вставляем <router-link> для ссылок
Заменяем элемент <li> на <router-link>
10.
@click.prevent модификатор события (аналогичен preventDefault)
@click.prevent="$emit('click')" передаём пользовательское событие наверх
Пользовательское событие может иметь любое название
Внутри MainLayout слушаем данное событие через v-on:click или @click
В том же файле внутри свойства data создаём состояние isOpen
@click="isOpen = !isOpen" переключаем значение состояния
:class="{full: !isOpen}" добавляем класс "full" если isOpen === false
<Sidebar v-model="isOpen" /> передаём состояние в Sidebar
Внутри Sidebar принимаем состояние через props
Фактически value принимает значение isOpen
:class="{open: value}" показываем Sidebar если isOpen === true
********** 4 **********
11.
Заменяем ссылку внутри <div class="fixed-action-btn"> на <router-link>
Инициализируем элемент Dropdown из MaterializeCSS в Navbar.vue
mounted() это life cycle хук, который вызывается после монтирования dom-дерева
ref="dropdown" это ссылка на элемент (название любое)
this.$refs это обращение к ссылкам
@click.prevent="logout" вызывает метод methods.logout()
this.$router.push('/login?message=logout') редирект на другую страницу
'/login?message=logout' передаём параметр
{{date}} вставляем значение, которое хранится в data
beforeDestroy() это life cycle хук, который вызывается перед размонтированием компонента
src/filters хранит фильтры, которые помогают модифицировать данные
В файле main.js импортируем те или иные фильтры и там же их регистрируем
Vue.filter('date', dateFilter) передаём название и функцию фильтра
{{date | date('datetime')}} применяем фильтр к параметру внутри компонента
Фильтр date становится виден в глобальной области
В него можно передавать дополнительные параметры
********** 5 **********
12.
Валидация формы с помощью плагина Vuelidate
Login.vue
@submit.prevent="submitHandler" вешаем обработчик на форму
type="submit" на элементе <button> позволяет вызвать данное событие
Устанавливаем плагин Vuelidate
npm install --save vuelidate
Внутри main.js импортируем данный плагин
Vue.use(Vuelidate) и регистрируем его
В каждом компоненте появляется новое свойство validations
Названия полей validations соответствуют названиям полей data
Импортируем валидаторы
import {} from 'vuelidate/lib/validators'
v-model.trim="email" привязываем модель из data
trim это модификатор, который удаляет пробелы
$v.email через объект $v мы получаем результат валидации
$v.email.$dirty в поле уже что-то вписывалось
$v.email.required поле должно быть заполнено
$v.email.email поле должно иметь формат email
Запускаем валидацию при нажатии на кнопку (событие submit)
this.$v.$invalid форма находится в состоянии invalid
this.$v.$touch() запускаем валидацию
v-if=""
v-else-if="" показывать тег при выполнении условия
$v.password.minLength соответствует ли пароль заданной длине
$v.password.$params.minLength.min динамически извлекаем ограничение по количеству символов
formData для отправки на сервер
********** 6 **********
13.
Валидация формы Register.vue
agree: {checked: v => v} наш собственный валидатор
Он принимает значение true или false и возвращает значение true или false
Выход из системы обрабатывается с помощью плагина Toast библиотеки MaterializeCSS (всплывающее сообщение)
Оборачиваем данный плагин в наш собственный плагин
Он расположен в src/utils/message.plugin.js
Импортируем и регистрируем его в файле main.js
Vue.use(messagePlugin)
На странице Login.vue вызываем данный плагин в хуке mounted()
Внутри src/utils создаём файл messages.js
В нём хранятся ключи и соответствующие им сообщения
Например, при выходе из системы мы передаём параметр /login?message=logout
logout соответствует сообщению 'Вы вышли из системы'
this.$route.query.message извлекаем параметр logout
********** 7 **********
14.
Сохраняем конфиг Firebase в файле main.js
Устанавливаем Firebase
npm install --save firebase
В файле main.js импортируем модули Firebase
Инициализируем Firebase
firebase.auth().onAuthStateChanged() обработчик события
Срабатывает когда пользователь авторизовался
В самом коллбэке создаётся приложение Vue один раз, так как обработчик может срабатывать
15.
В папке store создаём файл auth.js
Это модуль для Vuex
Импортируем его в файле store/index.js
В поле modules добавляем auth
Модуль auth содержит логику авторизации, регистрации и log-out
store/auth.js
Поле actions содержит методы, которые мы будем диспатчить
login() пользователь пробует авторизоваться в Firebase с помощью email и пароля
Пробуем на странице Login.vue задиспатчить данный метод
Данные операции являются ассинхронными и используют async/await
При ошибке авторизации метод login() пробрасывает ошибку, которую мы перехватываем в Login.vue
Это необходимо, чтобы не допустить редирект на главную страницу после ошибки авторизации
********** 8 **********
store/auth.js
logout() очищает информацию при выходе пользователя
Диспатчим данный экшен в компоненте Navbar.vue
register() регистрация нового пользователя в Firebase
getUid() возвращает id пользователя в Firebase, если он там есть
dispatch('getUid') экшены можно вызывать только через dispatch()
При регистрации заносим в базу данных начальные параметры по пользователю
await firebase.database().ref(`/users/${uid}/info`).set({
bill: 10000,
name
})
ref() ведёт к нужной таблице
set() вносит данные в таблицу
Внутри Firebase в разделе Database необходимо создать базу данных
В данном проекте использовался вариант Realtime Database
В правилах необходимо разрешить доступ к базе данных авторизованным пользователям
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
В Register.vue делаем диспатч экшена register()
16.
Создаём шину обработки ошибок
store/index.js
Создаём state error
Поле mutations используется для изменения state
Поле getters используется для доступа к значениям state
В EmptyLayout.vue создаём компьютед свойство error(),
которое меняется при изменении state error
this.$store.getters.error получение геттера внутри лейаута
watch.error() следит за изменением компьютед свойства и принимает на вход его значение
Данная цепочка действий начинается с мутации стейта через вызов setError()
store/auth.js
Внутри login() вызываем метод commit(), который изменяет стейт
commit('setError', e) в setError() передаём объект ошибки при авторизации или регистрации
Код данных ошибок имеет значение 'auth/user-not-found'
Переносим его в utils/messages.js
watch.error() в EmptyLayout.vue фактически выводит сообщение об ошибке
********** 9 **********
17.
store/info.js
В данном модуле создаём состояние info
fetchInfo() получаем из базы данных поле info, которое мы создали следующим образом
await firebase.database().ref(`/users/${uid}/info`).set({
bill: 10000,
name
})
Следующая строка извлекает данные из Firebase
const info = (await firebase.database().ref(`/users/${uid}/info`).once('value')).val()
setInfo() изменяет состояние
mutations необходимо вызывать через метод commit
MainLayout.vue
Проверяем, чтобы объект состояния не был пустым
Превращаем объект в массив и смотрим его длину
!Object.keys(this.$store.getters.info).length
Navbar.vue
computed.name() возвращает имя пользователя info.name из состояния
18.
Декомпозируем страницу Home.vue на компоненты HomeBill.vue и HomeCurrency.vue
Создаём компонент Loader.vue
Лоадер возьмём из Preloader MaterializeCSS
Цвет лоадера будем выбирать случайно из массива
В main.js импортируем лоадер
Vue.component('Loader', Loader) регистрируем лоадер глобально
Home.vue
Подключаем лоадер
data хранит модели
Лоадер останавливаем, когда загрузим список валют
Курсы валют мы получим через Fixer API
Создаём файл .env
Он позволяет скрывать переменные, которые содержат ключи, при заливке на гитхаб
Перезапускаем билд проекта, чтобы подхватились данные переменные
Экшен fetchCurrency() создаём в store/index.js (не выносим в отдельный модуль)
fetchCurrency() возвращает курсы валют
Данный экшен использует нативный метод fetch() для обращения к серверу Fixer API
fetch() можно заменить библиотекой Axios для работы с запросами
Используем следующую строку запроса
fetch(`http://data.fixer.io/api/latest?access_key=${key}&symbols=USD,EUR,RUB`)
Внутри Home.vue диспатчим данный экшен
Передаём полученные данные в компоненты
currency.rates содержит непосредственно курсы валют
По умолчанию основная валюта евро
HomeBill.vue
computed.base() меняет основную валюту на рубли
То есть мы приводим bill к евро, а затем переводим в другие валюты относительно курса евро
filters/currency.filter.js задаёт денежный формат счёта
В main.js регистрируем фильтр
Vue.filter('currency', currencyFilter)
Следующие итераторы идентичны
v-for="cur of currencies"
v-for="cur in currencies"
********** 10 **********
19.
Декомпозируем страницу Categories.vue на компоненты CategoryCreate и CategoryEdit
CategoryCreate.vue
Добавляем валидацию формы
В методе mounted() вызываем window.M.updateTextFields()
Это позволит активировать анимацию полей MaterializeCSS
v-model.number="limit" приводит к числу модель данных
store/category.js
Создаём модуль для работы с категориями
Следующая строка создаёт категорию в базе данных
const category = await firebase.database().ref(`/users/${uid}/categories`).push(title, limit)
Метод возвращает объект category, у которого есть дополнительное поле key
key это уникальный идентификатор категории
this.$v.$reset() очищает форму и модель после отправки
В CategoryCreate.vue эмитим событие
this.$emit('created', category)
Слушаем его в родителе Categories.vue
<CategoryCreate @created="addNewCategory" />
Вызываем обработчик addNewCategory()
********** 11 **********
20.
Categories.vue
Получаем начальный список всех категорий в базе данных
await this.$store.dispatch('fetchCategories')
store/category.js
Object.keys(categories).map() преобразуем объект в массив
CategoryEdit.vue
window.M.FormSelect.init() запускаем элемент select формы
destroyed() это life cycle хук, который запускается после уничтожения компонента
this.select.destroy() очищаем обработчики, чтобы не было утечек памяти
props: {} такая запись используется для детального описания входящих пропсов
created() это life cycle хук, который вызывается до mounted()
В created() задаём значения модели data по умолчанию
Это необходимо, чтобы выставить стартовые значения формы
<select v-model="current">
<option :value="c.id">
v-model хранит id отображаемого <option>
:value указывает на id конкретного <option>
watch.current() наблюдает за изменением id селекта через модель current
store/category.js
updateCategory() обновляет данные по категории
await firebase.database().ref(`/users/${uid}/categories`).child(id).update({title, limit})
.child() находит категорию по id
.update() обновляет данные
После обновления данных на сервере необходимо обновить клиент
В компоненте CategoryEdit.vue вызываем пользовательское событие
this.$emit('updated', categoryData)
Внутри Categories.vue слушаем данное событие
@updated="updateCategories"
С обновлением плагина Select MaterializeCSS есть проблема
Он не реагирует на обновление модели данных Categories.vue
Фактически нам требуется форсированно перерисовать компонент CategoryEdit.vue
Для этого можно использовать хак, связанный с атрибутом :key
<CategoryEdit :key="categories.length + updateCount" />
updateCount заводим в модели data
Привязка :key идёт к меняющимся величинам
Изменение :key вызывает перерисовку компонента
При создании категории меняется длина массива categories
При редактировании категории мы меняем величину updateCount
Две данные операции вызовут перерисовку компонента CategoryEdit.vue
В случае отсутствия категорий мы показываем вместо компонента текст
********** 12 **********
21.
Работаем со страницей Record.vue
Инициализировать select необходимо строго после окончания лоадинга
Однако инициализация селекта происходит быстрее отрисовки элемента
Поэтому необходимо обернуть инициализацию в setTimeout(), чтобы сделать задержку
Чтобы в Record.vue получить доступ до геттеров в store/info.js, необходимо импортировать метод из Vuex
import {mapGetters} from 'vuex'
...mapGetters(['info']) добавляем геттер info в computed
Логику по записям выносим в store/record.js
Экшен updateInfo реализован в store/info.js
Для обновления записи необходимо передать объект данных с теми же полями
********** 13 **********
22.
Работаем со страницей Planning.vue
Для каждой категории (categories) проходимся по всем записям (records)
и суммируем расходы
Модифицируем массив categories путём добавления новых полей
progressPercent
progressColor
spend
:class="[c.progressColor]" добавляем классы из массива
tooltip это всплывающий элемент подсказки из библиотеки MaterializeCSS
Чтобы добавить tooltip на страницу, необходимо вызвать следующий метод
и передать элемент, к которому будет привязан tooltip
window.M.Tooltip.init()
Для передачи элемента мы будем использовать пользовательскую директиву
src/directives/tooltip.directive.js
bind() стандартный хук жизненного цикла, который вызывается однократно при первичном связывании директивы с элементом
unbind() вызывается при уничтожении директивы
В main.js регистрируем директиву
Vue.directive('tooltip', tooltipDirective)
Чтобы тултипы не оставались висеть в html, их необходимо очищать в хуке unbind()
Внутри Planning.vue применяем директиву следующим образом
<div class="progress" v-tooltip="c.tooltip">
Таким образом в качестве параметров bind() передаются
el равный элементу div
value равное строке c.tooltip
currency filter можно импортировать внутри блока <script>
и применить его к значению тултипа
не в самой директиве v-tooltip, а именно в блоке <script>
********** 14 **********
23.
Как защитить роуты от перехода через строку браузера
Добавляем дополнительное meta поле auth в роутах
auth: true требует авторизации для перехода на страницу
router.beforeEach() вызывается перед каждой сменой роута
to.matched возвращает массив из одного объекта, содержащего свойства роута
next('/login?message=login') позволяет сделать редирект на другую страницу
next() без параметра продолжает загрузку страницы
********** 15 **********
24.
Работаем со страницей History.vue
Создаём компонент данной страницы HistoryTable.vue
********** 16 **********
25.
Работаем со страницей Detail.vue
В store/record.js создаём экшен fetchRecordById()
В store/category.js создаём экшен fetchCategoryById()
Любое присваивание данных в модели data приводит к перерендерингу
this.$route.params.id получаем параметр id из строки запроса
/detail/:id
/detail/-MExt3UU7R-2wQH0ANi7
<router-link> в отличие от <a> не перезагружает страницу
********** 17 **********
26.
Реализовываем пагинацию (навигацию по сайту)
Устанавливаем плагин vuejs-paginate
npm install --save vuejs-paginate
В файле main.js регистрируем данный компонент
Vue.component('Paginate', Paginate)
В библиотеке MaterializeCSS есть стилизация компонента Pagination
:container-class="'pagination'" данный класс добавляется элементу <ul>
:page-class="'waves-effect'" данный класс добавляется элементу <li>
Задача разбить таблицу на отдельные блоки для навигации
Создаём миксин
@/mixins/pagination.mixin.js
Функционал миксина будет смешан с функционалом компонента, в который мы его добавим
allItems: это все данные, загруженные с сервера
pageSize: это количество элементов в одном отображаемом блоке на странице
pageCount: это количество блоков (равняется длине allItems)
items: это подмассив/блок, который мы отображаем в текущий момент
page: это номер дефолтного блока данных
allItems имеет структуру [ [], [], ... ]
С помощью библиотеки Lodash разбиваем allItems на 5 подмассивов
Устанавливаем пакет Lodash
npm install --save lodash
Внутри pagination.mixin.js импортируем библиотеку Lodash
import _ from 'lodash'
На странице History.vue импортируем данный миксин
mixins: [] специальное поле для миксинов
mixins/pagination.mixin.js
Добавляем query-параметр, чтобы отследить, на каком блоке мы находимся
this.$router.push(`${this.$route.path}?page=${page}`)
views/History.vue
При загрузке страницы выставляем номер блока из query-параметра
this.page = +this.$route.query.page || 1
********** 18 **********
27.
Создаём график на странице History.vue
Устанавливаем плагин vue-chartjs
npm install --save vue-chartjs chart.js
vue-chartjs это оболочка над chart.js
На странице History.vue импортируем нужный тип графика
import {Pie} from 'vue-chartjs'
В поле extends указываем данный объект
Это позволит получить дополнительные методы
Часть логики хука mounted() выносим в methods.setup()
В метод this.renderChart() передаём настройки и данные
Внутри вёрстки добавляем элемент со определённым рефом
<canvas ref="canvas"></canvas>
В labels передаём массив названий категорий
В data передаём массив расходов по каждой категории
Между массивами labels и data должно быть соответствие
backgroundColor и borderColor также соответствуют размерности labels
********** 19 **********
28.
Работаем со страницей Profile.vue
Добавляем элемент Switches из библиотеки MaterializeCSS для переключения языков
<style scoped> локальные стили
mapActions() можно использовать для получения экшенов вместо this.$store.dispatch()
import {mapGetters, mapActions} from 'vuex'
Локализацию будем хранить в Firebase
info/locale
Локализацию будем делать через фильтры
filters/localize.filter.js
При импорте индексный файл подключается автоматически
import store from '@/store'
Регистрируем фильтр локализации
Vue.filter('localize', localizeFilter)
Создаём папку src/locales
В ней будем хранить ключи для локализации
Импортируем их внутри localize.filter.js
В файле date.filter.js добавляем динамическую локализацию даты и времени
Для перерендеринга сайдбара внутри MainLayout.vue добавим динамическое компьютид свойство locale()
Привяжем его к атрибуту :key сайдбара, чтобы форсировать перерендеринг
********** 20 **********
29.
Устанавливаем пакет vue-meta
Он позволяет изменять <title> в блоке <head> для каждой страницы
npm install --save vue-meta
В файле main.js добавляем данный плагин и регистрируем его
import VueMeta from 'vue-meta'
Vue.use(VueMeta)
После чего на каждой странице можно добавлять поле metaInfo в <script>
Если записать его как метод, то <title> будет динамически изменяться
При записи его как свойства такого эффекта нет
Добавляем к <title> название приложения
Прописываем его в глобальной переменной VUE_APP_TITLE внутри .env
Создаём плагин utils/title.plugin.js
Регистрируем его в файле main.js
Vue.use(titlePlugin)
Деплой проекта
npm install -g firebase-tools
firebase login
firebase init
firebase deploy