Вступление
По умолчанию при написании одностраничного приложения (SPA) Vue.js все необходимые ресурсы, такие как файлы JavaScript и CSS, загружаются вместе при загрузке страницы. При работе с большими файлами это может привести к неудовлетворительному взаимодействию с пользователем.
С помощью Webpack можно загружать страницы по запросу в Vue.js,
используя функцию import()
вместо ключевого слова import
.
Зачем загружать по запросу?
Типичный SPA в Vue.js работает за счет того, что все функциональные возможности и ресурсы упаковываются и доставляются вместе, чтобы пользователи могли использовать приложение без необходимости обновлять страницы. Если вы явно не разработали приложение для загрузки страниц по запросу, все страницы будут загружены либо сразу, либо предварительно загружены / загружены заранее, с использованием ненужной полосы пропускания и замедлением загрузки страницы.
Это особенно плохо для больших SPA с большим количеством страниц. Люди с медленным интернет-соединением или низкопроизводительными устройствами, такими как мобильные телефоны, будут иметь плохой пользовательский опыт. Загружая по запросу, пользователям никогда не потребуется загружать больше, чем им нужно.
Vue.js не имеет индикатора загрузки динамических модулей. Даже с предварительной выборкой и предварительной загрузкой - ни один визуальный индикатор не позволяет пользователям узнать, как идет загрузка. Мы также добавим индикатор выполнения, чтобы улучшить взаимодействие с пользователем.
Подготовка проекта
Во-первых, нам нужен способ взаимодействия нашего индикатора выполнения с Vue Router. Для этого мы будем использовать шаблон шины событий .
Шина событий - это, по сути, одноэлементный экземпляр Vue. Поскольку все
экземпляры Vue имеют систему событий с использованием $on
и $emit
,
мы можем использовать ее для передачи событий в любом месте нашего
приложения.
Создадим новый файл eventHub.js
в каталоге components
import Vue from 'vue'
export default new Vue()
Теперь мы настроим Webpack, чтобы отключить предварительную выборку и
предварительную загрузку. Мы можем сделать это индивидуально для каждой
функции или отключить глобально. Создайте vue.config.js
в корневой
папке и добавьте конфигурацию для отключения предварительной выборки и
предварительной загрузки:
module.exports = {
chainWebpack: (config) => {
// Disable prefetching and preloading
config.plugins.delete('prefetch')
config.plugins.delete('preload')
},
}
Добавление маршрутов и страниц
Мы будем использовать Vue Router. Для этого мы будем использовать npx
для его установки:
$ npx vue add router
Теперь давайте отредактируем наш файл маршрутизатора, обычно
расположенный в router/index.js
и обновим наши маршруты, чтобы
использовать функцию import()
вместо оператора import
Это конфигурация по умолчанию:
import About from '../views/About.vue'
{
path: '/about',
name: 'About',
component: About
},
Мы изменили его на:
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
},
Если вы предпочитаете выбирать, какие страницы загружать по запросу, а
не отключать предварительную выборку и предварительную загрузку
глобально, используйте специальные комментарии Webpack вместо настройки
Webpack в vue.config.js
:
import(
/* webpackPrefetch: true */
/* webpackPreload: true */
'../views/About.vue'
)
Основное различие между import()
и import
заключается в том, что
модули ES, загруженные с помощью import()
, загружаются во время
выполнения, а import
, загружаются во время компиляции. Это означает,
что мы можем отложить загрузку модулей с помощью import()
и загрузить
только при необходимости.
Реализация индикатора выполнения
Так как невозможно точно оценить , когда страница загружается (или , если это будет загружать вообще), мы не можем сделать прогресс бар. Также нет возможности проверить, насколько загружена страница. Что мы можем сделать, так это создать индикатор выполнения, который заканчивается при загрузке страницы.
Все, что находится между ними, на самом деле не отражает прогресс, поэтому в большинстве случаев отображаемый прогресс представляет собой просто случайные прыжки.
Давайте lodash.random
, поскольку мы будем использовать этот пакет для
выбора некоторых случайных чисел во время генерации индикатора
выполнения:
$ npm i lodash.random
Затем создадим компонент Vue - components/ProgressBar.vue
:
<template>
<div :class="{'loading-container': true, loading: isLoading, visible: isVisible}">
<div class="loader" :style="{ width: progress + '%' }">
<div class="light"></div>
</div>
<div class="glow"></div>
</div>
</template>
Теперь к этому компоненту мы добавим скрипт. В этом скрипте мы сначала
импортируем random
и $eventHub
, так как мы будем их использовать:
<script>
import random from 'lodash.random'
import $eventHub from '../components/eventHub'
</script>
Теперь, после импорта, в том же скрипте мы можем определить некоторые переменные, которые мы будем использовать:
// Assume that loading will complete under this amount of time.
const defaultDuration = 8000
// How frequently to update
const defaultInterval = 1000
// 0 - 1. Add some variation to how much the bar will grow at each interval
const variation = 0.5
// 0 - 100. Where the progress bar should start from.
const startingPoint = 0
// Limiting how far the progress bar will get to before loading is complete
const endingPoint = 90
Имея это на месте, давайте напишем логику для асинхронной загрузки компонентов:
export default {
name: 'ProgressBar',
data: () => ({
isLoading: true, // Once loading is done, start fading away
isVisible: false, // Once animate finish, set display: none
progress: startingPoint,
timeoutId: undefined,
}),
mounted() {
$eventHub.$on('asyncComponentLoading', this.start)
$eventHub.$on('asyncComponentLoaded', this.stop)
},
methods: {
start() {
this.isLoading = true
this.isVisible = true
this.progress = startingPoint
this.loop()
},
loop() {
if (this.timeoutId) {
clearTimeout(this.timeoutId)
}
if (this.progress >= endingPoint) {
return
}
const size = (endingPoint - startingPoint) / (defaultDuration / defaultInterval)
const p = Math.round(this.progress + random(size * (1 - variation), size * (1 + variation)))
this.progress = Math.min(p, endingPoint)
this.timeoutId = setTimeout(
this.loop,
random(defaultInterval * (1 - variation), defaultInterval * (1 + variation))
)
},
stop() {
this.isLoading = false
this.progress = 100
clearTimeout(this.timeoutId)
const self = this
setTimeout(() => {
if (!self.isLoading) {
self.isVisible = false
}
}, 200)
},
},
}
В функции mounted()
вы увидите, что мы используем шину событий для
прослушивания асинхронной загрузки компонентов. Он запустит анимацию
загрузки, как только маршрутизатор сообщит нам, что мы перешли на
страницу, которая еще не была загружена.
И, наконец, добавим к нему немного стиля:
<style scoped>
.loading-container {
font-size: 0; /* remove space */
position: fixed;
top: 0;
left: 0;
height: 5px;
width: 100%;
opacity: 0;
display: none;
z-index: 100;
transition: opacity 200;
}
.loading-container.visible {
display: block;
}
.loading-container.loading {
opacity: 1;
}
.loader {
background: #23d6d6;
display: inline-block;
height: 100%;
width: 50%;
overflow: hidden;
border-radius: 0 0 5px 0;
transition: 200 width ease-out;
}
.loader > .light {
float: right;
height: 100%;
width: 20%;
background-image: linear-gradient(to right, #23d6d6, #29ffff, #23d6d6);
animation: loading-animation 2s ease-in infinite;
}
.glow {
display: inline-block;
height: 100%;
width: 30px;
margin-left: -30px;
border-radius: 0 0 5px 0;
box-shadow: 0 0 10px #23d6d6;
}
@keyframes loading-animation {
0% {
margin-right: 100%;
}
50% {
margin-right: 100%;
}
100% {
margin-right: -10%;
}
}
</style>
Теперь давайте добавим наш ProgressBar
в наш App.vue
или компонент
макета, если он находится в том же компоненте, что и представление
маршрутизатора. Мы хотим, чтобы он был доступен на протяжении всего
жизненного цикла приложения:
<template>
<div>
<progress-bar></progress-bar>
<router-view></router-view>
<!--- your other components -->
</div>
</template>
<script>
import ProgressBar from './components/ProgressBar.vue'
export default {
components: { ProgressBar },
}
</script>
В результате получается гладкий индикатор выполнения, который выглядит следующим образом:
{.ezlazyload}
Индикатор выполнения запуска для страниц с отложенной загрузкой
Наш ProgressBar
прослушивает по шине событий событие загрузки
асинхронного компонента. Когда что-то загружается таким образом, мы
хотим запустить анимацию. Давайте добавим к маршрутизатору защиту
маршрута, чтобы регистрировать эти события:
import $eventHub from '../components/eventHub'
router.beforeEach((to, from, next) => {
if (typeof to.matched[0]?.components.default === 'function') {
$eventHub.$emit('asyncComponentLoading', to) // Start progress bar
}
next()
})
router.beforeResolve((to, from, next) => {
$eventHub.$emit('asyncComponentLoaded') // Stop progress bar
next()
})
Чтобы определить, загружена ли страница лениво, нам нужно проверить,
определен ли компонент как динамический импорт, то есть
component: () => import('...')
вместо component: MyComponent
.
Это делается с помощью
typeof to.matched[0]?.components.default === 'function'
. Компоненты,
которые были загружены с помощью import
, не будут классифицироваться
как функции.
Заключение
В этой статье мы исследовали необходимость отложенной загрузки определенных страниц. Мы отключили предварительную выборку и предварительную загрузку в нашем приложении Vue и создали компонент индикатора выполнения, который появляется, чтобы имитировать фактический прогресс, выполняемый при загрузке страницы.