SPA单页面应用和SSR服务端渲染对比

2021
10/03

1.什么是单页面应用(SPA)?

整个web项目只有一个页面,使用路由机制进行组件之间的切换。

优点:客户端渲染、数据传输量小、减少服务器端压力、交互/响应速度快、前后端完全分离。

缺点:首屏加载慢、对SEO不友好,不利于搜索引擎收录和排名。

2.什么是服务端渲染(SSR)?

将组件或页面通过服务器端生成HTML字符串,再将整体页面数据发送到浏览器端渲染。

优点:对于SEO友好、首屏加载速度快。

缺点:页面重复加载次数高、开发效率低、数据传输量大、服务器压力大。

3.SPA、SSR分别适合什么样的应用场景?

SPA:对项目性能要求高、页面加载速度快、要求客户端渲染、对SEO要求低。

SSR:对项目SEO要求高、首次打开响应速度快。

4.SPA与SSR本质区别是什么?

1.传输数据不同:

SPA与服务器通过API接口拿到局部数据(json对象),然后由客户端拼接为html进行渲染。

SSR直接请求页面拿到整个页面的html进行渲染。

2.SEO优化问题:

SPA在客户端源代码中无法看到动态渲染的html,无法被爬虫爬到。而SSR可以在源代码看到整个页面所有html数据。

细说后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)

2019
18/05

前端与后端渲染方式的发展大致经历了这样几个阶段:后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)。

1. 后端模板渲染

前端与后端最初的渲染方式是后端模板渲染,就是由后端使用模板引擎渲染好 html 后,返回给前端,前端再用 js 去操作 dom 或者渲染其他动态的部分。

这个过程大致分成以下几个步骤:

说明:

  1. 前端请求一个地址 url
  2. 后端接收到这个请求,然后根据请求信息,从数据库或者其他地方获取相应的数据
  3. 使用模板引擎(如 java>jspphp>smarty)将这些数据渲染成 html
  4. 将 html 文本返回给前端

在这个过程中,前端的 html 代码需要嵌入到后端代码中(如 javaphp),并且在很多情况下,前端源代码和后端源代码是在一个工程里的。

所以,不难看出,这种方式的有这样的几个不足:

  1. 前后端杂揉在一起,不方便本地开发、本地模拟调试,也不方便自动化测试
  2. 前端被约束在后端开发的模式中,不能充分使用前端的构建生态,开发效率低下
  3. 项目难以管理和维护,也可能会有前后端职责不清的问题

尽管如此,但因为这种方式是最早出现的方式,并且这种渲染方式有一个好处,就是前端能够快速呈现服务器端渲染好的页面,而不用等客户端渲染,这能够提供很好的用户体验与 SEO 友好,所以当下很多比较早的网站或者需要快速响应的展示性网站仍然是使用这种方式。

2. 客户端渲染

随着前端工程化与前后端分离的发展,以及前端组件化技术的出现,如 react、vue 等,客户端渲染已经慢慢变成了主要的开发方式了。

与后端模板渲染刚好相反,客户端渲染的页面渲染都是在客户端进行,后端不负责任何的渲染,只管数据交互。

这个过程大致分成以下几个步骤:

CSR

说明:

  1. 前端请求一个地址 url
  2. 后端接收到这个请求,然后把相应的 html 文件直接返回给前端
  3. 前端解析 js 后,然后通过 ajax 向后台获取相应的数据
  4. 然后由 js 将这些数据渲染成页面

这样一来,前端与后端将完全解耦,数据使用全 ajax 的方式进行交互,如此便可前后端分离了。

其实,不难看出,客户端渲染与前后端分离有很大的好处:

  1. 前端独立出来,可以充分使用前端生态的强大功能
  2. 更好的管理代码,更有效率的开发、调试、测试
  3. 前后端代码解耦之后,能更好的扩展、重构

所以,客户端渲染与前后端分离现在已经是主流的开发方式了。

但这种方式也有一些不足:

  1. 首屏加载缓慢,因为要等 js 加载完毕后,才能进行渲染
  2. SEO 不友好,因为 html 中几乎没有可用的信息

3. node 中间层

为了解决客户端渲染的不足,便出现了 node 中间层的理念。

传统的 B/S 架构中,是 浏览器->后端服务器->浏览器,上文所讲的都是这种架构。

而加入了 node 中间层之后,就变成 浏览器->node->后端服务器->node->浏览器。

这个过程大致分成以下几个步骤:

SSR

说明:

  1. 前端请求一个地址 url
  2. node 层接收到这个请求,然后根据请求信息,向后端服务器发起请求,获取数据
  3. 后端服务器接收到请求,然后根据请求信息,从数据库或者其他地方获取相应的数据,返回给 node 层
  4. node 层根据这些数据渲染好首屏 html
  5. node 层将 html 文本返回给前端

