阅读建议:建议通过左侧导航栏进行阅读
文章简介:本文是Vue.js服务器端渲染的另一种解决方案-SSRServer-Side Rendering)学习笔记

Vue SSR是什么

官方文档解释:Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。

  • Vue SSRVue.js Server-Side Rendering)是 Vue.js 官方提供的服务端渲染解决方案
  • 使用SSR,可以构建基于vue.js的同构应用

Vue SSR使用场景

在使用SSR之前,要从以下两方面考虑是否真的需要它。

技术层面

  • 有 利于SEO
  • 更快的首屏渲染速度

业务层面

  • 不适合管理系统
  • 适合移动网站和门户资讯类网站,如企业官网、知乎、简书等

Vue SSR实现方案

基于 Vue SSR 官方文档提供的解决方案

官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中,也会对Vue SSR有更加深入的了解。
该方式需要你熟悉 Vue.js 本身,并且具有 Node.jswebpack 的相当不错的应用经验。

Nuxt.js 开发框架

Nuxt.js提供了平滑的开箱即用的体验,它建立在同等的Vue.js技术栈之上,但抽象出很多模板,并提供了一些额外的功能,例如静态站点生成。通过 Nuxt.js 可以快速的使用 Vue SSR 构建同构应用。

Vue SSR基于官方文档的基本使用

渲染一个Vue实例

体会服务端渲染中最基础的工作-模板渲染,了解如何使用 Vue SSR 将一个 Vue 实例渲染为 HTML 字符串,也就是如何在服务端使用 Vue.js 的方式解析替换字符串。

新建一个项目,在项目根目录下依次执行以下命令

//初始化项目,新建package.json
yarn init
//安装项目基本依赖包
yarn add vue vue-server-renderer express

在项目根目录下分别新建server.jsindex.html

server.js

const Vue = require('vue');
const express = require('express');
const fs = require('fs');
const renderer = require('vue-server-renderer').createRenderer({//使用utf-8进行编码的index.html作为渲染模板template: fs.readFileSync('./index.html', 'utf-8')
});
const server = express();//设置路由,客户端以get请求网站根路径
server.get('/', (req, res) => {//创建一个vue实例const app = new Vue({template:`<div><h1>{{message}}</h1></div>`,data: {message: '创建一个vue实例' }});//将vue实例转换为html字符串,并发送给客户端renderer.renderToString(app, {title: '服务器端渲染',meta: `<meta name="desc" content="服务器">`}, (err, html) => {if(err) {res.status(500).end('server error...');};res.end(html);})
})//启动web服务
server.listen(3000, () => {console.log('server running...')})

index.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">{{{ meta }}}<title>{{title}}</title>
</head>
<body><!--标记页面模板--><!--vue-ssr-outlet-->
</body>
</html>

使用nodemon server.js(nodemon需要在本机安装)启动项目。
在客户端访问http://localhost:3000/,可以看到渲染结果,页面结构:

案例总结:

  • 解决页面显示乱码的两种方案

1、设置响应头

res.setHeader('Content-Type', 'text/html;charset=utf8');
res.end(html);

2、发送完整的html页面,使用charset="UTF-8"进行编码

