服务端渲染(SSR)服务端渲染(SSR)服务端渲染(SSR)将一个 Vue 组件在服务端渲染成 HTML 字符串并发送到浏览器,最后将这些静态标记“激活”为可交互应用程序的过程就叫服务端渲染(SSR)
服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行为什么使用 服务端渲染(SSR)为什么使用 服务端渲染(SSR)为什么使用 服务端渲染(SSR)

更好的 SEO:传统的 spa 页面数据都是异步加载,搜索引擎爬虫无法抓取,服务端渲染(SSR)使搜索引擎爬虫抓取工具可以直接查看完全渲染的页面,解决 vue 项目的 seo 问题

更快的内容到达时间 (首屏加载更快):请求页面时,服务端将渲染好的页面直接发送给浏览器进行渲染,浏览器只需要解析渲染 HTML,无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记
更好的 SEO:传统的 spa 页面数据都是异步加载,搜索引擎爬虫无法抓取,服务端渲染(SSR)使搜索引擎爬虫抓取工具可以直接查看完全渲染的页面,解决 vue 项目的 seo 问题更快的内容到达时间 (首屏加载更快):请求页面时,服务端将渲染好的页面直接发送给浏览器进行渲染,浏览器只需要解析渲染 HTML,无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记服务端渲染(SSR)缺点服务端渲染(SSR)缺点服务端渲染(SSR)缺点

开发条件所限:浏览器特定的代码,只能在某些生命周期钩子函数中使用;一些外部扩展库可能需要特殊处理,才能在服务器渲染应用程序中运行

涉及构建设置和部署的要求:与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境

的服务器端负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源,因此如果你预料在高流量环境下使用,需要准备相应的服务器负载,并采用缓存策略


开发条件所限:浏览器特定的代码,只能在某些生命周期钩子函数中使用;一些外部扩展库可能需要特殊处理,才能在服务器渲染应用程序中运行涉及构建设置和部署的要求:与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境的服务器端负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源,因此如果你预料在高流量环境下使用,需要准备相应的服务器负载,并采用缓存策略

服务端渲染(SSR)vs 预渲染(Prerendering)服务端渲染(SSR)vs 预渲染(Prerendering)服务端渲染(SSR)vs 预渲染(Prerendering)如果你只是想改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染,无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时简单地生成针对特定路由的静态 HTML 文件,优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点
如果你使用 webpack,你可以使用 prerender-spa-plugin (npm地址) 插件轻松地添加预渲染prerender-spa-pluginnpm地址服务端渲染(SSR)原理服务端渲染(SSR)原理服务端渲染(SSR)原理构建流程:所有的文件拥有一个公共入口 app.js,进入服务端入口 entry-server.js 和客户端入口 entry-client.js ,项目完成后通过使用 webpack 打包生成服务端 server bundle(一个供服务端 SSR 使用的 json 文件)和客户端 client bundle(用于浏览器),当请求页面时,服务端将 vue 组件组装成 HTML 字符串发送到浏览器,混入到客户端访问的 HTML 模板中,完成页面渲染
app.jsentry-server.jsentry-client.jsserver bundleclient bundle通过 vuecli 创建 vue 项目通过 vuecli 创建 vue 项目通过 vuecli 创建 vue 项目
vue create vue-ssr-demo

vue create vue-ssr-demo
vue-server-renderervue-server-renderervue-server-renderer 是 SSR 渲染的核心,提供 createRenderer 方法,这个方法的 renderToString 可以把 app 渲染成字符串。createBundleRenderer 方法可以通过预打包应用程序代码创建 bundleRenderer 实例,来渲染 bundle 和 HTML 模板vue-server-renderercreateRendererrenderToStringcreateBundleRenderer安装 vue-server-renderer
npm install vue-server-renderer --save

npm install vue-server-renderer --save
注意:

vue-server-renderer 和 vue 必须匹配版本

vue-server-renderer 依赖一些 Node.js 原生模块,因此只能在 Node.js 中使用
vue-server-renderer 和 vue 必须匹配版本vue-server-renderer 依赖一些 Node.js 原生模块,因此只能在 Node.js 中使用 避免状态单例避免状态单例避免状态单例Node.js 服务器是一个长期运行的进程,当我们的代码进入该进程时,它将进行一次取值并留存在内存中,这意味着如果创建一个单例对象,它将在每个传入的请求之间共享,所以我们应该暴露一个可以重复执行的工厂函数,为每个请求创建一个新的根 Vue 实例,如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染(同样的规则也适用于 router、store 和 event bus 实例)创建 路由 router创建 路由 router安装 vue-router
npm install vue-router --save
npm install vue-router --save在 src 目录下创建 router 文件夹和 index.js
在 components 目录下创建 Home.vue 和 About.vue 页面(根据项目需求自定义创建)srcrouterindex.jsrouter/index.js:
import Vue from "vue"
import Router from "vue-router"