一个典型的 node 中间层应用就是后端提供数据、node 层渲染模板、前端动态渲染。

这个过程中,node 层由前端开发人员掌控,页面中哪些页面在服务器上就渲染好,哪些页面在客户端渲染,由前端开发人员决定。

这样做,达到了以下的目的:

  1. 保留后端模板渲染、首屏快速响应、SEO 友好
  2. 保留前端后分离、客户端渲染的功能(首屏服务器端渲染、其他客户端渲染)

但这种方式也有一些不足:

  1. 增加了一个中间层,应用性能有所降低
  2. 增加了架构的复杂度、不稳定性,降低应用的安全性
  3. 对开发人员要求高了很多

4. 服务器端渲染(ssr)

大部分情况下,服务器端渲染(ssr)与 node 中间层是同一个概念。

服务器端渲染(ssr)一般特指,在上文讲到的 node 中间层基础上,加上前端组件化技术在服务器上的渲染,特别是 react 和 vue。

react、vue、angular 等框架的出现,让前端组件化技术深入人心,但在一些需要首屏快速加载与 SEO 友好的页面就陷入了两难的境地了。

因为前端组件化技术天生就是给客户端渲染用的,而在服务器端需要被渲染成 html 文本,这确实不是一件很容易的事,所以服务器端渲染(ssr)就是为了解决这个问题。

好在社区一直在不断的探索中,让前端组件化能够在服务器端渲染,比如 next.js、nuxt.js、razzle、react-server、beidou 等。

一般这些框架都会有一些目录结构、书写方式、组件集成、项目构建的要求,自定义属性可能不是很强。

以 next.js 为例,整个应用中是没有 html 文件的,所有的响应 html 都是 node 动态渲染的,包括里面的元信息、 css、js 路径等。渲染过程中, next.js 会根据路由,将首页所有的组件渲染成 html,余下的页面保留原生组件的格式,在客户端渲染。

5. SSR优缺点

1)优点:

  • 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
  • 首屏加载更快:SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

2)缺点:

  • 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
  • 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。

6. spa & ssr选择

  1. 不需要首屏快速加载、SEO 友好的,用全客户端渲染。
  2. 需要首屏快速加载、SEO 友好的,如果用了如 react、 vue 等组件化技术,将不得不用 node 中间层与服务器端渲染。
  3. 如果技术团队不支持,不建议在需要首屏快速加载、SEO 友好的地方使用如 react、 vue 等组件化技术。
  4. 前后端分离之后也可以做后端模板渲染,这样前端的调试可以搭配 handlebars、ejs 等模板引擎进行本地调试,而后端的调试则需要到测试机了。

Vue 服务端渲染简介和实践

2019
18/05

SSR , Server Side Render的简称, 服务端渲染. 首先服务端渲染并不神秘, 在 ajax 兴起之前, 所有 web 应用都是服务端渲染, 服务器直接返回 html 文本给浏览器, 用户操作比如在 A 页面注册提交表单, 跳转到B 页面, 服务器需要返回两个页面. 这样的弊端显而易见, 加大了服务器的消耗, 随着 JavaScript 的发展, ajax 技术的出现, 客户端的操作通过请求接口的形式与服务器交互, 服务器不用返回整个页面, 而只是数据. 后来出现了后端模版, 比如 jsp, cshtml

<table>
    <c:forEach var="data" items="${datas}" varStatus="loop">
    <tr>
        <td>${loop.index + 1}</td>
        <td>${data.time}</td>
        <td>${data.msg}</td>
    </tr>
    </c:forEach>
</table>

用户在首次进入页面的时候, 通过服务端渲染给出 html, 用户操作使用 ajax 与服务端交互, 动静混合的形式.

后来随着 JavaScript 的发展, 前端模版和近年SPA 框架的发展, 呈现页面完全静态化, 动态内容交给前端(Javscript), 服务器只提供数据(一般以 json 的形式). 用户看到页面, 大致上需要如下过程(忽略 cdn 等)

1.浏览器加载所有静态资源(html,css,js等)–> 2.js 发起请求获取数据 –> 3.渲染页面 –> 呈现用户

好处是前后端完全分离(开发部署), 各司其职, 同时也节约服务器资源(只有数据交互).
此时用户所获取的 html 只是如下的片段:

<!DOCTYPE html><html class=has-full>
<head><meta charset=utf-8>
<title>人事管理系统</title>
<link href=/static/css/app.0c7e1e58d27be30db979adc44f7cd4eb.css rel=stylesheet>
</head>
<body><div id=app></div>
<script type=text/javascript src=/static/js/manifest.ca2797291add890279b8.js></script>
<script type=text/javascript src=/static/js/vendor.ee32e29412ede428634a.js></script>
</body>
</html>

其中2, 3 步骤是最耗费时间的, 因为获取数据受到用户网络, 服务器带宽等条件的显示, 而且可以通过业务数据再次加载一些静态资源. 随着业务的复杂, 打包处理 bundle 逐渐增大, 用户看到页面的时间(首屏), 即内容到达时间(time-to-content)将延长, 降低用户体验, 对电商网站流量转换率影响比较明显.

ssr 所做的事情

借用 react ssr 的两张图说明问题( vue 的 ssr 和 react 同理)

SSR服务端渲染:
SSR
CSR客户端渲染:
CSR
最大的差异是, 服务端直接返回的 渲染完毕html 页面, 获取业务数据, 填充业务组件都在服务端完成, 用户能够更快的看到页面内容, 同时也有利于爬虫抓取(SEO).

但是 ssr 也不是万能的, 需要 node 服务器, 很耗费性能, 需要做好缓存和优化, 相当于空间换时间. 全站 ssr 明显不可取, 现在流行较多的是 首屏 ssr ,甚至 首屏部分 ssr

参考资料
前后端渲染之争
Vue 全站服务器渲染 SSR 实践

Nuxt

有上述可知, ssr 应该有两个代码入口, 服务端和客户端, 通过 webpack 打包之后为别为 server-bundle 和 client-bundle, 页面第一次呈现, 通过 server-bundle , 获取业务数据, 填充数据, 渲染组件, 发送 html 给浏览器, 之后用户操作通过 client-bundle, 依旧是在浏览器范围内.

从零开始配置vue ssr 是比较困难的, 幸好有 nuxt api.

nuxt预设了 vue 服务端渲染的一些配置, 约定大于配置,

pages 路由, vuex 模块划分

Quick start

vue init nuxt-community/starter-template my-project
# 安装依赖
yarn install
# 开发模式运行
yarn run dev
# build 生成环境
yarn run build
# 运行已 build 的代码
yarn run start

目录结构
layouts, middleware, pages, static, store 目录必须存在

配置文件nuxt.config.js,
尽可能罗列了 nuxt.config.js可配置项和默认值

module.exports = {
  cache: {},
  css: [
    // 加载一个 node.js 模块
    //  'hover.css/css/hover-min.css',
    //  // 同样加载一个 node.js 模块,不过我们定义所需的预处理器
    //  { src: 'bulma', lang: 'sass' },
    //  // 项目中的 CSS 文件
    //  '~assets/css/main.css',
    //  // 项目中的 Sass 文件
    //  { src: '~assets/css/main.scss', lang: 'scss' } // 指定 scss 而非 sass
  ],

  // 默认 true
  dev: process.env.NODE_ENV !== 'production',

  // 创建环境变量
  env: {},

  // 配置 Nuxt.js 应用生成静态站点的具体方式。
  genetate: {
    dir: '',
    minify: '',
    routes: [],
  },

  /*
    * vue-meta
    * Headers of the page
    */
  head: {
    title: 'ssr-vue',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt.js project' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress bar color
  */
  loading: { color: '#3B8070' },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  },

  performance: {
    gzip: false,
    prefetch: true
  },

  // 引入 Vue.use 的插件
  plugins: [],

  // 默认当前路径
  rootDir: process.cwd(),

  router: {
    base: '',
    mode: 'history',
    linkActiveClass: 'nuxt-link-active',
    scrollBehavior: (to, from, savedPosition) => {
      // savedPosition 只有在 popstate 导航(如按浏览器的返回按钮)时可以获取。
      if (savedPosition) {
        return savedPosition
      } else {
        let position = {}
        // 目标页面子组件少于两个
        if (to.matched.length < 2) {
          // 滚动至页面顶部
          position = { x: 0, y: 0 }
        }
        else if (to.matched.some((r) => r.components.default.options.scrollToTop)) {
          // 如果目标页面子组件中存在配置了scrollToTop为true
          position = { x: 0, y: 0 }
        }
        // 如果目标页面的url有锚点,  则滚动至锚点所在的位置
        if (to.hash) {
          position = { selector: to.hash }
        }
        return position
      }
    },
    // default
    middleware: 'user-agent',
    // 扩展路由
    extendRoutes: () => {},

    // 默认同 rootDir
    srcDir: this.rootDir,

    transition: {
      name: 'page',
      mode: 'out-in'
    },
    watchers: {
      chokidar: {}, // 文件监控
      webpack: {
        aggregateTimeout: 300,
        poll: 1000
      }
    }
  }
}

