模块四(一):搭建自己的SSR

模块四(一):搭建自己的SSRvue ssr 的使用 搭建 ssr 服务器

大家好,欢迎来到IT知识分享网。

(一)简单入门案例

服务器端渲染是在服务器端生成完整的 html 页面,返回给客户端,客户端直接渲染服务器端返回的 html 。首先介绍如何使用Vue 提供的服务端渲染工具生成一个Vue实例。

1、渲染一个Vue实例
// 第 1 步:创建一个 Vue 实例 const Vue = require('vue') const app = new Vue({ 
    template: ` <div id="app"> <h1>{ 
    { message }}</h1> </div>`, data: { 
    message: 'Hello World!' } }) // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:将 Vue 实例渲染为 HTML // 参数一:Vue实例 // 参数二:回调函数 renderer.renderToString(app, (err, html) => { 
    if (err) throw err console.log(html) }) 

⑤ 控制台运行node server.js运行代码,查看输出
可以看到在Node环境中,vue-server-renderer 将Vue实例渲染成字符串,并且给根元素增加了data-server-rendered属性
在这里插入图片描述

2、结合到web服务中

接下来介绍如何将 Vue SSR 渲染之后的静态 HTML发送给Web服务器中,然后渲染到页面上。这里会使用到 express 框架。Express是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。可以灵活处理 http 请求、路由、中间件、静态资源托管等业务场景。
① 安装 NodeJS 框架 npm install express --save
② 在 server.js 当中加载 express

// 加载express const express = require('express') 

③ 给服务配置路由,并且在访问服务的时候,创建 Vue 实例,并使用 vue-server-renderer 渲染成静态HTML。如果发生错误,就返回状态码500,并且返回错误信息;如果正确执行,就把 vue-server-renderer 生成的静态HTML返回给客户端

server.get('/',(req,res)=>{ 
    const app = new Vue({ 
    template: `<div>Hello World</div>` }) // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer() // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { 
    if (err) { 
    return res.status(500).end('服务器错误') } res.end(html) }) }) 

④ 启动服务并监听端口号

// 启动服务 server.listen(3000, ()=>{ 
    console.log('服务启动') }) 

⑤ 使用nodemon server.js启动服务
nodemon 是一个 Node.js 应用的实时监视工具。它可以监视你的代码,如果发现有任何更改,它会自动重启你的应用程序,不必手动重新启动它。如果没安装的话,记得npm i nodemon安装一下。
启动服务之后,控制台会打印启动成功的消息:
在这里插入图片描述
访问 http://localhost:3000/,可以看到页面中展示 Vue SSR 生成的页面内容
在这里插入图片描述
还可以从网络的响应中看到,服务端响应的就是静态html
在这里插入图片描述
⑥ 把模板中的文本改成中文之后







const app = new Vue({ 
    template: `<div>哈哈哈</div>` }) 
 renderer.renderToString(app, (err, html) => { 
    if (err) { 
    return res.statue(500).end('服务器错误') } res.setHeader('Content-Type','text/html; charset=utf8') res.end(html) }) 

可以在响应头中看到content-type的设置
在这里插入图片描述
2️⃣ 给模板中加入meta标签
上面只是生成了一个div标签,即html片段,并不是一个完整的页面结构。返回的时候可以返回一个完整的页面结构,其中使用meta标签执行charset编码方式


res.end(`<!doctype html> <html class="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> </head> <body> ${ 
     html} </body> </html>`) }) }) 
3、使用页面模板

可以将页面基础结构的模板放在单独的文件中进行维护,vue-server-renderer 会自动将模板和 Vue 实例的template结合成一个完整的html
① 在根目录下创建index.template.html存放页面模板代码,其中,需要用 Vue 实例的template属性填充的地方,使用注释<!--vue-ssr-outlet-->占位,这句是固定的,不能修改格式。

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!--vue-ssr-outlet--> </body> </html> 

② 创建 renderer 时传入页面模板的配置选项

const fs = require('fs') // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createRenderer({ 
    template: fs.readFileSync('./index.template.html', 'utf-8') }) 

③ 返回给客户端时,直接返回参数html即可

// 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString(app, (err, html) => { 
    if (err) { 
    return res.status(500).end('服务器错误') } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) 
4、在模板中使用外部数据

renderer.renderToString()方法的第二个参数可以定义传递给模板的数据,在模板中使用插值表达式绑定即可。如果想给模板传递html标签,则模板内部要使用{
{
{}}}
,这样就不会进行解析,会直接原文输出
server.js

renderer.renderToString(app,{ 
    title:'Vue SSR', meta:`<meta name="description" content="今天是个好日子"></meta>` }, (err, html) => { 
    if (err) { 
    return res.status(500).end('服务器错误') } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) 

index.template.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> { 
  { 
  { meta }}} <title>{ 
  {title}}</title> </head> <body> <!--vue-ssr-outlet--> </body> </html> 

(二)构建配置

Vue 项目需要处理 ES6 代码,以及处理 css 等资源以保证在旧版浏览器中程序也能正确运行,因此我们需要使用 webpack 进行打包;Node.js 环境是完全支持 ES6 语法的,但是 webpack 提供的很多 loader 在 Node.js 环境中是不生效的,所以服务端和客户端需要两套不同的配置和打包步骤。Vue SSR 官网给我们提供了一套可供参考的代码结构。

1、源码结构
<template> <div id="app">{ 
  { message }}</div> </template> <script> export default { 
      data() { 
      return { 
      message: '哈哈哈' } } } </script> 

② 创建src/app.js,里面创建并导出一个创建 Vue 应用的工厂函数,这是为了每次请求都创建一个新的 Vue 实例,避免相互污染。Node.js 一旦进入一个进程,变量的状态会一直保存,所以多次请求会使用同一个 Vue 实例。所以这里要导出一个工厂函数,保证每次请求到的都是新的 Vue 实例。

/ * 同构应用通用的启动入口 * */ import Vue from 'vue' import App from './App.vue' // 导出一个工厂函数,用于创建新的 // 应用程序、router 和 store 实例 export function createApp () { 
    const app = new Vue({ 
    // 根实例简单的渲染应用程序组件。 render: h => h(App) }) return { 
    app } } 
/ * 客户端入口 * */ import { 
    createApp } from './app' // 客户端特定引导逻辑…… const { 
    app } = createApp() // 这里假定 App.vue 模板中根元素具有 `id="app"` app.$mount('#app') 
