前言

书接上文,昨天简单的说到了 SSR 服务端渲染的相关内容《二十五║初探SSR服务端渲染
<https://www.cnblogs.com/laozhang-is-phi/p/9670342.html>
》,主要说明了相关概念,以及为什么使用等,昨天的一个小栗子因为时间问题,没有好好的给大家铺开来讲,今天呢,咱们就继续说一下这个 SSR 服务端渲染,并结合着
Client
客户端渲染,一起说一说相关的内容,当然还是围绕着原理来的,并不是要搭建项目,项目我会在下一个系列说到,经过和群里小伙伴的商量,并采纳大家的意见,我初步考虑了下,下一个系列我会说下
Nuxt.js 相关内容(我感觉这个很有必要的说,现在网站SEO是灰常重要滴 ),然后再下一个系列就是搭建一个功能丰富的 后台管理系统
作为开源项目,手里有货的小伙伴来群里,咱们一起开源吧哈哈哈。

 

 这个时候细心的小伙伴会发现,每天的那个脑图不见了,哈哈,并没有,而是在最下边,看文末就知道了。

一、Client 浏览器端渲染是怎样运行的

为了介绍浏览器渲染是怎么回事,我们运行一下npm run build 看看我们之前的项目——就是我们的个人博客第一版,大家应该还记得《
 二十二║Vue实战:个人博客第一版(axios+router)
<https://www.cnblogs.com/laozhang-is-phi/p/9640974.html>》,发布版本的文件,到底有哪些东西,

执行 
npm run build
这里我们通过 Webpack 打包,将我们的项目打包,生成一个 dist 目录 ,我们可以看到里面有 css+fonts+js 文件夹,还有一个
index.html 静态页面,我们打开这个静态页面,可以看到下面内容:
<!DOCTYPE html> <html lang=en> <head> <meta charset=utf-8> <meta
http-equiv=X-UA-Compatible content="IE=edge"> <meta name=viewport content="
width=device-width,initial-scale=1"> <link rel=icon href=/favicon.ico>
<title>blogvue3</title> <link href=/js/about.143cb27a.js rel=prefetch> <link
href=/css/app.51e9ecbc.css rel=preloadas= style> <link
href=/css/chunk-vendors.5aa02cc7.css rel=preloadas= style> <link
href=/js/app.16d68887.js rel=preloadas=script> <link
href=/js/chunk-vendors.1c001ffe.js rel=preloadas=script> <link
href=/css/chunk-vendors.5aa02cc7.css rel=stylesheet> <link
href=/css/app.51e9ecbc.css rel=stylesheet>//全部都是样式文件,可忽略研究 </head> <body>
<noscript> <strong>We're sorry but blogvue3 doesn't work properly without
JavaScript enabled. Please enable it tocontinue.</strong> </noscript> <div
id=app />//页面挂载入口 <script src=/js/chunk-vendors.1c001ffe.js />//vue
用到的区块文件,vue-cli全家桶默认配置里面这个chunk就是将所有从node_modules/里require(import)的依赖都打包到这里
<script src=/js/app.16d68887.js />//这个就是我们项目的核心内容,主要就是 app.vue
的内容,封装了所有方法,包括路由和页面渲染之类的 </body> </html>


 

大家观察生成的文件,只有一个div挂载入口,并没有多余的dom元素,那么页面要怎么呈现呢?答案是js append拼接,对,下面的那些 js
会负责innerHTML。而js是由浏览器解释执行的,所以呢,我们称之为浏览器渲染,相信这里大家应该很明白这个原理了,和我们平时用 jQuery
写局部异步加载是一样的,但是,这有几个致命的缺点:

* js放在dom结尾,如果js文件过大,那么必然造成页面阻塞。
* 随着我们的业务需求增大,打包后的 js 文件愈来愈大,页面白屏更加明显,用户体验明显不好,特别是首页,几个,几十个组件一起渲染,天讷!不敢相信
* 不利于SEO
* 客户端运行在老的JavaScript引擎上


 


这个时候,我们就想其他的一些办法,比如会单独给我们的首页写一个静态处理,为了应对相应速度,但是这个并不是一个好的办法,我们需要处理两套逻辑,基于以上的一些问题,服务端渲染呼之欲出....

总结:相信大家看到这里应该都能明白,客户端渲染的工作原理了,其实就是开发的时候组件化,然后通过 webpack 打包工具,将我们的逻辑处理 js
,打包成文件,然后和前端页面一起部署,这样就能讲数据在 DOM 上展示出来了。

 

二、Server 服务端渲染是怎样运行的

上边咱们看了客户端浏览器渲染,明白了原理和弊端,咱们这个时候就需要用到服务器渲染,SSR , Server Side Render的简称, 服务端渲染.
首先服务端渲染的思想由来已久, 在ajax 兴起之前, 所有 web 应用都是服务端渲染, 服务器直接返回 html 文本给浏览器,
用户操作比如在登录页面提交表单, 成功后跳转到首页, 服务器需要返回两个页面. 这样的弊端显而易见, 加大了服务器的消耗,到了 vue 时代,咱们虽然是通过
api 返回的Json,但是需要 node 服务器, 很耗费性能, 需要做好缓存和优化, 相当于空间换时间。

这里咱们先说下原理



 

从这个图里大家应该也能看到,我们的SSR打包流程变化了,在客户端渲染的时候,我们 webpack
是打包成js约束文件,直接发给浏览器,然后再获取数据渲染DOM,

网络解释有点儿羞涩难懂:ssr 有两个入口文件,client.js 和 server.js, 都包含了应用代码,webpack
通过两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle.
当服务器接收到了来自客户端的请求之后,会创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server
bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 之后,会和服务端生成的DOM
进行 Hydration(判断这个DOM 和自己即将生成的DOM 是否相同,如果相同就将客户端的vue实例挂载到这个DOM上, 否则会提示警告)。

可以看出来,我们增加了一个步骤:就是之前我们是在浏览器里,通过JavaScript框架来渲染数据的,但是现在我们的请求中间走了一遍 node 服务器,然后
node 服务器帮我们生成相应的 Html 片段,直接发送给浏览器,那浏览器肯定是认识html的,所以不用再通过 js
去获取数据渲染了,直接就渲染了,嗯大概就是这样,就好像多了一个中间件。

相信大家看内容可能不是很清楚,关键时候还是得上代码才能说的更清晰。

 

三、通过代码实现服务端渲染

客户端渲染咱们就不写代码了吧,这些天都写了很多了

1、首先我们新建一个文件夹 Vue_SSR_Demo 并对其 node 服务初始化

执行
npm install vue vue-server-renderer --save
会看到生成一个 node_modules 文件夹 和 package-lock.json 文件。

然后执行
npm install express --save
安装 express 的node服务。

 

2、然后创建一个 index.html 页面,作为一个承载页面,类似我们 vue-cli 脚手架中的 index.html
<!-- 如同vue-cli创建项目中的index.html --> <!DOCTYPE html> <html lang="en"> <head>
<meta charset="UTF-8"> <title>{{title}}</title> {{{meta}}} </head> <body>
<!--vue-ssr-outlet--> <!--↑↑↑↑↑ 注意上边的格式一定要有,并且不能带空格 ↑↑↑↑↑--> </body> </html>
 

