实现ssr服务端渲染的方法步骤

前言

前段时间寻思做个个人网站,然后就立马行动了。  个人网站如何实现选择什么技术方案,自己可以自由决定。  刚好之前有大致想过服务端渲染,加载速度快,还有seo挺适合个人网站的。  所以就自己造了个轮子用koa+react来实现ssr服务端渲染。

什么是ssr

最初听说有单页面的服务端渲染的时候,就理解为类似传统的服务端路由+模板渲染,只是需要用单页面应用的框架写。后面寻思这样好像有点傻,再一了解,原来只是在首次加载的时候,后端进行当前路径页面的组件渲染和数据请求,组装成html返回给前端,用户就能很快看到看到页面,当html中的js资源加载完成后,剩下执行和运行的就是一般的单页面应用。  所以ssr是后端模板渲染和单页面的组合。  ssr有两种模式,单页面和非单页面模式,第一种是后端首次渲染的单页面应用,第二种是完全使用后端路由的后端模版渲染模式。他们区别在于使用后端路由的程度。

优势

ssr的两个明显的优势:首次加载快和seo。  为什么说首次加载快呢。  一个普通的单页面应用,首次加载的时候需要把所有相关的静态资源加载完毕,然后核心js才会开始执行,这个过程就会消耗一定的时间,接着还会请求网络接口,最终才能完全渲染完成。

ssr模式下,后端拦截到路由,找到对应组件,准备渲染组件,所有的js资源在本地,排除了js资源的网络加载时间,接着只需要对当前路由的组件进行渲染,而页面的ajax请求,可能在同一台服务器上,如果是的话速度也会快很多。最后后端把渲染好的页面反回给前端。  注意:页面能很快的展示出来,但是由于当前返回的只是单纯展示的dom、css,其中的js相关的事件等在客户端其实并没有绑定,所以最终还是需要js加载完以后,对当前的页面再进行一次渲染,称为同构。  所以ssr就是更快的先展示出页面的内容,先让用户能够看到。  为什么seo友好呢,因为搜索引擎爬虫在爬取页面信息的时候,会发送HTTP请求来获取网页内容,而我们服务端渲染首次的数据是后端返回的,返回的时候已经是渲染好了title,内容等信息,便于爬虫抓取内容。

如何实现

大致对ssr有了一个了解,我们现在需要对实现整理一下大致实现思路和流程。

1.选择一个单页面框架(我目前选择的是react)

2.选择node服务端框架(我目前选择的是koa2)

3.实现核心逻辑,让node服务端能够路由和渲染单页面组件(这一点分为很多小实现点,后面说)

4.优化开发和发布环境自动化构建工具(webpack)

开始实现之前创建一个react-ssr项目,项目下创建client和server目录用于写客户端和服务端代码,webpack目录用于weppack文件配置。

1.react应用

安装react依赖,在client中创建好一个基础的react文件夹结构,并写好一个可以运行的有路由配置的应用,client文件目录如下:

2.server应用

安装koa和相关依赖,在server中创建好一个基础的服务端文件夹结构,并写好一个简单的可运行的后端应用服务。server文件夹如下:

3.核心实现

因为有仓库代码就不对基础代码做解释,现在我们有一个可以单独运行的react单页面应用和一个后端应用,他们都有各自的路由。接下来我们做改造,实现ssr的单页面模式(非单页面模式仅仅是做部分调整,因此这里只讲实现单页面模式)。

核心实现分为以下几步:

1) 后端拦截路由,根据路径找到需要渲染的react页面组件X

2)调用组件X初始化时需要请求的接口,同步获取到数据后,使用react的renderToString方法对组件进行渲染,使其渲染出节点字符串。

3)后端获取基础html文件,把渲染出的节点字符串插入到body之中,同时也可以操作其中的title,script等节点。返回完整的html给客户端。

4)客户端获取后端返回的html,展示并加载其中的js,最后完成react同构。

