Vue服务端渲染SSR

663 阅读6分钟

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

  1. 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
  2. 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备 使用SSR之前,需要考虑以下几点:
  • 开发条件所限:浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library)可能需要特殊处理,才能在服务器渲染应用程序中运行
  • 涉及构建设置和部署的更多要求
  • 更多的服务器端负载

2.服务端渲染的过程

  • 有别于jspphp等技术,前端领域的服务端渲染还有一层概念– 同构技术isomorphic
  • 前后端同构,实质上是指前端脚本代码和后端服务器代码使用同一份代码,更广泛的含义是指复用同一套逻辑,并且最终的输出要保持前后端一致
  • 同构技术,使前端在维持现有的技术栈和开发模式的前提下,即可以实现服务端渲染,也能继续享受SPA带来的红利。

3.Web方案横向对比

页面渲染方案工作原理首屏时间弱网扩展性
SPA单页面应用;采用前端框架,如reactvue等,通过http请求把动态数据拉取到客户端后完成页面渲染的过程请求入口html + 静态资源+ 接口数据+ 渲染差,渲染过程全依赖数据请求好,Web 的开放性
Pre-RenderSPA的改造,静态直出无接口数据的页面SPA次差,首次请求html有骨架好,同上
离线包基于hybird模式,前端文件以离线文件包的形式被提前下载到客户端,通过http请求把动态数据拉取到客户端后完成页面渲染的过程请求接口数据+ 页面渲染好,只需要请求接口数据差,依赖App
SSR服务端直出业务数据和页面内容,形式上类似jspphp,技术上采用nodejs解决方案请求页面,直出内容,数据请求在内网完成次好,页面内容直出,无抖动好,Web 的开放性

4.基础

  • (1)app.js入口文件
    • app.js是我们的通用entry,它的作用就是构建一个Vue的实例以供服务端和客户端使用
    • 注意一下,在纯客户端的程序中我们的app.js将会挂载实例到dom中
    • 而在ssr中这一部分的功能放到了Client entry中去做了。
  • 两个entry
    • 接下里我们来看Client entryServer entry,这两者分别是客户端的入口和服务端的入口。
    • Client entry的功能很简单,就是挂载我们的Vue实例到指定的dom元素上;
    • Server entry是一个使用export导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。这个函数会在每一次的渲染中重复的调用。
  • webpack打包构建
    • 然后我们的服务端代码和客户端代码通过webpack分别打包,生成Server BundleClient Bundle
    • 前者会运行在服务器上通过node生成预渲染的HTML字符串,发送到我们的客户端以便完成初始化渲染;
    • 而客户端bundle就自由了,初始化渲染完全不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,使其变成由Vue动态管理的DOM,以便响应后续数据的变化。

5.注意事项

  1. 避免状态单例

    • 为每个请求创建一个新的根 Vue 实例:这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染
  2. 使用webpack的源码结构

src
├── components
│   ├── Foo.vue
│   ├── Bar.vue
│   └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器,客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中
└── entry-server.js # 仅运行于服务器
  1. 使用 vue-router的路由

    • 我们的服务器代码使用了一个 * 处理程序,它接受任意 URL。这允许我们将访问的 URL 传递到我们的 Vue 应用程序中,然后对客户端和服务器复用相同的路由配置
    • 类似于 createApp,我们也需要给每个请求一个新的 router 实例
  2. 代码分割

    • 应用程序的代码分割或惰性加载,有助于减少浏览器在初始渲染中下载的资源体积,可以极大地改善大体积 bundle 的可交互时间
    • 需要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子
  3. 数据预取存储容器

    • 如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。
    • 在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据
    • 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。
    • 为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container))"中。
      • 首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。
      • 此外,我们将在 HTML中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态

6.SSR本地调试命令:

可在document目录下新建一个chromeData文件夹,用来打开一个新的chrome页面(disable网络安全)来跨域调试

open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir=/Users/username/Documents/chromeData/ --disable-features=CrossSiteDocumentBlockingIfIsolating

7.框架选择

  • Nuxt.jsNuxt 是一个基于 Vue 生态的更高层的框架,为开发服务端渲染的 Vue 应用提供了极其便利的开发体验。更酷的是,你甚至可以用它来做为静态站生成器。
  • Quasar Framework SSR + PWA:Quasar 是一个基于 Node.jswebpack 的开发环境,它可以通过一套代码完成 SPA、PWA、SSR、Electron、Capacitor 和 Cordova 应用的快速开发。

参考链接:解密Vue SSR

8.Nuxt

views.png

  • 重新加载当前页:
<template>
  <div>
    <div>{{ content }}</div>
    <button @click="refresh">Refresh</button>
  </div>
</template>

<script>
  export default {
    asyncData() {
      return { content: 'Created at: ' + new Date() }
    },
    methods: {
      refresh() {
        this.$nuxt.refresh()
      }
    }
  }
</script>
  • nuxt.config.js
const path = require('path')
const webpack = require('webpack')
const basePath = ''

module.exports = {
  mode: 'universal',
  typescript: {
    typeCheck: {
      eslint: true
    }
  },
  /* 
  ** This option lets you configure the connection variables for the server instance of your Nuxt.js application.
  */
  server: {
    port: 3000, // default: 3000
    host: '0.0.0.0' // default: localhost
  },
  /* 
  ** This option lets you define the source directory of your Nuxt.js application. 
  */
  srcDir: 'client/',
  head: {
    meta: [
      { charset: 'utf-8' },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1, user-scalable=no'
      },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: basePath + '/favicon.ico' }
    ]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [
    '~/assets/style/index.less'
  ],
  /*
   ** Plugins to load before mounting the App , This option lets you define JavaScript plugins that should be run before instantiating the root Vue.js application.
   */
  plugins: [
    { src: '~/plugins/inject-redirect', ssr: false },
    { src: '~/plugins/vant', ssr: true },
    { src: '~/plugins/touchable', ssr: false },
    '~/plugins/filter'
  ],

  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module',
    '@nuxt/typescript-build'
  ],
  /*
   ** Nuxt.js modules   With this option you can add Nuxt.js modules to your project.
   */
  modules: ['@nuxtjs/pwa'],
  /*
   ** Build configuration,This option lets you configure various settings for the `build` step, including `loaders`, `filenames`, the `webpack` config and `transpilation`
   */
  build: {
    transpile: [/vant.*?less/],
    babel: {
      plugins: [
        ['import', {
          libraryName: 'vant',
          style: (name) => {
            return `${name}/style/less.js`
          }
        }, 'vant']
      ]
    },
    loaders: {
      // VantUI 定制主题配置
      less: {
        javascriptEnabled: true, // 开启 Less 行内 JavaScript 支持
        modifyVars: {
          hack: `true; @import "${path.join(
            __dirname,
            './client/assets/style/vant-ui.less'
          )}";`
        }
      }
    },
    /*
     ** You can extend webpack config here
     */
    postcss: {
      plugins: {
        'postcss-pxtorem': {
          rootValue: 50,
          unitPrecision: 5,
          propList: ['*', '!border*']
        },
        'postcss-import': {},
        'postcss-url': {},
        'postcss-preset-env': this.preset,
        cssnano: { preset: 'default' } // disabled in dev mode
      },
      preset: {
        autoprefixer: {
          grid: true
        }
      }
    },
    // /**
    //  *  You can extend webpack config here     https://nuxtjs.org/api/configuration-build/#publicpath
    //  */
    extend (config, ctx) {
      // Run ESLint on save
      if (ctx.isDev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(ts|js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/,
          options: {
            fix: true
          }
        })
      }
      // build
      if (!ctx.isDev) {
        config.output.filename = '[name].[chunkhash:8].js'
        config.output.chunkFilename = '[name].[chunkhash:8].chunk.js'
      } else {
        config.devtool = 'eval-source-map'
      }
      config.plugins.push(new webpack.HashedModuleIdsPlugin())
    }
  },
  /*
   ** Router extendRoutes, With the `router` option you can overwrite the default Nuxt.js configuration of Vue Router.
   */
  router: {
    base: basePath
  },
  workbox: {
    workboxURL: `${basePath}workbox/4.3.1/workbox-sw.js`,
    config: {
      modulePathPrefix: `${basePath}workbox/4.3.1/`,
      debug: false
    }
  }
}