服务器端渲染-Vue SSR搭建

阅读建议:建议通过左侧导航栏进行阅读
文章简介:本文是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/', //请求打包资源的请求前缀/dist
    filename: '[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 ready
  const onReady = new Promise(r => ready = r)

  // 监视打包构建 -> 更新Renderer渲染器
  let template
  let serverBundle
  let clientManifest

  const 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
  //第三方包:chokidar
  chokidar.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
  ]
  // 热更新模式下确保一致的 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)

  return onReady
}

封装处理模块总结:

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

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

    • memfs是一个兼容 Node 中 fs 模块 API的内存文件系统,通过它我们可以轻松的实现把 webpack 构建结果输出到内存中进行管理。
    • 使用webpack-dev-middlewarewebpack-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 返回一个 Promise
      async 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,返回 Promise
    return 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搭建

猜你喜欢

转载自blog.csdn.net/weixin_41269811/article/details/112948477