/ * 服务端入口 * */ import { 
    createApp } from './app' export default context => { 
    const { 
    app } = createApp() // 服务端路由处理、数据预取 ... return app } 

创建好源码结构后,需要使用 webpack 进行打包构建,服务器端代码需要打包成「服务器 bundle」,然后用于服务器端渲染(SSR),而客户端代码要打包成「客户端 bundle」,处理客户端渲染。

2、安装依赖
说明
vue Vue.js 核心库
vue-server-renderer Vue 服务端渲染工具
express 基于 Node 的 Web 服务框架
cross-env 通过 npm scripts 设置跨平台环境变量,用于区分不同模式的打包环境:生产模式、开发模式
说明
webpack webpack 核心包
webpack-cli webpack 的命令行工具
webpack-merge webpack 配置信息合并工具;把服务端打包的配置文件和客户端打包的配置文件中的共同部分抽取出来进行合并
webpack-node-externals 排除 webpack 中的 Node 模块,例如fs、http、path等不需要进行打包
rimraf 基于 Node 封装的一个跨平台 rm -rf 工具;可以在命令行执行删除操作,主要用于删除之前的打包出来的dist
friendly-errors-webpack-plugin 友好的 webpack 错误提示
@babel/core、@babel/plugin-transform-runtime、@babel/preset-env、babel-loader Babel 相关工具,把项目中的es6转换成es5
vue-loader、vue-template-compiler 处理 .vue 资源
file-loader 处理字体资源
css-loader 处理 CSS 资源
url-loader 处理图片资源

运行上述命令安装生产环境的依赖和开发环境的依赖。

3、webpack配置文件
/ * 公共配置 */ // 处理.vue资源的插件 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) // 环境变量中的NODE_ENV const isProd = process.env.NODE_ENV === 'production' module.exports = { 
    mode: isProd ? 'production' : 'development', output: { 
    path: resolve('../dist/'), // 打包结果输出到dist文件夹 publicPath: '/dist/', // 所有的打包结果在请求的时候都以 /dist 开头,避免和路由匹配规则冲突 filename: '[name].[chunkhash].js' // 文件名 + hash 生成文件名;一旦文件内容发生改变,生成的文件名也会发生变化,强制浏览器请求新的资源 }, 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/preprocessors.html // 例如处理 Less 资源 // { 
    // test: /\.less$/, // use: [ // 'vue-style-loader', // 'css-loader', // 'less-loader' // ] // }, ] }, plugins: [ new VueLoaderPlugin(), new FriendlyErrorsWebpackPlugin() ] } 

webpack.client.config.js

/ * 客户端打包配置 */ // merge: 合并webpack配置信息 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' // 客户端打包入口 相对路径是相对于vue-ssr目录 }, 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() ] }) 

关于 chunk,参考解释

在 Web 开发中,Webpack 是一个常用的模块打包工具,用于将前端应用程序的源代码和资源文件打包成可在浏览器中运行的静态文件。Webpack 的核心概念之一就是「chunk」(代码块)。

一个「chunk」代表着一个独立的代码块,其中包含了一个或多个模块。Webpack 在打包过程中会根据配置和依赖关系将应用程序的代码拆分成多个不同的「chunk」。这些「chunk」可以理解为被划分的逻辑模块,它们可以被异步加载,从而实现按需加载和代码分割的功能。

Webpack 在打包过程中会根据不同的策略将代码拆分成多个「chunk」。常见的拆分策略包括:

总结起来,Webpack 中的「chunk」是指将代码拆分成的独立模块,可以通过异步加载方式进行按需加载。这种代码拆分和按需加载的策略有助于提高应用程序的性能和加载速度。

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)。 // 常见可选值:web(输出面向浏览器的代码)/node target: 'node', output: { 
    filename: 'server-bundle.js', // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) // 即 module.exports 导出;require 导入 libraryTarget: 'commonjs2' }, // 不打包 node_modules 第三方包,而是保留 require 方式直接加载 externals: [nodeExternals({ 
    // 白名单中的资源依然正常打包 allowlist: [/\.css$/] })], plugins: [ // 这是将服务器的整个输出构建为单个 JSON 文件的插件。 // 默认文件名为 `vue-ssr-server-bundle.json` new VueSSRServerPlugin() ] }) 
4、配置构建命令以及打包命令

在学习完 webpack 的配置之后要学习打包命令的配置。在 package.json 的 scripts 中配置命令
cross-env NODE_ENV=product 用来设置环境变量,表示进行生产模式的打包; --config 用来指定配置文件;
rimraf dist 删除dist文件夹

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

运行npm run build:client,报错:
在这里插入图片描述
这个插件再下载一遍:yarn add -D friendly-errors-webpack-plugin,再运行构建命令
报错:
在这里插入图片描述
发现用yarn add一次安装所有的依赖好像有问题,列表中的好多依赖没有安装
另外就是版本问题,安装的Vue是2.7的,要是挨个都指定版本那也太麻烦了,还是把 Vue 改成最新版本的吧。
执行打包命令之后,可以看到已经打包出来了dist文件夹
在这里插入图片描述







5、启动应用

关于bundle 参考解释
在软件开发中,“bundle”(捆绑)一词通常用来描述将多个文件或资源组合在一起形成单个文件或包的过程。它旨在简化部署、分发和使用这些文件或资源的过程。
“Bundle” 可以指代不同类型的捆绑,具体取决于上下文和所涉及的技术栈。以下是一些常见的 bundle 类型:
🌀 前端资源捆绑:在前端开发中,“bundle” 通常指代将多个 JavaScript、CSS 和其他静态资源文件合并到一个或多个文件中的过程。这样做可以减少网络请求的数量,加快网页加载速度,并简化前端代码的部署和管理。常见的前端资源捆绑工具包括 webpack、Parcel 和 Rollup。
🌀 应用程序捆绑:在应用程序开发中,“bundle” 通常指将应用程序的源代码、依赖项和其他资源打包到一个或多个可执行文件或库中的过程。这样做可以简化应用程序的部署和分发,并提供更好的性能和安全性。常见的应用程序捆绑工具包括 webpack、Parcel、Browserify 和 Rollup。
🌀 代码库捆绑:在软件开发中,“bundle” 有时也用于指代将多个代码文件或模块打包成一个独立的、可重用的代码库或包的过程。这样做可以简化代码的共享和复用,并提供更好的封装性。常见的代码库捆绑工具包括 webpack 和 Rollup。
总的来说,“bundle” 是将多个文件或资源组合在一起形成单个文件或包的过程,旨在简化部署、分发和使用这些文件或资源的过程。具体的 bundle 类型和实现方式取决于所涉及的技术栈和开发场景。





根据上面的解释,webpack 打包将资源列表、文件内容都打包成一个文件,这个文件就叫 bundle 。createBundleRenderer 就是获取 webpack 打包生成的 bundle,生成一个 bundle renderer ,bundle renderer 可以根据 bundle 的配置和内容生成HTML字符串。

介绍一下 createBundleRenderer API的用法。在创建 renderer 的时候,之前使用的是 createRenderer() 方法,将其替换为 createBundleRenderer() 方法。createBundleRenderer()方法在使用的时候,
第一个参数要接受 server bundle,server bundle就是webpack对服务端代码打包后生成的结果,其中包括了服务端代码所依赖的资源列表,直接从 dist 目录下引入;bundle renderer 可以监听这个 server bundle 修改,从而更新生成的要传递给客户端的 HTML 字符串;
第二个参数接收选项对象,选项对象同样可以传入模板,还需要传入 clientManiFest ,它是webpack打包客户端代码生成的结果,里面存放着客户端代码的依赖列表等内容,这个数据也从 dist 目录下通过 require 引入
server.js


const template = fs.readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManiFest = require('./dist/vue-ssr-client-manifest.json') // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, { 
    template, clientManiFest // 客户端打包出来的资源构建清单 }) 
// 第 1 步:创建一个 Vue 实例 const Vue = require('vue') // 加载express const express = require('express') // 创建server实例 const server = express() const fs = require('fs') // 设置路由 server.get('/', (req, res) => { 
    const template = fs.readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManiFest = require('./dist/vue-ssr-client-manifest.json') // 第 2 步:创建一个 renderer const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, { 
    template, clientManiFest // 客户端打包出来的资源构建清单 }) // 第 3 步:将 Vue 实例渲染为 HTML // 这里的第一个参数 app 删除,renderer 会自动获取 renderer.renderToString({ 
    title: 'Vue SSR', meta: `<meta name="description" content="今天是个好日子"></meta>` }, (err, html) => { 
    if (err) { 
    return res.status(500).end('服务器错误') } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) }) // 启动服务 server.listen(3000, () => { 
    console.log('服务启动') }) 

然后使用 nodemon server.js 启动服务,报了一个版本不匹配的错误:
在这里插入图片描述
vue 和 vue-server-renderer 版本必须相同,都则会报错
运行代码


yarn remove vue-server-renderer vue yarn add vue@2.6.14 vue-server-renderer@2.6.14 
{ 
    "name": "vue-ssr", "private": true, "version": "1.0.0", "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", "start": "cross-env NODE_ENV=production node server.js", "dev": "node server.js" }, "dependencies": { 
    "axios": "^0.19.2", "chokidar": "^3.4.0", "cross-env": "^7.0.2", "express": "^4.17.1", "nodemon": "^3.0.1", "vue": "^2.6.11", "vue-meta": "^2.4.0", "vue-router": "^3.3.4", "vue-server-renderer": "^2.6.11", "vuex": "^3.5.1" }, "devDependencies": { 
    "@babel/core": "^7.10.4", "@babel/plugin-transform-runtime": "^7.10.4", "@babel/preset-env": "^7.10.4", "babel-loader": "^8.1.0", "css-loader": "^3.6.0", "file-loader": "^6.0.0", "friendly-errors-webpack-plugin": "^1.7.0", "rimraf": "^3.0.2", "url-loader": "^4.1.0", "vue-loader": "^15.9.3", "vue-template-compiler": "^2.6.11", "webpack": "^4.43.0", "webpack-cli": "^3.3.12", "webpack-dev-middleware": "^3.7.2", "webpack-hot-middleware": "^2.25.0", "webpack-merge": "^5.0.9", "webpack-node-externals": "^2.5.0" } } 
// 加载静态资源的中间件 server.use('/dist', express.static('./dist')) 
6、解析渲染过程
renderer.renderToString((err, html) => { 
    if (err) { 
    return res.status(500).end(JSON.stringify(err.message)) } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) 

看一下 renderer 的创建:

const serverBundle = require('./dist/vue-ssr-server-bundle.json') const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, { 
    template, clientManifest // 客户端打包出来的资源构建清单 }) 

createBundleRenderer() 的第一个参数 serverBundle 是从 ./dist/vue-ssr-server-bundle.json 文件引入的,这个文件是服务端打包出来的结果,里面描述了服务端打包的信息。

{ 
    "entry": "server-bundle.js", // 服务端打包之后的入口 "files": { 
    "server-bundle.js": "module.exports={//...}" // 服务端入口文件代码直接输出并保存到这里,是对src/entry-server.js打包生成的结果 }, "maps": { 
    // sourcemap相关信息,用于开发调试 "server-bundle.js": { 
    "version": 3, "sources": [], "mappings": "", "file": "server-bundle.js", "sourcesContent": [], "sourceRoot": "" } } } 

renderer 在渲染的时候,会加载 ./dist/vue-ssr-server-bundle.json 文件中定义的入口文件并执行其中代码,即 server-bundle.js 里面定义的代码,代码会被打包直接放在JSON文件里面,这里我省略了,因为代码太长。 然后把渲染的结果注入template模板中并发送给客户端。
另外,客户端要激活服务端返回回来的内容,就需要将客户端打包出来的脚本注入的页面当中。虽然在 tamplate 中并没有显式注入js脚本,但是在服务端返回的html文档中,是有脚本注入的
在这里插入图片描述
那么服务端是怎么自动找到脚本文件并注入到 tamplate 当中的呢?这就是 createBundleRenderer() 方法中,第二个参数配置项中的 clientManifest 起的作用。clientManifest 是引入的 dist/vue-ssr-client-manifest.json 文件,是客户端打包资源的构建清单,清单中就描述了客户端构建出来的资源的相关信息。


{ 
    "publicPath": "/dist/", // 与客户端配置的打包出口一致 "all": [ // 客户端打包出来的所有资源文件名称 "app.831a25e2cd5f80130e6f.js", "app.831a25e2cd5f80130e6f.js.map" ], "initial": [ // renderer在渲染的时候就会把initial中的资源自动注入到模板页面的末尾处,通过script标签引入 "app.831a25e2cd5f80130e6f.js" ], "async": [], // 存储异步资源的资源信息,例如异步组件、异步js模块 "modules": { 
    // 针对原始模块做的依赖信息说明 "7a80a878": [ // 模块标识 0, // 依赖信息,指的是 all 里面的依赖资源的索引,0就表示app.js; 1 // 1就表示app.js.map ] } } 

客户端在获取到服务器端返回的HTML之后,需要将静态的HTML激活为由Vue接管的动态HTML。客户端激活过程参考官方文档。服务端输出的HTML,其根节点即 id="app" 的节点上,会挂载一个属性data-server-rendered="true",这个属性就是让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载。id是开发时需要开发者在模板组件中添加的:
App.vue

<div id="app">{ 
  { message }}</div> 

src/entry-client.js 文件里面,通过 id 名指定了应用的根节点 app.$mount('#app') 并挂载应用实例,如果要修改根节点的id,这里的代码要同步修改。
在开发模式下,Vue会推断客户端生成的虚拟DOM和服务器返回的DOM树的结构是否一致,如果不一致,将会丢弃服务器返回的虚拟DOM,重新进行渲染。
浏览器在渲染的时候可能会修改HTML结构以确保文档规范,例如,如果你的 table 标签中直接嵌套了 tr 标签,浏览器将会自动的增加 tbody 标签,因此在书写模板的时候,尽可能保证结构规范,保证服务端生成的DOM树和客户端生成的虚拟DOM树能够正确匹配。

(三)构建配置开发模式

目前打包的方式是,修改代码后需要重新执行 npm run build 执行构建,然后执行 nodemon server.js 启动服务。接下来要实现开发模式的构建,即修改代码立即自动构建、重启web服务并重新刷新浏览器页面内容。

1、基本思路

在开发环境中,我们想实现的功能是,修改代码之后,自动执行构建、自动重启web服务以及自动刷新浏览器页面。这个功能的实现主要依赖于:在代码修改的时候,要自动执行 require('vue-server-renderer').createBundleRenderer() 方法,根据最新打包结果生成新的renderer。但是这一流程是否进行,需要依赖于当前运行环境是不是开发环境,所以需要一个变量来判断当前环境是开发环境还是生产环境。
首先我们在 package.json 中创建两个构建命令,分别用来执行开发环境构建和生产环境构建:

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

在创建 renderer 的时候,先判断当前环境,如果是生产环境就直接根据打包生成的结果生成renderer,如果是开发环境,需要监听内容变化、自动执行构建并引入构建之后的结果。这个流程稍后再介绍,先熟悉基本思路

// 判断环境 const isProd = process.env.NODE_ENV === 'production' let renderer if (isProd) { 
    // 直接基于打包后的结果生成并启动renderer const template = fs.readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') // 第 2 步:创建一个 renderer renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, { 
    template, clientManifest // 客户端打包出来的资源构建清单 }) } else { 
    // 开发模式 -> 监视源代码的改动,自动打包构建 -> 重新生成 renderer } 

另外,在设置路由的时候,如果是生产环境,执行之前的代码,直接渲染模板是没有问题的;但如果是开发环境就有问题了,因为开发环境下的一套流程是很耗时间的,此时并不能确保 renderer 已经创建成功。所以开发环境要等待 renderer 创建成功后再执行渲染。

const render = (req, res) => { 
    // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString((err, html) => { 
    if (err) { 
    return res.status(500).end(JSON.stringify(err.message)) } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) } // 加载静态资源的中间件 server.use('/dist', express.static('./dist')) // 设置路由 server.get('/', isProd ? render : (req, res) => { 
    // 等待有了renderer再执行render }) 
2、提取处理模块

接下来要考虑的是,开发环境,【监视源代码的改动 -> 自动打包构建 -> 重新生成 renderer 】这个过程如何实现。将这个过程的实现封装成一个模块。
开发一个模块首先要考虑的是接收的参数和返回值。
模块在构建过程中,可能要给当前 server 实例注册一些插件或者获取 server 实例上的属性或方法,所以要接受 server 实例作为参数;还需要接收一个回调函数作为参数,这个回调函数的功能就是renderer 的创建,以便于模块在检测到代码打包构建完成之后,执行 renderer 的创建。
在访问路由的时候,需要检测 renderer 的创建完成时机,执行渲染,所以应该返回一个 Promise,在 renderer 的创建完毕后,改为成功状态。
由此可见,入参是server实例和回调函数;返回值是一个 Promise 对象。
这里对于 Promise 的作用有一些感悟,凡是一个异步方法,都可以返回 Promise ,先创建一个 Promise ,在异步操作执行完毕后,将 Promise 的状态改为 resolved。
先写出来该模块的结构:
build/setup-dev-server.js






