前言

闲来无事,钻研一下SSR,次要起因在于上周一位后端同学在一次组内技术分享的时候说,对前后端拆散、服务端渲染特地感兴趣,在他分享了后端微服务之后,专门点名邀请我下周分享服务端渲染,而后我还没批准,领导就内定让我下周分享了(其实就是下周违心下周分享,我是那个替死鬼)。

自己次要从集体角度介绍了对服务端渲染的了解,读完本文后,你将理解到:

  • 什么是服务端渲染,与客户端渲染的区别是什么?
  • 为什么须要服务端渲染,服务端渲染的利弊是什么?
  • 如何对VUE我的项目进行同构?

原文地址 欢送star

服务端渲染的定义

在讲服务度渲染之前,咱们先回顾一下页面的渲染流程:

  1. 浏览器通过申请失去一个HTML文本
  2. 渲染过程解析HTML文本,构建DOM树
  3. 解析HTML的同时,如果遇到内联款式或者款式脚本,则下载并构建款式规定(stytle rules),若遇到JavaScript脚本,则会下载执行脚本。
  4. DOM树和款式规定构建实现之后,渲染过程将两者合并成渲染树(render tree)
  5. 渲染过程开始对渲染树进行布局,生成布局树(layout tree)
  6. 渲染过程对布局树进行绘制,生成绘制记录
  7. 渲染过程的对布局树进行分层,别离栅格化每一层,并失去合成帧
  8. 渲染过程将合成帧信息发送给GPU过程显示到页面中

能够看到,页面的渲染其实就是浏览器将HTML文本转化为页面帧的过程。而现在咱们大部分WEB利用都是应用 JavaScript 框架(Vue、React、Angular)进行页面渲染的,也就是说,在执行 JavaScript 脚本的时候,HTML页面曾经开始解析并且构建DOM树了,JavaScript 脚本只是动静的扭转 DOM 树的构造,使得页面成为心愿成为的样子,这种渲染形式叫动静渲染,也能够叫客户端渲染(client side rende)。

那么什么是服务端渲染(server side render)?顾名思义,服务端渲染就是在浏览器申请页面URL的时候,服务端将咱们须要的HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不须要通过 JavaScript 脚本的执行,即可间接构建出心愿的 DOM 数并展现到页面中。这个服务端组装HTML的过程,叫做服务端渲染。

服务端渲染的由来

Web1.0

在没有AJAX的时候,也就是web1.0时代,简直所有利用都是服务端渲染(此时服务器渲染非当初的服务器渲染),那个时候的页面渲染大略是这样的,浏览器申请页面URL,而后服务器接管到申请之后,到数据库查问数据,将数据丢到后端的组件模板(php、asp、jsp等)中,并渲染成HTML片段,接着服务器在组装这些HTML片段,组成一个残缺的HTML,最初返回给浏览器,这个时候,浏览器曾经拿到了一个残缺的被服务器动静组装进去的HTML文本,而后将HTML渲染到页面中,过程没有任何JavaScript代码的参加。

客户端渲染

在WEB1.0时代,服务端渲染看起来是一个过后的最好的渲染形式,然而随着业务的日益简单和后续AJAX的呈现,也慢慢开始暴露出了WEB1.0服务器渲染的毛病。

  • 每次更新页面的一小的模块,都须要从新申请一次页面,从新查一次数据库,从新组装一次HTML
  • 前端JavaScript代码和后端(jsp、php、jsp)代码混淆在一起,使得日益简单的WEB利用难以保护

而且那个时候,基本就没有前端工程师这一职位,前端js的活个别都由后端同学 jQuery 一把梭。然而随着前端页面慢慢地简单了之后,后端开始发现js好麻烦,尽管很简略,然而坑太多了,于是让公司招聘了一些专门写js的人,也就是前端,这个时候,前后端的鄙视链就呈现了,后端鄙视前端,因为后端感觉js太简略,无非就是写写页面的特效(JS),切切图(CSS),基本算不上是真正的程序员。

随之 nodejs 的呈现,前端看到了翻身的契机,为了解脱后端的指指点点,前端开启了一场前后端拆散的运行,心愿能够脱离后端独立倒退。前后端拆散,外表上看上去是代码拆散,实际上是为了前后端人员拆散,也就是前后端分家,前端不再归属于后端团队。

前后端拆散之后,网页开始被独立的应用程序(SPA,Single Page Application),前端团队接管了所有页面渲染的事,后端团队只负责提供所有数据查问与解决的API,大体流程是这样的:首先浏览器申请URL,前端服务器间接返回一个空的动态HTML文件(不须要任何查数据库和模板组装),这个HTML文件中加载了很多渲染页面须要的 JavaScript 脚本和 CSS 样式表,浏览器拿到 HTML 文件后开始加载脚本和样式表,并且执行脚本,这个时候脚本申请后端服务提供的API,获取数据,获取实现后将数据通过JavaScript脚本动静的将数据渲染到页面中,实现页面显示。

这一个前后端拆散的渲染模式,也就是客户端渲染(CSR)。

服务端渲染

随着单页利用(SPA)的倒退,程序员们慢慢发现 SEO(Search Engine Optimazition,即搜索引擎优化)出了问题,而且随着利用的复杂化,JavaScript 脚本也一直的臃肿起来,使得首屏渲染相比于 Web1.0时候的服务端渲染,也慢了不少。

本人选的路,跪着也要走上来。于是前端团队抉择了应用 nodejs 在服务器进行页面的渲染,进而再次出现了服务端渲染。大体流程与客户端渲染有些类似,首先是浏览器申请URL,前端服务器接管到URL申请之后,依据不同的URL,前端服务器向后端服务器申请数据,申请实现后,前端服务器会组装一个携带了具体数据的HTML文本,并且返回给浏览器,浏览器失去HTML之后开始渲染页面,同时,浏览器加载并执行 JavaScript 脚本,给页面上的元素绑定事件,让页面变得可交互,当用户与浏览器页面进行交互,如跳转到下一个页面时,浏览器会执行 JavaScript 脚本,向后端服务器申请数据,获取完数据之后再次执行 JavaScript 代码动静渲染页面。

服务端渲染的利弊

相比于客户端渲染,服务端渲染有什么劣势?

利于SEO

有利于SEO,其实就是有利于爬虫来爬你的页面,而后在他人应用搜索引擎搜寻相干的内容时,你的网页排行能靠得更前,这样你的流量就有越高。那为什么服务端渲染更利于爬虫爬你的页面呢?其实,爬虫也分低级爬虫和高级爬虫。

  • 低级爬虫:只申请URL,URL返回的HTML是什么内容就爬什么内容。
  • 高级爬虫:申请URL,加载并执行JavaScript脚本渲染页面,爬JavaScript渲染后的内容。

也就是说,低级爬虫对客户端渲染的页面来说,几乎无能为力,因为返回的HTML是一个空壳,它须要执行 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 中, 模板能够了解成组件,数据能够了解为数据模型,即响应式数据。所以,对于同构利用来说,咱们必须实现客户端与服务端的路由、模型组件、数据模型的共享。

实际

晓得了服务端渲染、同构的原理之后,上面从头开始,一步一步实现一次同构,通过实际来理解SSR。

实现根底的NODEJS服务端渲染

首先,模仿一个最简略的服务器渲染,只须要向页面返回咱们须要的html文件。

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');});

启动之后关上localhost:3001能够看到页面显示了hello world。而且关上网页源代码:

也就是说,当浏览器拿到服务器返回的这一段HTML源代码的时候,不须要加载任何JavaScript脚本,就能够间接将hello world显示进去。

实现根底的VUE客户端渲染

咱们用 vue-cli新建一个vue我的项目,批改一个App.vue组件:

<template>      <div>            <p>hello world</p>            <button @click="sayHello">say hello</button>      </div></template><script>export default {    methods: {        sayHello() {              alert('hello ssr');        }    }}</script>

而后运行npm run serve启动我的项目,关上浏览器,一样能够看到页面显示了 hello world,然而关上咱们开网页源代码:

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

同构VUE我的项目

构建配置

模板组件的共享,其实就是应用同一套组件代码,为了实现 Vue 组件能够在服务端中运行,首先咱们须要解决代码编译问题。个别状况,vue我的项目应用的是webpack进行代码构建,同样,服务端代码的构建,也能够应用webpack,借用官网的一张。

第一步:构建服务端代码

由后面的图能够看到,在服务端代码构建完结后,须要将构建后果运行在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文件。

第二步:构建客户端代码

在构建客户端代码时,应用的是客户端的执行入口文件,构建完结后,将构建后果在浏览器运行即可,然而在服务端渲染中,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.js和vue.server.config.js都交给vue-cli内置的构建配置文件vue.config.js,依据环境变量应用不同的配置

// vue.config.jsconst 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 渲染实例写成通用代码,如下 createApp:

import Vue from 'vue';import App from './App';export default function createApp (context) {    const app = new Vue({        render: h => h(App)    });      return {          app    };};
第二步:客户端实例化VUE

新建客户端我的项目的入口文件,client-entry.js

import Vue from 'vue'import createApp from './createApp';const {app} = createApp();app.$mount('#app');

client-entry.js是浏览器渲染的入口文件,在浏览器加载了客户端编译后的代码后,组件会被渲染到id为app的元素节点上。

第三步:服务端实例化VUE

新建服务端代码的入口文件,server-entry.js

import createApp from './createApp'export default context => {    const { app } = createApp(context);    return app;}

server-entry.js是提供给服务器渲染vue组件的入口文件,在浏览器通过URL拜访到服务器后,服务器须要应用server-entry.js提供的函数,将组件渲染成html。

第四步:HTTP服务

所有货色的筹备好之后,咱们须要批改nodejs的HTTP服务器的启动文件。首先,加载服务端代码server-entry.js的webpack构建后果

const serverBundle = path.resolve(process.cwd(), 'serverDist', 'vue-ssr-server-bundle.json');const {createBundleRenderer} = require('vue-server-renderer');const path = require('path');const serverBundle = path.resolve(process.cwd(), 'serverDist', 'vue-ssr-server-bundle.json');

加载客户端代码client-enyry.js的webpack构建后果

const clientManifestPath = path.resolve(process.cwd(), 'dist', 'vue-ssr-client-manifest.json');const clientManifest = require(clientManifestPath);

应用 vue-server-renderer 的createBundleRenderer创立一个html渲染器:

const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');const renderer = createBundleRenderer(serverBundle, {    template,  // 应用HTML模板    clientManifest // 将客户端的构建后果清单传入});

创立HTML模板,index.html

<html>  <head>    <title>SSR</title>  </head>  <body>    <!--vue-ssr-outlet-->  </body></html>

在HTML模板中,通过传入的客户端渲染后果clientManifest,将主动注入所有link样式表标签,而占位符<!--vue-ssr-outlet-->将会被替换成模板组件被渲染后的具体的HTML片段和script脚本标签。

HTML筹备实现后,咱们在server中挂起所有路由申请

const express = require('express');const app = express();/* 实例化渲染器renderer */app.get('*', function(req, res) {    renderer.renderToString({}, (err, html) => {        if (err) {            res.send('500 server error');            return;        }        res.send(html);    })});

接下来,咱们构建客户端、服务端我的项目,而后执行 node server.js,关上页面源代码,

看起来是合乎预期的,然而发现控制台有报错,加载不到客户端构建css和js,报404,起因很明确,咱们没有把客户端的构建后果文件挂载到服务器的动态资源目录,在挂载路由前退出上面代码:

app.use(express.static(path.resolve(process.cwd(), 'dist')));

看起来功败垂成,点击say hello也弹出了音讯,仔细的同学会发现根节点有一个data-server-rendered属性,这个属性有什么作用呢?

因为服务器曾经渲染好了 HTML,咱们显然无需将其抛弃再从新创立所有的 DOM 元素。相同,咱们须要"激活"这些动态的 HTML,而后使他们成为动静的(可能响应后续的数据变动)。

如果查看服务器渲染的输入后果,应用程序的根元素上增加了一个非凡的属性:

<div id="app" data-server-rendered="true">

data-server-rendered是非凡属性,让客户端 Vue 晓得这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载。

路由的共享和同步

实现了模板组件的共享之后,上面实现路由的共享,咱们后面服务器应用的路由是*,承受任意URL,这容许所有URL申请交给Vue路由解决,进而实现客户端路由与服务端路由的复用。

第一步:创立ROUTER实例

为了实现复用,与createApp一样,咱们创立一个createRouter.js