3、新建一个 server.js 文件,用作我们的启服务入口
const Vue = require('vue')//引入 vue const server = require('express')()//引入
express 服务框架 const fs = require('fs') //读取 html 模版 const renderer = require('
vue-server-renderer').createRenderer({ template: fs.readFileSync('./index.html',
'utf-8')//文件地址路径 }) // 此参数是vue 生成Dom之外位置的数据 如vue生成的dom一般位于body中的某个元素容器中, //
此数据可在header标签等位置渲染,是renderer.renderToString()的第二个参数,//第一个参数是vue实例,第三个参数是一个回调函数。
const context = { title: '老张的哲学', meta:` <meta name="viewport" content="
width=device-width, initial-scale=1" /> <meta name="description" content="
vue-ssr"> <meta name="generator" content="GitBook 3.2.3"> ` } //定义服务 server.get(
'*', (req, res) => { //创建vue实例 主要用于替换index.html中body注释地方的内容, //index.html中
<!--vue-ssr-outlet-->的地方 ,约定俗成 const app = new Vue({ data: { url: req.url,
data: ['C#', 'SQL', '.NET', '.NET CORE', 'VUE'], title: '我的技能列表' }, //template
中的文本最外层一定要有容器包裹, 和vue的组件中是一样的,//只能有一个父级元素,这里是div! template: ` <div>
<p>{{title}}</p> <p v-for='item in data'>{{item}}</p> </div> ` }) //将 Vue
app实例渲染为字符串 (其他的API自己看用法是一样的) renderer.renderToString(app, context, (err, html)
=> { if (err) { res.status(500).end('err:' + err) return } //将模版发送给浏览器
res.end(html)//每次请求 都在node 服务器中打印 console.log('success') }) }) //服务端口开启并监听
server.listen(8060, () => { console.log('server success!') })
文档中的解释已经很详细了,大家可以自行看一看,这样我们就定义好了一个 node 服务,并通过 express 框架,将我们的 vue 实例通过
renderer.renderToString() 方法生成字符串,返回到浏览器。

 

4、开启 node 服务

执行
node server
注意,这里的 server 是我们的文件名,你也可以用其他的,比如 node aaa.js,或者 node aaa

 



 

这个时候,我们就发现我们已经成功的把我们的页面内容返回到了浏览器,为什么呢?因为我们的页面源代码已经有内容了,证明不是通过 js 后期渲染的。binggo!

大家有没有对 SSR 服务端渲染有一定的任何和了解,是不是品出来一点儿感觉了,这个还是最简单的一个 node 服务器渲染。

 代码就不上传了,大家粘贴复制就行,全部结构文件



 

 

四、通过 webpack 打包,来深入了解服务器渲染

 dang dang
dang,如果大家看到这里不费劲,或者看懂前边的了,好滴,你可以看这一块了,如果上边的不是很清晰,或者很难懂,好吧,这一块可能更羞涩了,不过没关系,慢慢来!

1、这个代码是昨天的,咱们这里重新说一下

结构如下:
├── dist      // 保存我们的打包后的文件 ├── node_modules   // 依赖包文件夹 ├── entry      //
打包入口文件夹 │ └── entry-server.js // 服务端 打包入口文件 ├── src      // 我们的项目的源码编写文件 │ ├──
views // view存放目录 │ │ ├── about.vue      //about 页面 │ │ ├── like.vue       
//like 页面 │ │ └── Home.vue     //Home 页面 │ └── App.vue        // App入口文件 │ └──
main.js         // 主配置文件 │ └── router.js       // 路由配置文件 └── .babelrc // babel
配置文件 └── package.json // 项目依赖包配置文件 └── package-lock.json // npm5 新增文件,优化性能 └──
server.js // server 文件 └── README.md // 说明文档


 咱们分块的说一说

2、普通的app代码块



这一块,就是对应的我们 src 文件夹下的模板,这些内容大家一定很熟悉了,就不多说了,就是 组件的定义、路由定义、app入口和 main.js
主方法,这里重点说下 main.js



在之前的 main.js 我们是直接实例化 vue() ,然后对 #appp 进行挂载的,但是现在咱们变成了
服务器渲染,这里就不能挂载了,而是把创建的vue实例返回出去。
//main.js import Vue from 'vue' import createRouter from './router' import App
from './App.vue' // 导出一个工厂函数,用于创建新的vue实例 export function createApp() { const
router = createRouter() const app = new Vue({ router, render: h => h(App) })
return app }
你会问了,但是返回给谁呢,欸?!这个问题好,请往下看。

3、讲我们的 vue实例封装到 promise



 

网友总结:所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise
是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称
Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

 简单来说,就是把我们 main入口文件中的vue实例,都封装到 promise,就像增加一个外衣,方便我们 webpack打包。对,重点来了

 

4、通过 Webpack 服务器打包

 

 
/* 5、webpack.server.js 服务端打包 */ const path = require('path');//获取路径对象 const
projectRoot = path.resolve(__dirname,'..');//根路径 //定义模块 module.exports = { //
此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)//
这里必须是node,因为打包完成的运行环境是node,在node端运行的,不是在浏览器端运行。 target: 'node', //
entry需要提供一个单独的入口文件 entry: ['babel-polyfill', path.join(projectRoot, '
entry/entry-server.js')], // 输出 output: { //
指定libraryTarget的类型为commonjs2,用来指定代码export出去的入口的形式。// 在node.js中模块是module.exports
= {...},commonjs2打包出来的代码出口形式就类似于此。 libraryTarget: 'commonjs2', path:
path.join(projectRoot,'dist'), // 打包出的路径 filename: 'bundle.server.js',//
打包最终的文件名,这个文件是给 node 服务器使用的 }, module: { // 因为使用webpack2,这里必须是rules,如果使用use, //
会报个错:vue this._init is not a function rules: [ //规则1、vue规则定义 { test: /\.vue$/
, loader:'vue-loader', },//js规则定义 { test: /\.js$/, loader: 'babel-loader',
include: projectRoot,// 这里会把node_modules里面的东西排除在外,提高打包效率 exclude: /node_modules/
,// ES6 语法 options: { presets: ['es2015'] } },//css定义 { test: /\.less$/,
loader:"style-loader!css-loader!less-loader" } ] }, plugins: [], resolve: {
alias: {'vue$': 'vue/dist/vue.runtime.esm.js' } } }
基本的内容就是上边这些,注释已经很清楚了,大家可以看一看,这个时候我们的准备工作就已经做好了,下一步就改打包了

 

5、执行打包命令,生成服务端约束文件 bundle.server.js
npm run server
这个时候,你会发现,我们的dist 文件夹内,多了一个 bundle.server.js 文件



 我们看一下生成的文件,部分截图,会发现,我们的这个文件包含了所有页面内的内容和方法,但是这个 bundle.server.js
并不是直接返回给前端的,而且在 node 服务器使用的



6、配置 node 服务器启动文件,这个更类似我们上文中提到的 server.js 文件
/*7、 server.js */ const express = require('express')()//引入express 服务框架 const
renderer = require('vue-server-renderer').createRenderer() const createApp =
require('./dist/bundle.server.js')['default']//引入我们刚刚打包文件 // 响应路由请求 express.get(
'*', (req, res) => { const context = { url: req.url } // 创建vue实例,传入请求路由信息
createApp(context).then(app => { renderer.renderToString(app, (err, html) => {
if (err) { return res.state(500).end('运行时错误') } res.send(` <!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8"> <title>Vue2.0 SSR渲染页面</title>
</head> <body> ${html} </body> </html> `) }) }, err => { if(err.code === 404) {
res.status(404).end('所请求的页面不存在') } }) }) // 服务器监听地址 express.listen(8089, () =>
{ console.log('服务器已启动!') })
 

7、启动服务
node server
这个时候我们就可以看到效果了

好啦,这个就是 SSR 服务端渲染的整个过程。

 

番外

哈喽大家好,在这里忙碌的日子又和大家见面了,咱们的前后端系列入门篇已经 26
篇了,按照我的计划,基本的讲解已经到这里了,相信如果大家按照我写的系列,能搭建自己的博客系统了,甚至如果你比较厉害,已经开始开发中型项目了哈哈,咱们这里先回顾下知识,包括
API ,Swagger 文档,Sugar 数据持久层的ORM,Repository仓储架构,Asyn/Await
异步编程,AOP面向切面编程,IoC控制反转和DI依赖注入,Dto数据传输对象,Redis缓存等后端知识,还有Vue 基础语法、JS高级、ES6、Vue 组件
、生命周期、数据绑定、开发环境搭建、Vue-Cli 脚手架、axios Http请求、vue-router 路由协议、webpack 打包、Vuex
状态管理等前端知识。虽然都是简单的说了下皮毛,也是都涵盖了这个框架内容,咱们可以看看咱们的结构树,这个每天都会出现的哈哈,这个就是这一个月咱们的辛苦,也是很有回报滴,群里的小伙伴都破50了,这是个大图,大家可以看看:




本来想着要换其他的系列,但是在群里小伙伴的建议下,还是在把Vue好好说说吧,思考了下,在国庆前的时间再说下 SSR 框架——Nuxt.js
吧,感觉这一块应该是要用到的,也是自学的一个吧,至于国庆之后,再慢慢考虑写其他的吧。