import Home from "@/components/Home"
import About from "@/components/About"

Vue.use(Router)

//每次用户请求都需要创建一个新的router实例
//创建createRouter工厂函数
export default function createRouter() {

//创建router实例

return new Router({

mode: "history",

routes: [

{

path: "/",

name: 'home',

component: Home

},

{

path: "/about",

name: 'about',

component: About

}

]

})
}

import Vue from "vue"
import Router from "vue-router"

import Home from "@/components/Home"
import About from "@/components/About"

Vue.use(Router)

//每次用户请求都需要创建一个新的router实例
//创建createRouter工厂函数
export default function createRouter() {

//创建router实例

return new Router({

mode: "history",

routes: [

{

path: "/",

name: 'home',

component: Home

},

{

path: "/about",

name: 'about',

component: About

}

]

})
}
修改 App.vue修改 App.vue修改 App.vue 页面,进行页面布局(根据项目需求自定义布局)App.vue:

创建 公共入口 app.js创建 公共入口 app.js创建 公共入口 app.js在 src 目录下创建 公共入口 app.js ,用于创建 vue 实例srcapp.jsapp.js:
import Vue from "vue"
import App from "./App.vue"
import createRouter from "./router"

//创建createApp工厂函数
export default function createApp() {

const router = createRouter()

//创建vue实例

const app = new Vue({

router,

render: h => h(App),

})

return { app, router }
}

import Vue from "vue"
import App from "./App.vue"
import createRouter from "./router"

//创建createApp工厂函数
export default function createApp() {

const router = createRouter()

//创建vue实例

const app = new Vue({

router,

render: h => h(App),

})

return { app, router }
}
创建 服务端入口 entry-server.js在 src 目录下创建 服务端入口 entry-server.js ,用于渲染首屏srcentry-server.jsentry-server.js:
import createApp from "./app"

export default context => {

return new Promise((resolve, reject) => {

const { app, router } = createApp()

//渲染首屏

router.push(context.url)

router.onReady(() => {

resolve(app)

}, reject)

})
}

import createApp from "./app"

export default context => {

return new Promise((resolve, reject) => {

const { app, router } = createApp()

//渲染首屏

router.push(context.url)

router.onReady(() => {

resolve(app)

}, reject)

})
}
创建 客户端入口 entry-client.js创建 客户端入口 entry-client.js创建 客户端入口 entry-client.js在 src 目录下创建 客户端入口 entry-client.js ,用于挂载激活 appsrcentry-client.jsentry-client.js:
import createApp from "./app"

const { app, router } = createApp()
router.onReady(() => {

//挂载激活app

app.$mount("#app")
})

import createApp from "./app"

const { app, router } = createApp()
router.onReady(() => {

//挂载激活app

app.$mount("#app")
})
创建 页面模板 index.temp.html创建 页面模板 index.temp.html创建 页面模板 index.temp.html在 public 目录下创建 index.temp.html ,作为渲染 Vue 应用程序时,renderer 生成 HTML 页面包裹容器,来包裹生成的 HTML 标记
注释将是应用程序 HTML 标记注入的地方publicindex.temp.htmlindex.temp.html:








vue ssr















vue ssr






创建 Node.js 服务器创建 Node.js 服务器创建 Node.js 服务器服务端渲染(SSR)需要使用 Node.js 服务器,这里使用 express 框架搭建
安装 expressexpress
npm install express --save

npm install express --save
根目录下创建 server.js 文件,用于搭建 Node.js 服务器server.jsserver.js:
//nodejs服务器
const express = require("express")
const Vue = require("vue")
const fs = require("fs")

//创建express实例
const app = express()
//创建渲染器
const { createBundleRenderer } = require("vue-server-renderer")
const serverBundle = require("./dist/server/vue-ssr-server-bundle.json")
const clientManifest = require("./dist/client/vue-ssr-client-manifest.json")
const renderer = createBundleRenderer(serverBundle, {

runInNewContext: false,

template: fs.readFileSync("./public/index.temp.html", "utf-8"), //页面模板

clientManifest
})

//中间件处理静态文件请求
app.use(express.static("./dist/client", {index: false}))

//将路由的处理交给vue
app.get("*", async (req, res) => {

try {

const context = {

url: req.url,

title: ""

}

const html = await renderer.renderToString(context)

res.send(html)

}catch {

res.status(500).send("服务器内部错误!")

}
})

