до чтения
- Зачем использовать серверный рендеринг? 👉официальное объяснение
- Простое понимание руководства VueSSR 👉официальная документация
- Простое понимание веб-пакета 👉официальная документация
- Простое понимание фреймворка Node.js Koa 👉официальная документация
Основное использование
Чтобы построить рендеринг на стороне сервера (SSR), нам нужно использоватьvue-server-renderer
, сначала попробуем демо официального документа, напишем server.js
// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello World</div>`
})
// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()
// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
// => <div data-server-rendered="true">Hello World</div>
})
воплощать в жизньnode server.js
Вы можете увидеть печать консоли<div data-server-rendered="true">Hello World</div>
Из этого кода мы должны понятьvue-server-renderer
Функция состоит в том, чтобы получить экземпляр vue и преобразовать его в html-структуру, но он не только делает одну вещь, но и вводит другие параметры конфигурации и позже создает с помощью веб-пакета.
Следующее, что нам нужно сделать, это получить html-структуру и отобразить ее на странице. Здесь официальный пример использует экспресс для сборки сервера. Здесь я использую Koa, зачем использовать Koa? Не буду выражаться 🤣. Запустить службу в Koa очень просто, нам также нужно использовать Koa-router для обработки маршрутизации. Изменить server.js
const Vue = require('vue')
const Koa = require('koa')
const Router = require('koa-router')
const renderer = require('vue-server-renderer').createRenderer()
// 第 1 步:创建koa、koa-router 实例
const app = new Koa()
const router = new Router()
// 第 2 步:路由中间件
router.get('*', async (ctx, next) => {
// 创建Vue实例
const app = new Vue({
data: {
url: ctx.url
},
template: `<div>访问的 URL 是: {{ url }}</div>`
})
// 有错误返回500,无错误返回html结构
try {
const html = await renderer.renderToString(app)
ctx.status = 200
ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`
} catch (error) {
console.log(error)
ctx.status = 500
ctx.body = 'Internal Server Error'
}
})
app
.use(router.routes())
.use(router.allowedMethods())
// 第 3 步:启动服务,通过http://localhost:3000/访问
app.listen(3000, () => {
console.log(`server started at localhost:3000`)
})
Из вышеприведенного кода мы можем увидеть основной принцип рендеринга на стороне сервера, на самом деле, когда нет рендеринга на стороне сервера, html, упакованный интерфейсом, содержит только часть заголовка, а часть тела динамически вставляется. в идентификатор как#app
в дом. Как показано на рисунке:
И рендеринг на стороне сервера (SSR) заключается в том, что сервер заранее компилирует Vue для генерации HTML и возвращает его веб-браузеру, так что контент, просканированный поисковым роботом, представляет собой весь презентабельный контент на веб-сайте. 🤓
Чтобы персонализировать страницу, мы можем извлечь структуру html в шаблон шаблона с помощью двойных фигурных скобок.{{}}
передать значение, создать новоеindex.template.html
Напишите следующий код согласно официальному сайту
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 三花括号不进行html转义 -->
{{{ meta }}}
<title>{{ title }}</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
Нам нужно передать модуль Nodefs
Прочитайте шаблон какvue-server-renderer
Параметр шаблона передается, и код модифицируется:
const renderer = require('vue-server-renderer').createRenderer({
// 读取传入template参数
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
// ...忽略无关代码
router.get('*', async (ctx, next) => {
// title、meta会插入模板中
const context = {
title: ctx.url,
meta: `
<meta charset="UTF-8">
<meta name="descript" content="基于webpack、koa搭建的SSR">
`
}
try {
// 传入context渲染上下文对象
const html = await renderer.renderToString(app, context)
ctx.status = 200
// 传入了template, html结构会插入到<!--vue-ssr-outlet-->
ctx.body = html
} catch (error) {
ctx.status = 500
ctx.body = 'Internal Server Error'
}
})
// ...忽略无关代码
Вы можете видеть, что наш заголовок и мета были вставлены! 👏👏👏. На данный момент мы достигли самого простого использования, и затем нам, наконец, нужно использовать веб-пакет для сборки нашего проекта.
Формальная среда сборки
Сервер Node.js — это длительный процесс, и когда наш код входит в процесс, он принимает значение и сохраняется в памяти. Это означает, что если создается одноэлементный объект, он будет совместно использоваться каждым входящим запросом, поэтому нам нужноСоздайте новый корневой экземпляр Vue для каждого запроса.
Не только экземпляр vue, но также vuex и vue-router, которые будут использоваться дальше. Мы используем веб-пакет для упаковки кода на стороне клиента и кода на стороне сервера отдельно, серверу нужен «серверный пакет», который затем используется для рендеринга на стороне сервера (SSR), а «клиентский пакет» отправляется в браузер для смешивание статической разметки. Вот официальная карта сборки:
Мы можем примерно понять, что серверная и клиентская стороны проходят через два входаServer entry
,Clinet entry
Получение исходного кода, а затем упакована на два на пачках WebPackvue-ssr-server-bundle.json
,vue-ssr-client-manifest.json
, со сгенерированным полным HTML, в то время какapp.js
Это общая часть кода двух записей, и ее роль заключается в предоставлении экземпляра vue. Таким образом, мы можем организовать каталог файлов в соответствии с официальной рекомендацией и следоватьОфициальный код деланаписано, что служитserver.js
Мы используем Koa, поэтому пока не меняйте его.
В приведенном выше коде необходимо обратить внимание на entry-server.js, который предоставляет функцию, которая принимает параметр контекста контекста рендеринга, а затем сопоставляет компонент в соответствии с URL-адресом. Так что параметры должны быть в нашем вызовеrenderToString
Передайте в контексте и включите атрибут url.
Два сгенерированных пакета фактически передаются в качестве параметровcreateBundleRenderer()
функцию, а затем превратить ее в html-структуру в renderToString с помощьюcreateRenderer
Разница в том, что первый должен получить компиляцию компонента vue через параметр пакета, а второй должен бытьrenderToString
При передаче в экземпляре vue 👉Документация. Сначала мы пишем webpack для успешной генерации бандлов, а затем пишем server.js, который поможет нам лучше понять и протестировать.
Во-первых, мы создаем папку сборки для хранения конфигураций, связанных с веб-пакетом.До vue-cli3 проекты, инициализированные vue init, имели папку сборки, и вы можете четко видеть конфигурацию веб-пакета. После vue-cli3 используйте webpack4 и скройте конфигурацию.Если вы хотите узнать, как webpack4 создает одностраничные приложения vue, вы можете зайти на мой github, чтобы проверить это 👉адрес. Мы можем имитировать vue-cli для создания общей конфигурации webpack.base.conf.js, конфигурации клиента webpack.client.conf.js и конфигурации сервера webpack.server.conf.js. Каталог файлов
├── build
│ ├── webpack.base.conf.js # 基本webpack配置
│ ├── webpack.client.conf.js # 客户端webpack配置
│ └── webpack.server.conf.js # 服务器端webpack配置
├── src
├── index.template.html
└── server.js
webpack.base.conf.js
Конфигурация в основном определяет общие правила, такие как компиляция vue-loader файлов .vue, компиляция js-файлов babel, обработка изображений, шрифтов и т. д. Его базовая конфигурация выглядит следующим образом:
const path = require('path')
// vue-loader v15版本需要引入此插件
const VueLoaderPlugin = require('vue-loader/lib/plugin')
// 用于返回文件相对于根目录的绝对路径
const resolve = dir => path.posix.join(__dirname, '..', dir)
module.exports = {
// 入口暂定客户端入口,服务端配置需要更改它
entry: resolve('src/entry-client.js'),
// 生成文件路径、名字、引入公共路径
output: {
path: resolve('dist'),
filename: '[name].js',
publicPath: '/'
},
resolve: {
// 对于.js、.vue引入不需要写后缀
extensions: ['.js', '.vue'],
// 引入components、assets可以简写,可根据需要自行更改
alias: {
'components': resolve('src/components'),
'assets': resolve('src/assets')
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
// 配置哪些引入路径按照模块方式查找
transformAssetUrls: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
},
{
test: /\.js$/, // 利用babel-loader编译js,使用更高的特性,排除npm下载的.vue组件
loader: 'babel-loader',
exclude: file => (
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
)
},
{
test: /\.(png|jpe?g|gif|svg)$/, // 处理图片
use: [
{
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/img/[name].[hash:7].[ext]'
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, // 处理字体
loader: 'url-loader',
options: {
limit: 10000,
name: 'static/fonts/[name].[hash:7].[ext]'
}
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
webpack.client.conf.js
В основном для упаковки клиентского кода, это черезwebpack-merge
Для реализации слияния базовой конфигурации, в которой должна быть реализована обработка стиля css, здесь я использую стилус, и одновременно загружаю соответствующий стилус-загрузчик для обработки. Здесь мы не рассматриваем среду разработки в первую очередь, и позже мы изменим webpack для среды разработки.
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
// css样式提取单独文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 服务端渲染用到的插件、默认生成JSON文件(vue-ssr-client-manifest.json)
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseWebpackConfig, {
mode: 'production',
output: {
// chunkhash是根据内容生成的hash, 易于缓存,
// 开发环境不需要生成hash,目前先不考虑开发环境,后面详细介绍
filename: 'static/js/[name].[chunkhash].js',
chunkFilename: 'static/js/[id].[chunkhash].js'
},
module: {
rules: [
{
test: /\.styl(us)?$/,
// 利用mini-css-extract-plugin提取css, 开发环境也不是必须
use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader']
},
]
},
devtool: false,
plugins: [
// webpack4.0版本以上采用MiniCssExtractPlugin 而不使用extract-text-webpack-plugin
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash].css',
chunkFilename: 'static/css/[name].[contenthash].css'
}),
// 当vendor模块不再改变时, 根据模块的相对路径生成一个四位数的hash作为模块id
new webpack.HashedModuleIdsPlugin(),
new VueSSRClientPlugin()
]
})
После написания нам нужно определить команды в package.json для выполнения команд упаковки webpack. Если у вас нет этого файла, вам нужно пройтиnpm init
Инициализировать генерацию
// package.json
"scripts": {
"build:client": "webpack --config build/webpack.client.conf.js", # 打包客户端代码
"build:server": "webpack --config build/webpack.server.conf.js", # 打包服务端代码
"start": "node server.js" # 启动服务
}
теперь мы можем пройтиnpm run build:client
Выполните команду упаковки. Перед выполнением команды загрузите зависимый пакет npm. Требуемые в настоящее время зависимости показаны на следующем рисунке:
При выполнении команды упаковки мы найдем дополнительную папку dist, в которой помимо статических файлов генерируется JSON-файл для рендеринга на стороне сервера: vue-ssr-client-manifest.json.
Точно так же нам нужно написать конфигурацию веб-пакета на стороне сервера, а также упаковать и сгенерировать vue-ssr-server-bundle.json. Код конфигурации выглядит следующим образом:
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseWebpackConfig = require('./webpack.base.conf')
const VueServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseWebpackConfig, {
mode: 'production',
target: 'node',
devtool: 'source-map',
entry: path.join(__dirname, '../src/entry-server.js'),
output: {
libraryTarget: 'commonjs2',
filename: 'server-bundle.js',
},
// 这里有个坑... 服务端也需要编译样式,但不能使用mini-css-extract-plugin,
// 因为它会使用document,但服务端并没document,导致打包报错。详情见
// https://github.com/webpack-contrib/mini-css-extract-plugin/issues/48#issuecomment-375288454
module: {
rules: [
{
test: /\.styl(us)?$/,
use: ['css-loader/locals', 'stylus-loader']
}
]
},
// 不要外置化 webpack 需要处理的依赖模块
externals: nodeExternals({
whitelist: /\.css$/
}),
plugins: [
new webpack.DefinePlugin({
'process.env.VUE_ENV': '"server"'
}),
// 默认文件名为 `vue-ssr-server-bundle.json`
new VueServerPlugin()
]
})
То же, что и выше, после выполнения команды мы обнаруживаем, что vue-ssr-server-bundle.json создается в файле dist, мы можем создать новыйbuild
Команда для выполнения упаковки вместе.
Хорошо, теперь мы можем изменить наш server.js, чтобы реализовать весь процесс рендеринга на стороне сервера. Нам нужно получить два файла JSON и шаблоны html для передачи в качестве параметров.createBundleRenderer
, экземпляр vue больше не нужен, а контекст нуждается в URL-адресе, потому что запись на стороне сервера (entry-server.js) должна получить путь доступа для соответствия соответствующему компоненту vue (упомянутому выше). Часть измененного кода выглядит следующим образом:
/* 将createRenderer替换成createBundleRenderer,不同之处在上面提到过... */
const { createBundleRenderer } = require('vue-server-renderer')
// ...忽略无关代码
// 获取客户端、服务器端生成的json文件、html模板文件
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const template = require('fs').readFileSync('./index.template.html', 'utf-8')
// 传入 json文件和template, 渲染上下文url需要传入,服务端需要匹配路由
router.get('*', async (ctx, next) => {
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false, // 推荐
template, // 页面模板
clientManifest // 客户端构建 manifest
})
const context = {
url: ctx.url,
// ...
}
// ...忽略无关代码
После замены запускаемnpm run start
, Обнаружено, что страница была успешно отрисована, но в это время есть проблема, не удалось загрузить ресурсы, а файл существует в дистрибутиве.Очевидно, это должно быть вызвано неправильным путем. В это время мы можем отправлять статические ресурсы через koa-send. Нам нужно добавить эту строку кода в server.js:
const send = require('koa-send')
// 引入/static/下的文件都通过koa-send转发到dist文件目录下
router.get('/static/*', async (ctx, next) => {
await send(ctx, ctx.path, { root: __dirname + '/dist' });
})
Запустите его еще раз, откройте консоль, вы увидите, что ресурс успешно загружен, а загруженный документ содержит все содержимое страницы. 👏
Сборка среды разработки
Мы прошли базовый процесс рендеринга на стороне сервера, но еще не затронули такие вопросы, как асинхронные данные и кэширование. Перед этим нам нужно сначала создать среду разработки, потому что каждая строка кода, которую мы не можем ввести, должна быть переупакована и обработана. Это плохо для отладки. И очень 🐷.
Подумайте о проекте, созданном vue-cli, мы можем пройтиnpm run dev
(vue-cli3 используетnpm run serve
) для запуска службы, а затем, когда файл будет изменен, страница также будет автоматически загружена без ручного обновления. Мы также хотим реализовать аналогичную среду разработки, поэтому нам нужно использовать узел для создания конфигурации веб-пакета и отслеживать изменения файлов в режиме реального времени.Когда изменения вносятся, мы должны переупаковывать, регенерировать два файла JSON и делать это снова.BundleRenderer.renderToString()
метод. За исключением повторного создания файла JSON, остальная логика в основном такая же, как реализованная ранее логика. Таким образом, мы можем вносить изменения на основе server.js, судить об окружающей среде на исходной основе и вносить различные изменения.render
. Нам нужна переменная среды, чтобы решить, какую логику выполнять.
Здесь мы используемcross-env
устанавливатьprocess.env.NODE_ENV
Переменная:
Мы установили команды сборки и запускаprocess.env.NODE_ENV
Для производственной среды, поэтому мы можем получить значение в файле, если нет, по умолчанию используем среду разработки. Итак, где нам нужно изменить наш server.js?
1. Первый — генерироватьBundleRenderer
Пример, до того, как мы получили файл JSON по фиксированному пути (в упакованной папке dist)
// 之前代码逻辑
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const template = require('fs').readFileSync('./index.template.html', 'utf-8')
//...忽略无关代码
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template, // 页面模板
clientManifest // 客户端构建 manifest
})
Нам нужно изменить логику в соответствии с переменными среды.Если это производственная среда, приведенный выше код остается без изменений.Если это среда разработки, нам нужна функция для динамического получения упакованного файла JSON и его повторной генерации.BundleRenderer
Например, сначала мы определяем эту функцию какsetupDevServer
, как следует из названия, эта функция предназначена для создания среды разработки.Его функция заключается в создании конфигурации веб-пакета с файлами nodeAPI и монитора. Мы можем выполнить регенерацию, передав функцию обратного вызова в server.js.BundleRenderer
операция экземпляра. Принятые параметры — это два вновь сгенерированных файла JSON.
// 假设已经实现
const setupDevServer = require('./build/setup-dev-server')
// 生成实例公共函数,开发、生产环境只是传入参数不同
const createBundle = (bundle, clientManifest) => {
return createBundleRenderer(bundle, {
runInNewContext: false,
template,
clientManifest
})
}
let renderer // 将实例变量提到全局变量,根据环境变量赋值
const template = require('fs').readFileSync('./index.template.html', 'utf-8') // 模板
// 第 2步:根据环境变量生成不同BundleRenderer实例
if (process.env.NODE_ENV === 'production') {
// 获取客户端、服务器端打包生成的json文件
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
// 赋值
renderer = createBundle(serverBundle, clientManifest)
// 静态资源,开发环境不需要指定
router.get('/static/*', async (ctx, next) => {
console.log('进来')
await send(ctx, ctx.path, { root: __dirname + '/dist' });
})
} else {
// 假设setupDevServer已经实现,并传入的回调函数会接受生成的json文件
setupDevServer(app, (bundle, clientManifest) => {
// 赋值
renderer = createBundle(bundle, clientManifest)
})
}
2. Во-вторых, мы также можем извлечь функцию промежуточного программного обеспечения и назвать ее функцией рендеринга.
const setupDevServer = require('./build/setup-dev-server')
// 第 2步:根据环境变量生成不同BundleRenderer实例
if (process.env.NODE_ENV === 'production') {
// 获取客户端、服务器端打包生成的json文件
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
// 赋值
renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest
})
// 静态资源,开发环境不需要指定
router.get('/static/*', async (ctx, next) => {
console.log('进来')
await send(ctx, ctx.path, { root: __dirname + '/dist' });
})
} else {
// 假设setupDevServer已经实现,并传入的回调函数会接受生成的json文件
setupDevServer(app, (bundle, clientManifest) => {
// 赋值
renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template,
clientManifest
})
})
}
Здесь мы сначала предполагаем, что реализована функция setupDevServer, а затем подробно поговорим о логике кода. Мы можем добавить журнал, в котором мы оцениваем производственную среду, и распечатывать, хотим ли мы выполнять другую логику для разных сред NODE_ENV.
В прошлом конфигурация веб-пакета, которую мы реализовывали, не отличала производственную среду от среды разработки, но на самом деле мы должны сделать разные оптимизации для среды, такой как vue-cli, такие как среда разработки devtool, которую мы можем использоватьcheap-module-eval-source-map
Компиляция будет быстрее, css стили не нужно упаковывать в отдельные файлы, используйтеvue-style-loader
Просто сделайте обработку, а так как среда разработки требует горячей перезагрузки модулей, то не нужно извлекать файлы. Среда разработки может делать более удобные подсказки об ошибках. Кроме того, производственная среда должна выполнять дополнительную оптимизацию упаковки, например сжатие, кэширование и т. д. В этом цикле статей мы не будем заниматься лучшей оптимизацией продакшн-окружения, потому что я тоже очень несведущ в этих знаниях 😑. Сначала изменим webpack.base.conf.js:
// ...
// 定义是否是生产环境的标志位,用于配置中
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
// 这里使用对象的格式,因为在setDevServer.js中需要添加一个热重载的入口
entry: {
app: resolve('src/entry-client.js')
},
// 开发环境启动sourcemap可以更好地定位错误位置
devtool: isProd
? false
: 'cheap-module-eval-source-map',
// ...... 省略
}
Модифицируем webpack.client.conf.js:
// 定义是否是生产环境的标志位,用于配置中
const isProd = process.env.NODE_ENV === 'production'
const pordWebpackConfig = merge(baseWebpackConfig, {
mode: process.env.NODE_ENV || 'development',
output: {
// chunkhash是根据内容生成的hash, 易于缓存。
// 开发环境不需要生hash、这个我们在setDevServer函数里面改
filename: 'static/js/[name].[chunkhash].js',
chunkFilename: 'static/js/[id].[chunkhash].js'
},
module: {
rules: [
{
test: /\.styl(us)?$/,
// 开发环境不需要提取css单独文件
use: isProd
? [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader']
: ['vue-style-loader', 'css-loader', 'stylus-loader']
},
]
},
// ... 省略
}
Конфигурация веб-пакета на стороне сервера не может быть изменена, потому что его функция только упаковывает файл JSON в конце и не требует внесения каких-либо изменений в среду.
Хорошо, теперь нам нужно написать set-dev-server.js Функция setDevServer в основном использует веб-пакет для ручной сборки приложения и реализации горячей загрузки. Сначала нам нужны два промежуточных ПОkoa-webpack-dev-middleware
а такжеkoa-webpack-hot-middleware
, в первом случае достигается горячая загрузка путем передачи компилятора, скомпилированного веб-пакетом, а во втором — горячая замена модуля.Горячая загрузка заключается в отслеживании изменений файлов для обновления веб-страницы, а горячая замена модуля не требует обновления страница на его основе. . Наша конфигурация веб-пакета на стороне клиента может автоматически обновляться с помощью вышеупомянутой реализации, а компилятору на стороне сервера мы передаемwatch
API для мониторинга. Когда одно из двух изменяется, нам нужно вызвать входящий обратный вызов и передать только что сгенерированный файл JSON. Весь процесс примерно такой, конкретный код такой:
const fs = require('fs')
const path = require('path')
// memory-fs可以使webpack将文件写入到内存中,而不是写入到磁盘。
const MFS = require('memory-fs')
const webpack = require('webpack')
const clientConfig = require('./webpack.client.conf')
const serverConfig = require('./webpack.server.conf')
// webpack热加载需要
const webpackDevMiddleware = require('koa-webpack-dev-middleware')
// 配合热加载实现模块热替换
const webpackHotMiddleware = require('koa-webpack-hot-middleware')
// 读取vue-ssr-webpack-plugin生成的文件
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
} catch (e) {
console.log('读取文件错误:', e)
}
}
module.exports = function setupDevServer(app, cb) {
let bundle
let clientManifest
// 监听改变后更新函数
const update = () => {
if (bundle && clientManifest) {
cb(bundle, clientManifest)
}
}
// 修改webpack配合模块热替换使用
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
clientConfig.output.filename = '[name].js'
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
// 编译clinetWebpack 插入Koa中间件
const clientshh = webpack(clientConfig)
const devMiddleware = webpackDevMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
noInfo: true
})
app.use(devMiddleware)
clientCompiler.plugin('done', stats => {
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
if (stats.errors.length) return
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem,
'vue-ssr-client-manifest.json'
))
update()
})
// 插入Koa中间件(模块热替换)
app.use(webpackHotMiddleware(clientCompiler))
const serverCompiler = webpack(serverConfig)
const mfs = new MFS()
serverCompiler.outputFileSystem = mfs
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
if (stats.errors.length) return
// vue-ssr-webpack-plugin 生成的bundle
bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
update()
})
}
мы использовалиmemory-fs
Запись полученного файла JSON в память, а не на диск, предназначена для более быстрого чтения и записи. Клиент не нужен, потому чтоwebpack-dev-middleware
уже сделал это за нас. Вот почему мы находимся в среде разработки и создали папку dist. теперь мы можем пройтиnpm run dev
Посетите localhost:3000, измените код, вы можете добиться горячей загрузки.
предварительная выборка данных
Во время рендеринга на стороне сервера (SSR) мы, по сути, визуализируем «моментальный снимок» нашего приложения, поэтому, если приложение зависит от некоторых асинхронных данных,Затем, прежде чем начать процесс рендеринга, вам нужно выполнить предварительную выборку и парсинг данных..
Как поясняется в официальной документации, SSR сначала выполняет приложение и возвращает HTML, поэтому нам нужен сервер для обработки данных и клиент для синхронизации с ними. Пример кода официального документа предварительной выборки данных очень подробный, мы можем следовать ему. Здесь нужно сказать, что документация экосистемы vue всегда была очень дружелюбной. И все они снабжены китайскими документами, что очень удобно для Сяобая вроде меня 🙈
Предварительная выборка данных на стороне сервера
Представляем как официальный сайтvuex
Напишите пример кода и внесите изменения. Изменить store/index.js
// ...
export function createStore() {
return new Vuex.Store({
state: {
movie: {}
},
actions: {
// 通过传入id请求电影数据,这里我们模拟一下,先返回id
fetchMovie({ commit }, id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id })
}, 500)
}).then(res => {
commit('setMoive', { res })
})
}
},
mutations: {
// 设置state
setMoive(state, { res }) {
state.movie = res
}
}
})
}
Изменить A.vue
<template>
<div>
A页 请求电影数据结果:{{ this.$store.state.movie }}
</div>
</template>
<script>
export default {
name: 'A',
// 定义asyncData, entry-server.js会编译所有匹配的组件中是否包含,包含则执行
// 将state值挂在到context上,会被序列化为window.__INITIAL_STATE__
//
asyncData ({ store, route }) {
// 请求电影数据, 传入 ID : 12345
return store.dispatch('fetchMovie', 12345)
},
}
</script>
<style lang="stylus" scoped>
h1
color blue
</style>
Принцип предварительной выборки на стороне сервера заключается в обходе всех соответствующих компонентов на сервере entry-server.js путем определения функции asyncData в компоненте для асинхронных запросов, выполнении, если он содержит asyncData, и подключении состояния к контекстному контексту.vue-server-renderer
Состояние будет сериализовано в window.__INITIAL_STATE__, чтобы клиент entry-client.js мог заменить состояние и добиться синхронизации. Запускаем код, открываем браузер и вы увидите
Предварительная выборка данных клиента
Поскольку запись будет выполнена только один раз при первом входе в приложение, переход на страницу не будет выполнять логику предварительной выборки данных на стороне сервера, поэтому нам необходима предварительная выборка данных на стороне клиента. веб-сайт, вот Просто попробуйте, используя навигационную защиту маршрутизатора, принцип заключается в выполнении функции asyncData, которая не выполнялась каждый раз, когда вы прыгаете,
// 官方代码
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()
}
// 这里如果有加载指示器(loading indicator),就触发
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 停止加载指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app')
})
На этот раз мы копируем и вставляем A.vue и модифицируем его на B.vue в качестве страницы B и передаем другой идентификатор (например, 666666), выполняем команду, проверяем результат, и вы можете видеть, что при переходе,state.movie
присвоено другое значение
Установить голову и кэш
заглавная инъекция
Делаем серверный рендеринг, будут разные мета, title. Поэтому нам также нужно вводить разные HEAD. Может использоваться в мощныхvue-metaИспользуйте с SSR. Здесь мы следуем официальной документации, чтобы реализовать простую инъекцию заголовка.Во-первых, вам нужно определить ее в своем шаблоне.<title>{{ title }}</title>
Основной принцип аналогичен предварительной выборке данных: мы получаем функцию заголовка или строку в компоненте в определенное время, а затем монтируем ее в контекст, чтобы заголовок мог динамически изменяться. Клиент звонит напрямуюdocument.title = title
могу. Мы представим официальный образец кодаtitle-mixin.jsПоместите его в папку миксина. ссылка в app.js, вызовVue.mixin(titleMixin)
, при посещении страницы А заголовок становится страницей А
// app.js
import titleMixin from './mixins/title-mixin'
Vue.mixin(titleMixin)
// A.vue
export default {
title: 'A页面', // 或者是 title () { return 'A页面' }
// ...
}
кеш уровня страницы
Основной принцип кэширования также четко прописан в официальном коде. Официальный код выглядит следующим образом:
// server.js
// 设置缓存参数
const microCache = LRU({
max: 100, // 最大缓存数
maxAge: 10000 // 10s过期,意味着10s内请求统一路径,缓存中都有
})
// 判断是否可以缓存,这里先模拟,当访问B就缓存
const isCacheable = ctx => {
return ctx.url === '/b'
}
const render = async (ctx) => {
// ...忽略无关代码
// 判断是否可缓存,如果可缓存则先从缓存中查找
const cacheable = isCacheable(ctx)
if (cacheable) {
const hit = microCache.get(ctx.url)
if (hit) {
console.log('取到缓存') // 便于调试
ctx.body = hit
return
}
}
// 存入缓存, 只有当缓存中没有 && 可以缓存
if (cacheable) {
console.log('设置缓存') // 便于调试
microCache.set(ctx.url, html)
}
}
Запускаем код, обновляем страницу, проверяем командную строку, видим, что при первом вводе B кеш установлен, сколько бы страница не обновлялась в течение 10 секунд, кеш получается. Вместо этого страница A не будет кэшироваться.
Суммировать
Это конец настройки рендеринга Vue на стороне сервера 😁. В статье основное внимание уделяется использованию веб-пакета для построения SSR в средах разработки и производства, потому что, насколько я понимаю, я провожу больше времени в этом месте. Такие вещи, как предварительная выборка данных, динамическая настройка Head и кэширование маршрутов, в основном основаны на официальных документах, и их несложно понять. Но еще многое предстоит сделать, если его можно будет использовать для разработки онлайн-проектов. НапримерnuxtЭто уже хорошо сделано, и моя компания также использует nuxt. Цель этой статьи — лучше понять рендеринг на стороне сервера. Полный код проекта 👉адрес, Если это вам поможет, не забудьте поставить звезду~