module.exports = (server,callback) =>{ 
    const onReady = new Promise() // 监视构建 -> 更新renderer -> 改为成功态 return onReady } 

在执行该模块的时候,需要一个变量接收返回值,并且在访问路由时,需要等待模块返回的Promise的状态变为resolved,再执行渲染
server.js

// 复用的方法抽离出来 const { 
   createBundleRenderer} = require('vue-server-renderer') const setupDevServer = require('./build/setup-dev-server') // 判断环境 const isProd = process.env.NODE_ENV === 'production' let renderer let onReady if (isProd) { 
    // 直接基于打包后的结果生成并启动renderer const template = fs.readFileSync('./index.template.html', 'utf-8'); const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') // 第 2 步:创建一个 renderer renderer = createBundleRenderer(serverBundle, { 
    template, clientManifest // 客户端打包出来的资源构建清单 }) } else { 
    // 开发模式 -> 监视源代码的改动,自动打包构建 -> 重新生成 renderer // 获取server实例 开发模式下需要给server实例挂载中间件 // setupDevServer()返回Promise,完成构建以后会变成成功状态 onReady = setupDevServer(server, (serverBundle, template, clientManifest) => { 
    renderer = createBundleRenderer(serverBundle, { 
    template, clientManifest // 客户端打包出来的资源构建清单 }) }) } const render = (req, res) => { 
    // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString((err, html) => { 
    if (err) { 
    return res.status(500).end(JSON.stringify(err.message)) } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) } // 加载静态资源的中间件 server.use('/dist', express.static('./dist')) // 设置路由 server.get('/', isProd ? render : async (req, res) => { 
    // 等待有了renderer再执行render await onReady render() }) 
4、update()更新函数

创建一个update()方法,专门由于调用callback(),更新renderer,并且执行Promiseresolve(),以便于在访问路由的时候,可以监测到renderer构建成功的时机,并执行渲染。
创建renderer需要三个资源:serverBundle, template, clientManifest。所以在模块中,要创建三个变量,接收这三个资源。如果这三个资源都构建完毕,就执行callback(),创建renderer。并且,这三个资源其中的任何一个发生变化,都需要执行update()方法,重新更新renderer
build/setup-dev-server.js

module.exports = (server, callback) => { 
    let ready // 接收resolve函数 const onReady = new Promise(r => { 
    ready = r }) // 监视构建 -> 更新renderer -> 改为成功态 let template let serverBundle let clientManifest const update = () => { 
    if (template && serverBundle && clientManifest) { 
    callback(serverBundle, template, clientManifest) // 执行Promise的resolve ready() } } // 监视构建 template -> 调用 update -> 更新 renderer // 监视构建 serverBundle -> 调用 update -> 更新 renderer // 监视构建 clientManifest -> 调用 update -> 更新 renderer return onReady } 
5、处理 template

template 资源的原始文件是index.template.html。资源在初始化的时候要获取模板内容以及在原始文件代码更新的时候需要更新。获取文件内容使用Node.js提供的fs模块

const fs = require('fs') const path = require('path') // 监视构建 template -> 调用 update -> 更新renderer const templatePath = path.resolve(__dirname, '../index.template.html') template = fs.readFileSync(templatePath, 'utf-8') update() 

监听文件内容修改,原生的Node.js提供的有 fs.watch()fs.watchFile() 都可以实现,但是都有各自的限制。推荐使用一个第三方库chokibar,它是对fs.watch()fs.watchFile()等方法的封装,使用起来更简洁方便。
cnpm i chokibar 安装
const chokidar = require('chokidar') 引入
③ 使用


// on()的第一个参数是监听的修改的类型 change监听内容修改 chokidar.watch(templatePath).on('change', (event, path) => { 
    console.log("template change"); }); 

先测试一下能否成功监听模板内容变化,在终端运行npm run dev 启动开发环境模式下的服务,并且修改index.template.html里面的内容,并保存,可以看到终端输出了:
在这里插入图片描述
所以此时已经能够正确监听模板内容变化了。
那么在监听到模板内容变化后,需要重新获取文件内容并且赋值给 template 变量,并且执行 update() 方法更新 renderer


chokidar.watch(templatePath).on('change', (event, path) => { 
    template = fs.readFileSync(templatePath, 'utf-8') update() }); 
6、服务端监视打包

服务端监视打包就是监听server.js的修改,需要使用 webpack 提供的方法。
① 引入webpack const webpack = require('webpack')
② 封装文件路径的获取方法

const resolve = file => path.resolve(__dirname, file) 

③ 引入服务端配置

const serverConfig = require('./webpack.server.config') 

④ 调用webpack()方法
webpack() 方法的返回值是一个 Webpack 编译器实例。Webpack 编译器实例提供了 watch() 方法,用于监视文件的变化并自动重新编译。当你调用 watch() 方法时,Webpack将会监听配置中指定的文件,并在文件发生变化时自动重新执行编译操作。

