关于javascript:万字长文警告从头到尾彻底理解服务端渲染SSR原理

39次阅读

共计 14796 个字符,预计需要花费 37 分钟才能阅读完成。

前言

闲来无事,钻研一下 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.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 渲染实例写成通用代码,如下 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 千字汇总长文)

正文完
 0