res.end(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title></head><body>${html} </body></html>
`)
  • 服务端向客户端发送完整页面的两种方式

1、利用ES6的模板语法直接在请求响应中发送完整页面模板
2、使用渲染模板,结合vue-server-renderer解析替换字符串,如server.js就采用这种方式

注意:
1.使用渲染模板接收外部数据时,如果修改的是静态模板如head中的内容,需要重启项目才能生效,因为模板是在项目启动时获取的
2.如果需要将外部数据渲染为html标签,需要使用三个“{}”,如{{{ meta }}}

Vue SSR同构渲染构建

构建流程

源码结构

1.使用webpack打包的原因

  • 通常 Vue 应用程序是由 webpackvue-loader 构建,并且许多 webpack 特定功能不能直接在 Node.js 中运行(例如通过 file-loader 导入文件,通过 css-loader 导入 CSS
  • 尽管 Node.js 最新版本能够完全支持 ES2015 特性,我们还是需要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。

对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。

2.使用webpack的源码结构
使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用 webpack 支持的所有功能,推荐的源码结构:

src
├── components
│   ├── Foo.vue
│   ├── Bar.vue
│   └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器

在项目根目录下新建src目录

src/app.vue

<template><div id="app"><h1>{{ message }}</h1><h2>客户端动态交互</h2><div><input v-model="message" /></div><div><button @click="onClick">点击按钮</button></div></div>
</template><script>
export default {data() {return {message: "创建一个vue实例",};},methods: {onClick() {console.log("---点击按钮---");},},
};
</script>

src/app.js

app.js 是我们应用程序的「通用 entry」,简单地导出一个工厂函数createApp 函数,用于创建新的应用程序、router 和 store 实例。

/*** 通用启动入口*/import Vue from 'vue'
import App from './App.vue'// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {const app = new Vue({// 根实例简单的渲染应用程序组件。render: h => h(App)})return { app }
}

src/entry-client.js

客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中。

import { createApp } from './app'// 客户端特定引导逻辑……const { app } = createApp()//将创建的vue实例绑定
app.$mount('#app')

src/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
}

安装依赖

安装生产依赖

cnpm i vue vue-server-renderer express cross-env --save
依赖包 说明
vue Vue.js 核心库
vue-server-renderer Vue 服务端渲染工具
express 基于 Node 的 Web 服务框
cross-env 通过 npm scripts 设置跨平台环境变量

安装开发依赖

cnpm i webpack webpack-cli webpack-merge webpack-node-externals
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader
css-loader url-loader file-loader rimraf vue-loader vue-template-compiler
friendly-errors-webpack-plugin --save-dev
依赖包 说明
webpack webpack 核心包
webpack-cli webpack 的命令行工具
webpack-merge webpack 配置信息合并工具
webpack-node-externals 去除 webpack 中的 Node 模块
rimraf 基于 Node 封装的一个跨平台rm -rf工具
friendly-errors-webpack-plugin 友好的 webpack 错误提示
file-loader 处理字体资源
css-loader 处理 CSS 资源
url-loade 处理图片资源
vue-loader vue-template-compiler 处理 .vue 资源
@babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader 处理图片资源

webpack配置文件

build
├── webpack.base.config.js # 公共配置
├── webpack.client.config.js # 客户端打包配置文件
└── webpack.server.config.js # 服务端打包配置文件

webpack.base.config.js

/** * 公共配置 * */
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = file => path.resolve(__dirname, file)const isProd = process.env.NODE_ENV === 'production'module.exports = {mode: isProd ? 'production' : 'development',output: {path: resolve('../dist/'), //打包文件的输出目录publicPath: '/dist/', //请求打包资源的请求前缀/distfilename: '[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` 文件中的 `<style>` 块      { test: /\.css$/,use: ['vue-style-loader','css-loader']},// CSS 预处理器,参考:https://vue-loader.vuejs.org/zh/guide/pre-processors.html// 例如处理 Less 资源// { //   test: /\.less$/,//   use: [ 'vue-style-loader', 'css-loader', 'less-loader' ] // },]},plugins: [ new VueLoaderPlugin(), new FriendlyErrorsWebpackPlugin()]
}

webpack.client.config.js

/*** 客户端打包配置 */
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge( baseConfig, {entry: {//相对于打包所处的路径app: './src/entry-client.js'},module: { rules: [// ES6 转 ES5{ test: /\.m?js$/, exclude: /(node_modules|bower_components)/,use: {loader: 'babel-loader',options: {presets: ['@babel/preset-env'],cacheDirectory: true,plugins: ['@babel/plugin-transform-runtime']}}}]},//重要信息:这将webpack运行时分离到一个引导chunk中,//以便可以在之后正确注入异步chunk。optimization: {splitChunks: {name: "manifest",minChunks: Infinity}},plugins: [//此插件在输出目录中生成 `vue-ssr-client-manifest.json`。new VueSSRClientPlugin()]
})

webpack.server.config.js

/** * 服务端打包配置 */
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')module.exports = merge(baseConfig, {// 将 entry 指向应用程序的 server entry 文件entry: './src/entry-server.js', // 这允许 webpack 以 Node 适用方式处理模块加载// 并且还会在编译 Vue 组件时,// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。target: 'node',output: {filename: 'server-bundle.js',// 此处告知server bundle使用 Node 风格导出模块(Node-style exports)libraryTarget: 'commonjs2'},// 不打包 node_modules 第三方包,而是保留 require 方式直接加载externals: [nodeExternals({// 白名单中的资源依然正常打包allowlist: [/\.css$/]})],plugins: [//这是将服务器的整个输出构建为单个 JSON 文件的插件。// 默认文件名为 `vue-ssr-server-bundle.json`new VueSSRServerPlugin()]
})

在package.json中配置npm scripts 中打包命令

"scripts": {"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js","build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js","build": "rimraf dist && npm run build:client && npm run build:server"
},

执行打包构建命令。

启动应用

执行构建命令后,可以在/dist目录下看到用于服务端渲染的资源文件以及用于客户端的资源文件,下面使用这些文件把同构应用启动。参考Bundle Renderer 指引,修改server.js如下。

const Vue = require('vue');
const express = require('express');
const fs = require('fs');
const serverBundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
const template = fs.readFileSync('./index.template.html', 'utf-8');  //使用utf-8进行编码的index.html作为渲染模板const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, { template, clientManifest });const server = express();
//在服务端开放静态资源,否则客户端无法获取资源
//使用express的static中间件处理静态资源,请求前缀应该和打包出口的publicPath保持一致,
server.use('/dist', express.static('./dist'));//设置路由,以get请求网站根路径,
server.get('/', (req, res) => {//将模板转换为字符串,并发送给客户端renderer.renderToString({title: '服务器端渲染',meta: `<meta name="desc" content="服务器">`}, (err, html) => {if(err) {res.status(500).end('server error...');};res.end(html);})
})//启动web服务
server.listen(3000, () => {console.log('server running...')})

执行nodemon server.js启动应用

注意:此处不需要显示的创建vue实例,渲染的是entry-server.js创建的vue实例。

同构应用渲染流程

  • 服务端渲染

    • renderer.renderToString 渲染了什么?
    • renderer 是如何拿到 entry-server 模块的?
      • createBundleRenderer 中的 serverBundle
    • server Bundle 是 Vue SSR 构建的一个特殊的 JSON 文件
      • entry:入口
      • files:所有构建结果资源列表
      • maps:源代码 source map 信息
    • server-bundle.js 就是通过 server.entry.js 构建出来的结果文件
    • 最终把渲染结果注入到模板中
  • 客户端渲染

构建开发模式

1、构建目的

代码修改以后

  • 自动构建
  • 自动重启web服务
  • 自动刷新浏览器

2、构建思路

  • 生产模式

    • npm run build 构建
    • node server.js 启动应用
  • 开发模式
    • 监视代码变动自动构建,热更新等功能
    • node server.js 启动应用

package.json中添加 scripts 启动脚本

"scripts": {...."start": "cross-env NODE_ENV=production node server.js","dev": "node server.js"
},

3、封装处理模块

server.js在开发模式下,可以直接把渲染的结果发送给客户端。而在开发模式下,需要在代码被修改以后,直接构建,刷新浏览器。

const express = require('express');
const fs = require('fs');
const { createBundleRenderer } = require('vue-server-renderer');
const setupDevServer = require('./build/setup-dev-server');const server = express();//在服务端开放静态资源,否则客户端无法获取资源
//使用express的static中间件处理静态资源,请求前缀应该和打包出口的publicPath保持一致,
//express.static处理的物理磁盘里的文件
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 clientManifest = require('./dist/vue-ssr-client-manifest.json');//使用utf-8进行编码的index.html作为渲染模板const template = fs.readFileSync('./index.template.html', 'utf-8'); 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 html = await renderer.renderToString({title: '拉勾教育',meta: `<meta name="description" content="拉勾教育">`,url: req.url})res.setHeader('Content-Type', 'text/html; charset=utf8')res.end(html)} catch (err) {res.status(500).end('Internal Server Error.')}
};//设置路由,以get请求网站根路径
server.get('/', isProd ?//开发模式下,直接将渲染结果返回给客户端render :async (req, res) => {//等待有了renderer 渲染器以后,调用 render 进行渲染await onReady;render(req, res);}
)//启动web服务
server.listen(3000, () => {console.log('server running...')
})

build/setup-dev-server.js监视打包过程,配置热更新,更新renderer渲染器

const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')const resolve = file => path.resolve(__dirname, file)module.exports = (server, callback) => {let readyconst onReady = new Promise(r => ready = r)// 监视打包构建 -> 更新Renderer渲染器let templatelet serverBundlelet clientManifestconst update = () => {if (template && serverBundle && clientManifest) {ready()callback(serverBundle, template, clientManifest)}}//1、更新模板(监视构建template -> 调用 update -> 更新Renderer渲染器)const templatePath = path.resolve(__dirname, '../index.template.html')template = fs.readFileSync(templatePath, 'utf-8')update()// fs.watch、fs.watchFile//第三方包:chokidarchokidar.watch(templatePath).on('change', () => {template = fs.readFileSync(templatePath, 'utf-8')update()})//2、更新服务端打包(监视构建serverBundle -> 调用update -> 更新Renderer渲染器)const serverConfig = require('./webpack.server.config')const serverCompiler = webpack(serverConfig)const serverDevMiddleware = devMiddleware(serverCompiler, {//关闭默认日志输出,由FriendlyErrorsWebpackPlugin处理//4.x.x版本不支持此属性logLevel: 'silent'})serverCompiler.hooks.done.tap('server', () => {serverBundle = JSON.parse(//从内存当中读取文件serverDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8'))update()})//3、更新客户端打包(监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器)const clientConfig = require('./webpack.client.config')//----配置热更新----clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())clientConfig.entry.app = [//和服务端交互处理热更新的一个客户端脚本'webpack-hot-middleware/client?quiet=true&reload=true', clientConfig.entry.app]// 热更新模式下确保一致的 hashclientConfig.output.filename = '[name].js'//----配置热更新----const clientCompiler = webpack(clientConfig)const clientDevMiddleware = devMiddleware(clientCompiler, {publicPath: clientConfig.output.publicPath,//关闭默认日志输出,由FriendlyErrorsWebpackPlugin处理//4.x.x版本不支持此属性logLevel: 'silent'})clientCompiler.hooks.done.tap('client', () => {clientManifest = JSON.parse(//从内存当中读取文件clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'),'utf-8'))update()})//挂载热更新的中间件server.use(hotMiddleware(clientCompiler, {log: false // 关闭它本身的日志输出}))// 重要!将内存中的资源通过 Express 中间件对外公开访问server.use(clientDevMiddleware)//将 clientDevMiddleware 挂载到 Express服务中,提供其对内存中数据的访问server.use(clientDevMiddleware)return onReady
}

封装处理模块总结:

  • 更新模板
    fs.watch、fs.watchFile
    第三方包:chokidar

  • 将打包结果存储到内存中
    webpack 默认会把构建结果存储到磁盘中,对于生产模式构建来说是没有问题的;但是我们在开发模式中会频繁的修改代码触发构建,也就意味着要频繁的操作磁盘数据,而磁盘数据操作相对于来说是比较慢的,所以我们有一种更好的方式,就是把数据存储到内存中,这样可以极大的提高构建的速度。而把把数据存储到内存中方式:

    • memfs是一个兼容 Node 中 fs 模块 API的内存文件系统,通过它我们可以轻松的实现把 webpack 构建结果输出到内存中进行管理。
    • 使用webpack-dev-middleware,webpack-dev-middleware 作用是以监听模式启动 webpack,将编译结果输出到内存中,然后将内存文件输出到 Express 服务中。

这里选择第二种方式,采用webpack-dev-middleware
build/setup-dev-server.js

//2、监视构建serverBundle -> 调用update -> 更新Renderer渲染器const serverConfig = require('./webpack.server.config')const serverCompiler = webpack(serverConfig)const serverDevMiddleware = devMiddleware(serverCompiler, {//关闭默认日志输出,由FriendlyErrorsWebpackPlugin处理//4.x.x版本不支持此属性logLevel: 'silent'})serverCompiler.hooks.done.tap('server', () => {serverBundle = JSON.parse(//从内存当中读取文件serverDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8'))update()})
  • 热更新
    开发模式下热更新功能需要使用到webpack-hot-middleware工具包,工作原理:

    • 中间件将自身安装为 webpack 插件,并侦听编译器事件。
    • 每个连接的客户端都有一个 Server Sent Events 连接,服务器将在编译器事件上向连接的客户端发布通知。
    • 当客户端收到消息时,它将检查本地代码是否为最新。如果不是最新版本,它将触发 webpack 热模块重新加载。

build/setup-dev-server.js

// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
const clientConfig = require('./webpack.client.config')
//----配置热更新----
clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
clientConfig.entry.app = [//和服务端交互处理热更新的一个客户端脚本'webpack-hot-middleware/client?quiet=true&reload=true', clientConfig.entry.app
]
// 热更新模式下确保一致的 hash
clientConfig.output.filename = '[name].js'
//----配置热更新----
const clientCompiler = webpack(clientConfig)
const clientDevMiddleware = devMiddleware(clientCompiler, {publicPath: clientConfig.output.publicPath,//关闭默认日志输出,由FriendlyErrorsWebpackPlugin处理//4.x.x版本不支持此属性logLevel: 'silent'
})
clientCompiler.hooks.done.tap('client', () => {clientManifest = JSON.parse(//从内存当中读取文件clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8') )update()
})//挂载热更新的中间件
server.use(hotMiddleware(clientCompiler, {log: false // 关闭它本身的日志输出
}))// 重要!将内存中的资源通过 Express 中间件对外公开访问server.use(clientDevMiddleware)
//将 clientDevMiddleware 挂载到 Express服务中,提供其对内存中数据的访问
server.use(clientDevMiddleware)

编写通用代码

编写通用代码的注意事项见Vue SSR 官网

页面路由和代码分割

分别新建pages/home.vuepages/about.vuepages/404.vue

新建路由模块

router/index.js vue-router在同构应用中的用法与在纯客户端的用法一致,只需要在少许的位置做一些配置就可以了。

注意:在同构应用中路由模式要使用'history',它可以兼容前后端

import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '@/pages/home';Vue.use(VueRouter);export const createRouter = () => {const router = new VueRouter({mode: 'history', //兼容前后端routes: [{path: '/',name: 'home',component: Home},{path: '/about',name: 'about',component: () => import('@/pages/about')},{path: '*',name: 'error',component: () => import('@/pages/404')}]});return router;
}

将路由注册到根实例

src/app.js

/*** 通用启动入口*/
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router/index.js'// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {const router = createRouter();const app = new Vue({router, //把路由挂载到Vue根实例中render: h => h(App) // 根实例简单的渲染应用程序组件。})return { app, router }
}

服务端路由适配

entry-server.js中实现服务器端路由逻辑

import { createApp } from './app'
//1、async、await方式
export default async context => { //因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,//以便服务器能够等待所有的内容在渲染前,就已经准备就绪。const { app, router } = createApp();//设置服务器端router的位置router.push(context.url)//等到router将可能的异步组件和钩子函数解析完await new Promise(router.onReady.bind(router));return app;
}

适配客户端路由入口

import { createApp } from './app'// 客户端特定引导逻辑……const { app, router } = createApp()// 这里假定 App.vue 模板中根元素具有 `id="app"`
router.onReady(() => {app.$mount('#app')
});

设置页面路由视图

src/app.vue

<template><div id="app"><ul><li><router-link to="/">Home</router-link></li><li><router-link to="/about">About</router-link></li><li><router-link to="/posts">Posts</router-link></li></ul><!--路由出口--><router-view /></div>
</template>

管理页面head内容——vue-meta

安装vue-meta,在app.js中注册并配置。

import VueMeta from 'vue-meta';
//注册VueMeta
Vue.use(VueMeta);
//设置页面标题模板
Vue.mixin({metaInfo: {titleTemplate: 'hello - %s'}
});

src/entry-server.js

...
const meta = app.$meta();
//设置服务器端router的位置
router.push(context.url)
context.meta = meta;
...

index.template.html

<head>...{{{ meta.inject().title.text() }}}{{{ meta.inject().meta.text() }}}...
</head>

pages/home.vue

...
export default {name: "HomePage",metaInfo: {title: '首页'}
}
...

数据预取和状态管理

数据预取和状态管理见Vue SSR 官网,核心思路就是把在服务端渲染期间获取的数据存储到 Vuex 容器中,然后把容器中的数据同步到客户端,这样就保持了前后端渲染的数据状态同步,避免了客户端重新渲染的问题。

数据预取大致流程如下:


下面以一个简单的业务需求,实现此流程:

  • 已知有一个数据接口,接口返回一个文章列表数据
  • 通过服务端渲染的方式来把异步接口数据渲染到页面中

安装Vuex,store/index.js 创建数据容器:

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) {state.posts = data;}},actions: {//在服务器渲染期间必须让 action 返回一个 Promiseasync getPosts({ commit }) {const { data } = await axios.get('https://cnodejs.org/api/v1/topics')commit('setPosts', data.data);}}});
}