app.listen(9999, () => {

console.log("服务器渲染成功!")
})

//nodejs服务器
const express = require("express")
const Vue = require("vue")
const fs = require("fs")

//创建express实例
const app = express()
//创建渲染器
const { createBundleRenderer } = require("vue-server-renderer")
const serverBundle = require("./dist/server/vue-ssr-server-bundle.json")
const clientManifest = require("./dist/client/vue-ssr-client-manifest.json")
const renderer = createBundleRenderer(serverBundle, {

runInNewContext: false,

template: fs.readFileSync("./public/index.temp.html", "utf-8"), //页面模板

clientManifest
})

//中间件处理静态文件请求
app.use(express.static("./dist/client", {index: false}))

//将路由的处理交给vue
app.get("*", async (req, res) => {

try {

const context = {

url: req.url,

title: ""

}

const html = await renderer.renderToString(context)

res.send(html)

}catch {

res.status(500).send("服务器内部错误!")

}
})

app.listen(9999, () => {

console.log("服务器渲染成功!")
})
webpack 打包配置webpack 打包配置webpack 打包配置根目录下创建 vue 配置文件 vue.config.js 进行 webpack 配置,该配置会覆盖 vue-cli 中 webpack 的默认配置vue.config.jsvue.config.js:
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin")
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin")

//环境变量,决定入口是客户端还是服务端
const TARGRT_NODE = process.env.WEBPACK_TARGET === "node"
const target = TARGRT_NODE ? "server" : "client"

module.exports = {

css: {

extract: false

},

outputDir: "./dist/" + target,

configureWebpack: () => ({

//将 entry 指向应用程序的 server entry 文件

entry: `./src/entry-${target}.js`,

//对 bundle renderer 提供 source map 支持

devtool: "source-map",

//这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import)

//并且还会在编译 Vue 组件时,告知 `vue-loader` 输送面向服务器代码(server-oriented code)

target: TARGRT_NODE ? "node" : "web",

node: TARGRT_NODE ? undefined : false,

output: {

//此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)

libraryTarget: TARGRT_NODE ? "commonjs2" : undefined

},

optimization: { splitChunks: TARGRT_NODE ? false : undefined },

//将服务器的整个输出构建为单个 JOSN 文件的插件

//服务端默认文件名为 vue-ssr-server-bundle.json

plugins: [TARGRT_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]

})
}

const VueSSRServerPlugin = require("vue-server-renderer/server-plugin")
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin")

//环境变量,决定入口是客户端还是服务端
const TARGRT_NODE = process.env.WEBPACK_TARGET === "node"
const target = TARGRT_NODE ? "server" : "client"

module.exports = {

css: {

extract: false

},

outputDir: "./dist/" + target,

configureWebpack: () => ({

//将 entry 指向应用程序的 server entry 文件

entry: `./src/entry-${target}.js`,

//对 bundle renderer 提供 source map 支持

devtool: "source-map",

//这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import)

//并且还会在编译 Vue 组件时,告知 `vue-loader` 输送面向服务器代码(server-oriented code)

target: TARGRT_NODE ? "node" : "web",

node: TARGRT_NODE ? undefined : false,

output: {

//此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)

libraryTarget: TARGRT_NODE ? "commonjs2" : undefined

},

optimization: { splitChunks: TARGRT_NODE ? false : undefined },

//将服务器的整个输出构建为单个 JOSN 文件的插件

//服务端默认文件名为 vue-ssr-server-bundle.json

plugins: [TARGRT_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]

})
}
打包脚本配置打包脚本配置打包脚本配置cross-env 插件:运行跨平台设置和使用环境变量的脚本
安装 cross-env 插件cross-env
npm install cross-env --save

npm install cross-env --save
在 package.json 文件中定义项目运行打包脚本package.jsonpackage.json:
{

......

"scripts": {


"server": "node server",


"build:client": "vue-cli-service build",


"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",


"build": "npm run build:server && npm run build:client"

},

......
}

{

......

"scripts": {


"server": "node server",


"build:client": "vue-cli-service build",


"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",


"build": "npm run build:server && npm run build:client"

},

......
}
终端执行命令打包项目:
npm run build

npm run build
打包完成后,终端执行命令启动 Node.js 服务器
npm run server
npm run server服务器启动后,浏览器打开 localhost:9999 即可访问 SSR 项目
查看网页源代码发现根元素上添加了一个特殊的属性:data-server-rendered,该属性让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载data-server-rendered
......


......

项目目录:打包后 dist 目录: