构建服务器端渲染(SSR)咱们须要借助vue-server-renderer
,咱们先尝试一下官方文档的一段demo,编写server.jsjavascript
// 第 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>
css
从这段代码咱们应该能够明白vue-server-renderer
的做用是拿到vue实例并渲染成html结构,但它不只仅只作着一件事,后面会介绍其余配置参数和配合webpack进行构建。html
拿到html结构渲染到页面上是咱们接下来要作的事情,这里官方事例用的是express搭建服务器,我这里采用Koa,为何用Koa?我不会express🤣。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只是包含head部分,body部分都是经过动态插入到id为#app
的dom中。如图:vue
而服务器端渲染(SSR)就是服务器来提早编译Vue生成HTML返回给web浏览器,这样网络爬虫爬取的内容就是网站上全部可呈现的内容。🤓java
为了能够个性化页面,咱们能够把html结构抽成一个模板template,经过双花括号{{}}
进行传值,新建index.template.html
按照官网编写以下代码node
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 三花括号不进行html转义 -->
{{{ meta }}}
<title>{{ title }}</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
复制代码
咱们须要经过Node模块fs
读取模板,做为vue-server-renderer
的template参数传入,修改代码:webpack
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'
}
})
// ...忽略无关代码
复制代码
能够看到咱们的标题和meta都被插入啦!👏👏👏。到这里,咱们才实现了最基本的用法,接下来咱们终于要使用webpack来构建咱们项目。git
Node.js服务器是一个长期运行的进程、当咱们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着若是建立一个单例对象,它将在每一个传入的请求之间共享,因此咱们须要为每一个请求建立一个新的根 Vue 实例github
不只vue实例,接下来要用到的vuex、vue-router也是如此。咱们利用webpack须要分别对客户端代码和服务器端代码分别打包, 服务器须要「服务器 bundle」而后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。这里贴一下官方构建图:
咱们能够大体的理解为服务器端、客户端经过俩个入口Server entry
、 Clinet entry
获取源代码,再经过webpack打包变成俩个bundlevue-ssr-server-bundle.json
、vue-ssr-client-manifest.json
,配合生成完成HTML,而app.js
是俩个入口通用的代码部分,其做用是暴露出vue实例。因此咱们能够按照官方建议整理文件目录,并按照官方事例代码编写,其中起服务的server.js
咱们用的是Koa,因此能够先不用改。
上面代码须要注意的是entry-server.js,它是暴露出一个函数,接受渲染上下文context参数,而后根据url匹配组件。因此说参数须要在咱们调用renderToString
传入context,并包括url属性。
生成的俩个bundle实际上是做为参数传入到createBundleRenderer()
函数中,而后在renderToString变成html结构,与createRenderer
不一样的是前者是经过bundle参数获取vue组件编译,后者是须要在renderToString
时传入vue实例👉文档。咱们先编写webpack成功生成bundle后,再去编写server.js,这样有利于咱们更好的理解和测试。
首先咱们创建build文件夹,用于存放webpack相关配置,在vue-cli3以前,vue init 初始化后的项目都是有build文件夹的,能够清楚看到webpack配置。而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
配置主要定义通用的rules,例如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样式的处理,此处我用了stylus,同时要下载对应的stylus-loader来处理。在这里咱们先不考虑开发环境,后面会针对开发环境对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。
同理,咱们须要编写服务端webpack配置,一样打包生成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()
]
})
复制代码
同上,咱们执行命令后发现dist文件下生成vue-ssr-server-bundle.json,咱们能够新建build
命令来一块儿执行打包。
好了,如今咱们能够修改咱们的server.js来实现整个服务器端渲染流程。咱们须要获取俩个JSON文件、html模板做为参数传入createBundleRenderer
,vue实例再也不须要,context须要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
,发现页面已经成功渲染出来,但这时有个问题,加载的资源都失败了,文件存在于dist中,很显然,必定是路径不对致使的。这时咱们能够经过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' });
})
复制代码
再从新运行,打开控制台能够看到资源加载成功,而且加载的doc里面包含页面上全部内容。👏
咱们跑通了基本的服务端渲染流程,但尚未涉及到异步数据、缓存等问题。在此以前,咱们须要先实现开发环境的搭建,由于咱们不可能敲的每一行代码都须要从新打包并起服务。这是不利于调试的。而且很🐷。
想想vue-cli构建出来的项目,咱们能够经过npm run dev
(vue-cli3使用了npm run serve
)起一个服务,而后更改文件的时候,页面也会自动的热加载,不须要手动刷新。咱们也要实现一个相似的开发环境,因此咱们须要利用node来构建webpack配置,而且实时监控文件的改变,当改变时应该从新进行打包,从新生成俩个JSON文件,并从新进行BundleRenderer.renderToString()
方法。咱们除了从新生成JSON文件意外,其余逻辑和以前实现的逻辑大致相同。因此咱们能够在server.js基础上进行修改,在原基础上进行环境的判断,作不一样的render
。咱们须要一个环境变量来决定执行哪一个逻辑。
这里咱们使用cross-env
来设置process.env.NODE_ENV
变量:
咱们把build、start命令都设置了process.env.NODE_ENV
为production生产环境,这样咱们在文件中能够获取到该值,若是没有咱们就默认是development开发环境。那咱们的server.js都须要修改哪里呢?
BundleRenderer
实例,以前咱们是经过固定路径(打包后的dist文件夹下)获取JSON文件// 以前代码逻辑
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构建webpack配置,而且作到监听文件。咱们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)
})
}
复制代码
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的功能,后面咱们再来仔细讲其中的代码逻辑。 咱们能够在判断生产环境的地方加上log,打印一下是否如咱们所愿,针对不一样的NODE_ENV环境执行不一样的逻辑。
在以前,咱们实现的webpack配置并无对生产环境与开发环境作区别,但其实,咱们应该像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']
},
]
},
// ... 省略
}
复制代码
关于服务器端webpack的配置能够不进行修改,由于它的功能最后只打包出一个JSON文件,并不须要针对环境作一些改变。
好的,接下来咱们要编写set-dev-server.js,setDevServer函数主要是利用webpack手动构建应用,并实现热加载。首先咱们须要俩个中间件koa-webpack-dev-middleware
和koa-webpack-hot-middleware
,前者是经过传入webpack编译好的compiler实现热加载,然后者是实现模块热更替,热加载是监听文件变化,从而进行刷新网页,模块热更替则在它的基础上作到不须要刷新页面。咱们客户端webpack配置能够经过前面说的实现自动更新,而服务端compiler,咱们经过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>
复制代码
服务端预取的原理就是,经过在组件内定义asyncData函数用于异步请求,在entry-server.js服务端中遍历全部匹配到的组件,若是包含asyncData则执行,并将state挂载到context上下文,vue-server-renderer
会将state序列化为window.__ INITIAL_STATE __,这样,entry-client.js客户端就能够替换state,实现同步。咱们运行代码,打开浏览器会看到
由于入口只会在第一次进入应用时执行一次,页面的跳转不会再执行服务端数据预取的逻辑,因此说咱们须要客户端数据预取,官网文档实现有俩种方式,这里就只尝试一种,利用router的导航守卫,原理就是在每次进行跳转时,执行没有执行过的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页面传入不一样id(如666666),执行命令,查看结果,能够看到在跳转时,state.movie
已经被赋予不一样的值
咱们作服务端渲染,根据不一样的页面会有不一样的meta、title。因此咱们还须要注入不一样的Head。能够用到强大的vue-meta 配合SSR使用。这里咱们就按照官方文档来实现一个简单的title注入,首先你须要在你的template模板中定义<title>{{ title }}</title>
基本原理跟数据预取相似,咱们在特定的时机来获取组件内title函数,或者字符串,而后将它挂载到context上,这样,就能够实现动态改变标题。客户端直接调用document.title = title
就能够。咱们将官方示例代码title-mixin.js放到mixin文件夹下。app.js中引用,调用Vue.mixin(titleMixin)
,在访问A页面时,title就变成A页面
// 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设置了缓存,10s内不管怎么刷新页面,都是取得缓存。反而A页面不会被缓存。
关于配置Vue服务端渲染到此就结束啦😁。文章主要偏重的仍是利用webpack来构建开发环境和生产环境的SSR,由于就我而言在这地方花费时间比较多。像数据的预取、Head的动态设置、路由的缓存基本上都是按照官方文档走下来的,理解起来并不难。但真要是作成能够用于线上项目开发仍是有许多要作的。好比nuxt就已经作到很好了,我所在公司也在使用nuxt。之因此有这篇文章,也是想对服务端渲染有更好的理解。项目的完整代码👉地址,若是对你有帮助,别忘给个star哈~