1)我们在客户端写react的时候,router常规的会定义一个数组,存放组件和对应的path,然后注册路由,如下:

上面说过,实现ssr就是实现单页面应用+首次服务端渲染,所以我们本身就是做的一个单页面应用。  现在实现了单页面应用,需要实现首次服务端渲染。  服务端的应用启动以后,接受到url请求,比如访问 http://localhost:9999/ ,后端服务获取到当前的path为/,这个时候我们就希望后端找到配置path为‘/'的上图的Index组件,对其进行渲染。  我们在client的router文件夹中建立两个js文件index和pages:

pages 里配置路由路径和组件的映射,代码大致如下,使其能被客户端路由和服务端路由同时使用。

在server路由中代码大致是这样的,在服务端获取到get请求以后,匹配路径,如果路径path是有映射页面组件的,获取到此组件并渲染,这就是我们的第一步:后端拦截路由,根据路径找到需要渲染的react页面组件。

2)如上图,匹配到组件以后,执行了组件的getInitialProps方法(和nextjs的命名保持一致),此方法是一个封装的静态方法,主要用于获取初始化所需要的ajax数据,在服务端会同步获取,而后通过ssrData参数传入组件prorps并执行组件渲染。  此方法在客户端依然是异步请求。  这一步比较重要,为什么我们需要一个静态方法,而不是直接把请求写在willmount中呢。  因为在服务端使用renderToString渲染组件时,生命周期只会执行到willmount之后的第一次render,在willmount内部,请求是异步的,第一次render完成的时候,异步的数据都没有获取到,这个时候renderToString就已经返回了。  那我们页面的初始化数据就没有了,返回的html不是我们所期望的。  因此定义了一个静态方法,在组件实例化之前获取到这个方法,同步执行,数据获取完成后,通过props把数据传入给组件进行渲染。  那么这个方法是如何实现的呢?  我们根据代码截图来看base.js:

首先在client的pages里新建一个base组件,base继承React.Component,所有pages里的页面组件都需要继承这个base,base有一个静态方法getInitialProps,此方法主要是返回组件初始化需要的异步数据。  如果有初始化的ajax请求,就应该重写在此方法里,并且return数据对象。 constructor判断了页面组件是否有初始化定义的state静态方法,有的话传递给组件实例化的state对象,如果props有传入ssrData,把ssrData传递值给组件state对象。   base中的componentWillMount会判断是否还需要去执行getInitialProps方法,如果在服务端渲染的时候,数据已经在组件实例化之前同步获取并传入了props,所以忽略。  如果在客户端环境,分两种情况,第一种:用户第一次进到页面,这时候是服务端去请求的数据,服务端获取到数据后在服务端渲染组件,同时也会把数据存放在html的script代码中,定义一个全局变量ssrData,如下图,react在注册单页面应用并且同构的时候会把全局ssrData传递给页面组件,这个时候页面组件在客户端同构渲染的时候,就可以延续使用服务端之前的数据,这样也保持了同构的一致性,也避免了一次重复请求。  第二种情况:就是当前用户在单页面之中切换路由,这样就没有服务端渲染,那么就执行getInitialProps方法,把数据直接返回给state,几乎等同于在willmount中执行请求。  这样封装我们就可以用一套代码兼容服务端渲染和单页面渲染。

client/app.js

再看看如何写页面组件,下面是页面组件Index的截图,Index继承Base,定义了静态state,组件constructor方法会把此对象传递给组件实例化的state对象中,之所以用静态方法来写默认数据,是想保证定义的默认state先传递给实例对象的state,接口请求传递的props数据后传递给实例对象的state。 为什么不直接写state属性而要加static,因为state属性会执行在constructor之后,这样会覆盖constructor定义的state,也就是会覆盖我们getInitialProps返回的数据。

注意:在服务端渲染环境下,执行renderToString的时候,组件会被实例化,并且返回字符串形式的dom,这个过程react组件的生命周期只会执行到willmount之后的render。

