全网最完整的 React SSR 同构技术原理解析与实践,从零开始手把手带你打造自己的同构应用开发骨架,帮助大家彻底深入理解服务端渲染及底层实现原理,学完本课程,你也可以打造自己的同构框架。

写在前面

前段时间一直在研究react ssr技术,然后写了一个完整的ssr开发骨架。今天写文,主要是把我的研究成果的精华内容整理落地,另外通过再次梳理希望发现更多优化的地方,也希望可以让更多的人少踩一些坑,让跟多的人理解和掌握这个技术。

相信看过本文(前提是能对你的胃口,也能较好的消化吸收)你一定会对 react ssr 服务端渲染技术有一个深入的理解,可以打造自己的脚手架,更可以用来改造自己的实际项目,当然这不仅限于 react ,其他框架都一样,毕竟原理都是相似的。

为什么要服务端渲染(ssr)

至于为什么要服务端渲染,我相信大家都有所闻,而且每个人都能说出几点来。

首屏等待

在 SPA 模式下,所有的数据请求和 Dom 渲染都在浏览器端完成,所以当我们第一次访问页面的时候很可能会存在“白屏”等待,而服务端渲染所有数据请求和 html内容已在服务端处理完成,浏览器收到的是完整的 html 内容,可以更快的看到渲染内容,在服务端完成数据请求肯定是要比在浏览器端效率要高的多。

没考虑SEO的感受

有些网站的流量来源主要还是靠搜索引擎,所以网站的 SEO 还是很重要的,而 SPA 模式对搜索引擎不够友好,要想彻底解决这个问题只能采用服务端直出。改变不了别人(搜索yinqing),只能改变自己。

SSR + SPA 体验升级

只实现 SSR 其实没啥意义,技术上没有任何发展和进步,否则 SPA 技术就不会出现。

但是单纯的 SPA又不够完美,所以最好的方案就是这两种体验和技术的结合,第一次访问页面是服务端渲染,基于第一次访问后续的交互就是 SPA 的效果和体验,还不影响SEO 效果,这就有点完美了。

单纯实现 ssr 很简单,毕竟这是传统技术,也不分语言,随便用 php 、jsp、asp、node 等都可以实现。

但是要实现两种技术的结合,同时可以最大限度的重用代码(同构),减少开发维护成本,那就需要采用 react 或者 vue 等前端框架相结合 node (ssr) 来实现。

本文主要说 React SSR 技术 ,当然 vue 也一样,只是技术栈不同而已。

核心原理

整体来说 react 服务端渲染原理不复杂,其中最核心的内容就是同构。

node server 接收客户端请求,得到当前的req url path,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props

、context或者store 形式传入组件,然后基于 react 内置的服务端渲染api renderToString() or renderToNodeStream() 把组件渲染为 html字符串或者 stream 流, 在把最终的 html 进行输出前需要将数据注入到浏览器端(注水),server 输出(response)后浏览器端可以得到数据(脱水),浏览器开始进行渲染和节点对比,然后执行组件的componentDidMount 完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束。

技术点确实不少,但更多的是架构和工程层面的,需要把各个知识点进行链接和整合。

这里放一个架构图

react ssr

从 ejs 开始

实现 ssr 很简单,先看一个 node ejs的栗子。

// index.html

react ssr

//node ssr

const ejs = require('ejs');

const http = require('http');

http.createServer((req, res) => {

if (req.url === '/') {

res.writeHead(200, {

'Content-Type': 'text/html'

});

// 渲染文件 index.ejs

ejs.renderFile('./views/index.ejs', {

title: 'react ssr',

data: '首页'},

(err, data) => {

if (err ) {

console.log(err);

} else {

res.end(data);

}

})

}

}).listen(8080);

jsx 到字符串

上面我们结合 ejs模板引擎 ,实现了一个服务端渲染的输出,html 和 数据直接输出到客户端。

参考以上,我们结合 react组件 来实现服务端渲染直出,使用 jsx 来代替 ejs,之前是在 html 里使用 ejs 来绑定数据,现在改写成使用jsx 来绑定数据,使用 react 内置 api 来把组件渲染为 html 字符串,其他没有差别。

为什么react 组件可以被转换为 html字符串呢?

简单的说我们写的 jsx 看上去就像在写 html(其实写的是对象) 标签,其实经过编译后都会转换成React.createElement方法,最终会被转换成一个对象(虚拟DOM),而且和平台无关,有了这个对象,想转换成什么那就看心情了。

const React = require('react');

const { renderToString} = require( 'react-dom/server');

const http = require('http');

//组件

class Index extends React.Component{

constructor(props){

super(props);

}

render(){

return

{this.props.data.title}

}

}

//模拟数据的获取

const fetch = function () {

return {

title:'react ssr',

data:[]

}

}

//服务

http.createServer((req, res) => {

if (req.url === '/') {

res.writeHead(200, {

'Content-Type': 'text/html'

});

const data = fetch();

const html = renderToString();

res.end(html);

}

}).listen(8080);

ps:以上代码不能直接运行,需要结合babel 使用 @babel/preset-react 进行转换

npx babel script.js --out-file script-compiled.js --presets=@babel/preset-react

引出问题

在上面非常简单的就是实现了 react ssr ,把jsx作为模板引擎,不要小看上面的一小段代码,他可以帮我们引出一系列的问题,这也是完整实现 react ssr 的基石。

双端路由如何维护?

首先我们会发现我在 server 端定义了路由 '/',但是在 react SPA 模式下我们需要使用react-router来定义路由。那是不是就需要维护两套路由呢?

获取数据的方法和逻辑写在哪里?

发现数据获取的fetch 写的独立的方法,和组件没有任何关联,我们更希望的是每个路由都有自己的 fetch 方法。

服务端 html 节点无法重用

虽然组件在服务端得到了数据,也能渲染到浏览器内,但是当浏览器端进行组件渲染的时候直出的内容会一闪而过消失。

好了,问题有了,接下来我们就一步一步的来解决这些问题。

同构才是核心

react ssr 的核心就是同构,没有同构的 ssr 是没有意义的。

所谓同构就是采用一套代码,构建双端(server 和 client)逻辑,最大限度的重用代码,不用维护两套代码。而传统的服务端渲染是无法做到的,react 的出现打破了这个瓶颈,并且现在已经得到了比较广泛的应用。

路由同构

双端使用同一套路由规则,node server 通过req url path 进行组件的查找,得到需要渲染的组件。

//组件和路由配置 ,供双端使用 routes-config.js

class Detail extends React.Component{

render(){

return

detail

}

}

class Index extends React.Component {

render() {

return

index

}

}

const routes = [

{

path: "/",

exact: true,

component: Home

},

{

path: '/detail', exact: true,

component:Detail,

},

{

path: '/detail/:a/:b', exact: true,

component: Detail

}

];

//导出路由表

export default routes;

//客户端 路由组件

import routes from './routes-config.js';

function App(){

return (

{

routes.map((item,index)=>{

return

})

}

);

}

export default App;

node server 进行组件查找

路由匹配其实就是对 组件path 规则的匹配,如果规则不复杂可以自己写,如果情况很多种还是使用官方提供的库来完成。

matchRoutes(routes, pathname)

//引入官方库

import { matchRoutes } from "react-router-config";

import routes from './routes-config.js';

const path = req.path;

const branch = matchRoutes(routes, path);

//得到要渲染的组件

const Component = branch[0].route.component;

//node server

http.createServer((req, res) => {

const url = req.url;

//简单容错,排除图片等资源文件的请求

if(url.indexOf('.')>-1) { res.end(''); return false;}

res.writeHead(200, {

'Content-Type': 'text/html'

});

const data = fetch();

//查找组件

const branch = matchRoutes(routes,url);

//得到组件

const Component = branch[0].route.component;

//将组件渲染为 html 字符串

const html = renderToString();

res.end(html);

}).listen(8080);

可以看下matchRoutes方法的返回值,其中route.component 就是 要渲染的组件

[

{

route:

{ path: '/detail', exact: true, component: [Function: Detail] },

match:

{ path: '/detail', url: '/detail', isExact: true, params: {} }

}

]

react-router-config 这个库由react 官方维护,功能是实现嵌套路由的查找,代码没有多少,有兴趣可以看看。

文章走到这里,相信你已经知道了路由同构,所以上面的第一个问题 : 【双端路由如何维护?】 解决了。

数据同构(预取同构)

这里开始解决我们最开始发现的第二个问题 - 【获取数据的方法和逻辑写在哪里?】

数据预取同构,解决双端如何使用同一套数据请求方法来进行数据请求。

先说下流程,在查找到要渲染的组件后,需要预先得到此组件所需要的数据,然后将数据传递给组件后,再进行组件的渲染。

我们可以通过给组件定义静态方法来处理,组件内定义异步数据请求的方法也合情合理,同时声明为静态(static),在 server 端和组件内都也可以直接通过组件(function) 来进行访问。

比如 Index.getInitialProps

//组件

class Index extends React.Component{

constructor(props){

super(props);

}

//数据预取方法 静态 异步 方法

static async getInitialProps(opt) {

const fetch1 =await fetch('/xxx.com/a');

const fetch2 = await fetch('/xxx.com/b');

return {

res:[fetch1,fetch2]

}

}

render(){

return

{this.props.data.title}

}

}

//node server

http.createServer((req, res) => {

const url = req.url;

if(url.indexOf('.')>-1) { res.end(''); return false;}

res.writeHead(200, {

'Content-Type': 'text/html'

});

//组件查找

const branch = matchRoutes(routes,url);

//得到组件

const Component = branch[0].route.component;

//数据预取

const data = Component.getInitialProps(branch[0].match.params);

//传入数据,渲染组件为 html 字符串

const html = renderToString();

res.end(html);

}).listen(8080);

另外还有在声明路由的时候把数据请求方法关联到路由中,比如定一个 loadData 方法,然后在查找到路由后就可以判断是否存在loadData这个方法。

看下参考代码

const loadBranchData = (location) => {

const branch = matchRoutes(routes, location.pathname)

const promises = branch.map(({ route, match }) => {

return route.loadData

? route.loadData(match)

: Promise.resolve(null)

})

return Promise.all(promises)

}

上面这种方式实现上没什么问题,但从职责划分的角度来说有些不够清晰,我还是比较喜欢直接通过组件来得到异步方法。

好了,到这里我们的第二个问题 - 【获取数据的方法和逻辑写在哪里?】 解决了。

渲染同构

假设我们现在基于上面已经实现的代码,同时我们也使用 webpack 进行了配置,对代码进行了转换和打包,整个服务可以跑起来。

路由能够正确匹配,数据预取正常,服务端可以直出组件的 html ,浏览器加载 js 代码正常,查看网页源代码能看到 html 内容,好像我们的整个流程已经走完。

但是当浏览器端的 js 执行完成后,发现数据重新请求了,组件的重新渲染导致页面看上去有些闪烁。

这是因为在浏览器端,双端节点对比失败,导致组件重新渲染,也就是只有当服务端和浏览器端渲染的组件具有相同的props 和 DOM 结构的时候,组件才能只渲染一次。

刚刚我们实现了双端的数据预取同构,但是数据也仅仅是服务端有,浏览器端是没有这个数据,当客户端进行首次组件渲染的时候没有初始化的数据,渲染出的节点肯定和服务端直出的节点不同,导致组件重新渲染。

数据注水

在服务端将预取的数据注入到浏览器,使浏览器端可以访问到,客户端进行渲染前将数据传入对应的组件即可,这样就保证了props的一致。

//node server 参考代码

http.createServer((req, res) => {

const url = req.url;

if(url.indexOf('.')>-1) { res.end(''); return false;}

res.writeHead(200, {

'Content-Type': 'text/html'

});

console.log(url);

//查找组件

const branch = matchRoutes(routes,url);

//得到组件

const Component = branch[0].route.component;

//数据预取

const data = Component.getInitialProps(branch[0].match.params);

//组件渲染为 html

const html = renderToString();

//数据注水

const propsData = `${JSON.stringify(data)}`;

// 通过 ejs 模板引擎将数据注入到页面

ejs.renderFile('./index.html', {

htmlContent: html,

propsData

}, // 渲染的数据key: 对应到了ejs中的index

(err, data) => {

if (err) {

console.log(err);

} else {

console.log(data);

res.end(data);

}

})

}).listen(8080);

//node ejs html

//组件 html内容

//组件 init state ,现在是个字符串

