服务端渲染SSR

118 阅读10分钟

一,补充:浏览器渲染HTML页面过程

在这里插入图片描述

二,客户端渲染CSR

  • 也叫动态渲染。

如今我们大部分WEB应用都是使用 JavaScript 框架(Vue、React、Angular)进行页面渲染的,也就是说,在执行 JavaScript 脚本的时候,HTML页面已经开始解析并且构建DOM树了(浏览器工作原理内容),JavaScript 脚本只是动态的改变 DOM 树的结构,使得页面成为希望成为的样子,这种渲染方式叫动态渲染,也可以叫客户端渲染(client side rende)。

三,服务端渲染SSR

理解

  • 服务端渲染就是在浏览器请求页面URL的时候,服务端将我们需要的HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出希望的 DOM 树并展示到页面中。这个服务端组装HTML的过程,叫做服务端渲染。

在这里插入图片描述

服务的渲染的历程

最初只有服务端渲染

在没有AJAX的时候,也就是web1.0时代,几乎所有应用都是服务端渲染(此时服务器渲染非现在的服务器渲染),那个时候的页面渲染大概是这样的

image-20211013183232887

出现客户端渲染(出现前后端分离)

前后端分离之后,网页开始被当成了独立的应用程序(SPA,Single Page Application),前端团队接管了所有页面渲染的事,后端团队只负责提供所有数据查询与处理的API

在这里插入图片描述

又出现服务端渲染

随着单页应用(SPA)的发展,程序员们渐渐发现 SEO(Search Engine Optimazition,即搜索引擎优化)出了问题,JavaScript 脚本也不断的臃肿起来,出现首屏渲染慢的问题,使用 nodejs 在服务器进行页面的渲染

在这里插入图片描述

相比于客户端渲染,这里多了在前端服务中向后端服务器请求数据然后组装html返回给浏览器,使其在第一次加载页面时的HTML不是空壳的

四,服务端渲染的利弊

优点

利于SEO

有利于SEO,别人使用搜索引擎搜索相关的内容时,你的网页排行能靠得更前。

有利于爬虫来爬你的页面

爬虫分为低级合高级

  • 低级爬虫:只请求URL,URL返回的HTML是什么内容就爬什么内容。(如果是客户端渲染就没什么鸟用了,因为放回的HTML只是一个空壳)
  • 高级爬虫:请求URL,加载并执行JavaScript脚本渲染页面,爬JavaScript渲染后的内容
首屏加载事件短

相对于客户端渲染,服务端渲染在浏览器请求URL之后已经得到了一个带有数据的HTML文本,浏览器只需要解析HTML,直接构建DOM树就可以。而客户端渲染,需要先得到一个空的HTML页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的 JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。

在这里插入图片描述

缺点

并不是所有的WEB应用都必须使用SSR,这需要开发者自己来权衡,因为服务端渲染会带来以下问题:

  • 代码复杂度增加。为了实现服务端渲染,应用代码中需要兼容服务端和客户端两种运行情况,而一部分依赖的外部扩展库却只能在客户端运行,需要对其进行特殊处理,才能在服务器渲染应用程序中运行。
  • 需要更多的服务器负载均衡。由于服务器增加了组装渲染HTML的需求,使得原本只需要输出静态资源文件的nodejs服务(前端服务器),新增了数据获取的IO和渲染HTML的CPU占用,如果流量突然暴增,有可能导致服务器down机,因此需要使用响应的缓存策略和准备相应的服务器负载。
  • 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。

所以在使用服务端渲染SSR之前,需要开发者考虑投入产出比,比如大部分应用系统都不需要SEO,而且首屏时间并没有非常的慢,如果使用SSR反而小题大做了。

五,同构

假如我们需要在项目中使用服务端渲染,我们需要做什么呢?那就是同构我们的项目。

同构的定义

在服务端渲染中,有两种页面渲染的方式:

  • 前端服务器通过请求后端服务器获取数据并组装HTML返回给浏览器,浏览器直接解析HTML后渲染页面(首次渲染)
  • 浏览器在交互过程中,请求新的数据并动态更新渲染页面(之后渲染)

这两种渲染方式有一个不同点就是,一个是在服务端中组装html的,一个是在客户端中组装html的,运行环境是不一样的。所谓同构,就是让一份代码,既可以在服务端中执行,也可以在客户端中执行,并且执行的效果都是一样的,都是完成这个html的组装,正确的显示页面。也就是说,一份代码,既可以客户端渲染,也可以服务端渲染。

同构的实现

对于同构应用来说,我们必须实现客户端与服务端的路由、模型组件、数据模型的共享。

在这里插入图片描述

我们思考一个应用中一个页面的组成,假如我们使用的是Vue.js,当我们打开一个页面时,首先是打开这个页面的URL,这个URL,可以通过应用的路由匹配,找到具体的页面,不同的页面有不同的视图,那么,视图是什么?从应用的角度来看,视图 = 模板 + 数据,那么在 Vue.js 中, 模板可以理解成组件,数据可以理解为数据模型,即响应式数据

六,实践

实现基础的NODEJS服务端渲染

const express = require('express');
const app = express();

app.get('/', function(req, res) {
    res.send(`
        <html>
            <head>
                <title>SSR</title>
            </head>
            <body>
                <p>hello world</p>
            </body>
        </html>
    `);
});