3)我们写好一个html文件,大致如下。  当前已经渲染出了相应的节点字符串,后端需要返回html文本,内容应该包含标题,节点和最后需要加载的打包好的js,依次去替换html占位部分。

index.html

server/router.js

4)最后客户端js加载完成后,会运行react,并且执行同构方法ReactDOM.hydrate,而不是平时用的ReactDOM.render。

以下是首次渲染过程大致流程图,点击查看大图

css处理

现在我们已经完成了最核心的逻辑,但是有一个问题。  我发现在后端渲染组件的时候,style-loader会报错,style-loader会找到组件依赖的css,并在组件加载时,把style载入到html header中,但是我们在服务端渲染的时候,没有window对象,因此style-loader内部代码会报错。    服务端webpack需要移除style-loader,用其他方法代替,后来我把样式赋值给组件静态变量,然后通过服务端渲染一并返回给前端,但是有个问题,我只能拿到当前组件的样式,子组件的样式没办法拿到,如果要给子组件再添加静态方法,再想办法去取,那就太麻烦了。  后来我找到了一个库isomorphic-style-loader可以支持我们想要的功能,看了下它的源码和使用方法,通过高阶函数把样式赋值给组件,然后利用react的Context,拿到当前需要渲染的所有组件的样式,最后把style插入到html中,这样解决了子组件样式无法导入的问题。  但是我觉得有点麻烦,首先需要定义所有组件的高阶函数和引入这个库,然后在router之中需要写相关代码收集style,最后插入到html中。  后来我定义了一个ProcessSsrStyle方法,入参是style文件,逻辑是判断环境,如果是服务端把style加载到当前组件的dom中,如果是客户端就不处理(因为客户端有style-loader)。  实现和使用非常简单,如下:

ProcessSsrStyle.js

使用:

服务端返回html的内容如下,用户马上能够看到完整的页面样式,而当客户端react同构完成后,dom会被替换为纯dom,因为ProcessSsrStyle方法在客户端不会输出style,最终style-loader执行后header中也会有样式,,页面不会出现不一致的变化,对于用户来说这一切都是无感的。

至此,最核心的功能已经实现,但是在后来的开发中,我发现事情还并没有那么简单,因为开发环境似乎太不友好了,开发效率低,需要手动重启。

开发环境

先说说最初的开发环境如何工作:

  • npm run dev启动开发环境
  • webpack.client-dev.js打包服务端代码,代码会被打包到dist/server中
  • webpack.server-dev.js打包客户端代码,代码会被打包到dist/client中
  • 启动服务端应用,端口9999
  • 启动webpack-dev-server, 端口8888

webpack打包后,启动了两个服务,一个是服务端的app应用、端口为9999,一个是客户端的dev-server、端口为8888,dev-server会监听和打包client代码,可以在客户端代码更新的时候,实时热更新前端代码。  当访问localhost:9999时,server会返回html,我们的server返回的html中的js脚本路径是指向的dev-serve端口的地址,如下图。  也就是说,客户端的程序和服务端的程序被分别打包,并且运行两个不同的端口服务。

在生产环境下,因为不需要dev-server去监听和热更新,因此只一个服务就足够, 如下图,服务端注册静态资源文件夹:

server/app.js

目前的构建系统,区分了生产环境和开发环境,现在的开发环境构建是没有什么问题的。  但是开发环境问题就比较明显,存在的最大问题是服务端没有热更新或者重新打包重启。  这样会导致很多问题,最严重的就是前端已经更新了组件,但是服务端并没有更新,所以在同构的时候会出现不一致,就会导致报错,有些报错会影响运行,解决办法只有重启。  这样的开发体验是无法忍受的。  后来我开始考虑做服务端的热更新。

监听、打包、重启

最初我的方法是监听修改,打包然后重启应用。  还记得我们的client/router/pages.js文件吗,客户端和服务端的路由都引入了这个文件,所以服务端和客户端的打包依赖都有pages.js,因此所有pages的组件相关的依赖都可以被客户端和服务端监听,当一个组件更新了,dev-server已经帮助我们监听和热更新了客户端代码,现在我们要自己来处理以下如何更新和重启服务端代码。  其实方法很简单,就是在服务端打包配置里开启监听,然后在插件配置中,写一个重启的插件,插件代码如下:

当webpack首次运行之后,插件会启动一个子进程,运行app.js,当文件发生变动后,再次编译,判断是否有子进程,如果有杀掉子进程,然后重启子进程,这样就实现了自动重启。  因为客户端和服务端是两个不同的打包服务和配置,当文件被修改,他们同时会重新编译,为了保证编译后运行符合预期,要保证服务端先编译完成,客户端后编译完成,所以在客户端的watch配置里,增加一点延迟,如下图,默认是300毫秒,所以服务端是300毫秒后执行编译,而客户端是1000毫秒后执行编译。

现在解决了重启问题,但是我觉得还不够,因为在开发的大部分时间里pages.js中组件,也就是展示端的代码更新频率会很高,如果老是去重启编译后端的代码,我觉得效率太低。  因此我觉得再做一次优化。

抽离client/router/pages单独打包

流程应该是这样的,增加一个webpack.server-dev-pages.js配置文件,单独监听和打包出dist/pages,服务端代码判断如果是开发环境,在路由监听方法中每次执行都重新获取dist/pages包,服务端监听配置忽略client文件夹。  看起来有点懵逼,其实最终的效果就是当pages中依赖的组件发生了更新,webpack.server-dev-pages.js重新编译并打包到dist/pages中,服务端app不编译和重启,只需要在服务端app路由中重新获取最新的dist/pages包,就保证了服务应用更新了所有客户端组件,而服务端应用并不会编译和重启。  当服务端本身的代码发生了修改,还是会自动编译和重启。  所以最终我们的开发环境需要启动3个打包配置

  • webpack.server-dev-pages
  • webpack.server-dev
  • webpack.client-dev

server/router,如何清除和更新pages包

至此,比较满意的开发环境基本实现了。  后来又觉得每次更新css都需要去重新打包后端的pages也没有必要,加上同构的时候css不一致,仅仅只有警告,没有实质影响,因此我在server-dev-pages中忽略了less文件(因为我用的less)。  这样会导致一个问题,因为没有更新pages,所以页面会刷新时会先展示旧的样式,然后同构完成又立马变成新样式,在开发环境中这一瞬间是可以接受的,也不影响什么。  但是避免了无谓的编译。

没有做的事情

  • 封装成一个更有包裹性的三方脚手架
  • css作用域控制
  • 封装性更强的webpack配置
  • 开发环境下,图片路径会出现不一致

最初做自己小站的目的是学习,加上自己使用,因此有太多个性的东西。  从自己的小站中抽离了出来,已经删去了很多包和代码,只为了让他人更能快速理解其中的核心代码。  代码中有很多注释都能帮助他人理解,如果大家想使用当前库开发一个自己的小站,是完全可以的,也可以帮助大家更好的理解它。  如果是用于商业项目,推荐nextjs。  css没有做作用域控制,因此如果想隔离作用域,手动添加上层css隔离,比如.index{ ..... }包裹一层,或者尝试自己引入三方包。  webpack通用的配置可以封装成一个文件,然后在每个文件里引入,再个性修改。  但是之前看其他代码的时候发现,这种方法,会增加阅读难度,加上本身配置内容不多,所以不做封装,看起来更直观。  开发环境下,图片路径会出现不一致,比如客户端地址请求地址是localhost...assets/xx.jpg,而服务端是assets/xx.jpg,可能会有警告,但是不影响。  因为只是一个是绝对路径,一个是相对路径。

最后

对于这次的ssr服务端渲染的实现还是挺满意的,也花费了挺多时间。  感受下加载速度吧,欢迎访问大诗人小站,https://dashiren.cn/ 。  部分页面有接口请求,比如https://dashiren.cn/space,加载速度依然很快。

