官方文档的解释:Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符 串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。 服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可 以在服务器和客户端上运行。
在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。
技术层面:
业务层面:
(1)基于 Vue SSR 官方文档提供的解决方案
官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中, 也会对Vue SSR有更加深入的了解。
该方式需要你熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。
(2)Nuxt.js 开发框架
NUXT提供了平滑的开箱即用的体验,它建立在同等的Vue技术栈之上,但抽象出很多模板,并提供了 一些额外的功能,例如静态站点生成。通过 Nuxt.js 可以快速的使用 Vue SSR 构建同构应用。
接下来我们以 Vue SSR 的官方文档为参考,来学习一下它的基本用法。
目标:
- 了解如何使用 VueSSR 将一个 Vue 实例渲染为 HTML 字符串
首先我们来学习一下服务端渲染中最基础的工作:模板渲染。 说白了就是如何在服务端使用 Vue 的方式解析替换字符串。 在它的官方文档中其实已经给出了示例代码,下面我们来把这个案例的实现过程以及其中含义演示一 下。
mkdir my-vue-ssr
cd my-vue-ssr
npm init -y
npm install vue vue-server-renderer nodemon
my-vue-ssr/server.js :
const Vue = require('vue')const renderer = require('vue-server-renderer').createRenderer()const app = new Vue({template: `{{ message }}
`,data: {message: '5coder前端开发'}
})renderer.renderToString(app, (err, html) => {if (err) throw errconsole.log(html)
})
命令行运行:nodemon server.js
后,可以看到模板已经被渲染为字符串。
使用express框架来实现web服务器,yarn add express
。
server.js
/***@date:2022/11/21*@Description:server*/const Vue = require('vue')
const express = require('express')const renderer = require('vue-server-renderer').createRenderer()const server = express()server.get('/', (req, res) => {const app = new Vue({template: `{{ message }}
`,data: {message: '5coder前端开发'}})renderer.renderToString(app, (err, html) => {if (err) {return res.status(500).end('Internal Server Error')}res.setHeader('Content-Type', 'text/html; charset=utf8')res.end(`Title ${html}`)})})server.listen(3000, () => {console.log('Server Running at port 3000...')
})
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qcF4iGoF-1669274022011)(http://5coder.cn/img/1668992955_b3b4b7dc77070ae56d89a77f587685c1.png)]
对于页面的模板,还有一种做法,就是将其放到单独的文件中。新建index.template.html
文件,并设置特殊注释vue-ssr-outlet
。在render中添加template参数。
index.template.html
Title
server.js
/***@date:2022/11/21*@Description:server*/const Vue = require('vue')
const express = require('express')
const fs = require('fs')// 传递读取到的index.template.html模板文件流
const renderer = require('vue-server-renderer').createRenderer({template: fs.readFileSync('./index.template.html', 'utf-8'),
}) const server = express()server.get('/', (req, res) => {const app = new Vue({template: `{{ message }}
`,data: {message: '5coder前端开发'}})renderer.renderToString(app, (err, html) => {if (err) {return res.status(500).end('Internal Server Error')}res.setHeader('Content-Type', 'text/html; charset=utf8')// 直接返回html,返回的html会替代index.template.html中的注释内容res.end(html)})})server.listen(3000, () => {console.log('Server Running at port 3000...')
})
页面模板也可以使用外部数据。在renderToString方法中,传递第二个可选参数{title: '5coder'}
。在页面模板中使用时,使用模板语法{{ title }}
进行使用。需要注意如果需要渲染一段HTML字符串,需要使用{{{ html_str }}}
三个括号。
server.js
/***@date:2022/11/21*@Description:server*/const Vue = require('vue')
const express = require('express')
const fs = require('fs')const renderer = require('vue-server-renderer').createRenderer({template: fs.readFileSync('./index.template.html', 'utf-8'),
})const server = express()server.get('/', (req, res) => {const app = new Vue({template: `{{ message }}
`,data: {message: '5coder前端开发'}})renderer.renderToString(app, {title: '5coder',content: `我是5coder,一个前端开发
`},(err, html) => {if (err) {return res.status(500).end('Internal Server Error')}res.setHeader('Content-Type', 'text/html; charset=utf8')res.end(html)})})server.listen(3000, () => {console.log('Server Running at port 3000...')
})
index.template.html
{{title}}
{{{ content }}}
服务端渲染只是把vue
实例处理成纯静态的HTML
字符串,发送给客户端,对于vue实例来说,这种需要客户护短动态交互的功能,其并没有提供。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7J3pQviL-1669274022013)(http://5coder.cn/img/1669013742_46f1cd652ca0a30e59bc7426ba346cc6.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HLhsIfGM-1669274022013)(http://5coder.cn/img/1669013785_ee65521cee395282396bd9e6df1ca2cf.png)]
到目前为止,我们还没有讨论过如何将相同的 Vue 应用程序提供给客户端。为了做到这一点,我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack 打包 Vue 应用程序,因为:
vue-loader
构建,并且许多 webpack 特定功能不能直接在 Node.js 中运行(例如通过 file-loader
导入文件,通过 css-loader
导入 CSS)。所以基本看法是,对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。
我们将在后面的章节讨论规划结构的细节 - 现在,先假设我们已经将构建过程的规划都弄清楚了,我们可以在启用 webpack 的情况下编写我们的 Vue 应用程序代码。
我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack 打包 Vue 应用程序,因为:
所以基本看法是,对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要 「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静 态标记。
现在我们正在使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写, 可以使用 webpack 支持的所有功能。同时,在编写通用代码时,有一些事项要牢记在心。 一个基本项目可能像是这样:
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
App.vue
{{ message }}
客户端动态交互
app.js
app.js
是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js
简单地使用 export 导出一个 createApp
函数:
import Vue from 'vue'
import App from './App.vue'// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {const app = new Vue({// 根实例简单的渲染应用程序组件。render: h => h(App)})return { app }
}
entry-client.js
:
客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:
import { createApp } from './app'// 客户端特定引导逻辑……const { app } = createApp()// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')
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
}
截止目前,以上代码还不能运行,原因是需要进行webpack打包后才可以正常使用。
安装依赖
npm i vue vue-server-renderer express cross-env
包 | 说明 |
---|---|
vue | Vue.js 核心库 |
vue-server-renderer | Vue 服务端渲染工具 |
express | 基于 Node 的 Web 服务框架 |
cross-env | 通过 npm scripts 设置跨平台环境变量 |
npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core
@babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader urlloader file-loader rimraf vue-loader vue-template-compiler friendly-errorswebpack-plugin
包 | 说明 |
---|---|
webpack | webpack核心包 |
webpack-cli | webpack的命令行工具 |
webpack-merge | webpack配置信息合并工具 |
webpack-node-externals | 排除webpack中的Node模块 |
rimraf | 基于Node封装的一个跨平台rm -rf 工具 |
friendly-errors-webpack-plugin | 友好的 webpack 错误提示 |
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader | Babel 相关工具 |
vue-loader vue-template-compiler | 处理 .vue 资源 |
file-loader | 处理字体资源 |
css-loader | 处理 CSS 资源 |
url-loader | 处理图片资源 |
配置文件及打包命令
build
├── webpack.base.config.js # 公共配置
├── webpack.client.config.js # 客户端打包配置文件
└── webpack.server.config.js # 服务端打包配置文件
相关webpack配置可以查看文章Webpack 4 和Webpack 5
webpack.base.config.js
/*** 公共配置*/
const VueLoaderPlugin = require('vue-loader/lib/plugin') // 处理.vue资源的插件
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin') // 友好的webpack日志输出
const resolve = file => path.resolve(__dirname, file)const isProd = process.env.NODE_ENV === 'production' // 环境变量中的envmodule.exports = {mode: isProd ? 'production' : 'development',output: {path: resolve('../dist/'), // 输出目录publicPath: '/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` 文件中的 `
配置好出口以后,启动应用:yarn dev
启动成功,访问页面。
测试路由导航,可以看到正常工作,那说明我们同构应用中的路由产生作用了。
现在我们的应用就非常的厉害了,当你首次访问页面的时候,它是通过服务端渲染出来的,服务端渲染拥有了更快的渲染速度以及更好的 SEO,当服务端渲染的内容来到客户端以后被客户端 Vue
结合 VueRouter
激活,摇身一变成为了一个客户端 SPA
应用,之后的页面导航也不需要重新刷新整个页面。这样我们的网站就既拥有了更好的渲染速度,也拥有了更好的用户体验。
除此之外,我们在路由中配置的异步组件(也叫路由懒加载)也是非常有意义,它们会被分割为独立的chunk
(也就是单独的文件),只有在需要的时候才会进行加载。这样就能够避免在初始渲染的时候客户端加载的脚本过大导致激活速度变慢的问题。关于它也可以来验证一下,通过 npm run build
打包构建,我们发现它们确实被分割成了独立的 chunk
。然后再来看一下在运行期间这些 chunk 文件是如何加载的。
你会发现除了 app
主资源外,其它的资源也被下载下来了,你是不是要想说:不是应该在需要的时候才加载吗?为什么一上来就加载了。
原因是在页面的头部中的带有 preload
和 prefetch
的 link 标签。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLqZYYaw-1669274022025)(http://5coder.cn/img/1669213082_828a1830948d8dc31cedc6c7a1fc37cb.png)]
我们期望客户端 JavaScript 脚本尽快加载尽早的接管服务端渲染的内容,让其拥有动态交互能力,但是如果你把 script 标签放到这里的话,浏览器会去下载它,然后执行里面的代码,这个过程会阻塞页面的渲染。
所以看到真正的 script 标签是在页面的底部的。而这里只是告诉浏览器可以去预加载这个资源。但是不要执行里面的代码,也不要影响网页的正常渲染。直到遇到真正的 script 标签加载该资源的时候才会去执行里面的代码,这个时候可能已经预加载好了,直接使用就可以了,如果没有加载好,也不会造成重复加载,所以不用担心这个问题。
而 prefetch 资源是加载下一个页面可能用到的资源,浏览器会在空闲的时候对其进行加载,所以它并不一定会把资源加载出来,preload 一定会预加载。所以你可以看到当我们去访问 about 页面的时候,它的资源是通过 prefetch 预取过来的,提高了客户端页面导航的响应速度。
好了,关于同构应用中路由的处理,以及代码分割功能就介绍到这里。
无论是服务端渲染还是客户端渲染,它们都使用的同一个页面模板。
页面中的 body 是动态渲染出来的,但是页面的 head 是写死的,也就说我们希望不同的页面可以拥有自己的 head 内容,例如页面的 title、meta 等内容,所以下面我们来了解一下如何让不同的页面来定制自己的 head 头部内容。
官方文档这里专门描述了关于页面 Head 的处理,相对于来讲更原生一些,使用比较麻烦,有兴趣的同学可以了解一下。
我这里主要给大家介绍一个第三方解决方案:vue-meta。
Vue Meta 是一个支持 SSR 的第三方 Vue.js 插件,可让你轻松的实现不同页面的 head 内容管理。使用它的方式非常简单,而只需在页面组件中使用 metaInfo
属性配置页面的 head 内容即可。
...
页面渲染出来的结果:
My Example App - Yay! ...
安装:npm i vue-meta
在通用入口app.js
中通过插件的方式将 vue-meta 注册到 Vue 中。
import Vue from 'vue'
import App from './App.vue'
import VueMeta from "vue-meta";
import {createRouter} from "./router";Vue.use(VueMeta)Vue.mixin({metaInfo: {titleTemplate: '%s - 拉勾教育'}
})// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp() {// 创建 router 实例const router = createRouter()const app = new Vue({router, // 把路由挂在到Vue根实例中// 根实例简单的渲染应用程序组件。render: h => h(App)})return {app, router}
}
然后在服务端渲染入口ertry-server.js
模块中适配 vue-meta:
// entry-server.js
import { createApp } from './app'export default async context => {// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,// 以便服务器能够等待所有的内容在渲染前,就已经准备就绪。const { app, router } = createApp()const meta = app.$meta()// 设置服务器端 router 的位置router.push(context.url)context.meta = meta// 等到 router 将可能的异步组件和钩子函数解析完await new Promise(router.onReady.bind(router))return app
}
最后在模板页面index.template.html
中注入 meta 信息:
{{{ meta.inject().title.text() }}}{{{ meta.inject().meta.text() }}}
下面就是直接在组件中使用即可:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NyfumoB2-1669274022027)(http://5coder.cn/img/1669215303_650b1de7c5433c1abe1192b39d5fae48.png)]
当然,还可以定制更多的内容,在官网中看到还可以定制title、titleTemplate、htmlAttrs、headAttrs等。
数据预取和状态官方文档
接下来我们来了解一下服务端渲染中的数据预取和状态管理。
官方文档中的描述比较枯燥,无法在很短的时间内搞清楚它到底要做什么,所以我们这里通过一个实际的业务需求来引入这个话题。
我们的需求就是:
这个需求看起来是不是很简单呢?无非就是在页面发请求拿数据,然后在模板中遍历出来,如果是纯客
户端渲染的话确实就是这样的,但是想要通过服务端渲染的方式来处理的话就比较麻烦了。
无论如何,我们都要来尝试一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mz07KTx0-1669274022028)(http://5coder.cn/img/1669215693_469ffc5ad9285655d33ffdb0743d2ec0.png)]
也就是说我们要在服务端获取异步接口数据,交给 Vue 组件去渲染。
我们首先想到的肯定是在组件的生命周期钩子中请求获取数据渲染页面,那我们可以顺着这个思路来试一下。
在组件中添加生命周期钩子,beforeCreate 和 created,服务端渲染仅支持这两个钩子函数的调用。然后下一个问题是如何在服务端发送请求?依然使用 axios,axios 既可以运行在客户端也可以运行在服务端,因为它对不同的环境做了适配处理,在客户端是基于浏览器的XMLHttpRequest 请求对象,在服务端是基于 Node.js 中的 http 模块实现,无论是底层是什么,上层的使用方式都是一样的。
首先创建Post.vue组件
Post List
- {{ post.title }}
我们尝试在created
生命周期函数中获取数据,运行服务后,我们发现页面确实出现了文章列表页面,但这真的是Post
中的created
生命周期函数中请求的吗?可以打开浏览器查看,在初始请求post/
的时候,打开预览页面,发现文章列表页面并没有被渲染出来,而是在客户端中再次请求后才渲染出来的。所以是服务端的请求没有生效吗,可以打开控制台,发现我们打印的日志也打印出来了。因此得出结论,服务端的created
和beforeCreated
生命周期函数并不会响应数据。
在network
中可以发现,还有一个topics
的请求,我们打开后可以发现,文章数据是客户端自己请求出来的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SVTDepTc-1669274022029)(http://5coder.cn/img/1669216793_b8ccc55db65220ed60507e8af71d2e00.png)]
这时候查看服务员控制台,我们发现日志打印也成功的输出了。所以印证了以上的现象:服务端的created
和beforeCreated
生命周期函数并不会响应数据。
在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。
另一个需要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。
为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container)"中。首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。
接下来我们就按照官方文档给出的参考来把服务端渲染中的数据预取以及状态管理来处理一下。
通过官方文档我们可以看到,它的核心思路就是把在服务端渲染期间获取的数据存储到 Vuex 容器中,然后把容器中的数据同步到客户端,这样就保持了前后端渲染的数据状态同步,避免了客户端重新渲染的问题。
所以接下来要做的第一件事儿就是把 Vuex 容器创建出来。
安装 Vuex:npm i vuex
创建 Vuex 容器:
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) {seate.posts = data}},actions: {// 在服务端渲染器件,务必让action返回Promiseasync getPosts({ commit }) {const { data } = await axios.get('https://cnodejs.org/api/v1/topics')commit('setPosts', data.data)}}})
}
在通用应用入口app.js
中将 Vuex
容器挂载到 Vue
根实例:
import Vue from 'vue'
import App from './App.vue'
import VueMeta from "vue-meta";
import { createRouter } from "./router";
import { createStore } from "./store";Vue.use(VueMeta)Vue.mixin({metaInfo: {titleTemplate: '%s - 拉勾教育'}
})// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp() {// 创建 router 实例const router = createRouter()const store = createStore()const app = new Vue({router, // 把路由挂在到Vue根实例中store, // 把容器挂在到Vue示例中// 根实例简单的渲染应用程序组件。render: h => h(App)})return { app, router, store }
}
Post List
- {{ post.title }}
在服务端渲染应用入口中将容器状态序列化到页面中
接下来我们要做的就是把在服务端渲染期间所获取填充到容器中的数据同步到客户端容器中,从而避免两个端状态不一致导致客户端重新渲染的问题。
window.__INITIAL__STATE = 容器状态
语句插入模板页面中window.__INITIAL__STATE
获取该数据】entry-server.js
// entry-server.js
import { createApp } from './app'export default async context => {// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,// 以便服务器能够等待所有的内容在渲染前,// 就已经准备就绪。const { app, router, store } = createApp()const meta = app.$meta()// 设置服务器端 router 的位置router.push(context.url)context.meta = meta// 等到 router 将可能的异步组件和钩子函数解析完await new Promise(router.onReady.bind(router))context.rendered = () => {// Renderer 会把 context.state 数据对象内联到页面模板中// 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state// 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中context.state = store.state}return app
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rKHaCiTo-1669274022030)(http://5coder.cn/img/1669218378_f4583f1a6b89c07dfd1580a1cadb0449.png)]
最后,在客户端渲染入口中把服务端传递过来的状态数据填充到客户端 Vuex 容器中:
entry-client.js
/*** 客户端入口*/
import { createApp } from './app'// 客户端特定引导逻辑……const { app, router, store } = createApp()if (window.__INITIAL_STATE__) {store.replaceState(window.__INITIAL_STATE__)
}router.onReady(() => {app.$mount('#app')
})
客户端更新问题:
...mounted () {if (!this.posts.length) {this.$store.dispatch('getPosts')}},beforeRouteLeave (to, from, next) {this.$store.commit('setPosts', [])next()}
...
这里主要针对是服务端层面的优化。尽管 Vue 的 SSR 速度相当快,但由于创建组件实例和虚拟 DOM 节点的成本,它无法与纯基于字符串的模板的性能相匹配。在 SSR 性能至关重要的情况下,明智地利用缓存策略可以极大地缩短响应时间并减少服务器负载。
缓存能够更快的将内容发送给客户端,提升 web 应用程序的性能,同时减少服务器的负载。
如官方文档中介绍的那样,对特定的页面合理的应用 micro-caching 能够大大改善服务器处理并发的能力(吞吐率 RPS )。但并非所有页面都适合应用 micro-caching 缓存策略,我们可以将资源分为三类:
js
、css
、images
等。只有“用户无关的动态资源”适合应用 micro-caching 缓存策略。
安装依赖:npm i lru-cache
server.js
const express = require('express')
const fs = require('fs')
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')
const LRU = require('lru-cache')
const cache = new LRU({max: 100,maxAge: 10000 // Important: entries expires after 1 second.
})
const isCacheable = req => {console.log(req.url)if (req.url === '/posts') {return true}
}
const server = express()
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 template = fs.readFileSync('./index.template.html', 'utf-8')const clientManifest = require('./dist/vue-ssr-client-manifest.json')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 cacheable = isCacheable(req)if (cacheable) {const html = cache.get(req.url)if (html) {return res.end(html)}}const html = await renderer.renderToString({title: '拉勾教育',meta: `
`,url: req.url})res.setHeader('Content-Type', 'text/html; charset=utf8')res.end(html)if (cacheable) {cache.set(req.url, html)}} catch (err) {res.status(500).end('Internal Server Error.')}
}
// 服务端路由设置为 *,意味着所有的路由都会进入这里
server.get('*', isProd? render: async (req, res) => {// 等待有了 Renderer 渲染器以后,调用 render 进行渲染await onReadyrender(req, res)}
)
server.listen(3000, () => {console.log('server running at port 3000.')
})
注意事项: