• 搭建自己的SSR


    Vue SSR介绍

    是什么

    • 官方文档:https://ssr.vuejs.org/
    • Vue SSR(Vue.js Server-Side Rendering) 是 Vue.js 官方提供的一个服务端渲染(同构应用)解 决方案
    • 使用它可以构建同构应用
    • 还是基于原有的 Vue.js 技术栈

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

    使用场景

    在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。

    技术层面:

    • 更快的首屏渲染速度
    • 更好的 SEO

    业务层面:

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

    如何实现Vue SSR

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

    官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中, 也会对Vue SSR有更加深入的了解。

    该方式需要你熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。

    (2)Nuxt.js 开发框架

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

    1.渲染一个Vue实例

    接下来我们以 Vue SSR 的官方文档为参考,来学习一下它的基本用法。

    目标:

    • 了解如何使用 VueSSR 将一个 Vue 实例渲染为 HTML 字符串

    首先我们来学习一下服务端渲染中最基础的工作:模板渲染。 说白了就是如何在服务端使用 Vue 的方式解析替换字符串。 在它的官方文档中其实已经给出了示例代码,下面我们来把这个案例的实现过程以及其中含义演示一 下。

    mkdir my-vue-ssr
    cd my-vue-ssr
    npm init -y
    npm install vue vue-server-renderer nodemon
    
    • 1
    • 2
    • 3
    • 4

    my-vue-ssr/server.js :

    const Vue = require('vue')
    
    const renderer = require('vue-server-renderer').createRenderer()
    
    const app = new Vue({
      template: `
      

    {{ message }}

    `
    , data: { message: '5coder前端开发' } }) renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) })
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    命令行运行:nodemon server.js后,可以看到模板已经被渲染为字符串。

    2.结合到Web服务中

    使用express框架来实现web服务器,yarn add express

    server.js

    /**
     *@date:2022/11/21
     *@Description:server
     */
    
    const Vue = require('vue')
    const express = require('express')
    
    const renderer = require('vue-server-renderer').createRenderer()
    
    const server = express()
    
    server.get('/', (req, res) => {
      const app = new Vue({
        template: `
        

    {{ message }}

    `
    , data: { message: '5coder前端开发' } }) renderer.renderToString(app, (err, html) => { if (err) { return res.status(500).end('Internal Server Error') } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(` Title ${html} `) }) }) server.listen(3000, () => { console.log('Server Running at port 3000...') })
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qcF4iGoF-1669274022011)(http://5coder.cn/img/1668992955_b3b4b7dc77070ae56d89a77f587685c1.png)]

    3.使用HTML模板

    对于页面的模板,还有一种做法,就是将其放到单独的文件中。新建index.template.html文件,并设置特殊注释vue-ssr-outlet。在render中添加template参数。

    index.template.html

    DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Titletitle>
    head>
    <body>
    
    
    body>
    html>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    server.js

    /**
     *@date:2022/11/21
     *@Description:server
     */
    
    const Vue = require('vue')
    const express = require('express')
    const fs = require('fs')
    
    // 传递读取到的index.template.html模板文件流
    const renderer = require('vue-server-renderer').createRenderer({
      template: fs.readFileSync('./index.template.html', 'utf-8'),
    })  
    
    const server = express()
    
    server.get('/', (req, res) => {
      const app = new Vue({
        template: `
        

    {{ message }}

    `
    , data: { message: '5coder前端开发' } }) renderer.renderToString(app, (err, html) => { if (err) { return res.status(500).end('Internal Server Error') } res.setHeader('Content-Type', 'text/html; charset=utf8') // 直接返回html,返回的html会替代index.template.html中的注释内容 res.end(html) }) }) server.listen(3000, () => { console.log('Server Running at port 3000...') })
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    4.在模板中使用外部数据

    页面模板也可以使用外部数据。在renderToString方法中,传递第二个可选参数{title: '5coder'}。在页面模板中使用时,使用模板语法{{ title }}进行使用。需要注意如果需要渲染一段HTML字符串,需要使用{{{ html_str }}}三个括号。

    server.js

    /**
     *@date:2022/11/21
     *@Description:server
     */
    
    const Vue = require('vue')
    const express = require('express')
    const fs = require('fs')
    
    const renderer = require('vue-server-renderer').createRenderer({
      template: fs.readFileSync('./index.template.html', 'utf-8'),
    })
    
    const server = express()
    
    server.get('/', (req, res) => {
      const app = new Vue({
        template: `
        

    {{ message }}

    `
    , data: { message: '5coder前端开发' } }) renderer.renderToString(app, { title: '5coder', content: `

    我是5coder,一个前端开发

    `
    },(err, html) => { if (err) { return res.status(500).end('Internal Server Error') } res.setHeader('Content-Type', 'text/html; charset=utf8') res.end(html) }) }) server.listen(3000, () => { console.log('Server Running at port 3000...') })
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    index.template.html

    DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>{{title}}title>
    head>
    <body>
    
    
    {{{ content }}}
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    5.构建配置-基本思路

    服务端渲染只是把vue实例处理成纯静态的HTML字符串,发送给客户端,对于vue实例来说,这种需要客户护短动态交互的功能,其并没有提供。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7J3pQviL-1669274022013)(http://5coder.cn/img/1669013742_46f1cd652ca0a30e59bc7426ba346cc6.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HLhsIfGM-1669274022013)(http://5coder.cn/img/1669013785_ee65521cee395282396bd9e6df1ca2cf.png)]

    到目前为止,我们还没有讨论过如何将相同的 Vue 应用程序提供给客户端。为了做到这一点,我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack 打包 Vue 应用程序,因为:

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

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

    我们将在后面的章节讨论规划结构的细节 - 现在,先假设我们已经将构建过程的规划都弄清楚了,我们可以在启用 webpack 的情况下编写我们的 Vue 应用程序代码。

    6.构建配置-源码结构

    我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack 打包 Vue 应用程序,因为:

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

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

    现在我们正在使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写, 可以使用 webpack 支持的所有功能。同时,在编写通用代码时,有一些事项要牢记在心。 一个基本项目可能像是这样:

    src
    ├── components
    │ ├── Foo.vue
    │ ├── Bar.vue
    │ └── Baz.vue
    ├── App.vue
    ├── app.js # 通用 entry(universal entry)
    ├── entry-client.js # 仅运行于浏览器
    └── entry-server.js # 仅运行于服务器
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    App.vue

    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    app.js

    app.js 是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js 简单地使用 export 导出一个 createApp 函数:

    import Vue from 'vue'
    import App from './App.vue'
    
    // 导出一个工厂函数,用于创建新的
    // 应用程序、router 和 store 实例
    export function createApp () {
      const app = new Vue({
        // 根实例简单的渲染应用程序组件。
        render: h => h(App)
      })
      return { app }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    entry-client.js:

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

    import { createApp } from './app'
    
    // 客户端特定引导逻辑……
    
    const { app } = createApp()
    
    // 这里假定 App.vue 模板中根元素具有 `id="app"`
    app.$mount('#app')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    截止目前,以上代码还不能运行,原因是需要进行webpack打包后才可以正常使用。

    7.构建配置-安装依赖

    安装依赖

    (1)安装生产依赖

    npm i vue vue-server-renderer express cross-env
    
    • 1
    说明
    vueVue.js 核心库
    vue-server-rendererVue 服务端渲染工具
    express基于 Node 的 Web 服务框架
    cross-env通过 npm scripts 设置跨平台环境变量

    (2)安装开发依赖

    npm i -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core
    @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader urlloader file-loader rimraf vue-loader vue-template-compiler friendly-errorswebpack-plugin
    
    • 1
    • 2
    说明
    webpackwebpack核心包
    webpack-cliwebpack的命令行工具
    webpack-mergewebpack配置信息合并工具
    webpack-node-externals排除webpack中的Node模块
    rimraf基于Node封装的一个跨平台rm -rf工具
    friendly-errors-webpack-plugin友好的 webpack 错误提示
    @babel/core
    @babel/plugin-transform-runtime
    @babel/preset-env
    babel-loader
    Babel 相关工具
    vue-loader
    vue-template-compiler
    处理 .vue 资源
    file-loader处理字体资源
    css-loader处理 CSS 资源
    url-loader处理图片资源

    8.构建配置-webpack配置文件

    配置文件及打包命令

    (1)初始化 webpack 打包配置文件

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

    相关webpack配置可以查看文章Webpack 4Webpack 5

    webpack.base.config.js

    /**
     * 公共配置
     */
    const VueLoaderPlugin = require('vue-loader/lib/plugin')  // 处理.vue资源的插件
    const path = require('path')
    const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')  // 友好的webpack日志输出
    const resolve = file => path.resolve(__dirname, file)
    
    const isProd = process.env.NODE_ENV === 'production'  // 环境变量中的env
    
    module.exports = {
      mode: isProd ? 'production' : 'development',
      output: {
        path: resolve('../dist/'),  // 输出目录
        publicPath: '/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` 文件中的 `
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    配置好出口以后,启动应用:yarn dev

    启动成功,访问页面。

    测试路由导航,可以看到正常工作,那说明我们同构应用中的路由产生作用了。

    现在我们的应用就非常的厉害了,当你首次访问页面的时候,它是通过服务端渲染出来的,服务端渲染拥有了更快的渲染速度以及更好的 SEO,当服务端渲染的内容来到客户端以后被客户端 Vue 结合 VueRouter 激活,摇身一变成为了一个客户端 SPA 应用,之后的页面导航也不需要重新刷新整个页面。这样我们的网站就既拥有了更好的渲染速度,也拥有了更好的用户体验。

    除此之外,我们在路由中配置的异步组件(也叫路由懒加载)也是非常有意义,它们会被分割为独立的chunk(也就是单独的文件),只有在需要的时候才会进行加载。这样就能够避免在初始渲染的时候客户端加载的脚本过大导致激活速度变慢的问题。关于它也可以来验证一下,通过 npm run build 打包构建,我们发现它们确实被分割成了独立的 chunk。然后再来看一下在运行期间这些 chunk 文件是如何加载的。

    你会发现除了 app 主资源外,其它的资源也被下载下来了,你是不是要想说:不是应该在需要的时候才加载吗?为什么一上来就加载了。

    原因是在页面的头部中的带有 preloadprefetch 的 link 标签。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLqZYYaw-1669274022025)(http://5coder.cn/img/1669213082_828a1830948d8dc31cedc6c7a1fc37cb.png)]

    我们期望客户端 JavaScript 脚本尽快加载尽早的接管服务端渲染的内容,让其拥有动态交互能力,但是如果你把 script 标签放到这里的话,浏览器会去下载它,然后执行里面的代码,这个过程会阻塞页面的渲染。

    所以看到真正的 script 标签是在页面的底部的。而这里只是告诉浏览器可以去预加载这个资源。但是不要执行里面的代码,也不要影响网页的正常渲染。直到遇到真正的 script 标签加载该资源的时候才会去执行里面的代码,这个时候可能已经预加载好了,直接使用就可以了,如果没有加载好,也不会造成重复加载,所以不用担心这个问题。

    而 prefetch 资源是加载下一个页面可能用到的资源,浏览器会在空闲的时候对其进行加载,所以它并不一定会把资源加载出来,preload 一定会预加载。所以你可以看到当我们去访问 about 页面的时候,它的资源是通过 prefetch 预取过来的,提高了客户端页面导航的响应速度。

    image-20221123222111125

    好了,关于同构应用中路由的处理,以及代码分割功能就介绍到这里。

    27.管理页面Head内容

    无论是服务端渲染还是客户端渲染,它们都使用的同一个页面模板。

    页面中的 body 是动态渲染出来的,但是页面的 head 是写死的,也就说我们希望不同的页面可以拥有自己的 head 内容,例如页面的 title、meta 等内容,所以下面我们来了解一下如何让不同的页面来定制自己的 head 头部内容。

    官方文档这里专门描述了关于页面 Head 的处理,相对于来讲更原生一些,使用比较麻烦,有兴趣的同学可以了解一下。
    我这里主要给大家介绍一个第三方解决方案:vue-meta

    Vue Meta 是一个支持 SSR 的第三方 Vue.js 插件,可让你轻松的实现不同页面的 head 内容管理。使用它的方式非常简单,而只需在页面组件中使用 metaInfo 属性配置页面的 head 内容即可。

    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    页面渲染出来的结果:

    
    
    	My Example App - Yay!
    	...
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    安装:npm i vue-meta

    在通用入口app.js中通过插件的方式将 vue-meta 注册到 Vue 中。

    import Vue from 'vue'
    import App from './App.vue'
    import VueMeta from "vue-meta";
    import {createRouter} from "./router";
    
    Vue.use(VueMeta)
    
    Vue.mixin({
      metaInfo: {
        titleTemplate: '%s - 拉勾教育'
      }
    })
    
    // 导出一个工厂函数,用于创建新的
    // 应用程序、router 和 store 实例
    export function createApp() {
      // 创建 router 实例
      const router = createRouter()
      const app = new Vue({
        router,  // 把路由挂在到Vue根实例中
        // 根实例简单的渲染应用程序组件。
        render: h => h(App)
      })
      return {app, router}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    然后在服务端渲染入口ertry-server.js模块中适配 vue-meta:

    // entry-server.js
    import { createApp } from './app'
    
    export default async context => {
      // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
      // 以便服务器能够等待所有的内容在渲染前,就已经准备就绪。
      const { app, router } = createApp()
    
      const meta = app.$meta()
    
      // 设置服务器端 router 的位置
      router.push(context.url)
    
      context.meta = meta
    
      // 等到 router 将可能的异步组件和钩子函数解析完
      await new Promise(router.onReady.bind(router))
      return app
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    最后在模板页面index.template.html中注入 meta 信息:

    DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      {{{ meta.inject().title.text() }}}
      {{{ meta.inject().meta.text() }}}
    head>
    
    <body>
      
    body>
    
    html>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    下面就是直接在组件中使用即可:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NyfumoB2-1669274022027)(http://5coder.cn/img/1669215303_650b1de7c5433c1abe1192b39d5fae48.png)]

    当然,还可以定制更多的内容,在官网中看到还可以定制title、titleTemplate、htmlAttrs、headAttrs等。

    image-20221123225720060

    28.数据预取和状态管理-思路分析

    数据预取和状态官方文档

    接下来我们来了解一下服务端渲染中的数据预取和状态管理。

    官方文档中的描述比较枯燥,无法在很短的时间内搞清楚它到底要做什么,所以我们这里通过一个实际的业务需求来引入这个话题。

    我们的需求就是:

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

    这个需求看起来是不是很简单呢?无非就是在页面发请求拿数据,然后在模板中遍历出来,如果是纯客

    户端渲染的话确实就是这样的,但是想要通过服务端渲染的方式来处理的话就比较麻烦了。

    无论如何,我们都要来尝试一下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mz07KTx0-1669274022028)(http://5coder.cn/img/1669215693_469ffc5ad9285655d33ffdb0743d2ec0.png)]

    也就是说我们要在服务端获取异步接口数据,交给 Vue 组件去渲染。

    我们首先想到的肯定是在组件的生命周期钩子中请求获取数据渲染页面,那我们可以顺着这个思路来试一下。

    在组件中添加生命周期钩子,beforeCreate 和 created,服务端渲染仅支持这两个钩子函数的调用。然后下一个问题是如何在服务端发送请求?依然使用 axios,axios 既可以运行在客户端也可以运行在服务端,因为它对不同的环境做了适配处理,在客户端是基于浏览器的XMLHttpRequest 请求对象,在服务端是基于 Node.js 中的 http 模块实现,无论是底层是什么,上层的使用方式都是一样的。

    首先创建Post.vue组件

    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    我们尝试在created生命周期函数中获取数据,运行服务后,我们发现页面确实出现了文章列表页面,但这真的是Post中的created生命周期函数中请求的吗?可以打开浏览器查看,在初始请求post/的时候,打开预览页面,发现文章列表页面并没有被渲染出来,而是在客户端中再次请求后才渲染出来的。所以是服务端的请求没有生效吗,可以打开控制台,发现我们打印的日志也打印出来了。因此得出结论,服务端的createdbeforeCreated生命周期函数并不会响应数据。

    network中可以发现,还有一个topics的请求,我们打开后可以发现,文章数据是客户端自己请求出来的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SVTDepTc-1669274022029)(http://5coder.cn/img/1669216793_b8ccc55db65220ed60507e8af71d2e00.png)]

    这时候查看服务员控制台,我们发现日志打印也成功的输出了。所以印证了以上的现象:服务端的createdbeforeCreated生命周期函数并不会响应数据。

    29.数据预取和状态管理-数据预取

    在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据

    另一个需要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。

    为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container)"中。首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。

    接下来我们就按照官方文档给出的参考来把服务端渲染中的数据预取以及状态管理来处理一下。

    通过官方文档我们可以看到,它的核心思路就是把在服务端渲染期间获取的数据存储到 Vuex 容器中,然后把容器中的数据同步到客户端,这样就保持了前后端渲染的数据状态同步,避免了客户端重新渲染的问题。

    所以接下来要做的第一件事儿就是把 Vuex 容器创建出来。

    (1)通过 Vuex 创建容器实例,并挂载到 Vue 根实例

    安装 Vuex:npm i vuex

    创建 Vuex 容器:

    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) {
            seate.posts = data
          }
        },
    
        actions: {
          // 在服务端渲染器件,务必让action返回Promise
          async getPosts({ commit }) {
            const { data } = await axios.get('https://cnodejs.org/api/v1/topics')
            commit('setPosts', data.data)
          }
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在通用应用入口app.js中将 Vuex 容器挂载到 Vue 根实例:

    import Vue from 'vue'
    import App from './App.vue'
    import VueMeta from "vue-meta";
    import { createRouter } from "./router";
    import { createStore } from "./store";
    
    Vue.use(VueMeta)
    
    Vue.mixin({
      metaInfo: {
        titleTemplate: '%s - 拉勾教育'
      }
    })
    
    // 导出一个工厂函数,用于创建新的
    // 应用程序、router 和 store 实例
    export function createApp() {
      // 创建 router 实例
      const router = createRouter()
      const store = createStore()
      const app = new Vue({
        router,  // 把路由挂在到Vue根实例中
        store,  // 把容器挂在到Vue示例中
        // 根实例简单的渲染应用程序组件。
        render: h => h(App)
      })
      return { app, router, store }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    (2)在组件中使用 serverPrefetch 触发容器中的 action

    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    30.数据预取和状态管理-将预取数据同步到客户端

    在服务端渲染应用入口中将容器状态序列化到页面中

    接下来我们要做的就是把在服务端渲染期间所获取填充到容器中的数据同步到客户端容器中,从而避免两个端状态不一致导致客户端重新渲染的问题。

    • 将容器中的 state 转为 JSON 格式字符串
    • 生成代码: window.__INITIAL__STATE = 容器状态 语句插入模板页面中
    • 【客户端通过 window.__INITIAL__STATE 获取该数据】

    entry-server.js

    // entry-server.js
    import { createApp } from './app'
    
    export default async context => {
      // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
        // 以便服务器能够等待所有的内容在渲染前,
        // 就已经准备就绪。
      const { app, router, store } = createApp()
    
      const meta = app.$meta()
    
      // 设置服务器端 router 的位置
      router.push(context.url)
    
      context.meta = meta
    
      // 等到 router 将可能的异步组件和钩子函数解析完
      await new Promise(router.onReady.bind(router))
    
      context.rendered = () => {
        // Renderer 会把 context.state 数据对象内联到页面模板中
        // 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
        // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中
        context.state = store.state
      }
    
      return app
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rKHaCiTo-1669274022030)(http://5coder.cn/img/1669218378_f4583f1a6b89c07dfd1580a1cadb0449.png)]

    最后,在客户端渲染入口中把服务端传递过来的状态数据填充到客户端 Vuex 容器中:

    entry-client.js

    /**
     * 客户端入口
     */
    import { createApp } from './app'
    
    // 客户端特定引导逻辑……
    
    const { app, router, store } = createApp()
    
    if (window.__INITIAL_STATE__) {
      store.replaceState(window.__INITIAL_STATE__)
    }
    
    router.onReady(() => {
      app.$mount('#app')
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    客户端更新问题:

    ...
      mounted () {
        if (!this.posts.length) {
          this.$store.dispatch('getPosts')
        }
      },
      beforeRouteLeave (to, from, next) {
        this.$store.commit('setPosts', [])
        next()
      }
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    31.服务端渲染优化

    这里主要针对是服务端层面的优化。尽管 Vue 的 SSR 速度相当快,但由于创建组件实例和虚拟 DOM 节点的成本,它无法与纯基于字符串的模板的性能相匹配。在 SSR 性能至关重要的情况下,明智地利用缓存策略可以极大地缩短响应时间并减少服务器负载。

    缓存能够更快的将内容发送给客户端,提升 web 应用程序的性能,同时减少服务器的负载。

    页面缓存

    官方文档中介绍的那样,对特定的页面合理的应用 micro-caching 能够大大改善服务器处理并发的能力(吞吐率 RPS )。但并非所有页面都适合应用 micro-caching 缓存策略,我们可以将资源分为三类:

    • 静态资源:如 jscssimages 等。
    • 用户特定的动态资源:不同的用户访问相同的资源会得到不同的内容。
    • 用户无关的动态资源:任何用户访问该资源都会得到相同的内容,但该内容可能在任意时间发生变化,如博客文章。

    只有“用户无关的动态资源”适合应用 micro-caching 缓存策略。

    • https://github.com/isaacs/node-lru-cache

    安装依赖:npm i lru-cache

    server.js

    const express = require('express')
    const fs = require('fs')
    const { createBundleRenderer } = require('vue-server-renderer')
    const setupDevServer = require('./build/setup-dev-server')
    const LRU = require('lru-cache')
    const cache = new LRU({
      max: 100,
      maxAge: 10000 // Important: entries expires after 1 second.
    })
    const isCacheable = req => {
      console.log(req.url)
      if (req.url === '/posts') {
        return true
      }
    }
    const server = express()
    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 template = fs.readFileSync('./index.template.html', 'utf-8')
      const clientManifest = require('./dist/vue-ssr-client-manifest.json')
      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 cacheable = isCacheable(req)
        if (cacheable) {
          const html = cache.get(req.url)
          if (html) {
            return res.end(html)
          }
        }
        const html = await renderer.renderToString({
          title: '拉勾教育',
          meta: `
    
    `,
          url: req.url
        })
        res.setHeader('Content-Type', 'text/html; charset=utf8')
        res.end(html)
        if (cacheable) {
          cache.set(req.url, html)
        }
      } catch (err) {
        res.status(500).end('Internal Server Error.')
      }
    }
    // 服务端路由设置为 *,意味着所有的路由都会进入这里
    server.get('*', isProd
      ? render
      : async (req, res) => {
        // 等待有了 Renderer 渲染器以后,调用 render 进行渲染
        await onReady
        render(req, res)
      }
    )
    server.listen(3000, () => {
      console.log('server running at port 3000.')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    Gzip 压缩

    注意事项:

    • 默认的过滤器功能使用 compressible 模块来确定 res.getHeader(‘Content-Type’)是否可压缩。

    组件级别缓存

  • 相关阅读:
    SQL通用语法与DDL操作
    都2022年了不会还有人不懂ajax吧(明天更新省市区三级联动)
    Apache mod_proxy_ajp链接Tomcat
    【C++】unordered_map与unorder_set的封装(哈希桶)
    《86盒应用于家居中控》——实现智能家居的灵动掌控
    [Linux入门]---文本编辑器vim使用
    【暑期每日一题】洛谷 P6153 询问
    确定Mac\Linux系统的架构类型是 x86-64(amd64),还是 arm64 架构
    小程序进阶-env(safe-area-inset-bottom)的使用
    自己动手写java虚拟机(第一话)
  • 原文地址:https://blog.csdn.net/weixin_42122355/article/details/128019231