Я сам столкнулся с массой проблем, когда впервые начал читать официальную документацию VUE SSR, она изначально была построена в предположении, что у вас есть исполняемая среда сборки, поэтому в ней прямо говорится о реализации кода, но для новых разработчиков Среды выполнения нет, поэтому все фрагменты кода не работают. Так почему же автор не рассказал сначала о построении, а потом о программной реализации? Я думаю, это может быть связано с тем, что сборка и запуск сильно зависят от конкретных реализаций кода.Нехорошо сначала понять весь процесс, поэтому это нехороший баланс.
В нашей демонстрации сначала будет рассказано о процессе построения, некоторые из которых, возможно, потребуется рассмотреть позже, но постарайтесь четко объяснить весь процесс. При этом каждый шаг в статье будет в этомDEMOОтражено, что с помощью различных коммитов этой демонстрации вы можете быстро найти различные этапы.Конкретные идентификаторы коммитов следующие:
* e06aee792a59ffd9018aea1e3601e220c37fedbd (HEAD -> master, origin/master) 优化:添加缓存
* c65f08beaff1dea1eaf05d02fb30a7e8776ce289 程序开发:初步完成demo
* 2fb0d28ee6d84d2b1bdbbe419c744efdad3227de 程序开发:完成store定义,api编写和程序同步
* 9604aec0de526726f4fe435385f7c2fa4009fa63 程序开发:第一个可独立运行版本,无store
* 7d567e254fc9dc5a1655d2f0abbb4b8d53bccfce 构建配置:webpack配置、server.js后端入口文件编写
* 969248b64af82edd07214a621dfd19cf357d6c53 构建配置:babel 配置
* a5453fdeb20769e8c9e9ee339b624732ad14658a 初始化项目,完成第一个可运行demo
При чтении и тестировании можно пройтиgit reset --hard commitid
Чтобы переключаться между различными этапами, см. конкретную реализацию.
Что такое рендеринг на стороне сервера (SSR)?
Vue.js — это фреймворк для создания клиентских приложений. По умолчанию компоненты Vue могут выводиться в браузере для создания DOM и управления DOM. Однако также возможно отображать тот же компонент в виде строк HTML на стороне сервера, отправлять их непосредственно в браузер и, наконец, «активировать» эти статические теги как полностью интерактивное приложение на стороне клиента.
Приложение Vue.js, отображаемое на сервере, также можно считать «изоморфным» или «универсальным», поскольку большую часть кода приложения можно найти всервера такжеклиентзапускать на.
Зачем использовать рендеринг на стороне сервера (SSR)?
По сравнению с традиционным SPA (одностраничным приложением) преимущества рендеринга на стороне сервера (SSR) в основном заключаются в следующем:
- Лучшее SEO, так как сканеры поисковых систем могут напрямую просматривать полностью обработанные страницы.
- Быстрое время прибытия контента (время к времени), особенно для случая медленных сетевых устройств или медленно.
Основное использование
Установите необходимые шаблоны
npm install vue vue-server-renderer express --save
новый/server.js
,/src/index.template.html
const server = require('express')()
const Vue = require('vue')
const fs = require('fs')
const Renderer = require('vue-server-renderer').createRenderer({
template:fs.readFileSync('./src/index.template.html', 'utf-8')
})
server.get('*', (req, res) => {
const app = new Vue({
data: {
name: 'vue app~',
url: req.url
},
template:'<div>hello from {{name}}, and url is: {{url}}</div>'
})
const context = {
title: 'SSR test#'
}
Renderer.renderToString(app, context, (err, html) => {
if(err) {
console.log(err)
res.status(500).end('server error')
}
res.end(html)
})
})
server.listen(4001)
console.log('running at: http://localhost:4001');
С помощью описанной выше процедуры вы можете увидеть, что черезvue-server-rendererСкомпилируйте экземпляр Vue и, наконец, выведите его в браузер через экспресс.
Но в то же время видно, что вывод представляет собой статическую чистую html-страницу.Поскольку файлы javascript не загружены, взаимодействие с интерфейсом пользователя не реализовано, поэтому приведенная выше демонстрация является лишь минимальным примером.Для достижения полная программа VUE ssr, также необходимо использоватьVueSSRClientPlugin(vue-server-renderer/client-plugin) Скомпилируйте файл в файл vue-ssr-client-manifest.json и js, css и другие файлы, которые могут быть запущены внешним браузером,VueSSRServerPlugin(vue-server-renderer/server-plugin) Скомпилируйте файл в vue-ssr-server-bundle.json, который может быть вызван узлом
Прежде чем начать, вам нужно понять некоторые концепции
написать общий код
Ограничения при «универсальном» коде — т. е. коде, который выполняется на сервере и клиенте, наш код не будет точно таким же при работе в разных средах из-за различий в вариантах использования и API-интерфейсах платформы.
Ответ данных на сервере
Каждый запрос должен быть новым независимым экземпляром приложения, чтобы не было загрязнения состояния перекрестного запроса.
Хуки жизненного цикла компонентов
Поскольку динамического обновления нет, из всех функций хуков жизненного цикла только beforeCreate и created будут вызываться во время рендеринга на стороне сервера (SSR).
Доступ к API для конкретных платформ
Общий код не поддерживает API-интерфейсы для конкретных платформ, поэтому, если ваш код напрямую использует глобальные переменные, такие как окно или документ, которые доступны только для браузера, при выполнении в Node.js будет выдана ошибка, и наоборот.
конфигурация сборки
Как обслуживать одно и то же приложение Vue на сервере и клиенте. Для этого нам нужно использовать webpack для упаковки приложения Vue.
-
Обычно приложения Vue создаются с помощью webpack и vue-loader, и многие функции, специфичные для webpack, не работают напрямую в Node.js (например, импорт файлов через файловый загрузчик и CSS через css-загрузчик).
-
Хотя последняя версия Node.js полностью поддерживает функции ES2015, нам по-прежнему необходимо транспилировать код на стороне клиента для поддержки старых браузеров. Это также включает этапы сборки.
Таким образом, основная идея заключается в том, что как для клиентского приложения, так и для серверного приложения мы используем веб-пакет для упаковки — серверу нужен «серверный пакет», который затем используется для рендеринга на стороне сервера (SSR), а «клиентский пакет» отправляется в браузер, для смешивания статической разметки.
Давайте посмотрим на конкретный процесс реализации
Вавилонская конфигурация
новый/.babelrcнастроить
// es6 compile to es5 相关配置
{
"presets": [
[
"env",
{
"modules": false
}
]
],
"plugins": ["syntax-dynamic-import"]
}
npm i -D babel-loader@7 babel-core babel-plugin-syntax-dynamic-import babel-preset-env
Конфигурация WebPack
Создайте папку Build для храненияwebpack
связанные файлы конфигурации
/
├── build
│ ├── setup-dev-server.js # 设置 webpack-dev-middleware 开发环境
│ ├── webpack.base.config.js # 基础通用配置
│ ├── webpack.client.config.js # 编译出 vue-ssr-client-manifest.json 文件和 js、css 等文件,供浏览器调用
│ └── webpack.server.config.js # 编译出 vue-ssr-server-bundle.json 供 nodejs 调用
Сначала установите соответствующие пакеты
Установите пакеты, связанные с веб-пакетом
npm i -D webpack webpack-cli webpack-dev-middleware webpack-hot-middleware webpack-merge webpack-node-externals
Установить зависимости сборки
npm i -D chokidar cross-env friendly-errors-webpack-plugin memory-fs rimraf vue-loader
Затем посмотрите на конкретное содержимое каждого файла:
webpack.base.config.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
context: path.resolve(__dirname, '../'),
devtool: isProd ? 'source-map' : '#cheap-module-source-map',
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dist/',
filename: '[name].[chunkhash].js'
},
resolve: {
// ...
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
}
// ...
]
},
plugins: [new VueLoaderPlugin()]
}
webpack.base.config.js
Это общая конфигурация, которая в основном такая же, как и наша предыдущая конфигурация разработки SPA.
webpack.client.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const config = merge(base, {
mode: 'development',
entry: {
app: './src/entry-client.js'
},
resolve: {},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(
process.env.NODE_ENV || 'development'
),
'process.env.VUE_ENV': '"client"'
}),
new VueSSRClientPlugin()
]
})
module.exports = config
webpack.client.config.js
В основном выполнил две задачи
- определить файл ввода
entry-client.js
- через плагин
VueSSRClientPlugin
генерироватьvue-ssr-client-manifest.json
На этот файл manifest.json ссылается server.js
const { createBundleRenderer } = require('vue-server-renderer')
const template = require('fs').readFileSync('/path/to/template.html', 'utf-8')
const serverBundle = require('/path/to/vue-ssr-server-bundle.json')
const clientManifest = require('/path/to/vue-ssr-client-manifest.json')
const renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
})
С указанными выше настройками HTML-код, отображаемый на сервере, созданный с использованием функции разделения кода, внедряется автоматически.
webpack.server.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const nodeExternals = require('webpack-node-externals') // Webpack allows you to define externals - modules that should not be bundled.
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(base, {
mode: 'production',
target: 'node',
devtool: '#source-map',
entry: './src/entry-server.js',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
},
resolve: {},
externals: nodeExternals({
whitelist: /\.css$/ // 防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new VueSSRServerPlugin()
]
})
webpack.server.config.js
Основная проделанная работа:
- пройти через
target: 'node'
Сообщите веб-пакету, что скомпилированный код каталога является приложением node. - пройти через
VueSSRServerPlugin
плагин, который компилирует код вvue-ssr-server-bundle.json
в созданииvue-ssr-server-bundle.json
После этого просто передайте путь к файлуcreateBundleRenderer
,существуетserver.js
реализуется следующим образом:
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
// ……renderer 的其他选项
})
На данный момент строительство в основном завершено
Завершите первый работающий экземпляр
Установите зависимости, связанные с VUE
npm i axios vue-template-compiler vue-router vuex vuex-router-sync
Добавьте и улучшите следующие файлы:
/
├── server.js # 实现长期运行的 node 程序
├── src
│ ├── app.js # 新增
│ ├── router.js # 新增 定义路由
│ ├── App.vue # 新增
│ ├── entry-client.js # 浏览器端入口
│ ├── entry-server.js # node程序端入口
└── views
└── Home.vue # 首页
Затем просмотрите эти файлы один за другим:
server.js
const fs = require('fs');
const path = require('path');
const express = require('express');
const { createBundleRenderer } = require('vue-server-renderer');
const devServer = require('./build/setup-dev-server')
const resolve = file => path.resolve(__dirname, file);
const isProd = process.env.NODE_ENV === 'production';
const app = express();
const serve = (path, cache) =>
express.static(resolve(path), {
maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
});
app.use('/dist', serve('./dist', true));
function createRenderer(bundle, options) {
return createBundleRenderer( bundle, Object.assign(options, {
basedir: resolve('./dist'),
runInNewContext: false
})
);
}
function render(req, res) {
const startTime = Date.now();
res.setHeader('Content-Type', 'text/html');
const context = {
title: 'SSR 测试', // default title
url: req.url
};
renderer.renderToString(context, (err, html) => {
res.send(html);
});
}
let renderer;
let readyPromise;
const templatePath = resolve('./src/index.template.html');
if (isProd) {
const template = fs.readFileSync(templatePath, 'utf-8');
const bundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json') // 将js文件注入到页面中
renderer = createRenderer(bundle, {
template,
clientManifest
});
} else {
readyPromise = devServer( app, templatePath, (bundle, options) => {
renderer = createRenderer(bundle, options);
}
);
}
app.get('*',isProd? render : (req, res) => {
readyPromise.then(() => render(req, res));
}
);
const port = process.env.PORT || 8088;
app.listen(port, () => {
console.log(`server started at localhost:${port}`);
});
server.js
В основном выполнены следующие работы
- при исполнении
npm run dev
при звонке/build/setup-dev-server.js
Запустите промежуточное ПО для разработки webpack-dev-middleware. - пройти через
vue-server-renderer
Скомпилировано перед вызовомvue-ssr-server-bundle.json
запустить службу узла - Буду
vue-ssr-client-manifest.json
вводить вcreateRenderer
Реализовать автоматическую инъекцию внешних ресурсов в - пройти через
express
иметь дело сhttp
просить
server.js
Это входная программа всего сайта, она вызывает скомпилированный файл и, наконец, выводит его на страницу, которая является ключевой частью всего проекта.
app.js
import Vue from 'vue'
import App from './App.vue';
import { createRouter } from './router';
export function createApp(context) {
const router = createRouter();
const app = new Vue({
router,
render: h => h(App)
});
return { app, router };
};
app.js
Предоставляет повторяемую фабричную функцию, которая создает новый экземпляр приложения для каждого запроса, отправленного в «entry-client.js» иentry-server.js
передача
entry-client.js
import { createApp } from './app';
const { app, router } = createApp();
router.onReady(() => {
app.$mount('#app');
});
entry-client.js
Регулярно создавайте экземпляры объектов vue и монтируйте их на странице.
entry-server.js
import { createApp } from './app';
export default context => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, router } = createApp(context);
// 设置服务器端 router 的位置
router.push(context.url);
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 });
}
resolve(app);
});
});
};
entry-server.js
Как сервер зайти, и наконец пройтиVueSSRServerPlugin
плагин, скомпилированный вvue-ssr-server-bundle.json
дляvue-server-renderer
передача
router.js
а такжеHome.vue
для обычногоvue
Процедура, которая не будет дальше расширена здесь.
На данный момент мы завершили первый полностью скомпилированный и запущенныйvue ssr
Пример
Предварительная выборка данных и управление состоянием
Программа, завершенная до этого, просто отображает предопределенные переменные в html и возвращает их клиенту, но если мы хотим реализовать действительно пригодную для использования веб-программу, мы должны поддерживать динамические данные.Теперь мы начинаем видеть, как получать данные из удаленного и затем отображается в формате html для клиента.
Во время рендеринга на стороне сервера (SSR) мы, по сути, рендерим «моментальный снимок» нашего приложения, поэтому, если приложение зависит от некоторых асинхронных данных, его необходимо предварительно загрузить и проанализировать перед запуском процесса рендеринга.
Контейнер хранилища предварительной выборки данных (хранилище данных)
Сначала определите получение данныхapi.js
,использоватьaxios
:
import axios from 'axios';
export function fetchItem(id) {
return axios.get('https://api.mimei.net.cn/api/v1/article/' + id);
}
export function fetchList() {
return axios.get('https://api.mimei.net.cn/api/v1/article/');
}
Мы будем использовать официальную библиотеку управления состоянием Vuex. Сначала создадим файл store.js, который будет получать список файлов и получать содержимое статьи по id:
import Vue from 'vue';
import Vuex from 'vuex';
import { fetchItem, fetchList } from './api.js'
Vue.use(Vuex);
export function createStore() {
return new Vuex.Store({
state: {
items: {},
list: []
},
actions: {
fetchItem({commit}, id) {
return fetchItem(id).then(res => {
commit('setItem', {id, item: res.data})
})
},
fetchList({commit}){
return fetchList().then(res => {
commit('setList', res.data.list)
})
}
},
mutations: {
setItem(state, {id, item}) {
Vue.set(state.items, id, item)
},
setList(state, list) {
state.list = list
}
}
});
}
затем изменитьapp.js
:
import Vue from 'vue'
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
export function createApp(context) {
const router = createRouter();
const store = createStore();
sync(store, router)
const app = new Vue({
router,
store,
render: h => h(App)
});
return { app, router, store };
};
Компоненты с логической конфигурацией
store action
После завершения определения давайте посмотрим, как инициировать запрос.Официальная рекомендация — поместить его в компонент маршрутизации. Далее см.Home.vue
:
<template>
<div>
<h3>文章列表</h3>
<div class="list" v-for="i in list">
<router-link :to="{path:'/item/'+i.id}">{{i.title}}</router-link>
</div>
</div>
</template>
<script>
export default {
asyncData ({store, route}){
return store.dispatch('fetchList')
},
computed: {
list () {
return this.$store.state.list
}
},
data(){
return {
name:'wfz'
}
}
}
</script>
Предварительная выборка данных на стороне сервера
существуетentry-server.js
, мы можем получить путем маршрутизации с помощьюrouter.getMatchedComponents()
соответствующий компонент, если компонент предоставляетasyncData
, мы называем этот метод. Затем нам нужно присоединить проанализированное состояние к контексту рендеринга.
// entry-server.js
import { createApp } from './app';
export default context => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, router, store } = createApp(context);
// 设置服务器端 router 的位置
router.push(context.url);
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 });
}
Promise.all(
matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({
store,
route: router.currentRoute
});
}
})
).then(() => {
context.state = store.state
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app);
});
});
});
};
когда используешьtemplate
час,context.state
будет использоваться какwindow.__INITIAL_STATE__
состояние, автоматически встроенное в окончательный HTML. На стороне клиента хранилище должно получить состояние перед его подключением к приложению:
// entry-client.js
const { app, router, store } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
Предварительная выборка данных клиента
На стороне клиента предварительная выборка данных обрабатывается двумя разными способами:在路由导航之前解析数据
а также匹配要渲染的视图后,再获取数据
, мы используем первую схему в нашей демонстрации:
// entry-client.js
import { createApp } from './app';
const { app, router, store } = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
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(component => {
if (component.asyncData) {
component.asyncData({
store,
route: to
});
}
})
)
.then(() => {
next();
})
.catch(next);
});
app.$mount('#app');
});
Путем проверки соответствия компонентов и выполнения в глобальной функции маршрутизацииasyncData
Функция получает данные интерфейса.
В связи с этимdemo
Это две страницы, что нужноrouter.js
Добавьте информацию о маршрутизации, добавьте компонент маршрутизацииItem.vue
, пока базовыйVUE SSR
пример.
оптимизация кеша
Так как рендеринг на стороне сервера требует больших вычислительных ресурсов, при большом параллелизме могут возникнуть проблемы с производительностью. Правильное использование стратегий кэширования может значительно повысить скорость отклика.
const microCache = LRU({
max: 100,
maxAge: 1000 // 重要提示:条目在 1 秒后过期。
})
const isCacheable = req => {
// 实现逻辑为,检查请求是否是用户特定(user-specific)。
// 只有非用户特定(non-user-specific)页面才会缓存
}
server.get('*', (req, res) => {
const cacheable = isCacheable(req)
if (cacheable) {
const hit = microCache.get(req.url)
if (hit) {
return res.end(hit)
}
}
renderer.renderToString((err, html) => {
res.end(html)
if (cacheable) {
microCache.set(req.url, html)
}
})
})
В основном, поnginx
и кеш, который может в значительной степени решить проблему узких мест производительности.