предисловие
Все знакомы с SSR.Посредством рендеринга на стороне сервера можно оптимизировать поисковое сканирование, повысить скорость загрузки домашней страницы и т. д. Когда я изучал SSR, я прочитал много статей, некоторые из которых меня очень вдохновили, и некоторые были просто скопированы с официальной документации сайта. После нескольких дней обучения у меня появилось некоторое представление о SSR, и я полностью настроил среду разработки SSR с нуля, поэтому я хочу обобщить некоторый опыт в этой статье и надеюсь, что смогу помочь друзьям, которые изучают SSR. .
Я проведу вас через пять шагов, чтобы выполнить настройку SSR шаг за шагом:
- чистый браузерный рендеринг
- Рендеринг на стороне сервера, не включая
AjaxДанные инициализации - Рендеринг на стороне сервера, включая
AjaxДанные инициализации - рендеринг на стороне сервера с использованием
serverBundleиclientManifestоптимизировать - полный на основе
Vue + VueRouter + VuexССР Инжиниринг
Если вы мало знаете о том, что я сказал выше, не беда, следуйте за мной шаг за шагом, и, наконец, вы сможете настроить проект разработки SSR самостоятельно.выложу все исходникиgithubвыше, вы можете использовать его в качестве ссылки.
текст
1. Чистый браузерный рендеринг
Я считаю, что у всех будет эта конфигурация, основанная наweback + vueОбщая конфигурация разработки, здесь я приведу некоторые ключевые коды, полный код можно найти здесьgithubПроверять.
Структура каталогов
- node_modules
- components
- Bar.vue
- Foo.vue
- App.vue
- app.js
- index.html
- webpack.config.js
- package.json
- yarn.lock
- postcss.config.js
- .babelrc
- .gitignore
app.js
import Vue from 'vue';
import App from './App.vue';
let app = new Vue({
el: '#app',
render: h => h(App)
});
App.vue
<template>
<div>
<Foo></Foo>
<Bar></Bar>
</div>
</template>
<script>
import Foo from './components/Foo.vue';
import Bar from './components/Bar.vue';
export default {
components: {
Foo, Bar
}
}
</script>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>纯浏览器渲染</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
components/Foo.vue
<template>
<div class="foo">
<h1>Foo Component</h1>
</div>
</template>
<style>
.foo {
background: yellowgreen;
}
</style>
components/Bar.vue
<template>
<div class="bar">
<h1>Bar Component</h1>
</div>
</template>
<style>
.bar {
background: bisque;
}
</style>
webpack.config.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
mode: 'development',
entry: './app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader', 'postcss-loader']
// 如果需要单独抽出CSS文件,用下面这个配置
// use: ExtractTextPlugin.extract({
// fallback: 'vue-style-loader',
// use: [
// 'css-loader',
// 'postcss-loader'
// ]
// })
},
{
test: /\.(jpg|jpeg|png|gif|svg)$/,
use: {
loader: 'url-loader',
options: {
limit: 10000 // 10Kb
}
}
},
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './index.html'
}),
// 如果需要单独抽出CSS文件,用下面这个配置
// new ExtractTextPlugin("styles.css")
]
};
postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
};
.babelrc
{
"presets": [
"@babel/preset-env"
],
"plugins": [
// 让其支持动态路由的写法 const Foo = () => import('../components/Foo.vue')
"dynamic-import-webpack"
]
}
package.json
{
"name": "01",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "yarn run dev",
"dev": "webpack-dev-server",
"build": "webpack"
},
"dependencies": {
"vue": "^2.5.17"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/preset-env": "^7.1.0",
"babel-plugin-dynamic-import-webpack": "^1.1.0",
"autoprefixer": "^9.1.5",
"babel-loader": "^8.0.4",
"css-loader": "^1.0.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^2.0.0",
"html-webpack-plugin": "^3.2.0",
"postcss": "^7.0.5",
"postcss-loader": "^3.0.0",
"url-loader": "^1.1.1",
"vue-loader": "^15.4.2",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.5.17",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.9"
}
}
Заказ
Запустите среду разработки
yarn start
Создание производственной среды
yarn run build
Скриншот финального эффекта:
полный просмотр кодаgithub
2. Рендеринг на стороне сервера, за исключениемAjaxДанные инициализации
Рендеринг SSR на стороне сервера аналогичен изоморфизму: в конце концов, часть кода может выполняться как на сервере, так и на клиенте. Если во время процесса SSR возникает проблема, вы также можете вернуться к чистому рендерингу браузера, чтобы пользователи могли нормально видеть страницу.
Тогда, вдоль этой линии мышления, должно быть дваwebpackвходной файл, один для рендеринга на стороне браузераweboack.client.config.js, один для рендеринга на стороне сервераwebpack.server.config.js, извлеките их общедоступные части какwebpack.base.cofig.js, с последующимwebpack-mergeОбъединить. В то же время также должно бытьserverпредоставлятьhttpсервис, я использую здесьkoa.
Давайте посмотрим на новую структуру каталогов:
- node_modules
- config // 新增
- webpack.base.config.js
- webpack.client.config.js
- webpack.server.config.js
- src
- components
- Bar.vue
- Foo.vue
- App.vue
- app.js
- entry-client.js // 新增
- entry-server.js // 新增
- index.html
- index.ssr.html // 新增
- package.json
- yarn.lock
- postcss.config.js
- .babelrc
- .gitignore
В клиентском приложении каждый пользователь использует новый экземпляр приложения в своем браузере. Для рендеринга на стороне сервера мы хотим того же: каждый запрос должен быть новым, независимым экземпляром приложения, чтобы не было загрязнения состояния перекрестного запроса.
Итак, мы должныapp.jsСделайте модификацию, оберните ее как фабричную функцию, и каждый вызов будет генерировать совершенно новый корневой компонент.
app.js
import Vue from 'vue';
import App from './App.vue';
export function createApp() {
const app = new Vue({
render: h => h(App)
});
return { app };
}
На стороне браузера мы напрямую создаем новый корневой компонент и монтируем его.
entry-client.js
import { createApp } from './app.js';
const { app } = createApp();
app.$mount('#app');
На стороне сервера мы собираемся вернуть функцию, функция которой состоит в том, чтобы получитьcontextпараметры, каждый раз возвращая новый корневой компонент. этоcontextМы пока не будем использовать его здесь, он будет использован в последующих шагах.
entry-server.js
import { createApp } from './app.js';
export default context => {
const { app } = createApp();
return app;
}
Тогда посмотри еще разindex.ssr.html
index.ssr.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>服务端渲染</title>
</head>
<body>
<!--vue-ssr-outlet-->
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>
</html>
<!--vue-ssr-outlet-->Роль является заполнителем, последующим черезvue-server-rendererПлагин, компонент, разбираемый серверомhtmlЗдесь вставляется строка.
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>чтобыwebpackпройти черезwebpack.client.config.jsЗапакованные файлы размещены здесь (это для простой демонстрации, позже будут и другие способы).
Потому что сервер выплюнулhtmlстрока, следующаяVueСоответствующая отзывчивость, реакция на события и т. д. — все это должно выполняться на стороне браузера, поэтому здесь необходимо представить файлы, упакованные для рендеринга на стороне браузера.
Официально это называетсягидратация на стороне клиента.
Так называемая активация на стороне клиента относится к процессу, когда Vue принимает статический HTML, отправленный сервером на стороне браузера, и превращает его в динамический DOM, управляемый Vue.
В entry-client.js монтируем приложение следующей строкой:
// 这里假定 App.vue template 根元素的 `id="app"`
app.$mount('#app')
Поскольку сервер уже отобразил HTML, нам, очевидно, не нужно его выбрасывать и заново создавать все элементы DOM. Вместо этого нам нужно «активировать» эти статические HTML, а затем сделать их динамическими (способными реагировать на последующие изменения данных).
Если вы изучите вывод, отображаемый сервером, вы заметите, что к корневому элементу приложения был добавлен специальный атрибут:
<div id="app" data-server-rendered="true">
VueНа стороне браузера это свойство используется для выделения сервера.htmlАктивируйте его, и мы увидим это в тот момент, когда сами построим его.
Далее мы смотрим наwebpackСвязанная конфигурация:
webpack.base.config.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode: 'development',
resolve: {
extensions: ['.js', '.vue']
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.(jpg|jpeg|png|gif|svg)$/,
use: {
loader: 'url-loader',
options: {
limit: 10000 // 10Kb
}
}
}
]
},
plugins: [
new VueLoaderPlugin()
]
};
webpack.client.config.js
const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = require('./webpack.base.config');
module.exports = merge(base, {
entry: {
client: path.resolve(__dirname, '../src/entry-client.js')
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../src/index.html'),
filename: 'index.html'
})
]
});
Обратите внимание, что файл ввода здесь становитсяentry-client.js, который упаковывает егоclient.bundle.jsвставить вindex.htmlсередина.
webpack.server.config.js
const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const base = require('./webpack.base.config');
module.exports = merge(base, {
target: 'node',
entry: {
server: path.resolve(__dirname, '../src/entry-server.js')
},
output: {
libraryTarget: 'commonjs2'
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../src/index.ssr.html'),
filename: 'index.ssr.html',
files: {
js: 'client.bundle.js'
},
excludeChunks: ['server']
})
]
});
Здесь следует отметить несколько моментов:
- Входной файл
entry-server.js - Поскольку это упакованный серверно-зависимый код, поэтому
targetустанавливатьnode,в то же время,outputизlibraryTargetустанавливатьcommonjs2
здесь оHtmlWebpackPluginКонфигурация означает, что неindex.ssr.htmlв упаковкеserver.bundle.js, быть упакованным для браузераclient.bundle.js, причина, упомянутая ранее, состоит в том, чтобы сделатьVueможет выплюнуть серверhtmlАктивация берет на себя последующие ответы.
Затем упакованоserver.bundle.jsГде он используется? Тогда посмотри вниз, чтобы узнать ~
package.json
{
"name": "01",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "yarn run dev",
"dev": "webpack-dev-server",
"build:client": "webpack --config config/webpack.client.config.js",
"build:server": "webpack --config config/webpack.server.config.js"
},
"dependencies": {
"koa": "^2.5.3",
"koa-router": "^7.4.0",
"koa-static": "^5.0.0",
"vue": "^2.5.17",
"vue-server-renderer": "^2.5.17"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/preset-env": "^7.1.0",
"autoprefixer": "^9.1.5",
"babel-loader": "^8.0.4",
"css-loader": "^1.0.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^2.0.0",
"html-webpack-plugin": "^3.2.0",
"postcss": "^7.0.5",
"postcss-loader": "^3.0.0",
"style-loader": "^0.23.0",
"url-loader": "^1.1.1",
"vue-loader": "^15.4.2",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.5.17",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.9",
"webpack-merge": "^4.1.4"
}
}
Далее мы видимserverОhttpКод для услуги:
server/server.js
const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const path = require('path');
const fs = require('fs');
const backendApp = new Koa();
const frontendApp = new Koa();
const backendRouter = new Router();
const frontendRouter = new Router();
const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')
});
// 后端Server
backendRouter.get('/index', (ctx, next) => {
// 这里用 renderToString 的 promise 返回的 html 有问题,没有样式
renderer.renderToString((err, html) => {
if (err) {
console.error(err);
ctx.status = 500;
ctx.body = '服务器内部错误';
} else {
console.log(html);
ctx.status = 200;
ctx.body = html;
}
});
});
backendApp.use(serve(path.resolve(__dirname, '../dist')));
backendApp
.use(backendRouter.routes())
.use(backendRouter.allowedMethods());
backendApp.listen(3000, () => {
console.log('服务器端渲染地址: http://localhost:3000');
});
// 前端Server
frontendRouter.get('/index', (ctx, next) => {
let html = fs.readFileSync(path.resolve(__dirname, '../dist/index.html'), 'utf-8');
ctx.type = 'html';
ctx.status = 200;
ctx.body = html;
});
frontendApp.use(serve(path.resolve(__dirname, '../dist')));
frontendApp
.use(frontendRouter.routes())
.use(frontendRouter.allowedMethods());
frontendApp.listen(3001, () => {
console.log('浏览器端渲染地址: http://localhost:3001');
});
Здесь отслеживаются два порта, порт 3000 — рендеринг на стороне сервера, а порт 3001 — прямой вывод.index.html, тогда он пойдет на стороне браузераVueНабор в основном используется для сравнения с рендерингом на стороне сервера.
Ключевой код здесь заключается в том, как вывести строку ``html`` на стороне сервера.
const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.bundle.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')
});
можно увидеть,server.bundle.jsОн используется здесь, потому что его вход — это функция, которая получаетcontextВ качестве параметра (не обязательно) вывести корневой компонентapp.
Здесь мы используемvue-server-rendererПлагин, у него есть два метода рендеринга, один из нихcreateRenderer, другойcreateBundleRenderer.
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer({ /* 选项 */ })
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer(serverBundle, { /* 选项 */ })
createRendererНевозможно получить пакеты, упакованные для сервераserver.bundle.jsфайл, поэтому здесь вы можете использовать толькоcreateBundleRenderer.
serverBundleПараметр может быть одним из следующих:
- Абсолютный путь, указывающий на уже построенный
bundleдокумент(.jsили.json). должен начинаться с/распознается как путь к файлу в начале файла. - Зависит от
webpack + vue-server-renderer/server-pluginСгенерированоbundleобъект. -
JavaScriptСтрока кода (не рекомендуется).
Здесь мы представляем файл .js, а позже расскажем, как использовать файл .json и каковы его преимущества.
renderer.renderToString((err, html) => {
if (err) {
console.error(err);
ctx.status = 500;
ctx.body = '服务器内部错误';
} else {
console.log(html);
ctx.status = 200;
ctx.body = html;
}
});
использоватьcreateRendererиcreateBundleRendererвозвращениеrendererФункция содержит два методаrenderToStringиrenderToStream, мы используем здесьrenderToStringВозвращает полную строку сразу после успеха,renderToStreamвозвращаетNodeпоток.
renderToStringслужба поддержкиPromise, но я используюPrmoiseКогда он в форме, стиль не будет отображаться.Я не знаю причину на данный момент.Если вы знаете это, вы можете оставить мне сообщение.
Настройка в основном завершена, давайте посмотрим, как она работает.
yarn run build:client // 打包浏览器端需要bundle
yarn run build:server // 打包SSR需要bundle
yarn start // 其实就是 node server/server.js,提供http服务
Отображение финального эффекта:
доступhttp://localhost:3000/index
Мы видели вышеупомянутоеdata-server-rendered="true"свойства, также будут загруженыclient.bundle.jsфайл, чтобы позволитьVueСделайте последующие поглощения на стороне браузера.
доступhttp://localhost:3001/indexЭто также то же самое, что и эффект, достигнутый на первом шаге, чистый рендеринг в браузере, поэтому здесь нет снимка экрана.
полный просмотр кодаgithub
3. Рендеринг на стороне сервера, включая данные инициализации Ajax
Если SSR необходимо инициализировать некоторые асинхронные данные, процесс становится немного сложнее.
Сначала зададим несколько вопросов:
- Каковы действия сервера для получения асинхронных данных?
- Как определить, какие компоненты должны получать асинхронные данные?
- Как запихнуть его обратно в компонент после получения асинхронных данных?
Перейдем к вопросам, надеюсь, вы нашли ответы на поставленные вопросы после прочтения этой статьи.
Существует разница между жизненным циклом компонентов рендеринга на стороне сервера и рендеринга на стороне браузера.На стороне сервера просто пройдитеbeforeCreateиcreatedдва жизненных цикла. Потому что сервер SSR выплевывает напрямуюhtmlСтроки в порядке, не отображают структуру DOM, поэтому не существуютbeforeMountиmounted, он не будет обновляться, поэтому его не существуетbeforeUpdateиupdatedЖдать.
Давайте сначала подумаем об этом, рендеринг в чистом браузереVueКак в проекте получить асинхронные данные и отрендерить их в компонент? обычно вcreatedилиmountedИнициировать асинхронный запрос в жизненном цикле, а затем выполнить его в обратном вызове успехаthis.data = xxx,VueСлушайте изменение данных, идите назадDom Diff,ударилpatch,ДелатьDOMвозобновить.
Так может ли рендеринг на стороне сервера сделать то же самое?Ответ - нет.
- существует
mountedКонечно нет, потому чтоSSRничего такогоmountedЖизненный цикл, так что определенно не здесь. - существует
beforeCreateМожно ли здесь инициировать асинхронный запрос или нет. Поскольку запрос является асинхронным, сервер может не дождаться возврата интерфейса.htmlСтроки объединены.
Итак, обратитесь кофициальная документация, мы можем получить следующие идеи:
- Перед рендерингом необходимо предварительно получить все необходимые асинхронные данные, а затем сохранить их в
Vuexизstoreсередина. - При рендеринге на бэкенде передайте
VuexВнесите полученные данные в соответствующий компонент. - Пучок
storeДанные установлены наwindow.__INITIAL_STATE__в свойствах. - В среде браузера через
Vuexбудетwindow.__INITIAL_STATE__Данные внутри вводятся в соответствующий компонент.
При нормальных обстоятельствах, выполняя эти шаги, сервер выплевываетhtmlВсе данные соответствующих компонентов строки актуальны, поэтому шаг 4 не вызываетDOMОбновляй, но если что не так, выкладывайhtmlСтрока не имеет соответствующих данных,VueВы также можете использовать ````Vuex на стороне браузера注入数据,进行Обновление DOM```.
Обновленная структура каталогов:
- node_modules
- config
- webpack.base.config.js
- webpack.client.config.js
- webpack.server.config.js
- src
- components
- Bar.vue
- Foo.vue
- store // 新增
store.js
- App.vue
- app.js
- entry-client.js
- entry-server.js
- index.html
- index.ssr.html
- package.json
- yarn.lock
- postcss.config.js
- .babelrc
- .gitignore
Давайте взглянемstore.js:
store/store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const fetchBar = function() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('bar 组件返回 ajax 数据');
}, 1000);
});
};
function createStore() {
const store = new Vuex.Store({
state: {
bar: ''
},
mutations: {
'SET_BAR'(state, data) {
state.bar = data;
}
},
actions: {
fetchBar({ commit }) {
return fetchBar().then((data) => {
commit('SET_BAR', data);
}).catch((err) => {
console.error(err);
})
}
}
});
if (typeof window !== 'undefined' && window.__INITIAL_STATE__) {
console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
store.replaceState(window.__INITIAL_STATE__);
}
return store;
}
export default createStore;
typeof window
Если вы не знаетеVuex, ты можешь пойти вОфициальный сайт ВексДавайте сначала рассмотрим некоторые основные понятия.
здесьfetchBarЕго можно рассматривать как асинхронный запрос, который используется здесьsetTimeoutмоделирование. успешный обратный вызовcommitсоответствующийmutationВнесите изменения состояния.
Вот ключевой фрагмент кода:
if (typeof window !== 'undefined' && window.__INITIAL_STATE__) {
console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
store.replaceState(window.__INITIAL_STATE__);
}
так какstore.jsОн также будет упакован для запуска на сервере.server.bundle.js, так что операционная среда не обязательно браузер, здесь нужноwindowВыносить суждения, чтобы предотвратить ошибки, и если естьwindow.__INITIAL_STATE__атрибут, указывающий, что сервер получил все асинхронные данные, необходимые для инициализации.storeПроизведите замену в государстве, чтобы обеспечить единство.
components/Bar.vue
<template>
<div class="bar">
<h1 @click="onHandleClick">Bar Component</h1>
<h2>异步Ajax数据:</h2>
<span>{{ msg }}</span>
</div>
</template>
<script>
const fetchInitialData = ({ store }) => {
store.dispatch('fetchBar');
};
export default {
asyncData: fetchInitialData,
methods: {
onHandleClick() {
alert('bar');
}
},
mounted() {
// 因为服务端渲染只有 beforeCreate 和 created 两个生命周期,不会走这里
// 所以把调用 Ajax 初始化数据也写在这里,是为了供单独浏览器渲染使用
let store = this.$store;
fetchInitialData({ store });
},
computed: {
msg() {
return this.$store.state.bar;
}
}
}
</script>
<style>
.bar {
background: bisque;
}
</style>
здесь, вBarВ объект экспорта компонента по умолчанию добавлен методasyncData, в этом методеdispatchсоответствующийaction, для асинхронного сбора данных.
Следует отметить, что яmountedКод для получения данных тоже написан на , почему так?Поскольку вы хотите добиться изоморфизма, не должно быть проблем с кодом, работающим только на стороне браузера, и поскольку сервер неmountedЖизненный цикл, поэтому я написал это здесь, чтобы решить проблему использования его в одиночку в среде браузера или инициировать тот же асинхронный запрос для инициализации данных.
components/Foo.vue
<template>
<div class="foo">
<h1 @click="onHandleClick">Foo Component</h1>
</div>
</template>
<script>
export default {
methods: {
onHandleClick() {
alert('foo');
}
},
}
</script>
<style>
.foo {
background: yellowgreen;
}
</style>
Здесь я добавил событие click к обоим компонентам, чтобы доказать, что сервер выдает домашнюю страницу.htmlПосле этого последующие шаги будут выполняться браузеромVueВозьмите на себя, и следующие операции могут выполняться в обычном режиме.
app.js
import Vue from 'vue';
import createStore from './store/store.js';
import App from './App.vue';
export function createApp() {
const store = createStore();
const app = new Vue({
store,
render: h => h(App)
});
return { app, store, App };
}
При создании корневого компонента поместитеVuex的storeПередайте его и одновременно верните, который будет использован позже.
Последний взглядentry-server.js, ключевые шаги здесь:
entry-server.js
import { createApp } from './app.js';
export default context => {
return new Promise((resolve, reject) => {
const { app, store, App } = createApp();
let components = App.components;
let asyncDataPromiseFns = [];
Object.values(components).forEach(component => {
if (component.asyncData) {
asyncDataPromiseFns.push(component.asyncData({ store }));
}
});
Promise.all(asyncDataPromiseFns).then((result) => {
// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中
context.state = store.state;
console.log(222);
console.log(store.state);
console.log(context.state);
console.log(context);
resolve(app);
}, reject);
});
}
Мы экспортируем поAppполучил все ниже этогоcomponents, затем перейдите, чтобы найти, какойcomponentимеютasyncDataметод, если есть, вызовите и передайтеstore, метод возвращаетPromise,Мы используемPromise.allПодождите, пока все асинхронные методы успешно вернутся, прежде чемresolve(app).
context.state = store.stateЭффект заключается в том, что при использованииcreateBundleRenderer, если установленоtemplateвариант, тоcontext.stateзначение какwindow.__INITIAL_STATE__Автоматически вставляется в шаблоныhtmlсередина.
Здесь нужно больше думать и разбираться в логике всего серверного рендеринга.
Как запустить:
yarn run build:client
yarn run build:server
yarn start
Скриншот финального эффекта:
Рендеринг на стороне сервера: включенhttp://localhost:3000/index
можно увидетьwindow.__INITIAL_STATE__автоматически вставляется.
Давайте сравнимSSRКак это влияет на производительность загрузки?
рендеринг на стороне сервераperformanceснимок экрана:
При рендеринге исключительно на стороне браузераperformanceснимок экрана:
то же самое вfast 3GВ сетевом режиме для загрузки первого экрана на стороне браузера требуется время.2.9s,так какclient.jsстоимость загрузки2.27s, потому что нетclient.jsнетVue, и за этим ничего нет.
Время рендеринга на стороне сервера для первого экрана0.8s,Несмотря на то чтоclient.jsстоимость броска груза2.27s, но он уже не нужен выше сгиба, он там позволяетVueПоследующие поглощения выполняются на стороне браузера.
Из этого мы действительно можем видеть, что рендеринг на стороне сервера очень полезен для улучшения скорости отклика первого экрана.
Конечно, некоторые студенты могут спросить, рендеринг на сервере, чтобы получить начальныйajaxМы также задержали 1 с при загрузке данных, и пользователь не мог видеть страницу в это время. Да, мы не можем избежать времени интерфейса. Даже если это чистый браузерный рендеринг, интерфейс все равно должен быть настроен на домашней странице. Если интерфейс работает медленно, то чистое время браузерного рендеринга, чтобы увидеть всю страницу будет медленнее.
полный просмотр кодаgithub
4. Используйте serverBundle и clientManifest для оптимизации
Ранее мы создали серверrendererМетод:
const bundle = fs.readFileSync(path.resolve(__dirname, '../dist/server.js'), 'utf-8');
const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
template: fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8')
});
serverBundleМы используем упакованныеserver.bundle.jsдокумент. Для этого требуется останавливать и перезапускать службу каждый раз, когда редактируется исходный код приложения. Это влияет на эффективность разработки во время разработки. Кроме того, Node.js изначально не поддерживает исходные карты.
vue-server-rendererобеспечитьcreateBundleRendererAPI , чтобы справиться с этим, используяwebpackпользовательский плагин,server bundleбудет генерировать как проходимый дляbundle rendererспециальныйJSONдокумент. созданныйbundle renderer, использование и общиеrendererто же самое, ноbundle rendererОбеспечивает следующие преимущества:
- Встроенный
source mapподдержка (вwebpackиспользуется в конфигурацииdevtool: 'source-map') - Горячая перезагрузка во время разработки или даже развертывания (прочитав обновленную
bundle, затем воссоздайтеrendererпример) - Самое важное
CSS(critical CSS)вводить (используя*.vueфайл): требуется для автоматического встраивания компонентов, используемых во время рендеринга.CSS. Для получения более подробной информации см.CSSглава. - использовать
clientManifestВыполните внедрение ресурсов: автоматически определите наилучшую предварительную загрузку (preload) и предварительная выборка (prefetch) инструкция и разделение кода, необходимое для первоначального рендерингаchunk.
preloadиprefetchЕсли вы их не понимаете, вы можете проверить их сами.
Итак, давайте изменимwebpackКонфигурация:
webpack.client.config.js
const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const base = require('./webpack.base.config');
module.exports = merge(base, {
entry: {
client: path.resolve(__dirname, '../src/entry-client.js')
},
plugins: [
new VueSSRClientPlugin(), // 新增
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../src/index.html'),
filename: 'index.html'
})
]
});
webpack.server.config.js
const path = require('path');
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const base = require('./webpack.base.config');
module.exports = merge(base, {
target: 'node',
// 对 bundle renderer 提供 source map 支持
devtool: '#source-map',
entry: {
server: path.resolve(__dirname, '../src/entry-server.js')
},
externals: [nodeExternals()], // 新增
output: {
libraryTarget: 'commonjs2'
},
plugins: [
new VueSSRServerPlugin(), // 这个要放到第一个写,否则 CopyWebpackPlugin 不起作用,原因还没查清楚
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../src/index.ssr.html'),
filename: 'index.ssr.html',
files: {
js: 'client.bundle.js'
},
excludeChunks: ['server']
})
]
});
Поскольку это справочный модуль на стороне сервера, его не нужно упаковывать.node_modulesзависимости в, непосредственно в кодеrequireСсылка хорошая, так что настраивайтеexternals: [nodeExternals()].
Два файла конфигурации будут сгенерированы отдельноvue-ssr-client-manifest.jsonиvue-ssr-server-bundle.json. в видеcreateBundleRendererпараметр.
приходите посмотретьserver.js
server.js
const serverBundle = require(path.resolve(__dirname, '../dist/vue-ssr-server-bundle.json'));
const clientManifest = require(path.resolve(__dirname, '../dist/vue-ssr-client-manifest.json'));
const template = fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8');
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template: template,
clientManifest: clientManifest
});
Эффект такой же, как и в третьем шаге, без скриншотов, полный кодgithub.
5. Настройте полный SSR на основе Vue + VueRouter + Vuex.
Отличие этого шага от четвертого состоит в том, что введениеvue-router, что ближе к фактическому проекту разработки.
существуетsrcдобавить подrouterсодержание.
router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Bar from '../components/Bar.vue';
Vue.use(Router);
function createRouter() {
const routes = [
{
path: '/bar',
component: Bar
},
{
path: '/foo',
component: () => import('../components/Foo.vue') // 异步路由
}
];
const router = new Router({
mode: 'history',
routes
});
return router;
}
export default createRouter;
Здесь мы ставимFooКомпонент вводится как асинхронный компонент и загружается по требованию.
существуетapp.jsвведен вrouterи экспорт:
app.js
import Vue from 'vue';
import createStore from './store/store.js';
import createRouter from './router';
import App from './App.vue';
export function createApp() {
const store = createStore();
const router = createRouter();
const app = new Vue({
router,
store,
render: h => h(App)
});
return { app, store, router, App };
}
ИсправлятьApp.vueИмпорт компонентов маршрутизации:
App.vue
<template>
<div id="app">
<router-link to="/bar">Goto Bar</router-link>
<router-link to="/foo">Goto Foo</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
beforeCreate() {
console.log('App.vue beforeCreate');
},
created() {
console.log('App.vue created');
},
beforeMount() {
console.log('App.vue beforeMount');
},
mounted() {
console.log('App.vue mounted');
}
}
</script>
Наиболее важная модификация находится вentry-server.jsсередина,
entry-server.js
import { createApp } from './app.js';
export default context => {
return new Promise((resolve, reject) => {
const { app, store, router, App } = createApp();
router.push(context.url);
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
console.log(context.url)
console.log(matchedComponents)
if (!matchedComponents.length) {
return reject({ code: 404 });
}
Promise.all(matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({ store });
}
})).then(() => {
// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中
context.state = store.state;
// 返回根组件
resolve(app);
});
}, reject);
});
}
упоминалось ранее здесьcontextсыграло большую роль, этоurlадрес передан дляvue-routerиспользовать. Поскольку есть асинхронные компоненты, поэтому вrouter.onReadyВ обратном вызове успеха найдитеurlДля совпадающих по маршруту компонентов набор получения асинхронных данных такой же, как и предыдущий.
Таким образом, мы завершили в основном полныйVue + VueRouter + VuexКонфигурация SSR, полный просмотр кодаgithub.
Демонстрация конечного эффекта:
доступhttp://localhost:3000/bar:
полный просмотр кодаgithub
следовать за
В приведенном выше примере мы завершили изоморфизм от чистого рендеринга в браузере до полного рендеринга на стороне сервера, выполнив пять шагов.Код может выполняться как на стороне браузера, так и на стороне сервера. Итак, оглядываясь назад, мы смотрим, есть ли место для оптимизации или есть идеи для расширения.
оптимизация:
- В настоящее время мы используем
renderToStringметод, полностью сгенерированныйhtmlПосле этого он будет возвращен клиенту.Если вы используетеrenderToStream,применениеbigpipeТехнология может непрерывно возвращать поток в браузер, поэтому браузер может отображать что-то как можно быстрее при загрузке файла.
const stream = renderer.renderToStream(context)
Возвращаемое значениеNode.js stream:
let html = ''
stream.on('data', data => {
html += data.toString()
})
stream.on('end', () => {
console.log(html) // 渲染完成
})
stream.on('error', err => {
// handle error...
})
В режиме потокового рендеринга, когдаrendererПрошедший виртуальныйDOMДерево(virtual DOM tree), данные отправляются как можно скорее. Это означает, что мы можем получить «первыйchunk", и начните отправлять его клиенту быстрее.
Однако, когда появились первые данныеchunkПри испускании дочерние компоненты могут даже не создаваться, и их обработчики жизненного цикла не будут вызываться. Это означает, что если дочерний компонент должен находиться в своей функции подключения жизненного цикла, прикрепите данные к контексту рендеринга (render context), когда поток (stream), эти данные будут недоступны. Это связано с тем, что большое количество контекстной информации (context information) (например, информация заголовка (head information) или встроенный ключCSS(inline critical CSS))необходимо отметить в приложении (markup), нам в основном нужно дождаться потока (stream), прежде чем вы сможете начать использовать данные контекста.
Поэтому не рекомендуется использовать потоковый режим, если вы полагаетесь на контекстные данные, заполняемые обработчиками жизненного цикла компонентов.
-
webpackоптимизация
webpackОптимизация - еще одна большая тема, я не буду ее здесь обсуждать, заинтересованные студенты могут найти информацию самостоятельно, возможно, в будущем я тоже напишу статью.webpackоптимизация.
считать
- Нужно ли использовать
vuex?
Ответ - нет.VuexПросто чтобы помочь вам реализовать набор механизмов хранения, обновления и получения данных, вам не нужно вкладывать средства в акции.Vuex, то вы должны придумать план хранения данных, полученных асинхронно, и ввести их в компонент в соответствующее время.Есть некоторые статьи, которые предлагают некоторые планы, я помещу их в справочную статью, вы можете Читать дальше .
- использовать или нет
SSRдолжно быть хорошо?
Это тоже не обязательно, любая технология имеет сценарии использования.SSRЭто может помочь вам улучшить скорость загрузки домашней страницы и оптимизировать поисковую систему.SEO, но в то же время, поскольку это необходимоnodeсредний рендерингVue, который возьмет на себя нагрузку на сервер и будет выполнять толькоbeforeCreateиcreatedДва жизненных цикла, некоторые внешние библиотеки расширений необходимо обработать, прежде чем их можно будет использовать вSSRбег и так далее.
Эпилог
В этой статье рассматриваются пять шагов, начиная с чистого рендеринга на стороне браузера и заканчивая настройкой полногоVue + vue-router + VuexСреда SSR платформы .SSR.
Наконец, весь исходный код этой статьи размещен в моемgithubО, если это поможет вам, пожалуйста, поставьте лайк~~
Добро пожаловать, чтобы обратить внимание на мой общедоступный номер
Ссылка на ссылку
- ssr.vuejs.org/zh/
- zhuanlan.zhihu.com/p/35871344
- блог woo woo woo.cn на.com/Qingming San…
- nuggets.capable/post/684490…
- GitHub.com/молодой ветер/нет…
Мой блог будет синхронизирован с Tencent Cloud + Community, и я приглашаю всех присоединиться:cloud.Tencent.com/developer/ это…