Универсальные приложения на VUE.JS c использованием NUXT

Виталий Мосин
6 min readSep 12, 2018

--

Для начала кратко о том, что такое универсальные (изоморфные) веб приложения.

В 2011г. впервые термин «изоморфный» встречается в статье Чарли Роббинса «Масштабирование изоморфного JS кода».

«Под «изоморфным» мы подразумеваем такой код, каждая строка которого может одинаково исполняться как на серверной стороне, так и на клиентской» (https://blog.nodejitsu.com/scaling-isomorphic-javascript-code/)

Схематичное представление универсального приложения

Универсальные веб приложения это среднее между классическими серверными и одностраничными приложениями, они сочетают в себе свойства и тех и других:

  1. Генерация статического html на стороне сервера;
  2. Поисковая оптимизация;
  3. Отзывчивый UI;

Минусы универсальных приложений это:

  1. Обязательное наличие сервера с node.js
  2. Увеличенная нагрузка на сервер во время рендеринга тяжелых приложений.
  3. Необходимо учитывать особенности серверной и клиентской платформ при разработке.

А теперь ближе к делу. Представим, мы написали одностраничное приложение на Vue.JS — каталог аниме фильмов. Приложение хорошее, популярное. Но, со временем, пользователи начинают жаловаться на долгую загрузку приложения, заказчики — на низкие позиции в поисковых системах.

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

Рассмотрим типовую структуру универсального приложения:

Структура универсального приложения

Видим, что нам нужно:

  1. Добавить точки входа приложения для серверной и клиентской сборок;
  2. Изменить основной модуль приложения, чтобы его можно было использовать из обеих точек входа;
  3. Изменить конфигурацию webpack для раздельной сборки клиента и сервера;
  4. Добавить модуль для серверного рендеринга нашего приложения.

Точка входа клиентской сборки 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

В общем, как-то это все заработало, поставленные задачи решены, но аппетиты растут и в итоге появляется список требований к нашему приложению:

  1. Поддержка горячей замены модулей в разработке;
  2. Поддержка различных препроцессоров;
  3. И хорошо бы в недалекой перспективе добавить регистрацию/авторизацию пользователей…

И тут нам на помощь приходит NUXT.

Nuxt — это фреймворк, упрощающий создание универсальных приложений на Vue.JS.

Nuxt умеет:

  1. Серверный рендеринг;
  2. Маршрутизацию с поддержкой асинхронных данных;
  3. Управление метаданными страниц;
  4. Горячую замену модулей при разработке;
  5. Поддержку препроцессоров;
  6. Работать как отдельно, так и в виде middleware в составе серверного приложения;
  7. И многое другое…

Перед тем, как перевести наше приложение на этот фреймворк, вполне логично было бы с ним познакомиться поближе, чем мы собственно и займемся.

Начнем знакомство со структуры проекта. 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х режимах:

  1. Универсальное приложение — запускается командами nuxt build && nuxt start
  2. Одностраничное приложение без серверного рендеринга, nuxt build --spa
  3. Статическая версия приложения, чистый html, nuxt generate

Наша цель была — внедрить серверный рендеринг, поэтому будем публиковать приложение в режиме универсального приложения.

nuxt-helloworld.now.sh

Выводы.

Как мы увидели, по сравнению с императивным способом создания универсального приложения, Nuxt позволяет декларативно и без особых усилий создать действительно работающее универсальное приложение.

Из коробки умеет работать с основными продуктами экосистемы Vue, и сам является полноправным членом этой экосистемы.

--

--