app.js在通用启动入口将数据容器挂载到Vue根实例中并导出。

import { createStore } from './store/index'export function createApp () {...const store = createStore();const app = new Vue({store, //把store挂载到Vue根实例中render: h => h(App) // 根实例简单的渲染应用程序组件。})return { store }
}

src/entry-server.js将服务端预取的数据同步到客户端

export default async context => {...context.rendered = () => {// Renderer 会把 context.state 数据对象内联到页面模板中// 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state// 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中context.state = store.state;}...
}

src/entry-client.js切换页面路由时,刷新客户端数据

...
if (window.__INITIAL_STATE__) {store.replaceState(window.__INITIAL_STATE__)
}
...

src/pages/posts.vue在页面组件中获取数据

<script>
import { mapState, mapActions } from 'vuex'export default {name: 'PostList',metaInfo: {title: 'Posts'},computed: {...mapState(['posts'])},// Vue SSR 特殊为服务端渲染提供的一个生命周期钩子函数serverPrefetch () {// 发起 action,返回 Promisereturn this.getPosts()},methods: {...mapActions(['getPosts'])}
</script>

src/pages/posts.vue在页面组件中展示数据

<template><div><h1>Post List</h1><ul><li v-for="post in posts" :key="post.id">{{ post.title }}</li></ul></div>
</template>

注意:
1、服务端不支持响应式数据。
2、服务端渲染只支持Vue.jsbeforeCreatecreated,不能在这两个生命周期中获取异步数据,因为渲染过程不会等待异步请求的结果。
3、服务端发送请求依然使用 axiosaxios 既可以运行在客户端也可以运行在服务端,因为它对不同的环境做了适配处理,在客户端是基于浏览器的 XMLHttpRequest 请求对象,在服务端是基于 Node.js 中的 http 模块实现,无论是底层是什么,上层的使用方式都是一样的。

服务器端渲染基础
服务器端渲染-Nuxt.js基础
服务器端渲染-Nuxt.js综合案例
服务器端渲染-Nuxt.js综合案例发布部署
服务器端渲染-Vue SSR搭建

服务器端渲染-Vue SSR搭建相关推荐

  1. 为什么使用服务器端渲染(SSR)?

    为什么使用服务器端渲染(SSR) 一.什么是服务器渲染(SSR)? 二.服务器渲染的优势: 三.使用服务器端渲染 (SSR) 时还需要有一些权衡之处: 一.什么是服务器渲染(SSR)? Vue.js ...

  2. vue ssr搭建服务端渲染项目

    什么是服务器端渲染 (SSR) Vue.js 是构建客户端应用程序的框架.默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM.然而,也可以将同一个组件渲染为服务器端的 HT ...

  3. SSR——服务器端渲染(Vue)基础用法(一)

    学习来源 1.简介 服务器端渲染:Vue.js 是构建客户端应用程序的框架.默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM.然而,也可以将同一个组件渲染为服务器端的 H ...

  4. 服务器端渲染(SSR)和客户端渲染

    什么是服务器端渲染和客户端渲染? 互联网早期,用户使用浏览器浏览的都是一些没有复杂逻辑的.简单的页面,这些页面都是在后端将html拼接好的然后将之返回给前端完整的html文件,浏览器拿到这个html文 ...

  5. php 服务器端渲染vue,vue服务器端渲染

    1.服务器端渲染时.第一次请求页面的时候,由服务器帮忙发送请求拼接好数据,并将拼接好的页面数据返回交给前端渲染. 2.当下次客户端要去跳转页面的时候,此时页面结构页面中的数据全部交给客户端渲染 npm ...

  6. 修改vue项目到服务器端渲染,现有vue-cli3搭建的vue项目改ssr服务器渲染

    项目简介 vue+node+koa2 安装ssr依赖 npm install vue-server-renderer webpack-node-externals cross-env --save-d ...

  7. 详解服务器端渲染 页面(SSR)

  8. 【Vue】服务器端渲染

    一.什么是服务器端渲染? server side render 前端页面的产生是由服务器端生成的,我们就称之为服务端渲染 1.1 新建server文件夹 server 1.2 生成一个node项目 n ...

  9. SAP Spartacus 手动开启服务器端渲染 (SSR) 所必须的步骤

    使用服务器端渲染,我们可以保证搜索引擎,与浏览器的Javascript禁用,或没有JavaScript的浏览器仍然可以访问我们的网站内容. https://b2bspastore.cg79x9wuu9 ...

最新文章

  1. pl/sql块的基本语法
  2. CNN模型 int8量化实现方式(二)
  3. python爬虫项目-32个Python爬虫实战项目,满足你的项目慌
  4. java final类 能被继承吗_Java中的类被final关键字修饰后,该类将不可以被继承()...
  5. 代码解释n |= n >>> 16
  6. 在GridView中的批量删除!
  7. linux如何修改网卡序号,CentOS双网卡时改变网卡编号和配置静态路由的方法
  8. TCP面向连接的socket通信
  9. Windows消息机制-PreTranslateMessage
  10. jooq sql_使用jOOQ和JavaFX将SQL数据转换为图表
  11. Camel 2.11 –没有Spring的Camel Web应用程序
  12. django时间问题和时区设置
  13. Spring—集成Junit
  14. 直方图均衡 视觉显著_视觉图像:对比度受限直方图均衡化CLAHE
  15. 连接linux桌面命令,连接Linux远程桌面的四个方法
  16. jQuery源码分析笔记-构造jQuery对象(三)
  17. deliphi 字符串分割_delphi中拆分字符串的函数
  18. 将“光头”识别为“足球”,AI 摄像头如何犯的错?
  19. 海康摄像SDK开发笔记(一):海康威视网络摄像头SDK介绍与模块功能
  20. 深度学习系列18:开源人脸识别库

热门文章

  1. todolist从无到有
  2. 联想扬天 V14 、V15 锐龙版 2023款 评测
  3. Java -- 乒乓球 乒乓弹球游戏
  4. 第60天:攻防世界Mobile两道题
  5. 问题解决:ERROR: Cannot uninstall 'llvmlite'.
  6. Java Web图书管理系统(MVC框架)-包含源码
  7. 软件开发不是一门艺术
  8. 托攻击的多种攻击方式-----WZW托攻击学习日记(五)
  9. Ubuntu 安装uwsgi出错
  10. 黑客与技术提示:电脑出现文中现象说明你已经被黑客入侵