Универсальные приложения на VUE.JS c использованием NUXT
Для начала кратко о том, что такое универсальные (изоморфные) веб приложения.
В 2011г. впервые термин «изоморфный» встречается в статье Чарли Роббинса «Масштабирование изоморфного JS кода».
«Под «изоморфным» мы подразумеваем такой код, каждая строка которого может одинаково исполняться как на серверной стороне, так и на клиентской» (https://blog.nodejitsu.com/scaling-isomorphic-javascript-code/)
Универсальные веб приложения это среднее между классическими серверными и одностраничными приложениями, они сочетают в себе свойства и тех и других:
- Генерация статического html на стороне сервера;
- Поисковая оптимизация;
- Отзывчивый UI;
Минусы универсальных приложений это:
- Обязательное наличие сервера с node.js
- Увеличенная нагрузка на сервер во время рендеринга тяжелых приложений.
- Необходимо учитывать особенности серверной и клиентской платформ при разработке.
А теперь ближе к делу. Представим, мы написали одностраничное приложение на Vue.JS — каталог аниме фильмов. Приложение хорошее, популярное. Но, со временем, пользователи начинают жаловаться на долгую загрузку приложения, заказчики — на низкие позиции в поисковых системах.
Принимаем решение внедрить серверный рендеринг для того, чтобы уменьшить время доставки контента до пользователей и добавить возможность поисковой оптимизации.
Рассмотрим типовую структуру универсального приложения:
Видим, что нам нужно:
- Добавить точки входа приложения для серверной и клиентской сборок;
- Изменить основной модуль приложения, чтобы его можно было использовать из обеих точек входа;
- Изменить конфигурацию webpack для раздельной сборки клиента и сервера;
- Добавить модуль для серверного рендеринга нашего приложения.
Точка входа клиентской сборки entry-client.js
:
import { createApp } from './app
const { app } = createApp()router.onReady(() => {
app.$mount('#app')
})
Точка входа серверной сборки entry-server.js
:
import { createApp } from './app
export default context => {
return new Promise(((resolve, reject) => {
const { app } = createApp()
router.push(context.url)
router.onReady(async () => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
try {
await Promise.all(matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({
route: router.currentRoute
})
}
}))
resolve(app)
} catch (e) {
reject(e)
}
}, reject)
}))
}
Файл app.js
Было:
new Vue({
router,
render: h => h(App)
}).$mount('#app')
Стало:
export function createApp () {
const router = createRouter()
const app = new Vue({
router,
render: h => h(App)
})
return { app, router }
}
Довольно-таки прилично кода по сравнению с исходным приложением. :(
Далее, настраиваем webpack
, устанавливаем express
и пишем серверный модуль, который будет рендерить наше приложение при каждом запросе.
https://vue-ssr-helloworld.now.sh
В общем, как-то это все заработало, поставленные задачи решены, но аппетиты растут и в итоге появляется список требований к нашему приложению:
- Поддержка горячей замены модулей в разработке;
- Поддержка различных препроцессоров;
- И хорошо бы в недалекой перспективе добавить регистрацию/авторизацию пользователей…
И тут нам на помощь приходит NUXT.
Nuxt — это фреймворк, упрощающий создание универсальных приложений на Vue.JS.
Nuxt умеет:
- Серверный рендеринг;
- Маршрутизацию с поддержкой асинхронных данных;
- Управление метаданными страниц;
- Горячую замену модулей при разработке;
- Поддержку препроцессоров;
- Работать как отдельно, так и в виде middleware в составе серверного приложения;
- И многое другое…
Перед тем, как перевести наше приложение на этот фреймворк, вполне логично было бы с ним познакомиться поближе, чем мы собственно и займемся.
Начнем знакомство со структуры проекта. Nuxt предлагает достаточно хорошо организованную структуру каталогов, беглого взгляда на которую становится понятно, что и где должно находиться.
/assets - импортируемые ресурсы
/components - пользовательские компоненты
/layouts - шаблоны приложения
/middleware - функции промежуточной обработки запросов
/pages - компоненты страниц
/plugins - пользовательские js плагины
/static - статические ресурсы
/store - Vuex хранилище состояния приложения
Обязательным для работы фреймворка является каталог pages
— файловая структура в этом каталоге используется для формирования маршрутов приложения.
МАРШРУТИЗАЦИЯ
Nuxt поддерживает 3 вида маршрутов: базовые (статические), динамические и вложенные.
Базовые маршруты
my-app
my-app/user
my-app/user/one
Для обработки таких статических адресов достаточно создать в каталоге pages файловую структуру:
pages/
— | user/
— — -| index.vue
— — -| one.vue
— | index.vue
Nuxt автоматически сгенерирует соответствующие маршруты
Динамические маршруты
На одной статике далеко не уедешь, поэтому поддерживаются динамические маршруты.
Чтобы Nuxt сгенерировал динамический маршрут, нужно в каталоге pages
создать файл _*.vue
или подкаталог с префиксом _
в названии.
Например, для обработки адресов
my-app
my-app/users
my-app/users/<_id>
Нужно создать такое дерево:
pages/
— | users/
— — -| _id.vue
— | index.vue
Вложенные маршруты
Nuxt позволяет создавать вложенные маршруты с помощью дочерних путей vue-router
Чтобы сформировать такой маршрут, рядом с каталогом, в котором лежат дочерние компоненты, нужно создать файл .vue
с таким же именем.
pages/
--| users/
-----| _id.vue
--| users.vue
Теперь вернемся к нашему приложению, и создадим структуру для его работы. Приложение должно обслуживать 2 маршрута:
vue-ssr-demo.now.sh/
vue-ssr-demo.now.sh/_id
Для этого положим 2 файла в каталог pages
:
pages/
--| index.vue
--| _id.vue
С маршрутизацией разобрались, следующий шаг — отображение страниц нашего приложения.
В Nuxt есть 2 типа представлений: шаблоны и страницы.
Шаблоны — это аналог корневого элемента Vue в обычном Vue приложении. В Nuxt есть встроенный шаблон, который используется по умолчанию, но его можно переопределить, создав в каталоге layouts
файл с названием default.vue
, либо можно создать файл с любым названием и прописать это название в ключе layout
страницы, которая должна его использовать. Единственное требование к оформлению шаблона - использование компонента <nuxt>
.
Страницы — это однофайловые компоненты Vue “на стероидах” — дополнительных ключах Nuxt, которые расширяют функционал компонент.
asyncData - то же самое, что и data, только асинхронное
fetch - используется для заполнения хранилища перед рендерингом страницы
head - управляет мета данными страницы
layout - с помощью его можно указать шаблон для страницы
middleware - устанавливает промежуточные методы для страницы, которые выполняются перед рендерингом.
Наше исходное приложение состоит из 3х компонент Vue:
App.vue - корневой узел Vue
Index.vue - компонент индексной страницы
Item.vue - компонент страницы деталей
App.vue будем использовать в качестве шаблона, скопируем его в layouts/default.vue
и заменим тэг router-view
на nuxt
. Компоненты Index.vue
и Item.vue
скопируем в pages/index.vue
и pages/_id.vue
соответственно и добавим в pages/_id.vue
ключ head, который будет менять заголовок страницы.
export default {
...
head() {
return {
title: this.item && this.item.attributes.canonicalTitle
}
},
...
}
Маршрутизация и представления у нас готовы, осталось получить данные с нашего REST API.
Работа с данными
Как мы уже видели, Nuxt добавляет в страницы методы, позволяющие работать с асинхронными данными, но это еще не все. Nuxt позволяет заполнять хранилище Vuex
на стороне сервера, для этого используется действие nuxtServerInit.
Кроме того Nuxt упрощает создание самого хранилища Vuex. Достаточно положить в каталог store
файл index.js содержащий:
// store/index.js
// store.state.x
export const state = () => ({ … })
export const actions = { … }
export const mutations = { … }
Классический способ создания хранилища так же поддерживается.
В нашем приложении будем использовать метод asyncData
для того, чтобы получать данные с API и возвращать их в страницу.
для index.vue:
async asyncData() {
const { data: { data: items} } = await api.getList()
return {
items
}
}
для _id.vue:
async asyncData({ route }) {
const { data: { data: item} } = await api.getItem(route.params.id)
return {
item
}
}
Ну вот, наше приложение готово. Можно готовить к публикации в продакшн.
Nuxt дает возможность подготовить приложение для работы в 3х режимах:
- Универсальное приложение — запускается командами
nuxt build && nuxt start
- Одностраничное приложение без серверного рендеринга,
nuxt build --spa
- Статическая версия приложения, чистый html,
nuxt generate
Наша цель была — внедрить серверный рендеринг, поэтому будем публиковать приложение в режиме универсального приложения.
Выводы.
Как мы увидели, по сравнению с императивным способом создания универсального приложения, Nuxt позволяет декларативно и без особых усилий создать действительно работающее универсальное приложение.
Из коробки умеет работать с основными продуктами экосистемы Vue, и сам является полноправным членом этой экосистемы.