1.为什么使用服务器端渲染 (SSR)
- 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
- 更快的内容到达时间 (
time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备 使用SSR之前,需要考虑以下几点:
- 开发条件所限:浏览器特定的代码,只能在某些生命周期钩子函数 (
lifecycle hook) 中使用;一些外部扩展库 (external library)可能需要特殊处理,才能在服务器渲染应用程序中运行 - 涉及构建设置和部署的更多要求
- 更多的服务器端负载
2.服务端渲染的过程
- 有别于
jsp、php等技术,前端领域的服务端渲染还有一层概念– 同构技术isomorphic - 前后端同构,实质上是指前端脚本代码和后端服务器代码使用同一份代码,更广泛的含义是指复用同一套逻辑,并且最终的输出要保持前后端一致
- 同构技术,使前端在维持现有的技术栈和开发模式的前提下,即可以实现服务端渲染,也能继续享受
SPA带来的红利。
3.Web方案横向对比
| 页面渲染方案 | 工作原理 | 首屏时间 | 弱网 | 扩展性 |
|---|---|---|---|---|
| SPA | 单页面应用;采用前端框架,如react、vue等,通过http请求把动态数据拉取到客户端后完成页面渲染的过程 | 请求入口html + 静态资源+ 接口数据+ 渲染 | 差,渲染过程全依赖数据请求 | 好,Web 的开放性 |
| Pre-Render | 对SPA的改造,静态直出无接口数据的页面 | 同SPA | 次差,首次请求html有骨架 | 好,同上 |
| 离线包 | 基于hybird模式,前端文件以离线文件包的形式被提前下载到客户端,通过http请求把动态数据拉取到客户端后完成页面渲染的过程 | 请求接口数据+ 页面渲染 | 好,只需要请求接口数据 | 差,依赖App |
| SSR | 服务端直出业务数据和页面内容,形式上类似jsp、php,技术上采用nodejs解决方案 | 请求页面,直出内容,数据请求在内网完成 | 次好,页面内容直出,无抖动 | 好,Web 的开放性 |
4.基础
- (1)app.js入口文件
app.js是我们的通用entry,它的作用就是构建一个Vue的实例以供服务端和客户端使用- 注意一下,在纯客户端的程序中我们的app.js将会挂载实例到dom中
- 而在
ssr中这一部分的功能放到了Client entry中去做了。
- 两个entry
- 接下里我们来看
Client entry和Server entry,这两者分别是客户端的入口和服务端的入口。 Client entry的功能很简单,就是挂载我们的Vue实例到指定的dom元素上;Server entry是一个使用export导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。这个函数会在每一次的渲染中重复的调用。
- 接下里我们来看
webpack打包构建- 然后我们的服务端代码和客户端代码通过
webpack分别打包,生成Server Bundle和Client Bundle - 前者会运行在服务器上通过
node生成预渲染的HTML字符串,发送到我们的客户端以便完成初始化渲染; - 而客户端
bundle就自由了,初始化渲染完全不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,使其变成由Vue动态管理的DOM,以便响应后续数据的变化。
- 然后我们的服务端代码和客户端代码通过
5.注意事项
-
避免状态单例
- 为每个请求创建一个新的根
Vue实例:这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染
- 为每个请求创建一个新的根
-
使用
webpack的源码结构
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器,客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中
└── entry-server.js # 仅运行于服务器
-
使用
vue-router的路由- 我们的服务器代码使用了一个
*处理程序,它接受任意URL。这允许我们将访问的 URL 传递到我们的 Vue 应用程序中,然后对客户端和服务器复用相同的路由配置 - 类似于
createApp,我们也需要给每个请求一个新的router实例
- 我们的服务器代码使用了一个
-
代码分割
- 应用程序的代码分割或惰性加载,有助于减少浏览器在初始渲染中下载的资源体积,可以极大地改善大体积
bundle的可交互时间 - 需要在挂载
app之前调用router.onReady,因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子
- 应用程序的代码分割或惰性加载,有助于减少浏览器在初始渲染中下载的资源体积,可以极大地改善大体积
-
数据预取存储容器
- 如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。
- 在挂载 (
mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。
- 为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(
data store)或"状态容器(state container))"中。- 首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到
store中。 - 此外,我们将在 HTML中序列化(
serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从store获取到内联预置(inline)状态
- 首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到
6.SSR本地调试命令:
可在document目录下新建一个chromeData文件夹,用来打开一个新的chrome页面(disable网络安全)来跨域调试
open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir=/Users/username/Documents/chromeData/ --disable-features=CrossSiteDocumentBlockingIfIsolating
7.框架选择
- Nuxt.js:
Nuxt是一个基于Vue生态的更高层的框架,为开发服务端渲染的Vue应用提供了极其便利的开发体验。更酷的是,你甚至可以用它来做为静态站生成器。 - Quasar Framework SSR + PWA:
Quasar是一个基于Node.js和webpack的开发环境,它可以通过一套代码完成SPA、PWA、SSR、Electron、Capacitor 和 Cordova应用的快速开发。
参考链接:解密Vue SSR
8.Nuxt
- 重新加载当前页:
<template>
<div>
<div>{{ content }}</div>
<button @click="refresh">Refresh</button>
</div>
</template>
<script>
export default {
asyncData() {
return { content: 'Created at: ' + new Date() }
},
methods: {
refresh() {
this.$nuxt.refresh()
}
}
}
</script>
- nuxt.config.js
const path = require('path')
const webpack = require('webpack')
const basePath = ''
module.exports = {
mode: 'universal',
typescript: {
typeCheck: {
eslint: true
}
},
/*
** This option lets you configure the connection variables for the server instance of your Nuxt.js application.
*/
server: {
port: 3000, // default: 3000
host: '0.0.0.0' // default: localhost
},
/*
** This option lets you define the source directory of your Nuxt.js application.
*/
srcDir: 'client/',
head: {
meta: [
{ charset: 'utf-8' },
{
name: 'viewport',
content: 'width=device-width, initial-scale=1, user-scalable=no'
},
{
hid: 'description',
name: 'description',
content: process.env.npm_package_description || ''
}
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: basePath + '/favicon.ico' }
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
'~/assets/style/index.less'
],
/*
** Plugins to load before mounting the App , This option lets you define JavaScript plugins that should be run before instantiating the root Vue.js application.
*/
plugins: [
{ src: '~/plugins/inject-redirect', ssr: false },
{ src: '~/plugins/vant', ssr: true },
{ src: '~/plugins/touchable', ssr: false },
'~/plugins/filter'
],
/*
** Nuxt.js dev-modules
*/
buildModules: [
// Doc: https://github.com/nuxt-community/eslint-module
'@nuxtjs/eslint-module',
'@nuxt/typescript-build'
],
/*
** Nuxt.js modules With this option you can add Nuxt.js modules to your project.
*/
modules: ['@nuxtjs/pwa'],
/*
** Build configuration,This option lets you configure various settings for the `build` step, including `loaders`, `filenames`, the `webpack` config and `transpilation`
*/
build: {
transpile: [/vant.*?less/],
babel: {
plugins: [
['import', {
libraryName: 'vant',
style: (name) => {
return `${name}/style/less.js`
}
}, 'vant']
]
},
loaders: {
// VantUI 定制主题配置
less: {
javascriptEnabled: true, // 开启 Less 行内 JavaScript 支持
modifyVars: {
hack: `true; @import "${path.join(
__dirname,
'./client/assets/style/vant-ui.less'
)}";`
}
}
},
/*
** You can extend webpack config here
*/
postcss: {
plugins: {
'postcss-pxtorem': {
rootValue: 50,
unitPrecision: 5,
propList: ['*', '!border*']
},
'postcss-import': {},
'postcss-url': {},
'postcss-preset-env': this.preset,
cssnano: { preset: 'default' } // disabled in dev mode
},
preset: {
autoprefixer: {
grid: true
}
}
},
// /**
// * You can extend webpack config here https://nuxtjs.org/api/configuration-build/#publicpath
// */
extend (config, ctx) {
// Run ESLint on save
if (ctx.isDev && ctx.isClient) {
config.module.rules.push({
enforce: 'pre',
test: /\.(ts|js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/,
options: {
fix: true
}
})
}
// build
if (!ctx.isDev) {
config.output.filename = '[name].[chunkhash:8].js'
config.output.chunkFilename = '[name].[chunkhash:8].chunk.js'
} else {
config.devtool = 'eval-source-map'
}
config.plugins.push(new webpack.HashedModuleIdsPlugin())
}
},
/*
** Router extendRoutes, With the `router` option you can overwrite the default Nuxt.js configuration of Vue Router.
*/
router: {
base: basePath
},
workbox: {
workboxURL: `${basePath}workbox/4.3.1/workbox-sw.js`,
config: {
modulePathPrefix: `${basePath}workbox/4.3.1/`,
debug: false
}
}
}