const serverCompiler = webpack(serverConfig) 
serverCompiler.watch({ 
   }, (err, stats) => { 
    if (err) throw err // webpack本身错误,例如配置写错 if (stats.hasErrors()) return // 源代码中的错误 // 获取打包后的资源 // 不能使用require加载,因为require加载有缓存 // 默认读取的是buffer二进制,要手动设置utf-8 serverBundle = JSON.parse(fs.readFileSync('../dist/vue-ssr-server-bundle.json', 'utf-8')) update() }) 
7、把数据写到内存中

webpack 打包构建会默认把构建结果存储到磁盘中进行读写操作,在开发模式下,频繁修改代码触发构建会频繁地读写磁盘数据,这个过程比较慢,所以最好将数据存放到内存中,读写会更快速。
webpack 提供了一个API custom-file-systems,用来自定义webpack的文件系统。
可以使用webpack提供的中间件webpack-dev-middleware 进行配置,这个中间件会自动将打包结果存到内存中。
① 加载 webpack-dev-middleware


const devMiddleware = require('webpack-dev-middleware') 

② 使用 webpack-dev-middleware
webpack-dev-middleware 会自动替我们进行打包构建,所以构建服务端就不需要手动调用 watch() 方法。中间件构建过程会打印很多日志,传递 logLevel: 'silent' 配置是阻止日志输出,因为我们的项目中已经使用了 friendly-errors-webpack-plugin 插件统一管理日志输出。webpack-dev-middleware 打包的结果不会存储到磁盘上,而是会存储到内存中。需要将serverCompiler.watch() 方法的执行注释掉。

// 监视构建 serverBundle -> 调用 update -> 更新renderer const serverConfig = require('./webpack.server.config') const serverCompiler = webpack(serverConfig) devMiddleware(serverCompiler,{ 
    logLevel: 'silent', // 关闭日志输出 }) 

我们先把dist/vue-ssr-server-bundle.json文件删除,然后运行npm run dev,可以看到,并没有重新创建该文件,其实是在内存里创建了该文件,在项目目录中看不到。
我们还需要监视构建,当打包构建完成之后,需要获取内存中的vue-ssr-server-bundle.json文件 ,并且重新执行 update() 方法。获取vue-ssr-server-bundle.json文件就不能再使用fs模块读取磁盘内容了,需要通过 devMiddleware() 方法的返回值获取。监听构建完成事件需要给 serverCompiler 增加钩子函数的回调。done是构建完成的生命周期,tap是监听生命周期的方法,里面接受的第一个参数是一个标识符,没有特殊的作用;第二个参数是回调函数。serverDevMiddleware.fileSystem 就是内存中的文件系统,和 fs 模块的使用一样,里面的路径都不需要修改。

const serverDevMiddleware = devMiddleware(serverCompiler,{ 
    logLevel: 'silent', // 关闭日志输出 }) serverCompiler.hooks.done.tap('server',()=>{ 
    serverBundle = JSON.parse(serverDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json', 'utf-8'))) update() }) 
8、客户端监视打包

客户端监视打包和服务端监视打包的思路是一样的,同样是使用中间件 devMiddleware,我们只需要仿照上面服务端监视打包的代码写就可以了。和服务端监视打包不同的是,还有一个特殊的配置需要做,就是 publicPath,这个配置是指定中间件绑定的公共路径,也就是客户端请求时配置的路由前缀,当访问这个前缀下的路由时,中间件便会生效。

// 监视构建 clientManifest -> 调用 update -> 更新renderer const clientConfig = require('./webpack.client.config') const clientCompiler = webpack(clientConfig) const clientDevMiddleware = devMiddleware(clientCompiler,{ 
    publicPath: clientConfig.output.publicPath, logLevel: 'silent', // 关闭日志输出 }) clientCompiler.hooks.done.tap('client',()=>{ 
    clientManifest = JSON.parse(clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json', 'utf-8'))) update() }) 

以上代码,就实现了自动监听代码修改,生成 renderer 的过程。回到server.js中,当setupDevServer()返回的 Promise 对象 onReady 的状态变为 resolved 之后,需要执行 render() 方法,将 bundle renderer 转换为HTML字符串
server.js

// 设置路由 server.get('/', isProd ? render : async (req, res) => { 
    console.log('111') // 等待有了renderer再执行render await onReady console.log('222') render(req, res) }) 

写到这里我们可以测试一下,在命令行运行 npm run dev,并且访问 localhost:3000。看一下网络请求,发现有一个 404 错误❌!找不到这个js文件。这是为什么呢?
在这里插入图片描述
我们看一下 localhost 的返回值,这个 app.js 是在里面的,为什么加载不了呢?
在这里插入图片描述
思考一下,我们处理客户端打包构建时,使用到了 devMiddleware 中间件,把打包后的资源放在了内存中,而在 server.js 中,我们配置了处理静态资源的中间件: server.use('/dist', express.static('./dist')),这里 express.static() 处理的是物理磁盘中的文件,也就是服务端接收到来自客户端的请求的时候,会去物理磁盘上找文件,但是我们把打包结果放在了内存中,所以就找不到打包生成的js文件了。
解决办法就是将 clientDevMiddleware 挂载到 Express 服务中,提供对其内部内存中数据的访问,Express 在找文件的时候就会尝试去内存中找文件。在客户端打包构建的最后增加一句代码




server.use(clientDevMiddleware) 
9、热更新
 const hotMiddleware = require("webpack-hot-middleware") // 监视构建 clientManifest -> 调用 update -> 更新renderer const clientConfig = require('./webpack.client.config') // 新增插件 clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin()) // 增加一个打包出口 // quiet=true 表示阻止控制台打印日志 // reload=true 表示如果页面加载失败则自动重新加载 clientConfig.entry.app = [ 'webpack-hot-middleware/client?quiet=true&reload=true', // 用来和服务端交互处理热更新的脚本 clientConfig.entry.app ] clientConfig.output.filename = `[name].js` // 热更新模式下不使用hash // ...  // 注册中间件,需要传递客户端编译实例 server.use(hotMiddleware(clientCompiler, { 
    log: false // 关闭它本身的日志输出 })) server.use(clientDevMiddleware) 

运行 npm run dev 启动项目,并且访问3000端口,在网络请求中可以看到一个 hotMiddleware 创建的请求
在这里插入图片描述
它的工作原理是在开发服务器和浏览器之间建立一个WebSocket连接。当开发服务器检测到代码更改时,它会通过WebSocket将更新的模块信息发送到浏览器。浏览器接收到更新的模块后,会使用热模块替换技术将新模块应用到正在运行的应用程序中,从而实现实时的模块替换。

此时,修改src/App.vue中的代码,并且保存,浏览器无需刷新,其中的内容就会自动更新。

(四)路由处理

const router = VueRouter.createRouter({ 
    routes, // `routes: routes` 的缩写 }) 

④ 将路由实例挂载到应用实例上 app.use(router)

1、配置 VueRouter
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', // 大多数服务端不接受hash路由 routes:[ { 
    path:'/', name:'home', component: Home }, { 
    path:'/about', name:'about', // 异步懒加载 component:()=>import('@/pages/Abput') }, { 
    path:'*', name:'error404', // 异步懒加载 component:()=>import('@/pages/404') } ] }) return router } 
2、将路由注册到Vue实例

src/app.js 中引入创建路由实例的方法,并且创建路由,把路由挂载到 App 根应用实例上。导出应用实例的时候,把路由一并导出,这样就可以在其他文件中获取路由,便于操作路由。
src/app.js

/ * 同构应用通用的启动入口 * */ import Vue from 'vue' import App from './App.vue' import { 
   createRouter} from "./router"; // 导出一个工厂函数,用于创建新的 // 应用程序、router 和 store 实例 export function createApp () { 
    const router = createRouter() const app = new Vue({ 
    router, // 把路由挂载到 Vue 根实例 // 根实例简单的渲染应用程序组件。 render: h => h(App) }) return { 
    app, router } } 
3、服务器端路由逻辑
// entry-server.js import { 
    createApp } from './app' export default context => { 
    // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, // 以便服务器能够等待所有的内容在渲染前, // 就已经准备就绪。 return new Promise((resolve, reject) => { 
    const { 
    app, router } = createApp() // 设置服务器端 router 的位置 router.push(context.url) // 等到 router 将可能的异步组件和钩子函数解析完 router.onReady(() => { 
    // Promise 应该 resolve 应用程序实例,以便它可以渲染 resolve(app) }, reject) }) } 

以上是 Promise 的形式,返回了一个 Promise,其中的逻辑是,首先创建 app 实例和 router 实例,然后执行 router.push(),将传进来的参数的 url 增加到路由表中,这个具体的用法后续会讲到;最后是 router.onReady() 事件,它是路由初始化事件,里面的两个参数分别是初始化成功的回调和初始化失败的回调,当路由初始化成功后,Promise 的状态改为 resolved
所以整一个 Promise 的作用就是要等 router.onReady() 事件执行成功。可以将其改写为 async/await 形式,看起来更加的清晰。

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

上面服务器端的路由配置需要接受一个 context 对象,里面包含 url 属性。接下来我们要处理一下传递的 context 参数。context 参数是从哪里传进来的呢?我们要回到 server.js 文件,其中我们配置了路由

server.get('/', isProd ? render : async (req, res) => { 
    // 等待有了renderer再执行render await onReady render(req, res) }) 

这里的意思是如果路由匹配到 /,就执行 render() 方法。在 render() 方法中,执行了renderer.renderToString() 方法,这个方法的第一个参数是传递给模板的参数,在模版中可以使用插值表达式使用这里传递的参数

const render = (req, res) => { 
    // 第 3 步:将 Vue 实例渲染为 HTML renderer.renderToString({ 
    title:'Vue SSR' },(err, html) => { 
    if (err) { 
    return res.status(500).end(JSON.stringify(err.message)) } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) } 

看一下 renderToString() 方法的定义
在这里插入图片描述

可以看出第一个参数接收的是 context 上下文对象,我们扒一下源码的执行,源码会有点难看懂,全局搜一下这个方法,找到的结果有很多,怎么确定是我们要找的方法呢?首先我们要找的是函数的定义而非函数的调用,发现函数定义后,顺着代码往上找,发现下面这个函数是在 createBundleRendererCreator() 方法里面,显然这个函数是用来创建 bundle renderer 的,就是我们要找的目标
在这里插入图片描述
这个函数的关键在于调用了 run() 方法,并且把 context 参数传递了进去
在这里插入图片描述
关于 run() 方法,是调用 createBundleRunner() 方法的返回值
在这里插入图片描述
看一下第一个参数 entry 是怎么来的
在这里插入图片描述
bundle 就是 createBundleRenderer() 方法传递进来的第一个参数
在这里插入图片描述
就是 serverBundle
在这里插入图片描述
entry 是打包结果中的 server-bundle.jsserver-bundle.js 里的代码就是 entry-server.js 入口文件中的代码打包生成的结果
在这里插入图片描述
接下来我们看一下 run() 方法里面做了什么。 run() 方法由下面的一个高阶函数返回,其中通过 if...else... 分为两个分支,我们只看其中的一种情况。接收一个参数,就是 context 参数,我们主要看 context 参数会被用来做什么
在这里插入图片描述
关键就是这句















const res = evaluate(entry, createSandbox(userContext)); 

evaluate() 又是 compileModule() 高阶函数的返回值,这个函数的代码有点长,我只列出有用的部分,主要看传进来的上下文参数。可以看出,上下文参数传递给了 script.runInNewContext() 方法。

function compileModule(files, basedir, runInNewContext) { 
    function evaluateModule(filename, sandbox, evaluatedFiles = { 
    }) { 
    // ... // sandbox:包含用户传进来的context数据 const compiledWrapper = runInNewContext === false ? script.runInThisContext() : script.runInNewContext(sandbox); compiledWrapper.call(m.exports, m.exports, r, m); // ... return res; } return evaluateModule; } 

script 对象:创建了一个脚本对象
在这里插入图片描述

script.runInNewContext(sandbox) :在指定的上下文运行JavaScript代码。

因此,传入的上下文对象,会作为 entry-server.js 中的代码执行时的上下文,也就是说,entry-server.js 导出的函数,其接受的上下文对象,就是 renderer.renderToString() 方法的第一个参数。
entry-server.js 中,回顾一下代码:

// entry-server.js import { 
   createApp} from './app' export default async context => { 
    const { 
   app, router} = createApp() // 设置服务器端 router 的位置 router.push(context.url) // 等到 router 将可能的异步组件和钩子函数解析完 await new Promise(router.onReady.bind(router)) return app } 

需要将上下文对象中的 url 路径加入到路由中
因此,renderer.renderToString() 方法的第一个参数需要加上 url ,即 req 请求实例的 url ,顺便改为 async/await 形式。

const render = async (req, res) => { 
    try { 
    // 第 3 步:将 Vue 实例渲染为 HTML // 执行renderToString的时候,就会调用服务端入口entry-server.js中的 // 方法,将第一个参数传递给entry-server中的方法 // 并返回一个app实例 // 因此这里的第一个参数就是entry-server中方法的入参 const html = await renderer.renderToString({ 
    title: 'Vue SSR', url: req.url }) res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) } catch (err) { 
    res.status(500).end(JSON.stringify(err.message)) } } 
5、适配客户端入口

根据官网文档,在entry-client.js 客户端入口中,需要在挂载 app 之前调用 router.onReady(),确保路由解析完毕再执行渲染方法。
entry-client.js

import { 
    createApp } from './app' // 创建应用实例 const { 
    app, router } = createApp() router.onReady(() => { 
    app.$mount('#app') }) 
6、路由出口

App.vue 中配置 <router-link> 标签和 <router-view> 标签

<template> <div id="app"> <ul> <li> <router-link to="/">Home</router-link> </li> <li> <router-link to="/about">About</router-link> </li> </ul> <router-view></router-view> </div> </template> <script> </script> 

(五)HEAD管理

<head> <meta charset="UTF-8"> <title>{ 
  {title}}</title> </head> 
import VueMeta from "vue-meta"; Vue.use(VueMeta) 
Vue.mixin(({ 
    metaInfo: { 
    title: '默认的title', titleTemplate: '%s - 哈哈哈哈' } })) 
// 获取 meta 配置信息 const meta = app.$meta() // 设置服务器端 router 的位置 router.push(context.url) context.meta = meta; 
<head> <meta charset="UTF-8"> <title>{ 
  {title}}</title> { 
  { 
  { meta.inject().title.text() }}} { 
  { 
  { meta.inject().meta.text() }}} </head> 
<template> <h1>HOME</h1> </template> <script> export default { 
      name: 'HomePage', metaInfo:{ 
      title:'首页' } } </script> 

(六)数据预取与状态

我们使用服务端渲染的一个原因就是可以在页面渲染之前、在服务端渲染期间获取页面初始渲染需要的数据,从而优化首屏加载效果。在服务端渲染期间执行的生命周期是 beforeCeate()created(),获取数据的操作通常是在 created() 生命周期中执行的HTTP请求方法,因为只有在 created() 生命周期中才能获取到 data 数据。在客户端渲染中,我们通常在获取数据之后,通过 this.data.list = res.data 类似的代码,给 this.data 中的字段赋值。但是在服务端渲染期间,是不能通过这样的代码给 this.data 进行赋值操作的,因为此时的 this 上下文对象并不指向应用实例。
Vue SSR 为我们提供了解决方案。即将服务端获取数据的过程放在视图组件之外,在渲染之前就进行数据预取,需要使用到 Vuex 。官网也给我们提供了一系列的示例代码。
① 创建 store/index.js
为了避免多个应用共用同一个 store 容器,需要将容器定义为函数的形式,同样其中的 state 数据也定义成函数的形式
将需要预取的数据初始化到 state 中;修改 state 数据的方法存放到 mutations 中;获取数据的异步请求放在 actions 中;
src/store/index.js




import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export const createStore = () => { 
    return new Vuex.Store({ 
    // 需要预取的数据放在state中 // 使用函数返回的方式,避免单例,数据污染 state: () => ({ 
    posts: [] }), mutations: { 
    setPosts(state, data) { 
    state.posts = data } }, actions: { 
    // 在服务端渲染期间务必让 action 返回一个 Promise // actions 中的方法第一个参数默认是上下文对象 // async 函数默认返回 Promise async getPosts({ 
    commit}) { 
    const { 
   data} = await new Promise((resolve, reject) => { 
    resolve({ 
    status: 200, data: [ { 
   title: '背影', author: '朱自清'}, { 
   title: '匆匆', author: '朱自清'}, ] }) }) commit('setPosts', data) } } }) } 
/ * 同构应用通用的启动入口 * */ import Vue from 'vue' import App from './App.vue' import { 
   createRouter} from "./router"; import VueMeta from "vue-meta"; import { 
    createStore } from "./store"; Vue.use(VueMeta) Vue.mixin({ 
    metaInfo: { 
    title: '默认的title', titleTemplate: '%s - 哈哈哈哈' } }) // 导出一个工厂函数,用于创建新的 // 应用程序、router 和 store 实例 export function createApp () { 
    const router = createRouter() const store = createStore() const app = new Vue({ 
    router, // 把路由挂载到 Vue 根实例 store, // 根实例简单的渲染应用程序组件。 render: h => h(App) }) return { 
    app, router, store } } 
<template> <div> <h1>HOME</h1> <ul> <li v-for="(post, index) in posts" :key="index"> { 
  { post.title }} </li> </ul> </div> </template> <script> import { 
     mapState, mapActions} from 'vuex' export default { 
      name: 'HomePage', metaInfo: { 
      title: '首页' }, data() { 
      return { 
     } }, computed: { 
      ...mapState(['posts']) }, // vue ssr 特殊为服务端渲染提供的钩子函数 serverPrefetch() { 
      // 规则要求这个方法内部需要发起 axios 返回 Promise return this.getPosts() }, methods: { 
      ...mapActions(['getPosts']) } } </script> 
// entry-server.js import { 
   createApp} from './app' export default async context => { 
    // ... context.meta = meta; // 等待 router 把可能得异步组件和钩子函数解析完 await new Promise(router.onReady.bind(router)) // 服务端渲染完毕以后会调用 context.rendered context.rendered = () => { 
    // Renderer 会把 context.state 数据对象内联到页面模版中 // 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = ${context.state} // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端的容器 store 中 context.state = store.state } return app } 

此时看一下浏览器中的效果,服务端渲染的结果中包含 context.state 数据
在这里插入图片描述
② 在客户端解析 context.state
src/entry-client.js
引入 store 容器,const { app, router, store } = createApp()
先不做处理,先看一下客户端的 store 容器,
其中的 state.posts 是空数组
在这里插入图片描述
判断一下 window.__INITIAL_STATE__ 对象如果存在的话,就调用 store.replaceState() 方法,替换容器中的 state 数据







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

(七)总结

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/137739.html

(0)
上一篇 2025-06-18 14:10
下一篇 2025-06-18 14:15

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信