渲染方法 如何渲染

大家好,我是鄂茶哥。这次我们就以React为例,把服务器端渲染(“SSR”)学清楚。

这里附上这个项目的github地址:https://github.com/sanyuan0704/react-ssr.

欢迎大家订购星星,提问,共同进步!

第1部分:实现一个基本的React组件SSR。这一部分简要实现了React组件的SSR。

1.SSR vs CSR什么是服务器端渲染?

事不宜迟,只需启动一个express服务器。

var express=require(' express ')\ nvar app=express()\ n \ napp . get('/'),(req,RES)={ \ n RES . send(\ n `\ n html \ n head \ n title hello/title \ n/head \ n body \ Nh1 hello/h1 \ n world/p \ n/body \ n/html \ n `\ n \ napp . listen(3001,()={ \ nconsole . log(' listen:3001 ')\ n并打开网页源代码

bf5d6d82f03349a895eb2f7b926487e8?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=o22lUmm3czS9iqVzxPYM3DzQK1o%3D&index=0

也可以完成显示。

这是服务器端渲染。其实很好理解,就是服务器返回一堆html字符串,然后让浏览器显示。

与服务器端渲染相反的是客户端渲染。什么是客户端渲染?现在创建一个新的React项目,用scaffolding生成项目,然后运行它。这里可以看到React scaffold自动生成的主页。

491fcab1c5e745bcaed14b8785017960?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=KmeR1Zp2gVEot6IF0TfmI7VJp5o%3D&index=1

但是,打开网页源代码。

d856f99696fe41f3a3a3b4d67271ed57?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=y8QCO05IZsWDK8IgqDEKYJZhpyQ%3D&index=2

除了兼容的noscript标记之外,主体中只有一个id为root的标记。首页的内容从哪里来?显然,它是由以下脚本中的JS代码控制的。

所以CSR和SSR最大的区别是JS负责前者的页面渲染,而后者是服务器直接返回HTML让浏览器直接渲染。

为什么要使用服务器端渲染?

246cb32e1d7348979280644999f16d2e?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=zqR0M%2FZu40mI3zAohUDgXpu5ukM%3D&index=3

传统企业社会责任的缺点:

因为页面显示过程需要拉JS文件和执行React代码,所以首屏加载时间会比较慢。对于SEO(搜索引擎优化)来说,你无能为力,因为搜索引擎爬虫只知道html结构的内容,而无法识别JS代码的内容。SSR的出现就是为了解决这些传统CSR的弊端。

其次,实现React组件的服务器端渲染。刚刚开始的express服务返回的只是一个普通的html字符串,但是我们在讨论React的服务器端渲染如何渲染,那么怎么做呢?首先,编写一个简单的React组件:

//containers/home . js \ nim从“React”导入React;\ n const home=()={ \ n return(\ n div \ n div this is sanyuan/div \ n/div \ n)\ n } \ n导出默认home \ n现在复制代码的任务是将其转换成html代码并返回给浏览器。众所周知,JSX的标签实际上是基于虚拟的DOM,它最终会通过一定的方法转换成真实的DOM。虚拟DOM也是JS对象。可以看出,整个服务器的渲染过程都是通过虚拟DOM的编译来完成的,因此也可以看出虚拟DOM的巨大表现力。

react-dom库只是实现了编译虚拟dom的方法。做法如下:

//server/index . js \ nim从“express”导入express;\ n从“react-dom/server”导入{ render tostring };\ n从'导入主目录。/containers/Home '\ n \ n const app=express();\ n const content=render tostring(Home/);app.get('/'),function (req,RES){ \ n RES . send(\ n `\ n html \ n head \ n titles Sr/title \ n/head \ n body \ n div id=' root ' $ { content }/div \ n/body \ n/html \ n `\ n);\ n}) \ napp.listen (3001,()={ \ nconsole . log(' listen:3001 ')\ n \ n复制代码启动快递服务,然后在浏览器上打开相应的端口,页面显示'这里是三元'至此,初步实现了一个React组件,是服务器端渲染。当然,这只是一个非常初级的SSR,对于复杂的项目其实是无能为力的。它将逐步得到改进,以创建一个功能齐全的React SSR框架。

第二部分:同构的介绍。其实之前的SSR是不完整的。通常在开发过程中不可避免的会有一些事件绑定,比如添加一个按钮:

//容器/home。js \ nim从“做出反应”导入做出反应;\ n const Home=()={ \ n return(\ n div \ n div这是三元/div \ n button onClick={()={ alert(' 666 ')} } click/button \ n/div \ n)\ n } \ n导出默认主页\ n复制代码再试一下,你会惊奇的发现,事件绑定无效!那这是为什么呢?原因很简单,react-dom/server下的renderToString并没有做事件相关的处理,因此返回给浏览器的内容不会有事件绑定。

那怎么解决这个问题呢?

这就需要进行同构了。所谓同构,通俗的讲,就是一套反应代码在服务器上运行一遍,到达浏览器又运行一遍。服务端渲染完成页面结构,浏览器端渲染完成事件绑定。

那如何进行浏览器端的事件绑定呢?

唯一的方式就是让浏览器去拉取射流研究…文件执行,让射流研究…代码来控制。于是服务端返回的代码变成了这样:

eac014af61f043a687c56ede7aae92cb?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=H%2B%2Bvy75bdj60lg6UAANVDJ%2FV%2FU4%3D&index=4

有没有发现和之前的区别?区别就是多了一个脚本标签。而它拉取的射流研究…代码就是来完成同构的。

那么这个索引。射流研究…我们如何生产出来呢?

在这里,要用到反动派。具体做法其实就很简单了:

//客户端/索引js \从“做出反应”导入做出反应;\ n从“反应王国”导入ReactDom\ n从'导入主页'/containers/Home 'ReactDom.hydrate(Home /,document。getelementbyid(' root '))\ n复制代码然后用网络包将其编译打包成index.js:

//web pack。客户。js \ n const path=require(' path ');\ n const merge=require(' web pack-merge ');const config=require('/web pack。base’);\ n \ n常量客户端配置={ \ n模式:'开发'\ n条目:'/src/client/index.js '输出:{文件名:' index.js '路径:path.resolve(__dirname,' public') },\ n } \ n模块。exports=merge(配置,客户端配置);\ n \ n//web pack。基地。js \ n模块。出口={ \ n模块:{ \ n规则:[{测试:/\\ .js$/, loader: 'babel-loader ' exclude: /node_modules/,\ n options:{ \ n presets:[' @ babel/preset-react '['@babel/preset-env '{ \ n targets:{ \ n browsers:[' last 2 versions ']\ n } \ n } \ n }]\ n }]\ n } \ n//package。JSON的脚本部分\ n ' scripts '{ \ n ' dev '' NPM-运行-所有并行开发:* '\ n ' dev:start ''节点mon-观察构建-执行节点\ \ '/build/bundle.js\\ ' '\ n ' dev:build:server '' web pack-config web pack。服务器。' js-watch '\ n ' dev:build:client '' web pack-config web pack。客户。js-watch ' \ n },\ n复制代码在这里需要开启表达的静态文件服务:

const app=express();\纳普。使用(express。static(' public ');复制代码现在前端的脚本就能拿到控制浏览器的射流研究…代码啦。

绑定事件完成!

现在来初步总结一下同构代码执行的流程:

209cda78a0a1439284339f12ffcfe161?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=emKV3R4wDl%2FEjY97bUHA4203W5o%3D&index=5

二。同构中的路由问题现在写一个路由的配置文件:

//路由。js \ n从“做出反应”导入做出反应;\ n从” react-路由器-dom “导入{路由},\ n从”主目录”导入.集装箱/家'\ n从'导入登录/containers/log in ' \ n \ n导出默认值(\ n div \ n Route path='/' exact component={ Home }/Route \ n Route path='/log in ' exact component={ log in }/Route \ n/div \ n)\ n复制代码在客户端的控制代码,也就是上面写过的客户端/索引. js中,要做相应的更改:

从“做出反应”导入做出反应;\ n从“反应王国”导入ReactDom\ n从react-router-dom '导入{浏览器路由器} \ n从'导入路由'/Routes ' \ n \ n const App=()={ \ n return(\ n browser router \ n { Routes } \ n/browser router \ n)\ n react DOM。水合物(App/,文档。getelementbyid(' root '))\ n复制代码这时候控制台会报错,

e1dd4b12bd934f25896df57f8d746f87?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=bFHd%2BYsqvCVAmEGKk%2Bwl%2FSyunHI%3D&index=6

因为在Routes.js中,每个途径组件外面包裹着一层div,但服务端返回的代码中并没有这个div,所以报错。如何去解决这个问题?需要将服务端的路由逻辑执行一遍。

//服务器/索引。js \ nim从“快递”导入快递;\ n从以下位置导入{render} ./utils '\ n \ n const app=express();\纳普。使用(express。static(' public ');//注意这里要换成*来匹配app.get('* 'function (req,RES){ \ n RES . send(render(req));\ n }); app.listen(3001,()={ \ n控制台。log(' listen:3001 ')\ n });复制代码//server/utils。js \ n从'导入路由./Routes ' \ n从” react-DOM/服务器”导入{ render tostring };//重要是要用到静态路由器\从” react-路由器-dom “导入{静态路由器};\ n从' React '导入React \ n \ n导出const render=(req)={ \ n//构建服务端的路由\ n const content=render tostring(\ n静态路由器位置={ req。path } \ n { Routes } \ n/静态路由器\ n);\ n return `\ n html \ n head \ n titles SSR/title \ n/head \ n body \ n div id=' root ' $ { content }/div \ n script src='/index。js '/script \ n/body \ n/html \ n `\ n } \ n复制代码现在路由的跳转就没有任何问题啦。注意,这里仅仅是一级路由的跳转,多级路由的渲染在之后的系列中会用反应路由器配置中renderRoutes来处理。

第三部分:同构项目中引入Redux这一节主要是讲述Redux如何被引入到同构项目中以及其中需要注意的问题。

重新回顾一下回家的的运作流程:

01957a309098444b8325b25ce8358566?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=blsmy%2B3DEFa8s5lyqAOciordgPg%3D&index=7

再回顾一下同构的概念,即在反应代码客户端和服务器端各自运行一遍。

一、创建全局商店现在开始创建商店。在项目根目录的商店文件夹(总的商店)下:

从“还原”导入{createStore,applyMiddleware,combine reducers }。\ n从” redux-thunk “导入thunk\ n从'导入{作为主减速器的减速器}./containers/Home/store '//合并项目组件中商店的减速器\ n const reducer=联合减速器({ \ n home:home reducer \ n })\ n//创建店,并引入中间件砰的一声进行异步操作的管理\ n const store=createStore(reducer,applyMiddleware(thunk));//导出创建的存储导出默认存储\ n复制代码二、组件内行为和还原剂的构建主页文件夹下的工程文件结构如下:

e0b67490ecff411f8808cb4bed223994?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=yn2VkBcW%2B98aZJwzyFHyn%2FJQQqE%3D&index=8

在主页的商店目录下的各个文件代码示例:

//常量。js \ n导出常量CHANGE _ LIST=' HOME/CHANGE _ LIST '复制代码//操作。js \ n从” axios “导入axios\ n从'导入{更改列表} ./constants '//普通action \ n const changeList=LIST=({ \ n type:CHANGE _ LIST,\ n LIST \ n });//异步操作的动作(采用砰的一声中间件)\ n export const get home list=()={ \ n return(dispatch)={ \ n return axios。get(' XXX ')\ n . then((RES)={ \ n const list=RES . data。数据;\ n控制台。log(list)\ n dispatch(changeList(list))\ n });\ n };}复制代码//减速器。js \ n从'导入{更改列表} ./constants '\ n \ n const default state={ \ n name:' sanyuan '\ n list:[]\ n } \ n \ n export default(state=默认状态,action)={ \ n switch(action。type){ \ n默认:\ n返回状态; }}复制代码//索引。js \ n从'导入缩减器./减速器'//这么做是为了导出还原剂让全局的商店来进行合并//那么在全局的商店下的索引。射流研究…中只需引入家庭/商店而不需要Home/store/reducer.js//因为脚手架会自动识别文件夹下的指数文件导出{减速器}复制代码三、组件连接全局商店下面是主页组件的编写示例。

从“做出反应”导入React,{ Component };\ n从“反应-还原”导入{ connect };\ n从'导入{ getHomeList } ./store/actions ' \ n \ n class Home extends Component { \ n render(){ \ n const { list }=this。道具\ n返回列表。map(item=div key={ item。id } { item。title }/div)\ n } \ n \ n const mapStateToProps=state=({ \ n list:state。回家。newslist,\ n })\ n \ n const mapDispatchToProps=dispatch=({ \ n get homelist(){ \ n dispatch(get homelist()); }})//连接商店\导出默认连接(mapStateToProps,mapDispatchToProps)(首页);复制代码对于商店的连接操作,在同构项目中分两个部分,一个是与客户端商店的连接,另一部分是与服务端商店的连接。都是通过反应还原中的供应者来传递商店的。

客户端:

//src/客户端/索引。js \ n从“做出反应”导入做出反应;\ n从“反应王国”导入反应堆\ nim从” react-路由器-dom “导入{BrowserRouter,Route };\ n从“反应-还原”导入{提供商};\ n从'导入存储'/store ' \ n导入路由自'/路线。js ' \ n \ n const App=()={ \ n return(\ n Provider store={ store } \ n browser router \ n { routes } \ n/browser router \ n/Provider \ n)\ n } \ n react DOM。水合物(App/,文档。getelementbyid(' root '))\ n复制代码服务端:

//src/server/index.js的内容保持不变//下面是src/server/utils。js \ n从'导入路由./Routes ' \ n从” react-DOM/服务器”导入{ render tostring };\ n从” react-路由器-dom “导入{静态路由器};\ n从“反应-还原”导入{提供商};\ n import React from ' React ' \ n \ n export const render=(req)={ \ n const content=render tostring(\ n Provider store={ store } \ n静态路由器位置={ req。path } \ n { Routes } \ n/静态路由器\ n/提供商\ n);\ n return `\ n html \ n head \ n titles SSR/title \ n/head \ n body \ n div id=' root ' $ { content }/div \ n script src='/index。js '/script \ n/body \ n/html \ n `\ n } \ n复制代码四、潜在的坑其实上面这样的商店创建方式是存在问题的,什么原因呢?

上面的商店是一个单例,当这个单例导出去后,所有的用户用的是同一份店,这是不应该的。那么这么解这个问题呢?

在全局的商店/索引。射流研究…下修改如下:

//导出部分修改\ n export default()={ \ n return createStore(reducer,applyMiddleware(thunk))}复制代码这样在客户端和服务端的射流研究…文件引入时其实引入了一个函数,把这个函数执行就会拿到一个新的店,这样就能保证每个用户访问时都是用的一份新的商店。

第四部分:异步数据的服务端渲染方案(数据注水与脱水)一、问题引入在平常客户端的反应开发中,我们一般在组件的组件安装生命周期函数进行异步数据的获取。但是,在服务端渲染中却出现了问题。

现在我在组件安装钩子函数中进行埃阿斯请求:

从'导入{ getHomeList } ./store/actions' //.\ n componentdimount(){ \ n这。道具。getlist(); } //.\ n const mapDispatchToProps=dispatch=({ \ n getList(){ \ n dispatch(get homelist()); }})复制代码//操作。js \ n从'导入{更改列表} ./constants '从” axios “导入axios \ n \ n const changeList=LIST=({ \ n type:CHANGE _ LIST,\ n LIST \ n })\ n \ n export const get homelist=()={ \ n return dispatch={ \ n//另外起的本地的后端服务\ n返回axioinstance。get(' localhost:4000/API/news。JSON ')\ n . then((RES)={ \ n const list=RES . data。数据;\ n dispatch(changeList(LIST))\ n })\ n } \ n } \ n//缩减器。js \ nimport { CHANGE _ LIST }从./constants '\ n \ n const default state={ \ n name:' sanyuan '\ n LIST:[]\ n } \ n \ n export default(state=默认状态,action)={ \ n switch(action。type){ \ n case CHANGE _ LIST:\ n const new state={ \ n.状态,\ n列表:操作。list \ n } \ n返回新状态\ n默认值:\ n返回状态 }}复制代码好,现在启动服务。

5367de4b336b4371b613bc0d866c3e9c?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=KIJbe%2BAOwTOH8%2BxFf9jYFE4nBTA%3D&index=9

现在页面可以正常渲染了,但是打开网页源代码。

11ea6a8f9d8e40bb815ae96c9046eafc?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=CV3OVtsipbPxRGYZjOw5rT8hi38%3D&index=10

源代码中没有这样的列表数据!这是为什么呢?

我们来分析一下客户端和服务器的运行过程。当浏览器发送请求时,服务器接收请求。此时,服务器和客户机的存储都是空的。然后,客户端执行ComponentIdMount生命周期中的函数来获取数据并将其呈现给页面。但是服务器从不执行ComponentIdMount,所以不会得到数据,这也导致服务器的存储总是为空。换句话说,对异步数据的操作始终只是客户端呈现。

现在的工作就是让服务器再次执行获取数据的操作,从而达到服务器真正的渲染效果。

第二,改造路线。在完成这个方案之前,您需要转换原始路由,即routes.js

从'导入主页。/containers/Home '\ n从'导入登录。/containers/log in '\ n \ n导出默认值[\ n {\ n Path:'/'\ n Component: home,\ n Exact: true,\ n Load Data: home.loaddata,//服务器获取异步数据的函数\ n Key:' home' \ n},\ n {\ n Path component: Login, exact: true,\ n Key:' log in ' \ n } \ n }];复制代码。这时,写在客户端和服务器端的JSX代码也相应地发生了变化。

//Client //以下routes变量都引用routes . js \ n provider store={ store } \ n browser router \ n div \ n \ n routers . map(route={ \ n route {.route }/\ N })\ N } \ N/N/browser Router \ N/Provider \ N \ N复制代码//server \ N Provider Store={ Store } \ N静态路由器\ nDiv \ N { \ N routers . map(route={ \ N route {.route }/\ N })\ N } \ N/static router \ N/provider \ N复制代码,其中配置了loadData参数,表示服务器获取数据的功能。每次渲染组件获取异步数据时,都会调用对应组件的这个函数。所以在写这个函数的具体代码之前,我们有必要弄清楚,对于不同的路由,如何匹配不同的loadData函数。

将以下逻辑添加到server/utils.js中

从“react-router-config”导入{ match routes }; //调用matchRoutes匹配当前路由(支持多级路由)\ n const matched routes=match routes(routes,req.path) \ n//promise对象数组\ n const promises=[];\ n Matched routes . foreach(item={ //如果此路由对应的组件有loadData方法 if (item.route.loadData) { //然后执行一次并传入存储区\ n//注意调用loadData函数后需要返回promises . push(item . route . load data(store))\ n } \ n)\ n Promise . all(promises)。然后()={ //此时所有的数据应该都进了存储 //执行渲染过程(res.send操作)。

从'导入{gethomelist}。/store/actions ' \ n \ n home . load data=(store)={ \ n return store . dispatch(get homelist())\ n } \ n复制代码//actions . js \ n export const get homelist=()={ \ n return dispatch={ \ n return axios . get(' xxxx ')\ n . then((RES)={ \ n const list=RES . data . data;\ n Dispatch(changelist(list))\ n })\ n } \ n复制代码按照这个思路,完成了服务器端渲染中获取异步数据的功能。

三。注水和脱水的数据。其实这里还是有一些细节的。比如我在生命周期钩子中注释异步请求函数的时候,现在页面中不会有数据,但是当我打开网页的源代码的时候,我发现:

32288afaffbd46cf830e00daf2102f8f?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=dOJH9Msbh2GlWwFEYvBrNfZAudI%3D&index=11

数据已经安装在服务器返回的HTML代码中。这意味着服务器和客户端的存储不同步。

其实很好理解。当服务器获得存储并获得数据时,客户机的js代码再次被执行。当执行客户端代码时,会创建一个空存储,并且两个存储的数据不能同步。

如何才能让这两个存储的数据同步变化?

首先,在服务器获得它之后,将这样一个脚本标记添加到返回的html代码中:

script \ n window . context={ \ nstate:$ { JSON . stringify(store . getstate())} \ n } \ n/script \ n复制代码这叫做数据的“注水”,即把服务器的存储数据注入到window的全局环境中。下一步是“脱水”处理,换句话说,将绑定在窗口上的数据交给客户端的存储,这可以在客户端存储的源上完成,即在全局store/index.js中完成

//store/index . js \ nimport { createStore,applyMiddleware,combineReducers } from ' redux\ n从“redux-thunk”导入thunk;\ n将{减速器作为主减速器}从导入

‘../containers/Home/store’;const reducer = combineReducers({ home: homeReducer})//服务端的store创建函数export const getStore = () =>> { return createStore(reducer, applyMiddleware(thunk));}//客户端的store创建函数export const getClientStore = () =>> { const defaultState = window.context ? window.context.state : {}; return createStore(reducer, defaultState, applyMiddleware(thunk));}复制代码

至此,数据的脱水和注水操作完成。但是还是有一些瑕疵,其实当服务端获取数据之后,客户端并不需要再发送Ajax请求了,而客户端的React代码仍然存在这样的浪费性能的代码。怎么办呢?

还是在Home组件中,做如下的修改:

componentDidMount() { //判断当前的数据是否已经从服务端获取 //要知道,如果是首次渲染的时候就渲染了这个组件,则不会重复发请求 //若首次渲染页面的时候未将这个组件渲染出来,则一定要执行异步请求的代码 //这两种情况对于同一组件是都是有可能发生的 if (!this.props.list.length) { this.props.getHomeList() }}复制代码

一路做下来,异步数据的服务端渲染还是比较复杂的,但是难度并不是很大,需要耐心地理清思路。

至此一个比较完整的SSR框架就搭建的差不多了,但是还有一些内容需要补充,之后会继续更新的。加油吧!

part5: node作中间层及请求代码优化一、为什么要引入node中间层?

其实任何技术都是与它的应用场景息息相关的。这里我们反复谈的SSR,其实不到万不得已我们是用不着它的,SSR所解决的最大的痛点在于SEO,但它同时带来了更昂贵的成本。不仅因为服务端渲染需要更加复杂的处理逻辑,还因为同构的过程需要服务端和客户端都执行一遍代码,这虽然对于客户端并没有什么大碍,但对于服务端却是巨大的压力,因为数量庞大的访问量,对于每一次访问都要另外在服务器端执行一遍代码进行计算和编译,大大地消耗了服务器端的性能,成本随之增加。如果访问量足够大的时候,以前不用SSR的时候一台服务器能够承受的压力现在或许要增加到10台才能抗住。痛点在于SEO,但如果实际上对SEO要求并不高的时候,那使用SSR就大可不必了。

那同样地,为什么要引入node作为中间层呢?它是处在哪两者的中间?又是解决了什么场景下的问题?

在不用中间层的前后端分离开发模式下,前端一般直接请求后端的接口。但真实场景下,后端所给的数据格式并不是前端想要的,但处于性能原因或者其他的因素接口格式不能更改,这时候需要在前端做一些额外的数据处理操作。前端来操作数据本身无可厚非,但是当数据量变得庞大起来,那么在客户端就是产生巨大的性能损耗,甚至影响到用户体验。在这个时候,node中间层的概念便应运而生。

它最终解决的前后端协作的问题。

一般的中间层工作流是这样的:前端每次发送请求都是去请求node层的接口,然后node对于相应的前端请求做转发,用node去请求真正的后端接口获取数据,获取后再由node层做对应的数据计算等处理操作,然后返回给前端。这就相当于让node层替前端接管了对数据的操作。

74d82854f71f4ec09619e01f955d0186?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=WZFNZ2I0BVYLm%2Bbct%2BHRtdcQlFw%3D&index=12

二、SSR框架中引入中间层

在之前搭建的SSR框架中,服务端和客户端请求利用的是同一套请求后端接口的代码,但这是不科学的。

对客户端而言,最好通过node中间层。而对于这个SSR项目而言,node开启的服务器本来就是一个中间层的角色,因而对于服务器端执行数据请求而言,就可以直接请求真正的后端接口啦。

//actions.js//参数server表示当前请求是否发生在node服务端const getUrl = (server) =>> { return server ? ‘xxxx(后端接口地址)’ : ‘/api/sanyuan.json(node接口)’;}//这个server参数是Home组件里面传过来的,//在componentDidMount中调用这个action时传入false,//在loadData函数中调用时传入true, 这里就不贴组件代码了export const getHomeList = (server) =>> { return dispatch =>> { return axios.get(getUrl(server)) .then((res) =>> { const list = res.data.data; dispatch(changeList(list)) }) }}复制代码

在server/index.js应拿到前端的请求做转发,这里是直接用proxy形式来做,也可以用node单独向后端发送一次HTTP请求。

//增加如下代码import proxy from ‘express-http-proxy’;//相当于拦截到了前端请求地址中的/api部分,然后换成另一个地址app.use(‘/api’, proxy(‘http://xxxxxx(服务端地址)’, { proxyReqPathResolver: function(req) { return ‘/api’+req.url; }}));复制代码三、请求代码优化

其实请求的代码还是有优化的余地的,仔细想想,上面的server参数其实是不用传递的。

现在我们利用axios的instance和thunk里面的withExtraArgument来做一些封装。

//新建server/request.jsimport axios from ‘axios’const instance = axios.create({ baseURL: ‘http://xxxxxx(服务端地址)’})export default instance//新建client/request.jsimport axios from ‘axios’const instance = axios.create({ //即当前路径的node服务 baseURL: ‘/’})export default instance复制代码

然后对全局下store的代码做一个微调:

import {createStore, applyMiddleware, combineReducers} from ‘redux’;import thunk from ‘redux-thunk’;import { reducer as homeReducer } from ‘../containers/Home/store’;import clientAxios from ‘../client/request’;import serverAxios from ‘../server/request’;const reducer = combineReducers({ home: homeReducer})export const getStore = () =>> { //让thunk中间件带上serverAxios return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));}export const getClientStore = () =>> { const defaultState = window.context ? window.context.state : {}; //让thunk中间件带上clientAxios return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));}复制代码

现在Home组件中请求数据的action无需传参,actions.js中的请求代码如下:

export const getHomeList = () =>> { //返回函数中的默认第三个参数是withExtraArgument传进来的axios实例 return (dispatch, getState, axiosInstance) =>> { return axiosInstance.get(‘/api/sanyuan.json’) .then((res) =>> { const list = res.data.data; console.log(res) dispatch(changeList(list)) }) }}复制代码

至此,代码优化就做的差不多了,这种代码封装的技巧其实可以用在其他的项目当中,其实还是比较优雅的。

part6: 多级路由渲染(renderRoutes)

现在将routes.js的内容改变如下:

import Home from ‘./containers/Home’;import Login from ‘./containers/Login’;import App from ‘./App’//这里出现了多级路由export default [{ path: ‘/’, component: App, routes: [ { path: “/”, component: Home, exact: true, loadData: Home.loadData, key: ‘home’, }, { path: ‘/login’, component: Login, exact: true, key: ‘login’, } ]}]复制代码

现在的需求是让页面公用一个Header组件,App组件编写如下:

import React from ‘react’;import Header from ‘./components/Header’;const App = (props) =>> { console.log(props.route) return ( <div>> <Header>></Header>> </div>> )}export default App;复制代码

对于多级路由的渲染,需要服务端和客户端各执行一次。 因此编写的JSX代码都应有所实现:

//routes是指routes.js中返回的数组//服务端:<Provider store={store}>> <StaticRouter location={req.path} >> <div>> {renderRoutes(routes)} </div>> </StaticRouter>></Provider>>//客户端:<Provider store={getClientStore()}>> <BrowserRouter>> <div>> {renderRoutes(routes)} </div>> </BrowserRouter>></Provider>>复制代码

这里都用到了renderRoutes方法,其实它的工作非常简单,就是根据url渲染一层路由的组件(这里渲染的是App组件),然后将下一层的路由通过props传给目前的App组件,依次循环。

那么,在App组件就能通过props.route.routes拿到下一层路由进行渲染:

import React from ‘react’;import Header from ‘./components/Header’;//增加renderRoutes方法import { renderRoutes } from ‘react-router-config’;const App = (props) =>> { console.log(props.route) return ( <div>> <Header>></Header>> <!–拿到Login和Home组件的路由–>> {renderRoutes(props.route.routes)} </div>> )}export default App;复制代码

至此,多级路由的渲染就完成啦。

part7: css的服务端渲染思路(context钩子变量)一、客户端项目中引入CSS

还是以Home组件为例

//Home/style.cssbody { background: gray;}复制代码

现在,在Home组件代码中引入:

import styles from ‘./style.css’;复制代码

要知道这样的引入CSS代码的方式在一般环境下是运行不起来的,需要在webpack中做相应的配置。 首先安装相应的插件。

npm install style-loader css-loader –D复制代码

//webpack.client.jsconst path = require(‘path’);const merge = require(‘webpack-merge’);const config = require(‘./webpack.base’);const clientConfig = { mode: ‘development’, entry: ‘./src/client/index.js’, module: { rules: [{ test: /\\.css?$/, use: [‘style-loader’, { loader: ‘css-loader’, options: { modules: true } }] }] }, output: { filename: ‘index.js’, path: path.resolve(__dirname, ‘public’) },}module.exports = merge(config, clientConfig);复制代码

//webpack.base.js代码,回顾一下,配置了ES语法相关的内容module.exports = { module: { rules: [{ test: /\\.js$/, loader: ‘babel-loader’, exclude: /node_modules/, options: { presets: [‘@babel/preset-react’, [‘@babel/preset-env’, { targets: { browsers: [‘last 2 versions’] } }]] } }] }}复制代码

好,现在在客户端CSS已经产生了效果。

634023b5df534363947f27293decaf03?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=26W0eMbGb6ASaDDMxbC%2FviO4QtE%3D&index=13

可是打开网页源代码:

69c1f8e4909f4ed094bd38f9f92db549?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=mGNdCcayAUtKM5LpTWVkd%2B0JZmo%3D&index=14

咦?里面并没有出现任何有关CSS样式的代码啊!那这是什么原因呢?很简单,其实我们的服务端的CSS加载还没有做。接下来我们来完成CSS代码的服务端的处理。

二、服务端CSS的引入

首先,来安装一个webpack的插件,

npm install -D isomorphic-style-loader复制代码

然后再webpack.server.js中做好相应的css配置:

//webpack.server.jsconst path = require(‘path’);const nodeExternals = require(‘webpack-node-externals’);const merge = require(‘webpack-merge’);const config = require(‘./webpack.base’);const serverConfig = { target: ‘node’, mode: ‘development’, entry: ‘./src/server/index.js’, externals: [nodeExternals()], module: { rules: [{ test: /\\.css?$/, use: [‘isomorphic-style-loader’, { loader: ‘css-loader’, options: { modules: true } }] }] }, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘build’) }}module.exports = merge(config, serverConfig);复制代码

它做了些什么事情?

再看看这行代码:

import styles from ‘./style.css’;复制代码

引入css文件时,这个isomorphic-style-loader帮我们在styles中挂了三个函数。输出styles看看:

b7d6aebcda2a4b96aa448be4a1ef7851?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=tj2YWcxoLtA841nDN332slt1miU%3D&index=15

现在我们的目标是拿到CSS代码,直接通过styles._getCss即可获得。

那我们拿到CSS代码后放到哪里呢?其实react-router-dom中的StaticRouter中已经帮我们准备了一个钩子变量context。如下

//context从外界传入<StaticRouter location={req.path} context={context}>> <div>> {renderRoutes(routes)} </div>></StaticRouter>>复制代码

这就意味着在路由配置对象routes中的组件都能在服务端渲染的过程中拿到这个context,而且这个context对于组件来说,就相当于组件中的props.staticContext。并且,这个props.staticContext只会在服务端渲染的过程中存在,而客户端渲染的时候不会被定义。这就让我们能够通过这个变量来区分两种渲染环境啦。

现在,我们需要在服务端的render函数执行之前,初始化context变量的值:

let context = { css: [] }复制代码

我们只需要在组件的componentWillMount生命周期中编写相应的逻辑即可:

componentWillMount() { //判断是否为服务端渲染环境 if (this.props.staticContext) { this.props.staticContext.css.push(styles._getCss()) }}复制代码

服务端的renderToString执行完成后,context的CSS现在已经是一个有内容的数组,让我们来获取其中的CSS代码:

//拼接代码const cssStr = context.css.length ? context.css.join(‘\’) : ”;复制代码

现在挂载到页面:

//放到返回的html字符串里的header里面<style>>${cssStr}</style>>复制代码

8d7f3fc5035448ac8515d9978a0d05bf?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=Qm7VKbfmO73npiZAfg2xLWemJXY%3D&index=16

网页源代码中看到了CSS代码,效果也没有问题。CSS渲染完成!

三、利用高阶组件优化代码

也许你已经发现,对于每一个含有样式的组件,都需要在componentWillMount生命周期中执行完全相同的逻辑,对于这些逻辑我们是否能够把它封装起来,不用反复出现呢?

其实是可以实现的。利用高阶组件就可以完成:

//根目录下创建withStyle.js文件import React, { Component } from ‘react’;//函数返回组件//需要传入的第一个参数是需要装饰的组件//第二个参数是styles对象export default (DecoratedComponent, styles) =>> { return class NewComponent extends Component { componentWillMount() { //判断是否为服务端渲染过程 if (this.props.staticContext) { this.props.staticContext.css.push(styles._getCss()) } } render() { return <DecoratedComponent {…this.props} />> } }}复制代码

然后让这个导出的函数包裹我们的Home组件。

import WithStyle from ‘../../withStyle’;//……const exportHome = connect(mapStateToProps, mapDispatchToProps)(withStyle(Home, styles));export default exportHome;复制代码

这样是不是简洁很多了呢?将来对于越来越多的组件,采用这种方式也是完全可以的。

part8: 做好SEO的一些技巧,引入react-helmet

这一节我们来简单的聊一点SEO相关的内容。

一、SEO技巧分享

所谓SEO(Search Engine Optimization),指的是利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名。现在的搜索引擎爬虫一般是全文分析的模式,分析内容涵盖了一个网站主要3个部分的内容:文本、多媒体(主要是图片)和外部链接,通过这些来判断网站的类型和主题。因此,在做SEO优化的时候,可以围绕这三个角度来展开。

对于文本来说,尽量不要抄袭已经存在的文章,以写技术博客为例,东拼西凑抄来的文章排名一般不会高,如果需要引用别人的文章要记得声明出处,不过最好是原创,这样排名效果会比较好。多媒体包含了视频、图片等文件形式,现在比较权威的搜索引擎爬虫比如Google做到对图片的分析是基本没有问题的,因此高质量的图片也是加分项。另外是外部链接,也就是网站中a标签的指向,最好也是和当前网站相关的一些链接,更容易让爬虫分析。

当然,做好网站的门面,也就是标题和描述也是至关重要的。如:

6b704727530c42a697bff5e2bc5833c3?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=XTNCs7kajRJ3Ao0PQRbA58GrqUw%3D&index=17

网站标题中不仅仅包含了关键词,而且有比较详细和靠谱的描述,这让用户一看到就觉得非常亲切和可靠,有一种想要点击的冲动,这就表明网站的转化率比较高。

二、引入react-helmet

而React项目中,开发的是单页面的应用,页面始终只有一份title和description,如何根据不同的组件显示来对应不同的网站标题和描述呢?

其实是可以做到的。

npm install react-helmet –save复制代码

组件代码:(还是以Home组件为例)

import { Helmet } from ‘react-helmet’;//…render() { return ( <Fragment>> <!–Helmet标签中的内容会被放到客户端的head部分–>> <Helmet>> <title>>这是三元的技术博客,分享前端知识</title>> <meta name=”description” content=”这是三元的技术博客,分享前端知识”/>> </Helmet>> <div className=”test”>> { this.getList() } </div>> </Fragment>> );//…复制代码

这只是做了客户端的部分,在服务端仍需要做相应的处理。

其实也非常简单:

//server/utils.jsimport { renderToString } from ‘react-dom/server’;import { StaticRouter } from ‘react-router-dom’; import React from ‘react’;import { Provider } from “react-redux”;import { renderRoutes } from ‘react-router-config’;import { Helmet } from ‘react-helmet’;export const render = (store, routes, req, context) =>> { const content = renderToString( <Provider store={store}>> <StaticRouter location={req.path} context={context}>> <div>> {renderRoutes(routes)} </div>> </StaticRouter>> </Provider>> ); //拿到helmet对象,然后在html字符串中引入 const helmet = Helmet.renderStatic(); const cssStr = context.css.length ? context.css.join(‘\’) : ”; return ` <html>> <head>> <style>>${cssStr}</style>> ${helmet.title.toString()} ${helmet.meta.toString()} </head>> <body>> <div id=”root”>>${content}</div>> <script>> window.context = { state: ${JSON.stringify(store.getState())} } </script>> <script src=”/index.js”>></script>> </body>> </html>> `};复制代码

现在来看看效果:

17d707ffa41c490185be5acddaeb3ced?_iz=31825&from=article.detail&x-expires=1702420637&x-signature=x1cQH5TLmtHPGP5fiNNr1H8Vehc%3D&index=18

网页源代码中显示出对应的title和description, 客户端的显示也没有任何问题,大功告成!

关于React的服务端渲染原理,就先分享到这里,内容还是比较复杂的,对于前端的综合能力要求也比较高,但是坚持跟着学下来,一定会大有裨益的。相信你看了这一系列之后也有能力造出自己的SSR轮子,更加深刻地理解这一方面的技术。

原链接:https://juejin.im/post/5d1fe6be51882579db031a6d

如何渲染

最近在做一个功能,后端的家伙为了方便直接给了我20w条数据;

因为后端的家伙是个脾气暴躁的老大哥,我做不到;

我得自己想办法。毕竟条条大路通罗马。

留在青山在,不怕没柴烧吧?[眼泪][眼泪][眼泪]

所以我自己做了几种方法来麻醉自己。

这个方案有很大的优势,代码写起来非常简单,不需要太多考虑。

M1的电脑可以在5秒钟内完成渲染。

温馨提示:不要用I9以下的电脑尝试下面的代码,你掌握不了。[啜泣][啜泣][啜泣]

const Index=()={ \ n const list=200000 \ n \ n const render list=()={ \ n return div \ n { Array(list)。填充(0)。map( (item,index)={ \ N return div key={ index } { `第{ index }个绘制的节点`}/div \ N } } \ N \ N return div \ N { render list()} \ N/div \ N } \ N \ N导出默认索引\

比如数据量太大,在穿梭机的情况下,估计很多用户的电脑都“冒烟”了。

该方案也是使用率较大的方案,通常用于数据量较小的列表渲染,因此没有太大问题。

作为一个有经验、有技术、有纪律的程序员,在不确定列表渲染的时候肯定不会采用这种方案。

通过分页,减少周期数

这样渲染的第一个周期会减少一半,整体渲染速度会提高;

相比第一种,效率是提高了,但是对于20w的数据,还是杯水车薪,不太理想。

const render list=async()={ \ n const list=Array(200000)。fill(0)\ n const total=list . length \ n const page=0 \ n const limit=200 \ n const total page=math . ceil(total/limit)\ n \ n const render=(page)={ \ n if(page=total page)return \ n setTimeout(()={ \ n for(let I=page * limit;I页*极限极限;I){ \ n const item=list[I]\ n const div=document . createelement(' div ')\ n div . innerhtml=` img src=' $ { item . src } '/span $ { item . text }/span `\ n container . append child(div)\ n } \ n render(page 1)\ n },0) } render(page)}

RequestAnimationFrame()告诉浏览器——你要执行一个动画,要求浏览器在下次重绘前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该函数将在浏览器下一次重绘之前执行。

const Index=()={ \ n const list=Array(200000)。fill(0)\ N const page=0 \ N const limit=200 \ N const total page=math . ceil(list . length/limit)\ N \ N Use effect(()={ \ N const container=document . query selector(' # box ')as HTMLDivElement \ N const render list=async()={ \ N console . log(list)\ N const total=list . length \ N const page=0 \ N const limit=200 \ N const total page=math . ceil(total/limit)\I页*极限极限;I){ \ N const item=list[I]\ N const div=document . createelement(' div ')\ N div . innerhtml=' drawn node `\ N container . appendchild(div)\ N } \ N render(page 1)\ N } \ N } \ N render(page)\ N } \ N render list()\ N },[]) \ n \ n return div id=' box'/\ n} \ n导出默认索引是用此方法绘制的,打开它几乎需要几秒钟时间。

但是由于绘制的数据量巨大,很明显浏览器只是先绘制了一部分,其余的还在绘制;

使用第三方插件也可以解决这个问题。

这里推荐一个插件:react-virtualized。

它的渲染原理是每次只渲染你想要的数据,在滚动到临界值之前,继续获取上下页的数据,实际上并不是一次性渲染20w条数据;

日常开发中,如果不想在项目中引用太多插件,建议使用方案三;

基本上可以满足很多场景,扩展性很好;

如果数据量小,又不用考虑性能,不如做梭子。

人们通常是如何渲染列表的?请在评论区留言。

#头条创作挑战# #热门#\x02#web#\x02

大学素材

西南少数民族文字文献(全15册)PDF电子版下载【高清版】

2023-12-2 8:56:31

解说文案

《血网边缘》视频解说文案+片源网盘下载

2024-8-24 12:56:00

购物车
优惠劵
搜索