pages 路由

路由, 约定大于配置, 支持动态, 嵌套, 动态嵌套路由, 过渡效果和中间件,
通过文件夹目录名称, 组件名称, 生成路由配置,
默认的 transitionName 为 page, 可在 assets 中添加全局的过渡效果

路由中间件:
在匹配页面之前执行;
nuxt.config.js –> 执行middleware –> 匹配布局 –> 匹配页面

路由中间件

视图

模版

默认的 html 模版: 应用根目录下的 app.html 文件, 没有改文件, 则采用默认的模版

页面

页面就是我们最熟悉的.vue文件, 单文件组件, 但是 nuxt 有一些不同的地方, 混入了 asyncData, fetch, head 三个方法, 还有 指定 layout, transition, scrollToTop, validate, middleware配置项

asyncData 和 fetch都是获取数据的方法, 不同的是, asyncData是请求接口的数据, fetch 是用于填充store 数据, 不会设置组件的数据, 两者都在页面加载之前调用

代码build 之后有服务端和客户端两个入口, build 之后对应为 client.js 和 server.js,
asyncData,和 fetch 第一次在服务端执行, 第二次切换页面后在浏览器执行

head 方法相关使用方法, 可参考 vue-meta
nuxt.config.js 默认定义了全局的 mota 标签

页面相关 API

异步数据

通过 asyncData 获取异步数据, 第一个参数为上下文对象 context, 推荐使用 promise 或者 async/await

asyncData
context 对象

资源文件

项目中编写 js 文件和普通项目一样. 通过 webpack 处理, 对于一些不需要 webpack loader 处理的静态资源文件, 必须放在项目根目录下的static文件夹中, 项目中直接使用/引用相关资源,
需要 webpack 处理的 静态文件,

可以覆盖 nuxt.config.js 中build 字段中 loaders 中 url-loader 或者 file-loader的 默认配置, 进行自定义设置

loaders 配置

[
  {
    test: /\.(png|jpe?g|gif|svg)$/,
    loader: 'url-loader',
    query: {
      limit: 1000, // 1KO
      name: 'img/[name].[hash:7].[ext]'
    }
  },
  {
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
    loader: 'url-loader',
    query: {
      limit: 1000, // 1 KO
      name: 'fonts/[name].[hash:7].[ext]'
    }
  }
]

npm 模块 & 插件

服务端渲染, node 直接返回 html 给客户端, 所以 npm模块和插件应在 整个应用 实例化之前运行, 并且必须支持 ssr(服务端获取不到 window 等对象), 组件的生命周期只有beforeCreate 和created在client 和 server 均调用. 其余钩子函数只在 client 调用.

node_modules 中安装的模块在组件中可以直接使用

import someModules from 'some-module'

避免多个组件引用同一个模块重复打包的问题, 需要在 nuxt.config.js 中配置 vendor(路径和 plugin一致), 尽量将第三方模块打包至单独的文件中去

module.exports = {
    build: {
        vendor: ['path/to/your/modules'],
    },
    plugins: ['path/to/your/modules']
}

可以区分server 端插件和 client 端plugin, ssr 为 true 则只在服务端使用, 为 false 则反之

plugins: [{src: 'path/to/your/modules', ssr: false}]

ssr 部署及 pm2 的使用

使用 nuxt 官方模版新建的项目, 可以运行yarn build 命令进行构建, build 模板为项目根路径下的 .nuxt 文件夹, 其中 client.js 为客户端入口, server.js 为服务端入口, 通过命令nuxt start 启动 build 之后的代码.

通过简单命令启动在生成环境并不是好办法, 因此我们需要工具, 推荐pm2;

简单来说, PM2是node编写的, 进程管理工具,可以利用它来简化很多应用管理的繁琐任务,如性能监控、自动重启、负载均衡等。

安装之类的不在赘述, 具体参考文档 pm2

简单示例

在项目根目录新建 start.sh, 内容如下

#! /bin/bash
nuxt build
nuxt start

同样在项目根目录新建pm2.config.js, 内容如下

module.exports = {
  apps: [
    {
      name: 'test',
      script: './start.sh',
      env: {
        NODE_ENV: 'development'
      },
      env_production: {
        NODE_ENV: 'production'
      }
    }
  ]
}

参数设置可参考pm2 文档, 这只是个简单示例, 正式环境需要设置集群模式等
运行

pm2 start /path/to/pm2.config.js

即可开启 ssr 服务, 正式环境需要 nginx 代理到80或者443端口

作者:ethan_you
来源:简书