阅读建议:建议通过左侧导航栏进行阅读
文章简介:本文是Vue.js
服务器端渲染的另一种解决方案-SSR
(Server-Side Rendering
)学习笔记
Vue SSR是什么
官方文档解释:
Vue.js
是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出Vue
组件,进行生成DOM
和操作DOM
。然而,也可以将同一个组件渲染为服务器端的HTML
字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务器渲染的Vue.js
应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。
Vue SSR
(Vue.js Server-Side Rendering
)是Vue.js
官方提供的服务端渲染解决方案- 使用
SSR
,可以构建基于vue.js的同构应用
Vue SSR使用场景
在使用SSR之前,要从以下两方面考虑是否真的需要它。
技术层面
- 有 利于SEO
- 更快的首屏渲染速度
业务层面
- 不适合管理系统
- 适合移动网站和门户资讯类网站,如企业官网、知乎、简书等
Vue SSR实现方案
基于 Vue SSR 官方文档提供的解决方案
官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中,也会对Vue SSR
有更加深入的了解。
该方式需要你熟悉 Vue.js
本身,并且具有 Node.js
和 webpack
的相当不错的应用经验。
Nuxt.js 开发框架
Nuxt.js
提供了平滑的开箱即用的体验,它建立在同等的Vue.js
技术栈之上,但抽象出很多模板,并提供了一些额外的功能,例如静态站点生成。通过 Nuxt.js
可以快速的使用 Vue SSR
构建同构应用。
Vue SSR基于官方文档的基本使用
渲染一个Vue实例
体会服务端渲染中最基础的工作-模板渲染,了解如何使用
Vue SSR
将一个 Vue 实例渲染为 HTML 字符串,也就是如何在服务端使用 Vue.js 的方式解析替换字符串。
新建一个项目,在项目根目录下依次执行以下命令
//初始化项目,新建package.json
yarn init
//安装项目基本依赖包
yarn add vue vue-server-renderer express
在项目根目录下分别新建server.js
和index.html
server.js
const Vue = require('vue');
const express = require('express');
const fs = require('fs');
const renderer = require('vue-server-renderer').createRenderer({
//使用utf-8进行编码的index.html作为渲染模板
template: fs.readFileSync('./index.html', 'utf-8')
});
const server = express();
//设置路由,客户端以get请求网站根路径
server.get('/', (req, res) => {
//创建一个vue实例
const app = new Vue({
template:`
<div>
<h1>{
{message}}</h1>
</div>
`,
data: {
message: '创建一个vue实例'
}
});
//将vue实例转换为html字符串,并发送给客户端
renderer.renderToString(app, {
title: '服务器端渲染',
meta: `<meta name="desc" content="服务器">`
}, (err, html) => {
if(err) {
res.status(500).end('server error...');
};
res.end(html);
})
})
//启动web服务
server.listen(3000, () => {
console.log('server running...')})
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{
{
{ meta }}}
<title>{
{title}}</title>
</head>
<body>
<!--标记页面模板-->
<!--vue-ssr-outlet-->
</body>
</html>
使用nodemon server.js(nodemon需要在本机安装)启动项目。
在客户端访问http://localhost:3000/
,可以看到渲染结果,页面结构:
案例总结:
- 解决页面显示乱码的两种方案
1、设置响应头
res.setHeader('Content-Type', 'text/html;charset=utf8');
res.end(html);
2、发送完整的html
页面,使用charset="UTF-8"
进行编码
res.end(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
${
html}
</body>
</html>
`)
- 服务端向客户端发送完整页面的两种方式
1、利用ES6的模板语法直接在请求响应中发送完整页面模板
2、使用渲染模板,结合vue-server-renderer
解析替换字符串,如server.js就采用这种方式
注意:
1.使用渲染模板接收外部数据时,如果修改的是静态模板如head中的内容,需要重启项目才能生效,因为模板是在项目启动时获取的
2.如果需要将外部数据渲染为html标签,需要使用三个“{}”,如{ { { meta }}}
Vue SSR同构渲染构建
构建流程
源码结构
1.使用webpack
打包的原因
- 通常
Vue
应用程序是由webpack
和vue-loader
构建,并且许多webpack
特定功能不能直接在Node.js
中运行(例如通过file-loader
导入文件,通过css-loader
导入CSS
) - 尽管
Node.js
最新版本能够完全支持ES2015
特性,我们还是需要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。
对于客户端应用程序和服务器应用程序,我们都要使用 webpack
打包 - 服务器需要「服务器 bundle
」然后用于服务器端渲染(SSR
),而「客户端 bundle
」会发送给浏览器,用于混合静态标记。
2.使用webpack的源码结构
使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用 webpack 支持的所有功能,推荐的源码结构:
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
在项目根目录下新建src
目录
src/app.vue
<template>
<div id="app">
<h1>{
{ message }}</h1>
<h2>客户端动态交互</h2>
<div>
<input v-model="message" />
</div>
<div>
<button @click="onClick">点击按钮</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
message: "创建一个vue实例",
};
},
methods: {
onClick() {
console.log("---点击按钮---");
},
},
};
</script>
src/app.js
app.js 是我们应用程序的「通用 entry」,简单地导出一个工厂函数createApp 函数,用于创建新的应用程序、router 和 store 实例。
/**
* 通用启动入口
*/
import Vue from 'vue'
import App from './App.vue'
// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
const app = new Vue({
// 根实例简单的渲染应用程序组件。
render: h => h(App)
})
return {
app }
}
src/entry-client.js
客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中。
import {
createApp } from './app'
// 客户端特定引导逻辑……
const {
app } = createApp()
//将创建的vue实例绑定
app.$mount('#app')
src/entry-server.js
服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)
import {
createApp } from './app'
export default context => {
const {
app } = createApp();
return app
}
安装依赖
安装生产依赖
cnpm i vue vue-server-renderer express cross-env --save
依赖包 | 说明 |
---|---|
vue | Vue.js 核心库 |
vue-server-renderer | Vue 服务端渲染工具 |
express | 基于 Node 的 Web 服务框 |
cross-env | 通过 npm scripts 设置跨平台环境变量 |
安装开发依赖
cnpm i webpack webpack-cli webpack-merge webpack-node-externals
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader
css-loader url-loader file-loader rimraf vue-loader vue-template-compiler
friendly-errors-webpack-plugin --save-dev
依赖包 | 说明 |
---|---|
webpack | webpack 核心包 |
webpack-cli | webpack 的命令行工具 |
webpack-merge | webpack 配置信息合并工具 |
webpack-node-externals | 去除 webpack 中的 Node 模块 |
rimraf | 基于 Node 封装的一个跨平台rm -rf工具 |
friendly-errors-webpack-plugin | 友好的 webpack 错误提示 |
file-loader | 处理字体资源 |
css-loader | 处理 CSS 资源 |
url-loade | 处理图片资源 |
vue-loader vue-template-compiler | 处理 .vue 资源 |
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader | 处理图片资源 |
webpack配置文件
build
├── webpack.base.config.js # 公共配置
├── webpack.client.config.js # 客户端打包配置文件
└── webpack.server.config.js # 服务端打包配置文件
webpack.base.config.js
/**
* 公共配置
* */
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = file => path.resolve(__dirname, file)
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
mode: isProd ? 'production' : 'development',
output: {
path: resolve('../dist/'), //打包文件的输出目录
publicPath: '/dist/', //请求打包资源的请求前缀/dist
filename: '[name].[chunkhash].js'
},
resolve: {
alias: {
// 路径别名,@ 指向 src
'@': resolve('../src/')
},
// 可以省略的扩展名
// 当省略扩展名的时候,按照从前往后的顺序依次解析
extensions: ['.js', '.vue', '.json']
},
devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
module: {
rules: [
// 处理图片资源
{
test: /\.(png|jpg|gif)$/i,
use: [{
loader: 'url-loader',
options: {
limit: 8192
},
}, ],
},
// 处理字体资源
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader']
},
// 处理 .vue 资源
{
test: /\.vue$/,
loader: 'vue-loader'
},
//处理 CSS 资源, 它会应用到普通的 `.css` 文件
//以及`.vue` 文件中的 `<style>` 块
{
test: /\.css$/,
use: ['vue-style-loader','css-loader']
},
// CSS 预处理器,参考:https://vue-loader.vuejs.org/zh/guide/pre-processors.html
// 例如处理 Less 资源
// {
// test: /\.less$/,
// use: [ 'vue-style-loader', 'css-loader', 'less-loader' ]
// },
]
},
plugins: [
new VueLoaderPlugin(),
new FriendlyErrorsWebpackPlugin()
]
}
webpack.client.config.js
/**
* 客户端打包配置
*/
const {
merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge( baseConfig, {
entry: {
//相对于打包所处的路径
app: './src/entry-client.js'
},
module: {
rules: [
// ES6 转 ES5
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true,
plugins: ['@babel/plugin-transform-runtime']
}
}
}
]
},
//重要信息:这将webpack运行时分离到一个引导chunk中,
//以便可以在之后正确注入异步chunk。
optimization: {
splitChunks: {
name: "manifest",
minChunks: Infinity
}
},
plugins: [
//此插件在输出目录中生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
webpack.server.config.js
/**
* 服务端打包配置
*/
const {
merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
// 将 entry 指向应用程序的 server entry 文件
entry: './src/entry-server.js',
// 这允许 webpack 以 Node 适用方式处理模块加载
// 并且还会在编译 Vue 组件时,
// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
target: 'node',
output: {
filename: 'server-bundle.js',
// 此处告知server bundle使用 Node 风格导出模块(Node-style exports)
libraryTarget: 'commonjs2'
},
// 不打包 node_modules 第三方包,而是保留 require 方式直接加载
externals: [
nodeExternals({
// 白名单中的资源依然正常打包
allowlist: [/\.css$/]
})
],
plugins: [
//这是将服务器的整个输出构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
new VueSSRServerPlugin()
]
})
在package.json中配置npm scripts 中打包命令
"scripts": {
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
"build": "rimraf dist && npm run build:client && npm run build:server"
},
执行打包构建命令。
启动应用
执行构建命令后,可以在/dist
目录下看到用于服务端渲染的资源文件以及用于客户端的资源文件,下面使用这些文件把同构应用启动。参考Bundle Renderer 指引,修改server.js
如下。
const Vue = require('vue');
const express = require('express');
const fs = require('fs');
const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
const template = fs.readFileSync('./index.template.html', 'utf-8'); //使用utf-8进行编码的index.html作为渲染模板
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
template, clientManifest });
const server = express();
//在服务端开放静态资源,否则客户端无法获取资源
//使用express的static中间件处理静态资源,请求前缀应该和打包出口的publicPath保持一致,
server.use('/dist', express.static('./dist'));
//设置路由,以get请求网站根路径,
server.get('/', (req, res) => {
//将模板转换为字符串,并发送给客户端
renderer.renderToString({
title: '服务器端渲染',
meta: `<meta name="desc" content="服务器">`
}, (err, html) => {
if(err) {
res.status(500).end('server error...');
};
res.end(html);
})
})
//启动web服务
server.listen(3000, () => {
console.log('server running...')})
执行nodemon server.js启动应用
注意:此处不需要显示的创建
vue
实例,渲染的是entry-server.js
创建的vue
实例。
同构应用渲染流程
- 服务端渲染
- renderer.renderToString 渲染了什么?
- renderer 是如何拿到 entry-server 模块的?
- createBundleRenderer 中的 serverBundle
- server Bundle 是 Vue SSR 构建的一个特殊的 JSON 文件
- entry:入口
- files:所有构建结果资源列表
- maps:源代码 source map 信息
- server-bundle.js 就是通过 server.entry.js 构建出来的结果文件
- 最终把渲染结果注入到模板中
- 客户端渲染
构建开发模式
1、构建目的
代码修改以后
- 自动构建
- 自动重启web服务
- 自动刷新浏览器
2、构建思路
- 生产模式
- npm run build 构建
- node server.js 启动应用
- 开发模式
- 监视代码变动自动构建,热更新等功能
- node server.js 启动应用
在package.json
中添加 scripts
启动脚本
"scripts": {
....
"start": "cross-env NODE_ENV=production node server.js",
"dev": "node server.js"
},
3、封装处理模块
server.js
在开发模式下,可以直接把渲染的结果发送给客户端。而在开发模式下,需要在代码被修改以后,直接构建,刷新浏览器。
const express = require('express');
const fs = require('fs');
const {
createBundleRenderer } = require('vue-server-renderer');
const setupDevServer = require('./build/setup-dev-server');
const server = express();
//在服务端开放静态资源,否则客户端无法获取资源
//使用express的static中间件处理静态资源,请求前缀应该和打包出口的publicPath保持一致,
//express.static处理的物理磁盘里的文件
server.use('/dist', express.static('./dist'));
const isProd = process.env.NODE_ENV === 'production';
let renderer;
let onReady; //标记开发模式下,构建是否完成
if (isProd) {
//生产模式
const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
//使用utf-8进行编码的index.html作为渲染模板
const template = fs.readFileSync('./index.template.html', 'utf-8');
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
});
} else {
//开发模式 -> 监视打包构建 -> 重新生成renderer渲染器
onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {
renderer = createBundleRenderer(serverBundle, {
template,
clientManifest
})
});
}
const render = async (req, res) => {
//将模板转换为字符串,并发送给客户端
try {
const html = await renderer.renderToString({
title: '拉勾教育',
meta: `
<meta name="description" content="拉勾教育">
`,
url: req.url
})
res.setHeader('Content-Type', 'text/html; charset=utf8')
res.end(html)
} catch (err) {
res.status(500).end('Internal Server Error.')
}
};
//设置路由,以get请求网站根路径
server.get('/', isProd ?
//开发模式下,直接将渲染结果返回给客户端
render :
async (req, res) => {
//等待有了renderer 渲染器以后,调用 render 进行渲染
await onReady;
render(req, res);
}
)
//启动web服务
server.listen(3000, () => {
console.log('server running...')
})
build/setup-dev-server.js
监视打包过程,配置热更新,更新renderer渲染器
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')
const resolve = file => path.resolve(__dirname, file)
module.exports = (server, callback) => {
let ready
const onReady = new Promise(r => ready = r)
// 监视打包构建 -> 更新Renderer渲染器
let template
let serverBundle
let clientManifest
const update = () => {
if (template && serverBundle && clientManifest) {
ready()
callback(serverBundle, template, clientManifest)
}
}
//1、更新模板(监视构建template -> 调用 update -> 更新Renderer渲染器)
const templatePath = path.resolve(__dirname, '../index.template.html')
template = fs.readFileSync(templatePath, 'utf-8')
update()
// fs.watch、fs.watchFile
//第三方包:chokidar
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8')
update()
})
//2、更新服务端打包(监视构建serverBundle -> 调用update -> 更新Renderer渲染器)
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
const serverDevMiddleware = devMiddleware(serverCompiler, {
//关闭默认日志输出,由FriendlyErrorsWebpackPlugin处理
//4.x.x版本不支持此属性
logLevel: 'silent'
})
serverCompiler.hooks.done.tap('server', () => {
serverBundle = JSON.parse(
//从内存当中读取文件
serverDevMiddleware.fileSystem.readFileSync(
resolve('../dist/vue-ssr-server-bundle.json'),
'utf-8')
)
update()
})
//3、更新客户端打包(监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器)
const clientConfig = require('./webpack.client.config')
//----配置热更新----
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
clientConfig.entry.app = [
//和服务端交互处理热更新的一个客户端脚本
'webpack-hot-middleware/client?quiet=true&reload=true',
clientConfig.entry.app
]
// 热更新模式下确保一致的 hash
clientConfig.output.filename = '[name].js'
//----配置热更新----
const clientCompiler = webpack(clientConfig)
const clientDevMiddleware = devMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
//关闭默认日志输出,由FriendlyErrorsWebpackPlugin处理
//4.x.x版本不支持此属性
logLevel: 'silent'
})
clientCompiler.hooks.done.tap('client', () => {
clientManifest = JSON.parse(
//从内存当中读取文件
clientDevMiddleware.fileSystem.readFileSync(
resolve('../dist/vue-ssr-client-manifest.json'),
'utf-8')
)
update()
})
//挂载热更新的中间件
server.use(hotMiddleware(clientCompiler, {
log: false // 关闭它本身的日志输出
}))
// 重要!将内存中的资源通过 Express 中间件对外公开访问server.use(clientDevMiddleware)
//将 clientDevMiddleware 挂载到 Express服务中,提供其对内存中数据的访问
server.use(clientDevMiddleware)
return onReady
}
封装处理模块总结:
-
更新模板
fs.watch、fs.watchFile
第三方包:chokidar -
将打包结果存储到内存中
webpack
默认会把构建结果存储到磁盘中,对于生产模式构建来说是没有问题的;但是我们在开发模式中会频繁的修改代码触发构建,也就意味着要频繁的操作磁盘数据,而磁盘数据操作相对于来说是比较慢的,所以我们有一种更好的方式,就是把数据存储到内存中,这样可以极大的提高构建的速度。而把把数据存储到内存中方式:- memfs是一个兼容 Node 中 fs 模块 API的内存文件系统,通过它我们可以轻松的实现把 webpack 构建结果输出到内存中进行管理。
- 使用webpack-dev-middleware,
webpack-dev-middleware
作用是以监听模式启动webpack
,将编译结果输出到内存中,然后将内存文件输出到Express
服务中。
这里选择第二种方式,采用webpack-dev-middleware
:
build/setup-dev-server.js
//2、监视构建serverBundle -> 调用update -> 更新Renderer渲染器
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
const serverDevMiddleware = devMiddleware(serverCompiler, {
//关闭默认日志输出,由FriendlyErrorsWebpackPlugin处理
//4.x.x版本不支持此属性
logLevel: 'silent'
})
serverCompiler.hooks.done.tap('server', () => {
serverBundle = JSON.parse(
//从内存当中读取文件
serverDevMiddleware.fileSystem.readFileSync(
resolve('../dist/vue-ssr-server-bundle.json'),
'utf-8')
)
update()
})
- 热更新
开发模式下热更新功能需要使用到webpack-hot-middleware工具包,工作原理:- 中间件将自身安装为
webpack
插件,并侦听编译器事件。 - 每个连接的客户端都有一个
Server Sent Events
连接,服务器将在编译器事件上向连接的客户端发布通知。 - 当客户端收到消息时,它将检查本地代码是否为最新。如果不是最新版本,它将触发 webpack 热模块重新加载。
- 中间件将自身安装为
build/setup-dev-server.js
// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
const clientConfig = require('./webpack.client.config')
//----配置热更新----
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
clientConfig.entry.app = [
//和服务端交互处理热更新的一个客户端脚本
'webpack-hot-middleware/client?quiet=true&reload=true',
clientConfig.entry.app
]
// 热更新模式下确保一致的 hash
clientConfig.output.filename = '[name].js'
//----配置热更新----
const clientCompiler = webpack(clientConfig)
const clientDevMiddleware = devMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
//关闭默认日志输出,由FriendlyErrorsWebpackPlugin处理
//4.x.x版本不支持此属性
logLevel: 'silent'
})
clientCompiler.hooks.done.tap('client', () => {
clientManifest = JSON.parse(
//从内存当中读取文件
clientDevMiddleware.fileSystem.readFileSync(
resolve('../dist/vue-ssr-client-manifest.json'),
'utf-8')
)
update()
})
//挂载热更新的中间件
server.use(hotMiddleware(clientCompiler, {
log: false // 关闭它本身的日志输出
}))
// 重要!将内存中的资源通过 Express 中间件对外公开访问server.use(clientDevMiddleware)
//将 clientDevMiddleware 挂载到 Express服务中,提供其对内存中数据的访问
server.use(clientDevMiddleware)
编写通用代码
编写通用代码的注意事项见Vue SSR 官网
页面路由和代码分割
分别新建pages/home.vue
、pages/about.vue
、pages/404.vue
新建路由模块
router/index.js
vue-router
在同构应用中的用法与在纯客户端的用法一致,只需要在少许的位置做一些配置就可以了。
注意:在同构应用中路由模式要使用
'history'
,它可以兼容前后端
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '@/pages/home';
Vue.use(VueRouter);
export const createRouter = () => {
const router = new VueRouter({
mode: 'history', //兼容前后端
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: () => import('@/pages/about')
},
{
path: '*',
name: 'error',
component: () => import('@/pages/404')
}
]
});
return router;
}
将路由注册到根实例
src/app.js
/**
* 通用启动入口
*/
import Vue from 'vue'
import App from './App.vue'
import {
createRouter } from './router/index.js'
// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
const router = createRouter();
const app = new Vue({
router, //把路由挂载到Vue根实例中
render: h => h(App) // 根实例简单的渲染应用程序组件。
})
return {
app, router }
}
服务端路由适配
entry-server.js
中实现服务器端路由逻辑
import {
createApp } from './app'
//1、async、await方式
export default async context => {
//因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
//以便服务器能够等待所有的内容在渲染前,就已经准备就绪。
const {
app, router } = createApp();
//设置服务器端router的位置
router.push(context.url)
//等到router将可能的异步组件和钩子函数解析完
await new Promise(router.onReady.bind(router));
return app;
}
适配客户端路由入口
import {
createApp } from './app'
// 客户端特定引导逻辑……
const {
app, router } = createApp()
// 这里假定 App.vue 模板中根元素具有 `id="app"`
router.onReady(() => {
app.$mount('#app')
});
设置页面路由视图
src/app.vue
<template>
<div id="app">
<ul>
<li>
<router-link to="/">Home</router-link>
</li>
<li>
<router-link to="/about">About</router-link>
</li>
<li>
<router-link to="/posts">Posts</router-link>
</li>
</ul>
<!--路由出口-->
<router-view />
</div>
</template>
管理页面head内容——vue-meta
安装vue-meta
,在app.js
中注册并配置。
import VueMeta from 'vue-meta';
//注册VueMeta
Vue.use(VueMeta);
//设置页面标题模板
Vue.mixin({
metaInfo: {
titleTemplate: 'hello - %s'
}
});
src/entry-server.js
...
const meta = app.$meta();
//设置服务器端router的位置
router.push(context.url)
context.meta = meta;
...
index.template.html
<head>
...
{
{
{ meta.inject().title.text() }}}
{
{
{ meta.inject().meta.text() }}}
...
</head>
pages/home.vue
...
export default {
name: "HomePage",
metaInfo: {
title: '首页'
}
}
...
数据预取和状态管理
数据预取和状态管理见Vue SSR 官网,核心思路就是把在服务端渲染期间获取的数据存储到 Vuex 容器中,然后把容器中的数据同步到客户端,这样就保持了前后端渲染的数据状态同步,避免了客户端重新渲染的问题。
数据预取大致流程如下:
下面以一个简单的业务需求,实现此流程:
- 已知有一个数据接口,接口返回一个文章列表数据
- 通过服务端渲染的方式来把异步接口数据渲染到页面中
安装Vuex,store/index.js
创建数据容器:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios'
Vue.use(Vuex);
export const createStore = () => {
return new Vuex.Store({
state: () => {
posts: []
},
mutations: {
setPosts(state, data) {
state.posts = data;
}
},
actions: {
//在服务器渲染期间必须让 action 返回一个 Promise
async getPosts({
commit }) {
const {
data } = await axios.get('https://cnodejs.org/api/v1/topics')
commit('setPosts', data.data);
}
}
});
}
app.js
在通用启动入口将数据容器挂载到Vue根实例中并导出。
import {
createStore } from './store/index'
export function createApp () {
...
const store = createStore();
const app = new Vue({
store, //把store挂载到Vue根实例中
render: h => h(App) // 根实例简单的渲染应用程序组件。
})
return {
store }
}
src/entry-server.js
将服务端预取的数据同步到客户端
export default async context => {
...
context.rendered = () => {
// Renderer 会把 context.state 数据对象内联到页面模板中
// 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
// 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中
context.state = store.state;
}
...
}
src/entry-client.js
切换页面路由时,刷新客户端数据
...
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
...
src/pages/posts.vue
在页面组件中获取数据
<script>
import {
mapState, mapActions } from 'vuex'
export default {
name: 'PostList',
metaInfo: {
title: 'Posts'
},
computed: {
...mapState(['posts'])
},
// Vue SSR 特殊为服务端渲染提供的一个生命周期钩子函数
serverPrefetch () {
// 发起 action,返回 Promise
return this.getPosts()
},
methods: {
...mapActions(['getPosts'])
}
</script>
src/pages/posts.vue
在页面组件中展示数据
<template>
<div>
<h1>Post List</h1>
<ul>
<li v-for="post in posts" :key="post.id">
{
{ post.title }}
</li>
</ul>
</div>
</template>
注意:
1、服务端不支持响应式数据。
2、服务端渲染只支持Vue.js
的beforeCreate
和created
,不能在这两个生命周期中获取异步数据,因为渲染过程不会等待异步请求的结果。
3、服务端发送请求依然使用axios
,axios
既可以运行在客户端也可以运行在服务端,因为它对不同的环境做了适配处理,在客户端是基于浏览器的XMLHttpRequest
请求对象,在服务端是基于Node.js
中的http
模块实现,无论是底层是什么,上层的使用方式都是一样的。
服务器端渲染基础
服务器端渲染-Nuxt.js基础
服务器端渲染-Nuxt.js综合案例
服务器端渲染-Nuxt.js综合案例发布部署
服务器端渲染-Vue SSR搭建