app.listen(3001, function() {
    console.log('listen:3001');
});
//当浏览器拿到服务器返回的这一段HTML源代码的时候,不需要加载任何JavaScript脚本,就可以直接将hello world显示出来。

在这里插入图片描述

因为请求的东西都在前端服务器,使用只返回前端服务器的东西

实现基础的VUE客户端渲染

<template>
  	<div>
    		<p>hello world</p>
    		<button @click="sayHello">say hello</button>
  	</div>
</template>

<script>
export default {
    methods: {
        sayHello() {
	          alert('hello ssr');
        }
    }
}
</script>
使用vue脚手架新建一个vue项目,修改一个App.vue组件:

然后运行npm run serve启动项目,打开浏览器可以看到页面已经被渲了显示 hello world,但是打开检查看源代码

image-20211013193931212

除了简单的兼容性处理 noscript 标签以外,只有一个简单的id为app的div标签,没有关于hello world的任何字眼,可以说这是一个空的页面(白屏),而当加载了下面的 script 标签的 JavaScript 脚本之后,页面开始这行这些脚本,执行结束,hello world 正常显示。也就是说真正渲染 hello world 的是 JavaScript 脚本。

同构VUE项目

构建配置
  1. 第一步:构建服务端代码

    在服务端代码构建结束后,需要将构建结果运行在nodejs服务器上,但是,对于服务端代码的构建,有一下内容需要注意:

    • 不需要编译CSS,样式表只有在浏览器(客户端)运行时需要。
    • 构建的目标的运行环境是commonjs,nodejs的模块化模式为commonjs
    • 不需要代码切割,nodejs将所有代码一次性加载到内存中更有利于运行效率
    //服务端的 webpack 构建配置文件 vue.server.config.js
    const nodeExternals = require("webpack-node-externals");
    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
    
    module.exports = {
        css: {
            extract: false // 不提取 CSS
        },
        configureWebpack: () => ({
            entry: `./src/server-entry.js`, // 服务器入口文件
            devtool: 'source-map',
            target: 'node', // 构建目标为nodejs环境
            output: {
                libraryTarget: 'commonjs2' // 构建目标加载模式 commonjs
            },
            // 跳过 node_mdoules,运行时会自动加载,不需要编译
            externals: nodeExternals({
                allowlist: [/\.css$/] // 允许css文件,方便css module
            }),
            optimization: {
                splitChunks: false // 关闭代码切割
            },
          	plugins: [
                new VueSSRServerPlugin()
            ]
        })
    };
    //使用 vue-server-renderer提供的server-plugin,这个插件主要配合下面讲到的client-plugin使用,作用主要是用来实现nodejs在开发过程中的热加载、source-map、生成html文件。
    
  2. 第二步:构建客户端代码

    在构建客户端代码时,使用的是客户端的执行入口文件,构建结束后,将构建结果在浏览器运行即可,但是在服务端渲染中,HTML是由服务端渲染的,也就是说,我们要加载那些JavaScript脚本,是服务端决定的,因为HTML中的script标签是由服务端拼接的,所以在客户端代码构建的时候,我们需要使用插件,生成一个构建结果清单,这个清单是用来告诉服务端,当前页面需要加载哪些JS脚本和CSS样式表。

    //客户端的构建配置,vue.client.config.js
    const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
    
    module.exports = {
        configureWebpack: () => ({
            entry: `./src/client-entry.js`,
            devtool: 'source-map',
            target: 'web',
            plugins: [
                new VueSSRClientPlugin()
            ]
        }),
        chainWebpack: config => {
          	// 去除所有关于客户端生成的html配置,因为已经交给后端生成
            config.plugins.delete('html');
            config.plugins.delete('preload');
            config.plugins.delete('prefetch');
        }
    };
    //使用vue-server-renderer提供的client-server,主要作用是生成构建加过清单vue-ssr-client-manifest.json,服务端在渲染页面时,根据这个清单来渲染HTML中的script标签(JavaScript)和link标签(CSS)
    

    接下来,我们需要将vue.client.config.jsvue.server.config.js都交给vue-cli内置的构建配置文件vue.config.js,根据环境变量使用不同的配置

    // vue.config.js
    const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
    const serverConfig = require('./vue.server.config');
    const clientConfig = require('./vue.client.config');
    
    if (TARGET_NODE) {
        module.exports = serverConfig;
    } else {
        module.exports = clientConfig;
    }
    
    

    使用cross-env区分环境

    {
      "scripts": {
        "server": "babel-node src/server.js",
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server"
      }
    }
    
    
实现模板组件的共享

实现模板组件的共享:使vue的组件代码也可以在服务端中运行,首先我们需要解决代码编译问题。

假如vue项目使用webpack构建,那么服务端代码的构建,也可以使用webpack

image-20211013195908827

  1. 第一步:创建VUE实例

为了实现模板组件共享,我们需要将获取 Vue 渲染实例写成通用代码,如下 createApp:

import Vue from 'vue';
import App from './App';

export default function createApp (context) {
    const app = new Vue({
        render: h => h(App)
    });
  	return {
      	app
    };
};

实现路由的共享
数据模型的共享与状态同步

七,ssr的两种模式

第一种是后端首次渲染的单页面应用,第二种是完全使用后端路由的后端模版渲染模式。他们区别在于使用后端路由的程度。

....