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

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

При написании кода только для клиентской стороны можно быстро привыкнуть, что каждый раз код выполняется в новом контексте. Но сервер Node.js — длительный процесс. Поэтому после того как код впервые импортируется процессом и будет выполнен один раз, он затем останется в памяти. Это значит, если создать объект синглтон, то он станет использоваться для всех входящих запросов, что чревато загрязнением состояния при перекрёстных запросах.

// НЕПРАВИЛЬНО
import app from './app.js'

server.get('*', async (req, res) => {
  // приложение стало общим для всех пользователей
  const result = await renderToString(app)
  // ...
})
1
2
3
4
5
6
7
8
// ХОРОШО
function createApp() {
  return createSSRApp(/* ... */)
}

server.get('*', async (req, res) => {
  // каждый пользователь получит свой экземпляр приложения
  const app = createApp()
  const result = await renderToString(app)
  // ...
})
1
2
3
4
5
6
7
8
9
10
11

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




 
 






 
 























// server.js
const { createSSRApp } = require('vue')
const { renderToString } = require('@vue/server-renderer')
const express = require('express')

const server = express()

function createApp() {
  return createSSRApp({
    data() {
      return {
        user: 'Василий Пупкин'
      }
    },
    template: `<div>Текущий пользователь: {{ user }}</div>`
  })
}

server.get('*', async (req, res) => {
  const app = createApp()

  const appContent = await renderToString(app)
  const html = `
  <html>
    <body>
      <h1>Мой первый заголовок</h1>
      <div id="app">${appContent}</div>
    </body>
  </html>
  `

  res.end(html)
})

server.listen(8080)
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

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

# Добавление шага сборки

До сих пор, ещё не обсуждали каким образом доставлять клиенту такое приложение Vue. Чтобы сделать это, потребуется собрать приложение с использованием webpack.

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

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

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

архитектура

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

# Структура кода с webpack

Используя webpack для сборки приложения как для сервера, так и для клиента, большая часть исходного кода может быть написана универсально, используя все возможности webpack. В то же время есть ряд вещей, о которых следует помнить при написании универсального кода.

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

src
├── components
│   ├── MyUser.vue
│   └── MyTable.vue
├── App.vue # корень приложения
├── entry-client.js # запускается только в браузере
└── entry-server.js # запускается только на сервере
1
2
3
4
5
6
7

# App.vue

Как можно заметить, теперь есть файл App.vue в корне каталога src. Именно там будет храниться корневой компонент приложения. Теперь можно смело переносить код приложения из файла server.js в файл App.vue:

<template>
  <div>Текущий пользователь: {{ user }}</div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      user: 'Василий Пупкин'
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# entry-client.js

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

import { createSSRApp } from 'vue'
import App from './App.vue'

// логика инициализации, специфичная для клиента...

const app = createSSRApp(App)

// это предполагает, что в шаблоне App.vue будет корневой элемент с `id="app"`
app.mount('#app')
1
2
3
4
5
6
7
8
9

# entry-server.js

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

import { createSSRApp } from 'vue'
import App from './App.vue'

export default function() {
  const app = createSSRApp(App)

  return {
    app
  }
}
1
2
3
4
5
6
7
8
9
10