diff --git a/docs/ru/README.md b/docs/ru/README.md index ddf525f8..9b65459d 100644 --- a/docs/ru/README.md +++ b/docs/ru/README.md @@ -7,7 +7,7 @@ - vue-router 2.5.0+ - vue-loader 12.0.0+ & vue-style-loader 3.0.0+ -Если вы ранее использовали Vue 2.2 с серверным рендерингом, вы заметите, что рекомендуемая структура кода теперь [немного отличается](./guide/structure.md) (с новой опцией [runInNewContext](./api/README.md#runinnewcontext), установленной в `false`). Ваше существующее приложение по-прежнему будет работать, но лучше внесите изменения с учётом новых рекомендаций. +Если вы ранее использовали Vue 2.2 с серверным рендерингом, вы заметите, что рекомендуемая структура кода теперь [немного отличается](./guide/structure.md) (с новой опцией [runInNewContext](./api/#runinnewcontext), установленной в `false`). Ваше существующее приложение по-прежнему будет работать, но лучше внесите изменения с учётом новых рекомендаций. ::: ## Что такое серверный рендеринг (SSR)? diff --git a/docs/ru/api/README.md b/docs/ru/api/README.md index 2a0aaaa8..a2d8314e 100644 --- a/docs/ru/api/README.md +++ b/docs/ru/api/README.md @@ -84,6 +84,12 @@ bundleRenderer.renderToStream([context]): stream.Readable ### template +- **Тип:** + - `string` + - `string | (() => string | Promise)` (с версии 2.6) + +**При использовании строкового шаблона:** + Предоставляет шаблон для всей HTML-страницы. Шаблон должен содержать комментарий ``, который определяет место подстановки отрендеренного контента приложения. Шаблон также поддерживает базовые интерполяции с использованием контекста рендера: @@ -101,6 +107,8 @@ bundleRenderer.renderToStream([context]): stream.Readable С версии 2.5.0+, встраиваемый скрипт также автоматически удаляется в режиме production. + С версии 2.6.0+, если присутствует `context.nonce`, он будет добавлен как атрибут `nonce` во встраиваемый скрипт. Это позволит встраиваемому скрипту соответствовать CSP, который требует nonce. + Кроме того, когда предоставлен `clientManifest`, шаблон автоматически внедряет следующее: - JavaScript и CSS ресурсы для клиентской части, необходимые для рендеринга (с асинхронными фрагментами добавляемыми автоматически); @@ -108,20 +116,48 @@ bundleRenderer.renderToStream([context]): stream.Readable Вы можете отключить все автоматические внедрения передав `inject: false` в рендерер. +**При использовании функции шаблона:** + +::: warning ВНИМАНИЕ +Шаблоны в виде функции поддерживаются только с версии 2.6+ и при использовании `renderer.renderToString`. Это НЕ ПОДДЕРЖИВАЕТСЯ в `renderer.renderToStream`. +::: + +Опция `template` также может быть функцией, которая возвращает отрендеренный HTML или Promise, который разрешится отрендеренным HTML. Это позволит использовать собственные строковые шаблоны JavaScript и потенциальные асинхронные операции в процессе рендеринга шаблона. + +Функция принимает два аргумента: + +1. Результат рендеринга компонента приложения в виде строки; +2. Объект контекста рендеринга. + +Пример: + +``` js +const renderer = createRenderer({ + template: (result, context) => { + return ` + ${context.head} + ${result} + ` + } +}) +``` + +Обратите внимание, что при использовании собственной функции для шаблона ничего автоматически не будет добавляться — вы полностью контролируете то, что будет включаться в HTML, но также нести ответственность за включение всего необходимого (например, ссылки на ресурсы, если вы используете bundle renderer). + См. также: -- [Использование шаблона страниц](../guide/#using-a-page-template) -- [Внедрение ресурсов вручную](../guide/build-config.md#manual-asset-injection) +- [Использование шаблона страниц](../guide/#испоnьзование-шабnона-страниц) +- [Внедрение ресурсов вручную](../guide/build-config.md#внедрение-ресурсов-вручную) ### clientManifest -Предоставляет объект манифеста клиентской сборки, сгенерированный `vue-server-renderer/client-plugin`. Клиентский манифест предоставляет для рендерера сборки необходимую информацию для автоматического внедрения ресурсов в шаблон HTML. Подробнее в разделе [Генерация `clientManifest`](../guide/build-config.md#generating-clientmanifest). +Предоставляет объект манифеста клиентской сборки, сгенерированный `vue-server-renderer/client-plugin`. Клиентский манифест предоставляет для рендерера сборки необходимую информацию для автоматического внедрения ресурсов в шаблон HTML. Подробнее в разделе [Генерация `clientManifest`](../guide/build-config.md#генерация-clientmanifest). ### inject Контролирует, выполнять ли автоматические внедрения при использовании `template`. По умолчанию `true`. -См. также: [Внедрение ресурсов вручную](../guide/build-config.md#manual-asset-injection). +См. также: [Внедрение ресурсов вручную](../guide/build-config.md#внедрение-ресурсов-вручную). ### shouldPreload @@ -166,7 +202,7 @@ const renderer = createBundleRenderer(bundle, { - Используется только в `createBundleRenderer` - Возможные значения: `boolean | 'once'` (`'once'` поддерживается только с версии 2.3.1+) -По умолчанию, рендерер сборки будет создавать новый контекст V8 для каждого рендеринга и повторно исполнять всю сборку. Это имеет некоторые преимущества — например, код приложения изолирован от процесса сервера и не нужно беспокоиться [о проблеме «синглтона с состоянием»](../guide/structure.md#avoid-stateful-singletons), которая упоминалась ранее в руководстве. Однако этот режим требует значительных затрат производительности, поскольку повторное выполнение сборки обходится дорого, особенно когда приложение становится большим. +По умолчанию, рендерер сборки будет создавать новый контекст V8 для каждого рендеринга и повторно исполнять всю сборку. Это имеет некоторые преимущества — например, код приложения изолирован от процесса сервера и не нужно беспокоиться [о проблеме «синглтона с состоянием»](../guide/structure.md#избегайте-сингnтонов-с-состоянием), которая упоминалась ранее в руководстве. Однако этот режим требует значительных затрат производительности, поскольку повторное выполнение сборки обходится дорого, особенно когда приложение становится большим. По умолчанию эта опция имеет значение `true` для обеспечения обратной совместимости, но рекомендуется использовать `runInNewContext: false` или `runInNewContext: 'once'` всегда, когда это возможно. @@ -185,11 +221,11 @@ const renderer = createBundleRenderer(bundle, { - Используется только в `createBundleRenderer` -Указание пути базового каталога для серверной сборки для разрешения зависимостей из `node_modules` в нём. Это необходимо только в том случае, если сгенерированный файл сборки располагается в другом месте, в отличии от используемых NPM-зависимостей, или когда ваш `vue-server-renderer` подключен NPM-ссылкой в вашем текущем проекте. +Указание пути базового каталога для серверной сборки для разрешения зависимостей из `node_modules` в нём. Это необходимо только в том случае, если сгенерированный файл сборки располагается в другом месте, в отличии от используемых NPM-зависимостей, или когда ваш `vue-server-renderer` подключён NPM-ссылкой в вашем текущем проекте. ### cache -Реализация [кэширования на уровне компонентов](../guide/caching.md#component-level-caching). Объект кэша должен реализовать следующий интерфейс (соответствуя нотациям Flow): +Реализация [кэширования на уровне компонентов](../guide/caching.md#кэширование-на-уровне-компонентов). Объект кэша должен реализовать следующий интерфейс (соответствуя нотациям Flow): ``` js type RenderCache = { @@ -245,6 +281,32 @@ const renderer = createRenderer({ Например, можете посмотреть [серверную реализацию для директивы `v-show`](https://github.com/vuejs/vue/blob/dev/src/platforms/web/server/directives/show.js). +### serializer + +> Добавлено в версии 2.6 + +Пользовательская функция сериализатора для `context.state`. Поскольку сериализованное состояние будет частью вашего итогового HTML, важно использовать функцию, которая должным образом экранирует символы HTML по соображениям безопасности. Сериализатор по умолчанию — [serialize-javascript](https://github.com/yahoo/serialize-javascript) с опцией `{ isJSON: true }`. + +## Опции компонента только для сервера + +### serverCacheKey + +- **Тип:** `(props) => any` + + Возвращает ключ кэша для компонента на основе входных параметров. НЕ ИМЕЕТ доступа к `this`. + + Начиная с версии 2.6, вы можете явно отказываться от кэширования, возвращая значение `false`. + + Подробнее в [Кэшировании на уровне компонентов](../guide/caching.html#кэширование-на-уровне-компонентов). + +### serverPrefetch + +- **Тип:** `() => Promise` + + Загрузка асинхронных данных во время рендеринга на стороне сервера. Он должен сохранить полученные данные в глобальном хранилище или вернуть Promise. Рендерер сервера будет дожидаться разрешения Promise в этом хуке. Этот хук имеет доступ к экземпляру компонента через `this`. + + Подробнее в [Предзагрузке данных и состояния](../guide/data.html). + ## Плагины webpack Webpack плагины предоставляются как отдельные файлы, которые должны быть подключены напрямую: diff --git a/docs/ru/guide/build-config.md b/docs/ru/guide/build-config.md index 86ea3cee..fef608d4 100644 --- a/docs/ru/guide/build-config.md +++ b/docs/ru/guide/build-config.md @@ -70,7 +70,7 @@ const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', { Конфигурация клиентской части может оставаться практически такой же, как и базовой. Очевидно, вам нужно указать `entry` на файл входной точки клиентской части. Кроме того, если вы используете `CommonsChunkPlugin`, убедитесь, что используете его только в конфигурации клиентской части, потому что для серверной сборки требуется одна точка входа. -### Generating `clientManifest` +### Генерация `clientManifest` > требуется версия 2.3.0+ diff --git a/docs/ru/guide/caching.md b/docs/ru/guide/caching.md index 62ab93c6..361dcbe4 100644 --- a/docs/ru/guide/caching.md +++ b/docs/ru/guide/caching.md @@ -73,6 +73,10 @@ export default { При возвращении константы компонент всегда будет кэшироваться, что отлично подходит для чисто статических компонентов. +::: tip Исключение из кэширования +С версии 2.6.0 можно явно возвращать `false` в `serverCacheKey`, чтобы не использовать компонент из кэша, а отрисовывать заново. +::: + ### Когда использовать кэширование компонентов Если рендерер найдёт попадание в кэше для компонента во время рендеринга, он будет напрямую переиспользовать кэшированный результат для всего поддерева. Это означает, что вы **НЕ ДОЛЖНЫ** кэшировать компонент когда: diff --git a/docs/ru/guide/data.md b/docs/ru/guide/data.md index dcab2499..c3bcad8b 100644 --- a/docs/ru/guide/data.md +++ b/docs/ru/guide/data.md @@ -2,11 +2,9 @@ ## Хранение данных -Во время серверного рендеринга, мы собственно отображаем «снимок» нашего приложения, поэтому если приложение использует какие-то асинхронные данные **они должны быть предварительно загружены и разрешены до начала процесса рендеринга**. +Во время серверного рендеринга, мы отображаем «снимок» нашего приложения. Асинхронные данные из наших компонентов должны быть доступны до того, как мы смонтируем приложение на стороне клиента — в противном случае клиентское приложение будет отображено с использованием другого состояния, а гидратация завершится ошибкой. -Другая проблема заключается в том, что на клиенте эти же данные должны быть доступны перед моментом монтирования приложения на клиенте — иначе клиентское приложение будет отображено с использованием другого состояния и гидратация не будет выполнена. - -Чтобы решить эту проблему, полученные данные должны находиться вне компонентов представления, в специальном хранилище данных или в «контейнере состояния». На сервере мы можем предзагрузить и заполнить данные в хранилище перед рендерингом. Кроме того, мы будем сериализовывать и встраивать состояние в HTML. Хранилище на клиентской стороне сможет непосредственно получать вложенное состояние перед монтированием приложения. +Для решения этой проблемы, полученные данные должны находиться вне компонентов представления, в специальном хранилище данных или в «контейнере состояния». На сервере мы можем предзагрузить и заполнить данные в хранилище перед рендерингом. Кроме того, мы будем сериализовывать и встраивать состояние в HTML после успешного рендеринга приложения. Хранилище на клиентской стороне сможет непосредственно получать вложенное состояние перед монтированием приложения. Для этой цели мы будем использовать официальную библиотеку управления состоянием — [Vuex](https://github.com/vuejs/vuex/). Давайте создадим файл `store.js`, с некоторой симуляцией логики получения элемента на основе id: @@ -23,18 +21,22 @@ import { fetchItem } from './api' export function createStore () { return new Vuex.Store({ - state: { + // ВАЖНО: состояние должно быть функцией, + // чтобы модуль мог инстанцироваться несколько раз + state: () => ({ items: {} - }, + }), + actions: { fetchItem ({ commit }, id) { // возвращаем Promise через `store.dispatch()` - // чтобы мы могли понять когда данные будут загружены + // чтобы могли определять когда данные будут загружены return fetchItem(id).then(item => { commit('setItem', { id, item }) }) } }, + mutations: { setItem (state, { id, item }) { Vue.set(state.items, id, item) @@ -44,6 +46,11 @@ export function createStore () { } ``` +::: warning ВНИМАНИЕ +Большую часть времени вы должны оборачивать `state` в функцию, чтобы она не вызвала утечек памяти при следующих запусках на стороне сервера. +[Подробнее](./structure.md#избегайте-сингnтонов-с-состоянием) +::: + И обновляем `app.js`: ``` js @@ -78,36 +85,70 @@ export function createApp () { Итак, где мы должны размещать код, который вызывает действия по предзагрузке данных? -Данные, которые нам нужно получить, определяются посещённым маршрутом — что также определяет какие компоненты должны будут отображены. Фактически, данные необходимые для данного маршрута, также являются данными, необходимыми компонентам, отображаемым для этого маршрута. Поэтому будет логичным разместить логику получения данных внутри компонентов маршрута. +Данные, которые нам нужно получить, определяются посещённым маршрутом — что также определяет какие компоненты должны будут отображены. Фактически, данные необходимые для данного маршрута, также требуются компонентам, отображаемым для этого маршрута. Поэтому будет логичным разместить логику получения данных внутри компонентов маршрута. -Мы предоставим пользовательскую статичную функцию `asyncData` в наших компонентах маршрута. Обратите внимание, так как эта функция будет вызываться до инициализации компонентов, у неё не будет доступа к `this`. Информация хранилища и маршрута должна передаваться аргументами: +Мы будем использовать опцию `serverPrefetch` (добавлена в версии 2.6.0+) в компонентах. Эта опция распознаётся рендерингом на стороне сервера и приостанавливает отрисовку до тех пор, пока Promise не разрешится. Это позволяет нам «дожидаться» асинхронных данных в процессе отрисовки. + +::: tip Совет +Можно использовать `serverPrefetch` в любом компоненте, а не только в компонентах указываемых в маршрутах. +::: + +Вот пример компонента `Item.vue`, который отображается по маршруту `'/item/:id'`. Поскольку экземпляр компонента уже создан на этом этапе, он имеет доступ к `this`: ``` html ``` -## Загрузка данных на сервере +::: warning ВНИМАНИЕ +Необходимо проверять рендерился ли компонент на стороне сервера в хуке `mounted` во избежание выполнения логики загрузки дважды. +::: + +::: tip Совет +Можно увидеть одинаковую логику `fetchItem()`, повторяющуюся несколько раз (в коллбэках `serverPrefetch`, `mounted` и `watch`) в каждом компоненте — рекомендуется создать собственную абстракцию (например, примесь или плагин) для упрощения подобного кода. +::: -В `entry-server.js` мы можем получить компоненты, соответствующие маршруту, с помощью `router.getMatchedComponents()`, и вызвать `asyncData` если компонент предоставляет её. Затем нужно присоединить разрешённое состояние к контексту рендера. +## Инъекция финального состояния + +Теперь мы знаем что процесс отрисовки будет дожидаться получения данных в наших компонентах, но как же узнавать когда всё «готово»? Для этого потребуется использовать коллбэк `rendered` в контексте рендера (также добавлено в версии 2.6), который будет вызывать серверный рендер после завершения всего процесса рендеринга. В этот момент хранилище должно быть заполнено данными своего финального состояния. Затем мы можем внедрить его в контекст в этом коллбэке: ``` js // entry-server.js @@ -120,29 +161,17 @@ export default context => { router.push(context.url) router.onReady(() => { - const matchedComponents = router.getMatchedComponents() - if (!matchedComponents.length) { - return reject({ code: 404 }) - } - - // вызов `asyncData()` на всех соответствующих компонентах - Promise.all(matchedComponents.map(Component => { - if (Component.asyncData) { - return Component.asyncData({ - store, - route: router.currentRoute - }) - } - })).then(() => { - // После разрешения всех preFetch хуков, наше хранилище теперь - // заполнено состоянием, необходимым для рендеринга приложения. + // Хук `rendered` будет вызван, когда приложение завершит рендеринг + context.rendered = () => { + // После рендеринга приложение, наше хранилище теперь + // заполнено финальным состоянием из наших компонентов. // Когда мы присоединяем состояние к контексту, и есть опция `template` // используемая для рендерера, состояние будет автоматически // сериализовано и внедрено в HTML как `window.__INITIAL_STATE__`. context.state = store.state + } - resolve(app) - }).catch(reject) + resolve(app) }, reject) }) } @@ -153,105 +182,16 @@ export default context => { ``` js // entry-client.js -const { app, router, store } = createApp() +import { createApp } from './app' + +const { app, store } = createApp() if (window.__INITIAL_STATE__) { + // Инициализируем состояние хранилища данными, внедрёнными на сервере store.replaceState(window.__INITIAL_STATE__) } -``` - -## Загрузка данных на клиенте - -На клиенте существует два разных подхода к получению данных: - -1. **Разрешить данные перед навигацией по маршруту:** - - По этой стратегии приложение остаётся на текущем представлении до тех пор, пока данные необходимые для нового представления не будут загружены и разрешены. Преимущество заключается в том, что новое представление может уже рендерить полный контент, так как всё готово, но если загрузка данных занимает много времени пользователь будет ощущать «застревание» на текущей странице. Поэтому рекомендуется использовать индикатор загрузки данных при использовании этой стратегии. - - Мы можем реализовать эту стратегию на клиенте, проверяя соответствующие компоненты и вызывая их функцию `asyncData` внутри глобальных хуков маршрута. Обратите внимание, что мы должны зарегистрировать этот хук после готовности исходного маршрута, чтобы мы снова не забирали данные, полученные с сервера. - - ``` js - // entry-client.js - - // ...опустим лишний код - - router.onReady(() => { - // Добавляем хук маршрута для обработки asyncData. - // Выполняем его после разрешения первоначального маршрута, - // чтобы дважды не загружать данные, которые у нас уже есть. - // Используем `router.beforeResolve()`, чтобы все асинхронные компоненты были разрешены. - router.beforeResolve((to, from, next) => { - const matched = router.getMatchedComponents(to) - const prevMatched = router.getMatchedComponents(from) - - // мы заботимся только об отсутствующих ранее компонентах, - // поэтому мы сравниваем два списка, пока не найдём отличия - let diffed = false - const activated = matched.filter((c, i) => { - return diffed || (diffed = (prevMatched[i] !== c)) - }) - - if (!activated.length) { - return next() - } - // здесь мы должны вызвать индикатор загрузки, если используем его - - Promise.all(activated.map(c => { - if (c.asyncData) { - return c.asyncData({ store, route: to }) - } - })).then(() => { - - // останавливаем индикатор загрузки - - next() - }).catch(next) - }) - - app.$mount('#app') - }) - ``` - -2. **Загружать данные после отображения нового представления:** - - Эта стратегия располагает логику загрузки данных на стороне клиента в функции компонента `beforeMount`. Это позволяет переключаться мгновенно при срабатывании навигации по маршруту, поэтому приложение ощущается более отзывчивым. Однако на момент отображения нового представления у него не будет полных данных. Поэтому необходимо добавлять условие проверки загруженности состояния для каждого компонента, использующего эту стратегию. - - Этого можно достичь с помощью глобальной примеси на клиенте: - - ``` js - Vue.mixin({ - beforeMount () { - const { asyncData } = this.$options - if (asyncData) { - // присваиваем операцию загрузки к Promise - // чтобы в компонентах мы могли делать так `this.dataPromise.then(...)` - // для выполнения других задач после готовности данных - this.dataPromise = asyncData({ - store: this.$store, - route: this.$route - }) - } - } - }) - ``` - -Эти две стратегии в конечном счёте являются различными решениями UX и должны выбираться на основе фактического сценария разрабатываемого приложения. Но, независимо от выбранной вами стратегии, функция `asyncData` также должна вызываться при повторном использовании компонента маршрута (тот же маршрут, но параметры изменились, например с `user/1` на `user/2`). Мы также можем обрабатывать это с помощью глобальной примеси для клиентской части: - -``` js -Vue.mixin({ - beforeRouteUpdate (to, from, next) { - const { asyncData } = this.$options - if (asyncData) { - asyncData({ - store: this.$store, - route: to - }).then(next).catch(next) - } else { - next() - } - } -}) +app.$mount('#app') ``` ## Разделение кода хранилища @@ -262,21 +202,24 @@ Vue.mixin({ // store/modules/foo.js export default { namespaced: true, + // ВАЖНО: state должен быть функцией, чтобы // модуль мог инстанцироваться несколько раз state: () => ({ count: 0 }), + actions: { inc: ({ commit }) => commit('inc') }, + mutations: { inc: state => state.count++ } } ``` -Мы можем использовать `store.registerModule` для ленивой регистрации этого модуля в хуке `asyncData` компонента маршрута: +Мы можем использовать `store.registerModule` для ленивой регистрации этого модуля в хуке `serverPrefetch` компонента маршрута: ``` html // внутри компонента маршрута @@ -289,9 +232,30 @@ export default { import fooStoreModule from '../store/modules/foo' export default { - asyncData ({ store }) { - store.registerModule('foo', fooStoreModule) - return store.dispatch('foo/inc') + computed: { + fooCount () { + return this.$store.state.foo.count + } + }, + + // Только на стороне сервера + serverPrefetch () { + this.registerFoo() + return this.fooInc() + }, + + // Только на стороне клиента + mounted () { + // Мы уже увеличили значение 'count' на сервере + // Определяем это, проверив что 'foo' уже существует + const alreadyIncremented = !!this.$store.state.foo + + // Регистрируем модуль foo + this.registerFoo() + + if (!alreadyIncremented) { + this.fooInc() + } }, // ВАЖНО: избегайте дублирования регистрации модуля на клиенте @@ -300,9 +264,14 @@ export default { this.$store.unregisterModule('foo') }, - computed: { - fooCount () { - return this.$store.state.foo.count + methods: { + registerFoo () { + // Сохраняем предыдущее состояние, если оно внедрялось на стороне сервера + this.$store.registerModule('foo', fooStoreModule, { preserveState: true }) + }, + + fooInc () { + return this.$store.dispatch('foo/inc') } } } @@ -311,6 +280,6 @@ export default { Поскольку модуль теперь является зависимостью компонента маршрута, он будет перемещён в асинхронный фрагмент компонента маршрута с помощью Webpack. ---- - -Фух, это было много кода! Это связано с тем, что универсальная загрузка данных является, вероятно, самой сложной проблемой в приложении с рендерингом на стороне сервера, и таким образом мы закладываем хороший фундамент для облегчения дальнейшей разработки. После создания такой заготовки, создание отдельных компонентов будет приятным занятием. +::: warning ВНИМАНИЕ +Не забывайте использовать опцию `preserveState: true` для `registerModule` чтобы сохранять состояние, внедрённое сервером. +:::