Vue服务端渲染SSR实战
什么是 SSR ?
现代化的前端项目,大部分都是单页应用程序,也就是我们说的 SPA ,整个应用只有一个页面,通过组件的方式,展示不同的页面内容,所有的数据通过请求服务器获取后,在进行客户端的拼装和展示;这就是目前前端框架的默认渲染逻辑,我们称为:客户端渲染方案( Client Side Render 简称: CSR );
加载渲染过程如下: HTML/CSS 代码 --> 加载 JavaScript 代码 --> 执行 JavaScript 代码 --> 渲染页面数据
SPA 应用的客户端渲染方式,最大的问题有两个方面:
1:白屏时间过长,用户体验不好;
2:HTML 中无内容,SEO 不友好;
这个问题的原因在于,首次加载时,需要先下载整个 SPA 脚本程序,浏览器执行代码逻辑后,才能去获取页面真正要展示的数据,而 SPA 脚本的下载需要较长的等待和执行时间,同时,下载到浏览器的 SPA 脚本是没有页面数据的, 浏览器实际并没有太多的渲染工作,因此用户看到的是没有任何内容的页面,不仅如此,因为页面中没有内容,搜索引擎的爬虫爬到的也是空白的内容,也就不利于 SEO 关键字的获取;
相较于传统的站点,浏览器获取到的页面都是经过服务器处理的有内容的静态页面,有过后端编程经验的可能会比较熟悉一些,页面结构和内容,都是通过服务器处理后,返回给客户端;
两相比较我们会发现,传统站点的页面数据合成在后台服务器,而 SPA 应用的页面数据合成在浏览器,但是无论那种,最终的渲染展示,还是交给浏览器完成的,所以,不要误会,我们这里所说的 服务端渲染 和 客户端渲染,指的是页面结构和数据合成的工作,不是浏览器展示的工作;
那么能不能借助传统网站的思路来解决 SPA 的问题又能够保留SPA的优势呢?不管是白屏时间长还是 SEO 不友好,实际都是首屏的页面结构先回到浏览器,然后再获取数据后合成导致的问题,那么,首屏的页面结构和数据,只要像传统站点一样,先在服务端合成后再返回,同时将 SPA 脚本的加载依然放到首屏中,此时返回的页面就是结构和数据都有的完整内容了,这样浏览器在展示首页数据的同时也能加载 SPA 脚本,搜索引擎的爬虫同样也能获取到对应的数据,解决 SEO 的问题;为了更好的理解这个逻辑,我画了一个流程图:
没错,这就是我们所说的 服务端渲染的基本逻辑,服务端渲染也就是 SSR (Server Side Rendering) ;
白屏时间过长的问题得以解决,因为首次加载时,服务器会先将渲染好的静态页面返回,在静态页面中再次加载请求 SPA 脚本;
基本原理:首页内容及数据,在用户请求之前生成为静态页面,同时加入 SPA 的脚本代码引入,在浏览器渲染完成静态页面后,请求 SPA 脚本应用,之后的页面交互依然是客户端渲染;
SSR 的优势
- 更好的SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。相比客户端渲染,服务端渲染可以有效提高搜索引擎爬取的精度,进而提高网站的易传播性。
- 更快的首屏渲染速度。特别是对于网路速度慢或者运行缓慢的设备,无需等待所有的js都下载和解析完成才渲染页面,而是在服务端渲染好直接发送给客户端渲染页面。这就使得服务器渲染将不再需要进行数据请求,可以拥有更短的首屏时间。
为啥学习官网开发?
首先,同样作为 C 端网站,官网拥有与其他 C 端网站相同的技术架构选型,通过服务器端渲染(简称:SSR,后文均用 SSR 表示)来进行项目的开发和 SEO 优化,并且压测、备案、部署这些流程和大部分 C 端网站都是类似的。
官网开发的技术核心
那么官网开发的核心重点有哪些呢?我们从项目开发、用户体验、运营维护及部署流程四个方向来说明:
项目开发 ---- 服务端渲染
用户体验 ---- 1.首屏优化 2.低网速优化、多媒体适配
运营维护 ---- 数据的可灵活配置
部署流程 ---- 1.压力测试、埋点、SEO优化 2.备案
带你走近Vue服务器端渲染(VUE SSR)
在正式搭建项目之前,我们还是要回顾下vue服务器端渲染的一些特性。 服务器端渲染的 Vue.js 应用程序,是使vue应用既可以在客户端(浏览器)执行,也可以在服务器端执行,我们称之为“同构”或“通用”。
之所以能够实现同构,是因为在客户端和服务端都创建了vue应用程序,并都用webpack进行打包,生成了server bundle和client bundle。server bundle用于服务器渲染,client bundle是一个客户端的静态标记,服务器渲染好html页面片段后,会发送给客户端,然后混合客户端静态标记,这样应用就具有vue应用的特性。 需要注意是:
- 服务器端渲染过程中,只会调用
beforeCreate
和created
两个钩子函数,其它的只会在客户端执行。那么以前spa应用中,在created
中创建一个setInterval
,然后在destroyed
中将其销毁的类似操作就不能出现了,服务器渲染期间不会调用销毁钩子函数,所以这个定时器会永远保留下来,服务器很容易就崩了。 - 由于服务器可客户端是两种不同的执行平台环境,那么一些特定平台的API就不能用了,比如
window
和document
,在node.js(比如created
钩子函数)中执行就会报错。并且,我们使用的第三方API中,需要确保能在node和浏览器都能正常运行,比如axios,它向服务器和客户端都暴露相同的 API(浏览器的源生XHR就不行)。
一:服务器渲染从0到1
我们先不考虑同构、不考虑各种配置,先实现一个基础的服务器端渲染demo。
准备
npm install vue vue-server-renderer express --save
vue-server-renderer
是vue
服务器端渲染的核心模块,它需要匹配你的vue版本。安装express
是因为我们等会会使用它来起个服务看到我们的页面效果。
三步渲染一个Vue实例
// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello Vue SSR</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 Vue SSR</div>
})
使用模板
上面只是生产了一个html代码片段,一般来说,需要将html片段插入一个模板文件里。OK,那我们就来写一个模板文件index.html
。
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
在渲染的时候,html片段会被插入到<!--vue-ssr-outlet-->
这个注释标记这里。
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello Vue SSR</div>`
})
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html) // html 将是注入应用程序内容的完整页面
})
我们用fs
模块将文件读取进来丢入render的template
中,再重复上述步骤将html片段插入到我们的标记位。
在node.js服务器中使用
现在,我们将使用express来启动一个node服务,验证一下页面效果。
const Vue = require('vue')
// 第一步: 创建一个 express 应用
const server = require('express')()
// 第二步: 创建一个 Vue 实例
const app = new Vue({
data: {
msg: 'Hello Vue SSR'
},
template: `<div>{{msg}}</div>`
})
// 第三步: 创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.html', 'utf-8')
})
// 第四步: 设置路由,"*" 表示任意路由都可以访问它
server.get('*', (req, res) => {
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(html)
})
})
// 第五步: 启动服务并监听从8080端口进入的所有连接请求
server.listen(8080)
这样,我们的一个简单的页面渲染就完成了,看下页面效果和Response数据。
二:改造 —— 从SPA到SSR
知道了怎么在服务器端渲染出一个页面,下一步就是实现同构啦。为了跳过各种项目配置,我们就从熟悉的vue-cli模板下手。 官方提供了vue-cli的项目快速构建工具,可以用它也进行SPA项目的快速搭建,我们现在就把这个模板,改造成一个能够集成SSR的模板。
准备
安装vue-cli (至少v2.x版本)后,使用基础模板搭建个项目
vue init webpack spa_ssr
cd spa_ssr
跑一下确保项目能够正常运行,然后记得安装vue-server-renderer
模块
npm install vue-server-renderer --save-dev
安装完成,我们就开始进入下一步。
改造src
下的文件
我们需要在src
目录下创建两个js。
src
├── router
│ └── index.js
├── components
│ └── HelloSsr.vue
├── App.vue
├── main.js
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
这两个entry
之后会进行配置,先来改造main.js
。
在改造main.js
之前,需要说明一下,因单线程的机制,在服务器端渲染时,过程中有类似于单例的操作,那么所有的请求都会共享这个单例的操作,所以应该使用工厂函数来确保每个请求之间的独立性。比如在main.js
中,我们原先直接创建一个Vue实例,并直接挂载到DOM。现在的main.js
作为通用entry文件,它应该改造成一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例。挂载的工作,是由之后的客户端entry来完成。
import Vue from 'vue'
import App from './App'
import { CreateRouter } from './router'
export function createApp () {
const router = new CreateRouter()
const app = new Vue({
router,
render: h => h(App)
})
return { app, router }
}
在/router/index.js
中,我们同样需要使用工厂函数来创建路由实例。然后将路由配置改为history
模式(因为哈希不支持)
import Vue from 'vue'
import Router from 'vue-router'
import HelloSsr from '@/components/HelloSsr'
Vue.use(Router)
export function CreateRouter () {
return new Router({
mode: 'history',
routes: [{
path: '/ssr',
name: 'HelloSsr',
component: HelloSsr
}]
})
}
接下来我们来写客户端的entry和服务器端的entry。客户端的entry要做的很简单,就是将vue实例挂载到DOM上,只不过,考虑到可能存在异步组件,需要等到路由将异步组件加载完毕,才进行此操作。
// entry-client.js
import { createApp } from './main'
const { app, router } = createApp()
router.onReady(() => {
app.$mount('#app')
})
服务器entry要做的有两步:1.解析服务器端路由;2.返回一个vue实例用于渲染。
// entry-server.js
import { createApp } from './main'
export default context => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, router } = createApp()
// 设置服务器端 router 的位置
router.push(context.url)
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
// eslint-disable-next-line
return reject({ code: 404 })
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app)
}, reject)
})
}
webpack
配置
vue相关代码已处理完毕,接下来就需要对webpack
打包配置进行修改了。 官方推荐了下面配置:
build
├── webpack.base.conf.js # 基础通用配置
├── webpack.client.conf.js # 客户端打包配置
└── webpack.server.conf.js # 服务器端打包配置
我们的项目中的配置文件是base
,dev
,prod
,现在我们仍然保留这三个配置文件,只需要增加webpack.server.conf.js
即可。
webpack.base.conf.js
修改
我们首先修改webpack.base.conf.js
的entry
入口配置为:./src/entry-client.js
,来生成客户端的构建清单client manifest
。服务器端的配置由于引用base配置,entry
会通过merge
覆盖,来指向server-entry.js
。
// webpack.base.conf.js
module.exports = {
entry: {
// app: './src/main.js'
app: './src/entry-client.js' // <-修改入口文件改为
},
// ...
}
webpack.prod.conf.js
修改
在客户端的配置prod
中,我们需要引入一个服务器端渲染的插件client-plugin
,用来生成vue-ssr-client-manifest.json
(用作静态资源注入),同时,我们需要把HtmlWebpackPlugin
给去掉,在SPA应用中,我们用它来生成index.html
文件,但是这里我们有vue-ssr-client-manifest.json
之后,服务器端会帮我们做好这个工作。
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
// ...
plugins: [
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"' // 增加process.env.VUE_ENV
}),
// ...
// 以下内容注释(或去除)
// new HtmlWebpackPlugin({
// filename: config.build.index,
// template: 'index.html',
// inject: true,
// minify: {
// removeComments: true,
// collapseWhitespace: true,
// removeAttributeQuotes: true
// // more options:
// // https://github.com/kangax/html-minifier#options-quick-reference
// },
// // necessary to consistently work with multiple chunks via CommonsChunkPlugin
// chunksSortMode: 'dependency'
// }),
// ...
// 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
// ...
webpack.server.conf.js
配置
server
配置基本参考官方的配置,这里还是说明下:
- 我们需要去掉baseConfig中的打包css的配置;
- 这里使用了
webpack-node-externals
来加快构建速度和减小打包体积,所以我们要先安装一下它:npm install webpack-node-externals --save-dev
。 - 和
prod
配置一样,这里需要引入并使用server-plugin
插件来生成vue-ssr-server-bundle.json
。这东西是用来等会做服务器端渲染的。
const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.conf.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
// 去除打包css的配置
baseConfig.module.rules[1].options = ''
module.exports = merge(baseConfig, {
// 将 entry 指向应用程序的 server entry 文件
entry: './src/entry-server.js',
// 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
// 并且还会在编译 Vue 组件时,
// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
target: 'node',
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
output: {
libraryTarget: 'commonjs2'
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可以使服务器构建速度更快,
// 并生成较小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 需要处理的依赖模块。
// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
whitelist: /\.css$/
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
new VueSSRServerPlugin()
]
})
package.json
打包命令修改
"scripts": {
//...
"build:client": "node build/build.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
"build": "rimraf dist && npm run build:client && npm run build:server"
}
这里需要先安装cross-env
。(cross-env
用来防止使用NODE_ENV =production 来设置环境变量时,Windows命令提示会报错)
npm install --save-dev cross-env
修改index.html
如第一层说的,我们需要在这个index.html
外层模板文件中,插入一个<!--vue-ssr-outlet-->
注释标记,用来标识服务器渲染的html代码片段插入的地方,同时删掉原先的<div id="app">
。
服务器端会在这个标记的位置自动生成一个<div id="app" data-server-rendered="true">
,客户端会通过app.$mount('#app')
挂载到服务端生成的元素上,并变为响应式的。
- ps:这里单纯将模板改为服务器端渲染适用的模板,但是在dev模式下,会因为找不到#app而报错,这里就不做dev下的处理,如果需要,可以为dev模式单独建立一个html模板。
打包构建
npm run build
在dist目录下会生成两个json文件:vue-ssr-server-bundle.json
和vue-ssr-client-manifest.json
,用于服务端端渲染和静态资源注入。
构建服务器端
这里还是采用express
来作为服务器端,先进行安装:
npm install express --save
之后在根目录下创建server.js
,代码主要分为3步:
- 采用
createBundleRenderer
来创建renderer
,我们引入之前生成好的json文件,并读取index.html
作为外层模板; - 设置路由,当请求指定路由的时候,设置请求头,调用渲染函数,将渲染好的html返回给客户端;
- 监听3001端口。
const express = require('express')
const app = express()
const fs = require('fs')
const path = require('path')
const { createBundleRenderer } = require('vue-server-renderer')
const resolve = file => path.resolve(__dirname, file)
// 生成服务端渲染函数
const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
// 模板html文件
template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
// client manifest
clientManifest: require('./dist/vue-ssr-client-manifest.json')
})
function renderToString (context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html)
})
})
}
app.use(express.static('./dist'))
app.use(async(req, res, next) => {
try {
const context = {
title: '服务端渲染测试', // {{title}}
url: req.url
}
// 设置请求头
res.set('Content-Type', 'text/html')
const render = await renderToString(context)
// 将服务器端渲染好的html返回给客户端
res.end(render)
} catch (e) {
console.log(e)
// 如果没找到,放过请求,继续运行后面的中间件
next()
}
})
app.listen(3000)
完事后启动服务命令:
node server.js
访问localhost:3000/ssr,就能获取我们之前定义好的页面。
三:Nuxt.js
框架
Nuxt.js
是什么
Nuxt.js
是Vue官方推荐的一个项目,它是一个基于 Vue.js 的通用应用框架。预设了服务器端渲染所需的各种配置,如异步数据,中间件,路由,只要遵循其中的规则就能轻松实现SSR。开箱即用,体验友好。通过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI渲染。
Nuxt.js
能做什么
- 无需再为了路由划分而烦恼,只需要按照对应的文件夹层级创建 .vue 文件就行
- 无需考虑数据传输问题,nuxt 会在模板输出之前异步请求数据(需要引入 axios 库),而且对 vuex 有进一步的封装
- 内置了 webpack,省去了配置 webpack 的步骤,nuxt 会根据配置打包对应的文件
先来看一下上线之后搜索引擎的收录情况吧
Nuxt.js和纯Vue项目的简单对比
1. build后目标产物不同
vue: dist
nuxt: .nuxt
2. 网页渲染流程
vue: 客户端渲染,先下载js后,通过ajax来渲染页面;
nuxt: 服务端渲染,可以做到服务端拼接好html后直接返回,首屏可以做到无需发起ajax请求;
3. 部署流程
vue: 只需部署dist目录到服务器,没有服务端,需要用nginx等做Web服务器;
nuxt: 需要部署几乎所有文件到服务器(除node_modules,.git),自带服务端,需要pm2管理(部署时需要reload pm2),若要求用域名,则需要nginx做代理。
4. 项目入口
vue: /src/main.js
,在main.js可以做一些全局注册的初始化工作; nuxt: 没有main.js入口文件,项目初始化的操作需要通过nuxt.config.js
进行配置指定。
从零搭建一个Nuxt.js项目并配置
新建一个项目
直接使用脚手架进行安装:安装文档地址:zh.nuxtjs.org/guide/insta… 我们采用 create-nuxt-app
命令来安装
npx create-nuxt-app <项目名>
大概选上面这些选项。
值得一说的是,关于Choose custom server framework
(选择服务端框架),可以根据你的业务情况选择一个服务端框架,常见的就是Express、Koa,默认是None,即Nuxt默认服务器,我这里选了Express
。
- 选择默认的Nuxt服务器,不会生成
server
文件夹,所有服务端渲染的操作都是Nuxt帮你完成,无需关心服务端的细节,开发体验更接近Vue项目,缺点是无法做一些服务端定制化的操作。 - 选择其他的服务端框架,比如
Express
,会生成server
文件夹,帮你搭建一个基本的Node服务端环境,可以在里面做一些node端的操作。比如我公司业务需要(解析protobuf)使用了Express
,对真正的服务端api做一层转发,在node端解析protobuf后,返回json数据给客户端。
还有Choose Nuxt.js modules
(选择nuxt.js的模块),可以选axios
和PWA
,如果选了axios,则会帮你在nuxt实例下注册$axios
,让你可以在.vue文件中直接this.$axios
发起请求。
进入项目运行,浏览器进入 http://127.0.0.1:3000
,可以看到我们的项目已经可以运行了。
打包部署
一般来说,部署前可以先在本地打包,本地跑一下确认无误后再上传到服务器部署。命令:
// 打包
npm run build
// 本地跑
npm start
除node_modules,.git,.env,将其他的文件都上传到服务器,然后通过pm2
进行管理,可以在项目根目录建一个pm2.json
方便维护:
{
"name": "nuxt-test",
"script": "./server/index.js",
"instances": 2,
"cwd": "."
}
然后配置生产环境的环境变量,一般是直接用.env.prod
的配置:cp ./.env.prod ./.env
。 首次部署或有新的依赖包,需要在服务器上npm install
一次,然后就可以用pm2
启动进程啦:
// 项目根目录下运行
pm2 start ./pm2.json
需要的话,可以设置开机自动启动pm2: pm2 save && pm2 startup
。 需要注意的是,每次部署都得重启一下进程:pm2 reload nuxt-test
。
最后
Nuxt.js引入了Node,同时nuxt.config.js
替代了main.js
的一些作用,目录结构和vue项目都稍有不同,增加了很多的约定,还是得看一遍官方的文档。
转载自:https://juejin.cn/post/7158652858722680840