Структура исходного кода

Избегайте синглтонов с состоянием

При написании кода для клиентской стороны мы привыкли к тому, что наш код каждый раз будет выполняться в новом контексте. Однако сервер Node.js является длительным процессом (long-running process). Поэтому когда наш код потребуется, он будет выполнен один раз и останется в памяти. Это означает, что если вы создаёте объект синглтон, он будет использоваться для всех входящих запросов.

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

Поэтому, вместо непосредственного создания экземпляра приложения, мы должны предоставить функцию-фабрику, которую можно повторно вызывать для создания свежих экземпляров приложения на каждый запрос:

// app.js
const Vue = require('vue')

module.exports = function createApp (context) {
  return new Vue({
    data: {
      url: context.url
    },
    template: `<div>Вы открыли URL: {{ url }}</div>`
  })
}

Код нашего сервера станет таким:

// server.js
const createApp = require('./app')

server.get('*', (req, res) => {
  const context = { url: req.url }
  const app = createApp(context)

  renderer.renderToString(app, (err, html) => {
    // обработка ошибок...
    res.end(html)
  })
})

Это правило также применимо и к экземплярам маршрутизатора (router), хранилища (store) и шины событий (event bus). Вместо того, чтобы непосредственно экспортировать из модуля и импортировать везде в приложении, вам нужно создавать новый экземпляр в createApp и внедрять его из корневого экземпляра Vue.

Это ограничение можно обойти при использовании рендерера сборки с опцией { runInNewContext: true }, однако это сопряжено с некоторыми существенными затратами производительности, поскольку для каждого запроса потребуется создание нового контекста vm.

Представляем шаг сборки

До сих пор мы не обсуждали каким образом доставлять клиенту такое приложение Vue. Чтобы сделать это, мы должны использовать Webpack для сборки нашего приложения Vue. На самом деле, мы вероятно захотим использовать Webpack для сборки приложения Vue также и на сервере, потому что:

  • Типичные приложения Vue собраны с помощью Webpack и vue-loader, и многие Webpack-специфичные вещи, такие как импорт файлов через file-loader, импорт CSS через css-loader не будут работать напрямую в Node.js.

  • Несмотря на то, что последняя версия Node.js полностью поддерживает ES2015, нам всё же необходимо транспилировать код для клиентской части для совместимости со старыми браузерами. Это снова предполагает шаг сборки.

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

architecture

Мы обсудим подробности настройки в следующих разделах — а сейчас, давайте представим что у нас реализован шаг сборки и мы можем писать код нашего приложения Vue с использованием Webpack.

Структура кода с Webpack

Теперь, когда мы используем Webpack для обработки приложения как для сервера, так и для клиента, большая часть нашего исходного кода может быть написана в универсальном «стиле», с доступом ко всем функциям на основе Webpack. В тоже время, есть ряд вещей, которые вы должны иметь ввиду при написании универсального кода.

Простой проект может выглядеть подобным образом:

src
├── components
│   ├── Foo.vue
│   ├── Bar.vue
│   └── Baz.vue
├── App.vue
├── app.js # универсальная точка входа
├── entry-client.js # запускается только в браузере
└── entry-server.js # запускается только на сервере

app.js

app.js — универсальная точка входа в наше приложение. В только клиентском приложении, мы бы создавали корневой экземпляр Vue прямо в этом файле и монтировали непосредственно в DOM. Однако при использовании серверного рендеринга эта ответственность переносится в файл клиентской точки входа (entry-client.js). app.js просто экспортирует функцию createApp:

import Vue from 'vue'
import App from './App.vue'

// экспортируем функцию фабрику для создания экземпляров
// нового приложения, маршрутизатора и хранилища
export function createApp () {
  const app = new Vue({
    // корневой экземпляр просто рендерит компонент App
    render: h => h(App)
  })
  return { app }
}

entry-client.js:

Клиентская точка входа — просто создаёт приложение и монтирует его в DOM:

import { createApp } from './app'

// Специфичная для клиента логика загрузки...

const { app } = createApp()

// предполагается, что у корневого элемента в шаблоне App.vue есть элемент с `id="app"`
app.$mount('#app')

entry-server.js:

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

import { createApp } from './app'

export default context => {
  const { app } = createApp()
  return app
}