import Vue from 'vue';import Router from 'vue-router';import Home from './views/Home';import About from './views/About';Vue.use(Router)const routes = [{    path: '/',    name: 'Home',    component: Home}, {    path: '/about',    name: 'About',    component: About}];export default function createRouter() {    return new Router({        mode: 'history',        routes    })}

在createApp.js中创立router

import Vue from 'vue';import App from './App';import createRouter from './createRouter';export default function createApp(context) {    const router = createRouter(); // 创立 router 实例    const app = new Vue({        router, // 注入 router 到根 Vue 实例        render: h => h(App)    });    return { router, app };};
第二步:路由匹配

router筹备好了之后,批改server-entry.js,将申请的URL传递给router,使得在创立app的时候能够依据URL匹配到对应的路由,进而可晓得须要渲染哪些组件

import createApp from './createApp';export default context => {    // 因为有可能会是异步路由钩子函数或组件,所以咱们将返回一个 Promise,    // 以便服务器可能期待所有的内容在渲染前就曾经准备就绪。    return new Promise((resolve, reject) => {        const { app, router } = createApp();        // 设置服务器端 router 的地位        router.push(context.url)        // onReady 等到 router 将可能的异步组件和钩子函数解析完        router.onReady(() => {            const matchedComponents = router.getMatchedComponents();            // 匹配不到的路由,执行 reject 函数,并返回 404            if (!matchedComponents.length) {                return reject({                    code: 404                });            }            // Promise 应该 resolve 应用程序实例,以便它能够渲染            resolve(app)        }, reject)    })}

批改server.js的路由,把url传递给renderer

app.get('*', function(req, res) {    const context = {        url: req.url    };    renderer.renderToString(context, (err, html) => {        if (err) {            console.log(err);            res.send('500 server error');            return;        }        res.send(html);    })});

为了测试,咱们将App.vue批改为router-view

<template>    <div id="app">        <router-link to="/">Home</router-link>        <router-link to="/about">About</router-link>        <router-view />    </div></template>

Home.vue

<template>    <div>Home Page</div></template>

About.vue

<template>    <div>About Page</div></template>

编译,运行,查看源代码

点击路由并没有刷新页面,而是客户端路由跳转的,所有合乎预期。

数据模型的共享与状态同步

后面咱们简略的实现了服务端渲染,然而理论状况下,咱们在拜访页面的时候,还须要获取须要渲染的数据,并且渲染成HTML,也就是说,在渲染HTML之前,咱们须要将所有数据都筹备好,而后传递给renderer。

个别状况下,在Vue中,咱们将状态数据交给Vuex进行治理,当然,状态也能够保留在组件外部,只不过须要组件实例化的时候本人去同步数据。

第一步:创立STORE实例

首先第一步,与createApp相似,创立一个createStore.js,用来实例化store,同时提供给客户端和服务端应用

import Vue from 'vue';import Vuex from 'vuex';import {fetchItem} from './api';Vue.use(Vuex);export default function createStore() {    return new Vuex.Store({        state: {            item: {}        },        actions: {            fetchItem({ commit }, id) {                return fetchItem(id).then(item => {                    commit('setItem', item);                })            }        },        mutations: {            setItem(state, item) {                Vue.set(state.item, item);            }        }    })}

actions封装了申请数据的函数,mutations用来设置状态。

将createStore退出到createApp中,并将store注入到vue实例中,让所有Vue组件能够获取到store实例

export default function createApp(context) {    const router = createRouter();    const store = createStore();    const app = new Vue({        router,        store, // 注入 store 到根 Vue 实例        render: h => h(App)    });    return { router, store, app };};

为了不便测试,咱们mock一个近程服务函数fetchItem,用于查问对应item

export function fetchItem(id) {    const items = [        { name: 'item1', id: 1 },        { name: 'item2', id: 2 },        { name: 'item3', id: 3 }    ];    const item = items.find(i => i.id == id);    return Promise.resolve(item);}
第二步:STORE连贯组件

个别状况下,咱们须要通过拜访路由,来决定获取哪局部数据,这也决定了哪些组件须要渲染。事实上,给定路由所需的数据,也是在该路由上渲染组件时所需的数据。所以,咱们须要在路由的组件中搁置数据预取逻辑函数。

在Home组件中自定义一个动态函数asyncData,须要留神的是,因为此函数会在组件实例化之前调用,所以它无法访问 this。须要将 store 和路由信息作为参数传递进去

<template><div>    <div>id: {{item.id}}</div>    <div>name: {{item.name}}</div></div></template><script>export default {    asyncData({ store, route }) {        // 触发 action 后,会返回 Promise        return store.dispatch('fetchItems', route.params.id)    },    computed: {        // 从 store 的 state 对象中的获取 item。        item() {            return this.$store.state.item;        }    }}</script>
第三步:服务端获取数据

在服务器的入口文件server-entry.js中,咱们通过URL路由匹配 router.getMatchedComponents()失去了须要渲染的组件,这个时候咱们能够调用组件外部的asyncData办法,将所须要的所有数据都获取完后,传递给渲染器renderer上下文。

批改createApp,在路由组件匹配到了之后,调用asyncData办法,获取数据后传递给renderer

import createApp from './createApp';export default context => {    // 因为有可能会是异步路由钩子函数或组件,所以咱们将返回一个 Promise,    // 以便服务器可能期待所有的内容在渲染前就曾经准备就绪。    return new Promise((resolve, reject) => {        const { app, router, store } = createApp();        // 设置服务器端 router 的地位        router.push(context.url)        // onReady 等到 router 将可能的异步组件和钩子函数解析完        router.onReady(() => {            const matchedComponents = router.getMatchedComponents();            // 匹配不到的路由,执行 reject 函数,并返回 404            if (!matchedComponents.length) {                return reject({ code: 404 })            }            // 对所有匹配的路由组件调用 `asyncData()`            Promise.all(matchedComponents.map(Component => {                if (Component.asyncData) {                    return Component.asyncData({                        store,                        route: router.currentRoute                    });                }            })).then(() => {                // 状态传递给renderer的上下文,不便前面客户端激活数据                context.state = store.state                resolve(app)            }).catch(reject);        }, reject);    })}

将state存入context后,在服务端渲染HTML时候,也就是渲染template的时候,context.state会被序列化到window.__INITIAL_STATE__中,不便客户端激活数据。

第四步:客户端激活状态数据

服务端预申请数据之后,通过将数据注入到组件中,渲染组件并转化成HTML,而后吐给客户端,那么客户端为了激活后端返回的HTML被解析后的DOM节点,须要将后端渲染组件时用的store的state也同步到浏览器的store中,保障在页面渲染的时候放弃与服务器渲染时的数据是统一的,能力实现DOM的激活,也就是咱们后面说到的data-server-rendered标记。

在服务端的渲染中,state曾经被序列化到了window.__INITIAL_STATE__,比方咱们拜访http://localhost:3001?id=1,查看页面源代码

能够看到,状态曾经被序列化到window.__INITIAL_STATE__中,咱们须要做的就是将这个window.__INITIAL_STATE__在客户端渲染之前,同步到客户端的store中,上面批改client-entry.js

const { app, router, store } = createApp();if (window.__INITIAL_STATE__) {      // 激活状态数据    store.replaceState(window.__INITIAL_STATE__);}router.onReady(() => {    app.$mount('#app', true);});

通过应用store的replaceState函数,将window.__INITIAL_STATE__同步到store外部,实现数据模型的状态同步。

总结

当浏览器拜访服务端渲染我的项目时,服务端将URL传给到预选构建好的VUE利用渲染器,渲染器匹配到对应的路由的组件之后,执行咱们事后在组件内定义的asyncData办法获取数据,并将获取完的数据传递给渲染器的上下文,利用template组装成HTML,并将HTML和状态state一并吐给前端浏览器,浏览器加载了构建好的客户端VUE利用后,将state数据同步到前端的store中,并依据数据激活后端返回的被浏览器解析为DOM元素的HTML文本,实现了数据状态、路由、组件的同步,同时使得页面失去直出,较少了白屏工夫,有了更好的加载体验,同时更有利于SEO。

集体感觉理解服务端渲染,有助于晋升前端工程师的综合能力,因为它的内容除了前端框架,还有前端构建和后端内容,是一个性价比还挺高的常识,不学白不学,加油!

参考文献

  • 为什么当初又风行服务端渲染html?
  • Vue.js服务端渲染指南
  • 从头开始,彻底了解服务端渲染原理(8千字汇总长文)