react ssr php,一文吃透 React SSR 服务端渲染和同构原理相关推荐

  1. 【SSR】287- 从头开始,彻底理解服务端渲染原理

    本文出自「掘金社区」,欢迎戳「阅读原文」链接和作者进行技术交流 ?? 大家好,我是神三元,这一次,让我们来以 React 为例,把服务端渲染(Server Side Render,简称"SS ...

  2. 「react进阶」一文吃透React高阶组件(HOC)

    一 前言 React高阶组件(HOC),对于很多react开发者来说并不陌生,它是灵活使用react组件的一种技巧,高阶组件本身不是组件,它是一个参数为组件,返回值也是一个组件的函数.高阶作用用于强化 ...

  3. 「React 深入」一文吃透React v18 全部 Api(1.3w+ 字)

    大厂技术  高级前端  Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 大家好,我是小杜杜,俗话说的好,工欲善其事必先利其器,什么意思呢?就是说你想玩转React就 ...

  4. 17 SSR:使用 React.js 开发 Serverless 服务端渲染应用

    今天我想和你聊一聊怎么用 Serverless 开发一个服务端渲染(SSR)应用. 对前端工程师来说,Serverless 最大的应用场景之一就是开发服务端渲染(SSR)应用.因为传统的服务端渲染应用 ...

  5. 服务端渲染与 Universal React App

    随着 Webpack 等前端构建工具的普及,客户端渲染因为其构建方便,部署简单等方面的优势,逐渐成为了现代网站的主流渲染模式.而在刚刚发布的 React v16.0 中,改进后更为优秀的服务端渲染性能 ...

  6. SSR -- 服务端渲染基础

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 服务端渲染基础 基本概述 随着前端技术栈和工具链的迭代成熟,前端工程化.模块化也已成为了当下的主流技术方案,在这波前端 ...

  7. 服务端渲染SSR与客户端渲染

    文章目录 一.服务端渲染和客户端渲染的区别 服务端渲染(SSR -- server side render) 客户端渲染 二.使用服务端渲染(SSR)的利弊 优势 局限 三.实际应用 应用原则 举例 ...

  8. SSR 服务端渲染与 CSR 客户端渲染

    SSR 服务端渲染与 CSR 客户端渲染 SSR 服务端渲染 CSR 客户端渲染 本文要点: 介绍 SSR 服务端渲染概念.优点.缺点.案例及常用框架. 介绍 CSR 服务端渲染概念.优点.缺点. S ...

  9. 实现SSR服务端渲染

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

最新文章

  1. python pexpect
  2. Mysql异常问题排查与处理——mysql的DNS反向解析和客户端网卡重启
  3. 计算机四级的英文,计算机四级考试中英文术语对照
  4. 自己动手写PHP MVC框架
  5. 中国iOS和Android设备激活量将超美国
  6. wps分析工具库如何加载_量化分析|TALib超好用的技术库,再给它封装一层如何!...
  7. 测试手机游戏平均帧率软件,想测试手机游戏帧率吗?最Skr帧率测试步骤都在这里!...
  8. oppo手机解锁_OPPO手机忘记解锁密码怎么办 解锁手机密码
  9. 无人机-4无人机结构设计
  10. 数学:确定性的丧失---第二章 数学真理的繁荣
  11. android虚线边框_Android自定义View之绘制虚线
  12. 管理软件软件开发_管理在软件开发中的作用
  13. 新疆旅游攻略-禾木村
  14. antd自定义样式主题
  15. 计算机u盘驱动坏了如何的修复,u盘损坏怎么修复 u盘损坏再次使用
  16. 日语中di,ti,du,这些如何用片假名打出来
  17. 参加最牛逼的运营人年终聚会,是种什么样的体验
  18. 封闭解、解析解和数值解定义
  19. 快手+中科大 | 全曝光推荐数据集KuaiRec 2.0版本
  20. offer--刷题之路(持续更新)

热门文章

  1. Akka网络编程基本介绍
  2. 集合点(掌握)-并发
  3. java run() 返回值_java线程的run()没有返回值怎么办?
  4. 最大乘积java_《算法入门经典》-最大乘积(java实现)
  5. JSON序列化视图展示
  6. java 分隔函数split(,-1)的用途
  7. 【Lintcode】076.Longest Increasing Subsequence
  8. 【Uvalive 2531】 The K-League (最大流-类似公平分配问题)
  9. IOS开发之视图和视图控制器
  10. Bailian4121 股票买卖【最值】