仓库已经准备好,下载下来试试吧,安装依赖后,运行命令即可。https://github.com/zimv/react-ssr

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 详解Vue基于 Nuxt.js 实现服务端渲染(SSR)

    直接使用 Vue 构建前端单页面应用,页面源码时只有简单的几行 html,这并不利于网站的 SEO,这时候就需要服务端渲染 2016 年 10 月 25 日,zeit.co 背后的团队对外发布了一个 React 的服务端渲染应用框架 Next.js 几小时后,一个基于 Vue.js 的服务端渲染应用框架应运而生,与 Next.js 异曲同工,这就是Nuxt.js 一.快速模板 在已经安装了 vue-cli 的前提下,可以快速创建一个 nuxt 的项目模板 vue init nuxt-commun

  • 基于vue-ssr服务端渲染入门详解

    第一部分 基本介绍 1.前言 服务端渲染实现原理机制:在服务端拿数据进行解析渲染,直接生成html片段返回给前端.然后前端可以通过解析后端返回的html片段到前端页面,大致有以下两种形式: 1.服务器通过模版引擎直接渲染整个页面,例如java后端的vm模版引擎,php后端的smarty模版引擎. 2.服务渲染生成html代码块, 前端通过AJAX获取然后使用js动态添加. 2.服务端渲染的优劣 服务端渲染能够解决两大问题: 1.seo问题,有利于搜索引擎蜘蛛抓取网站内容,利于网站的收录和排名.

  • 详解vue服务端渲染(SSR)初探

    前言 首先来讲一下服务端渲染,直白的说就是在服务端拿数据进行解析渲染,直接生成html片段返回给前端.具体用法也有很多种比如: 传统的服务端模板引擎渲染整个页面 服务渲染生成htmll代码块, 前端 AJAX 获取然后js动态添加 服务端渲染的优劣 首先是seo问题,前端动态渲染的内容是不能被抓取到的,而使用服务端渲染就可以解决这个问题.还有就是首屏加载过慢这种问题,比如在SPA中,打开首页需要初始加载很多资源,这时考虑在首屏使用服务端渲染,也是一种折中的优化方案.但是使用SSR时,势必会增加服

  • 详解使用Nuxt.js快速搭建服务端渲染(SSR)应用

    安装 nuxt.js Nuxt.js 官方提功了两种方法来进行项目的初始化,一种是使用Nuxt.js团队的脚手架工具 create-nuxt-app ,一种是根据自己的需求自由配置 使用脚手架适合新手,对 nodejs 后台框架有所了解:按照自己需求自由配置,需要对如何配置 webpack 以及 nodejs 后台框架有所了解. 两种方式比较下就是原生和插件的区别. 使用脚手架安装 需要有 nodejs 或者yarn环境,推荐使用 vscode 自带的控制台输入命令行命令进行操作 在有了环境之后

  • 使用Node搭建reactSSR服务端渲染架构

    如题:本文所讲架构主要用到技术栈有: Node, Express, React, Mobx, webpack4, ES6, ES7, axios, ejs,  log4js, scss,echarts,ant desige SSR的概念 Server Slide Rendering,缩写为 ssr,即服务器端渲染,因为是后端出身,所以其实早就明白是怎么回事,只是没这个具体名词的概念罢了,这个词被频繁提起也是拜近年来前端飞速发展所赐,主要针对 SPA应用,目的大概有以下几个: 解决单页面应用的 S

  • VUE基于NUXT的SSR 服务端渲染

    Server Side Rendering(服务端渲染) SSR 目的是为了解决单页面应用的 SEO 的问题,对于一般网站影响不大,但是对于论坛类,内容类网站来说是致命的,搜索引擎无法抓取页面相关内容,也就是用户搜不到此网站的相关信息. 原理 将 html 在服务端渲染,合成完整的 html 文件再输出到浏览器. 适用场景 客户端的网络比较慢 客户端运行在老的或者直接没有 JavaScript 引擎上 NUXT 作用就是在 node.js 上进一步封装,然后省去我们搭建服务端环境的步骤,只需要遵

  • 详解React+Koa实现服务端渲染(SSR)

    React是目前前端社区最流行的UI库之一,它的基于组件化的开发方式极大地提升了前端开发体验,React通过拆分一个大的应用至一个个小的组件,来使得我们的代码更加的可被重用,以及获得更好的可维护性,等等还有其他很多的优点... 通过React, 我们通常会开发一个单页应用(SPA),单页应用在浏览器端会比传统的网页有更好的用户体验,浏览器一般会拿到一个body为空的html,然后加载script指定的js, 当所有js加载完毕后,开始执行js, 最后再渲染到dom中, 在这个过程中,一般用户只能

  • 实现ssr服务端渲染的方法步骤

    前言 前段时间寻思做个个人网站,然后就立马行动了.  个人网站如何实现选择什么技术方案,自己可以自由决定.  刚好之前有大致想过服务端渲染,加载速度快,还有seo挺适合个人网站的.  所以就自己造了个轮子用koa+react来实现ssr服务端渲染. 什么是ssr 最初听说有单页面的服务端渲染的时候,就理解为类似传统的服务端路由+模板渲染,只是需要用单页面应用的框架写.后面寻思这样好像有点傻,再一了解,原来只是在首次加载的时候,后端进行当前路径页面的组件渲染和数据请求,组装成html返回给前端,用

  • vuecli项目构建SSR服务端渲染的实现

    服务端渲染(SSR) 将一个 Vue 组件在服务端渲染成 HTML 字符串并发送到浏览器,最后将这些静态标记"激活"为可交互应用程序的过程就叫服务端渲染(SSR) 服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行 为什么使用 服务端渲染(SSR) 更好的 SEO:传统的 spa 页面数据都是异步加载,搜索引擎爬虫无法抓取,服务端渲染(SSR)使搜索引擎爬虫抓取工具可以直接查

  • vue的ssr服务端渲染示例详解

    为什么使用服务器端渲染 (SSR) 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面. 请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引.在这里,同步是关键.如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容.也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题. 更快的内容到达时间 (ti

  • vue ssr服务端渲染(小白解惑)

    >初学ssr入坑 初学vue服务端渲染疑惑非常多,我们大部分前端都是半路出家,上手都是前后端分离,对服务端并不了解,不说java.php语言了,连node服务都还没搞明白,理解服务端渲染还是有些困难的: 网上有非常多的vue服务渲染的入门案例,但看了很久,很多,还是一头雾水,搞不明白这些文件和关键字的联系和意思: server.js entrt-client.js server-js built-server-bundle.js vue-ssr-server-bundle.json vue-ss

  • Egg Vue SSR 服务端渲染数据请求与asyncData

    服务端渲染 Node 层直接获取数据 在 Egg 项目如果使用模板引擎规范时通是过 render 方法进行模板渲染,render 的第一个参数模板路径,第二个参数时模板渲染数据. 如如下调用方式: async index(ctx) { // 获取数据,可以是从数据库,后端 Http 接口 等形式 const list = ctx.service.article.getArtilceList(); // 对模板进行渲染,这里的 index.js 是 vue 文件通过 Webpack 构建的 JSB

  • vue ssr+koa2构建服务端渲染的示例代码

    之前做了活动投放页面在百度.360等渠道投放,采用 koa2 + 模版引擎的方式.发现几个问题 相较于框架开发页面效率较低,维护性差 兼容性问题,在页面中添加埋点后发现有些用户的数据拿不到,排查后发现通过各个渠道过来的用户的设备中仍然包含大量低版本的浏览器. 服务端渲染 服务端渲染和单页面渲染区别 查看下面两张图,可以看到如果是服务端渲染,那么在浏览器中拿到的直接是完整的 html 结构.而单页面是一些 script 标签引入的js文件,最终将虚拟dom去挂在到 #app 容器上. @vue/c

随机推荐