0

0

React项目的服务端渲染改造详解

小云云

小云云

发布时间:2018-03-20 09:10:38

|

2846人浏览过

|

来源于php中文网

原创

因为对网页seo的需要,要把之前的react项目改造为服务端渲染,经过一番调查和研究,查阅了大量互联网资料。成功踩坑。本文主要和大家介绍了详解react项目的服务端渲染改造(koa2+webpack3.11),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望能帮助到大家。

选型思路:实现服务端渲染,想用React最新的版本,并且不对现有的写法做大的改动,如果一开始就打算服务端渲染,建议直接用NEXT框架来写

项目地址:https://github.com/wlx200510/react_koa_ssr

脚手架选型:webpack3.11.0 + react Router4 + Redux + koa2 + React16 + Node8.x

主要心得:对React的相关知识更加熟悉,成功拓展自己的技术领域,对服务端技术在实际项目上有所积累

注意点:使用框架前一定确认当前webpack版本为3.x Node为8.x以上,读者最好用React在3个月以上,并有实际React项目经验

项目目录介绍


├── assets
│ └── index.css //放置一些全局的资源文件 可以是js 图片等
├── config
│ ├── webpack.config.dev.js 开发环境webpack打包设置
│ └── webpack.config.prod.js 生产环境webpack打包设置
├── package.json
├── README.md
├── server server端渲染文件,如果对不是很了解,建议参考[koa教程](http://wlxadyl.cn/2018/02/11/koa-learn/)
│ ├── app.js
│ ├── clientRouter.js // 在此文件中包含了把服务端路由匹配到react路由的逻辑
│ ├── ignore.js
│ └── index.js
└── src
 ├── app 此文件夹下主要用于放置浏览器和服务端通用逻辑
 │ ├── configureStore.js //redux-thunk设置
 │ ├── createApp.js  //根据渲染环境不同来设置不同的router模式
 │ ├── index.js
 │ └── router
 │  ├── index.js
 │  └── routes.js  //路由配置文件! 重要
 ├── assets
 │ ├── css    放置一些公共的样式文件
 │ │ ├── _base.scss  //很多项目都会用到的初始化css
 │ │ ├── index.scss
 │ │ └── my.scss
 │ └── img
 ├── components    放置一些公共的组件
 │ ├── FloatDownloadBtn 公共组件样例写法
 │ │ ├── FloatDownloadBtn.js
 │ │ ├── FloatDownloadBtn.scss
 │ │ └── index.js
 │ ├── Loading.js
 │ └── Model.js   函数式组件的写法
 │
 ├── favicon.ico
 ├── index.ejs    //渲染的模板 如果项目需要,可以放一些公共文件进去
 ├── index.js    //包括热更新的逻辑
 ├── pages     页面组件文件夹
 │ ├── home
 │ │ ├── components  // 用于放置页面组件,主要逻辑
 │ │ │ └── homePage.js
 │ │ ├── containers  // 使用connect来封装出高阶组件 注入全局state数据
 │ │ │ └── homeContainer.js
 │ │ ├── index.js  // 页面路由配置文件 注意thunk属性
 │ │ └── reducer
 │ │  └── index.js // 页面的reducer 这里暴露出来给store统一处理 注意写法
 │ └── user
 │  ├── components
 │  │ └── userPage.js
 │  ├── containers
 │  │ └── userContainer.js
 │  └── index.js
 └── store
  ├── actions   // 各action存放地
  │ ├── home.js
  │ └── thunk.js
  ├── constants.js  // 各action名称汇集处 防止重名
  └── reducers
   └── index.js  // 引用各页面的所有reducer 在此处统一combine处理

项目的构建思路

  1. 本地开发使用webpack-dev-server,实现热更新,基本流程跟之前react开发类似,仍是浏览器端渲染,因此在编写代码时要考虑到一套逻辑,两种渲染环境的问题。

  2. 当前端页面渲染完成后,其Router跳转将不会对服务端进行请求,从而减轻服务端压力,从而页面的进入方式也是两种,还要考虑两种渲染环境下路由同构的问题。

  3. 生产环境要使用koa做后端服务器,实现按需加载,在服务端获取数据,并渲染出整个HTML,利用React16最新的能力来合并整个状态树,实现服务端渲染。

本地开发介绍

查看本地开发主要涉及的文件是src目录下的index.js文件,判断当前的运行环境,只有在开发环境下才会使用module.hot的API,实现当reducer发生变化时的页面渲染更新通知,注意其中的hydrate方法,这是v16版本的一个专门为服务端渲染新增的API方法,它在render方法的基础上实现了对服务端渲染内容的最大可能重用,实现了静态DOM到动态NODES的过程。实质是代替了v15版本下判断checksum标记的过程,使得重用的过程更加高效优雅。


const renderApp=()=>{
 let application=createApp({store,history});
 hydrate(application,document.getElementById('root'));
}
window.main = () => {
 Loadable.preloadReady().then(() => {
 renderApp()
 });
};

if(process.env.NODE_ENV==='development'){
 if(module.hot){
 module.hot.accept('./store/reducers/index.js',()=>{
  let newReducer=require('./store/reducers/index.js');
  store.replaceReducer(newReducer)
 })
 module.hot.accept('./app/index.js',()=>{
  let {createApp}=require('./app/index.js');
  let newReducer=require('./store/reducers/index.js');
  store.replaceReducer(newReducer)
  let application=createApp({store,history});
  hydrate(application,document.getElementById('root'));
 })
 }
}

注意window.main这个函数的定义,结合index.ejs可以知道这个函数是所有脚本加载完成后才触发,里面用的是react-loadable的写法,用于页面的懒加载,关于页面分别打包的写法要结合路由设置来讲解,这里有个大致印象即可。需要注意的是app这个文件下暴露出的三个方法是在浏览器端和服务器端通用的,接下来主要就是说这部分的思路。

路由处理

接下来看以下src/app目录下的文件,index.js暴露了三个方法,这里面涉及的三个方法在服务端和浏览器端开发都会用到,这一部分主要讲其下的router文件里面的代码思路和createApp.js文件对路由的处理,这里是实现两端路由相互打通的关键点。

router文件夹下的routes.js是路由配置文件,将各个页面下的路由配置都引进来,合成一个配置数组,可以通过这个配置来灵活控制页面上下线。同目录下的index.js是RouterV4的标准写法,通过遍历配置数组的方式传入路由配置,ConnectRouter是用于合并Router的一个组件,注意到history要作为参数传入,需要在createApp.js文件里做单独的处理。先大致看一下Route组件中的几个配置项,值得注意的是其中的thunk属性,这是实现后端获取数据后渲染的关键一步,正是这个属性实现了类似Next里面的组件提前获取数据的生命周期钩子,其余的属性都可以在相关React-router文档中找到说明,这里不在赘述。


import routesConfig from './routes';
const Routers=({history})=>(
 <ConnectedRouter history={history}>
 <p>
  {
  routesConfig.map(route=>(
   <Route key={route.path} exact={route.exact} path={route.path} component={route.component} thunk={route.thunk} />
  ))
  }
 </p>
 </ConnectedRouter>
)
export default Routers;

查看app目录下的createApp.js里面的代码可以发现,本框架是针对不同的工作环境做了不同的处理,只有在生产环境下才利用Loadable.Capture方法实现了懒加载,动态引入不同页面对应的打包之后的js文件。到这里还要看一下组件里面的路由配置文件的写法,以home页面下的index.js为例。注意/* webpackChunkName: 'Home' */这串字符,实质是指定了打包后此页面对应的js文件名,所以针对不同的页面,这个注释也需要修改,避免打包到一起。loading这个配置项只会在开发环境生效,当页面加载未完成前显示,这个实际项目开发如果不需要可以删除此组件。


import {homeThunk} from '../../store/actions/thunk';

const LoadableHome = Loadable({
 loader: () =>import(/* webpackChunkName: 'Home' */'./containers/homeContainer.js'),
 loading: Loading,
});

const HomeRouter = {
 path: '/',
 exact: true,
 component: LoadableHome,
 thunk: homeThunk // 服务端渲染会开启并执行这个action,用于获取页面渲染所需数据
}
export default HomeRouter

这里多说一句,有时我们要改造的项目的页面文件里有从window.location里面获取参数的代码,改造成服务端渲染时要全部去掉,或者是要在render之后的生命周期中使用。并且页面级别组件都已经注入了相关路由信息,可以通过this.props.location来获取URL里面的参数。本项目用的是BrowserRouter,如果用HashRouter则包含参数可能略有不同,根据实际情况取用。

根据React16的服务端渲染的API介绍:

  1. 浏览器端使用的注入ConnectedRouter中的history为:import createHistory from 'history/createBrowserHistory'

    Tago AI
    Tago AI

    AI生成带货视频,专为电商卖货而生

    下载
  2. 服务器端使用的history为import createHistory from 'history/createMemoryHistory'

服务端渲染

这里就不会涉及到koa2的一些基础知识,如果对koa2框架不熟悉可以参考我的另外一篇博文。这里是看server文件夹下都是服务端的代码。首先是简洁的app.js用于保证每次连接都返回的是一个新的服务器端实例,这对于单线程的js语言是很关键的思路。需要重点介绍的就是clientRouter.js这个文件,结合/src/app/configureStore.js这个文件共同理解服务端渲染的数据获取流程和React的渲染机制。


/*configureStore.js*/
import {createStore, applyMiddleware,compose} from "redux";
import thunkMiddleware from "redux-thunk";
import createHistory from 'history/createMemoryHistory';
import { routerReducer, routerMiddleware } from 'react-router-redux'
import rootReducer from '../store/reducers/index.js';

const routerReducers=routerMiddleware(createHistory());//路由
const composeEnhancers = process.env.NODE_ENV=='development'?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;

const middleware=[thunkMiddleware,routerReducers]; //把路由注入到reducer,可以从reducer中直接获取路由信息

let configureStore=(initialState)=>createStore(rootReducer,initialState,composeEnhancers(applyMiddleware(...middleware)));

export default configureStore;

window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__这个变量是浏览器里面的Redux的开发者工具,开发React-redux应用时建议安装,否则会有报错提示。这里面大部分都是redux-thunk的示例代码,关于这部分如果看不懂建议看一下redux-thunk的官方文档,这里要注意的是configureStore这个方法要传入的initialState参数,这个渲染的具体思路是:在服务端判断路由的thunk方法,如果存在则需要执行这个获取数据逻辑,这是个阻塞过程,可以当作同步,获取后放到全局State中,在前端输出的HTML中注入window.__INITIAL_STATE__这个全局变量,当html载入完毕后,这个变量赋值已有数据的全局State作为initState提供给react应用,然后浏览器端的js加载完毕后会通过复用页面上已有的dom和初始的initState作为开始,合并到render后的生命周期中,从而在componentDidMount中已经可以从this.props中获取渲染所需数据。

但还要考虑到页面切换也有可能在前端执行跳转,此时作为React的应用不会触发对后端的请求,因此在componentDidMount这个生命周期里并没有获取数据,为了解决这个问题,我建议在这个生命周期中都调用props中传来的action触发函数,但在action内部进行一层逻辑判断,避免重复的请求,实际项目中请求数据往往会有个标识性ID,就可以将这个ID存入store中,然后就可以进行一次对比校验来提前返回,避免重复发送ajax请求,具体可看store/actions/home.js`中的逻辑处理。


import {ADD,GET_HOME_INFO} from '../constants'
export const add=(count)=>({type: ADD, count,})

export const getHomeInfo=(sendId=1)=>async(dispatch,getState)=>{
 let {name,age,id}=getState().HomeReducer.homeInfo;
 if (id === sendId) {
 return //是通过对请求id和已有数据的标识性id进行对比校验,避免重复获取数据。
 }
 console.log('footer'.includes('foo'))
 await new Promise(resolve=>{
 let homeInfo={name:'wd2010',age:'25',id:sendId}
 console.log('-----------请求getHomeInfo')
 setTimeout(()=>resolve(homeInfo),1000)
 }).then(homeInfo=>{
 dispatch({type:GET_HOME_INFO,data:{homeInfo}})
 })
}

注意这里的async/await写法,这里涉及到服务端koa2使用这个来做数据请求,因此需要统一返回async函数,这块不熟的同学建议看下ES7的知识,主要是async如何配合Promise实现异步流程改造,并且如果涉及koa2的服务端工作,对async函数用的更多,这也是本项目要求Node版本为8.x以上的原因,从8开始就可以直接用这两个关键字。

不过到具体项目中,往往会涉及到一些服务端参数的注入问题,但这块根据不同项目需求差异很大,并且不属于这个React服务端改造的一部分,没法统一分享,如果真是公司项目要用到对这块有需求咨询可以打赏后加我微信讨论。

以Home页面为例的渲染流程

为了方便大家理解,我以一个页面为例整理了一下数据流的整体过程,看一下思路:

  1. 服务端接收到请求,通过/home找到对应的路由配置

  2. 判断路由存在thunk方法,此时执行store/actions/thunk.js里面的暴露出的函数

  3. 异步获取的数据会注入到全局state中,此时的dispatch分发其实并不生效

  4. 要输出的HTML代码中会将获取到数据后的全局state放到window.__INITIAL_STATE__这个全局变量中,作为initState

  5. window.__INITIAL_STATE__将在react生命周期起作用前合并入全局state,此时react发现dom已经生成,不会再次触发render,并且数据状态得到同步


服务端直出HTML

基本的流程已经介绍结束,至于一些Reducer的函数式写法,还有actions的位置都是参考网上的一些分析来组织的,具体见仁见智,这个只要符合自己的理解,并且有助于团队开发就好。如果您符合我在文章一开始设定的读者背景,相信本文的讲述足够您点亮自己的服务端渲染技术点啦。如果对React了解偏少也没关系,可以参考这里来补充一些React的基础知识。

相关推荐:

详解React服务端渲染实例

webpack+react+nodejs服务端渲染_html/css_WEB-ITnose

Vue.js与 ASP.NET Core 服务端渲染功能

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

38

2026.03.10

Kotlin Android模块化架构与组件化开发实践
Kotlin Android模块化架构与组件化开发实践

本专题围绕 Kotlin 在 Android 应用开发中的架构实践展开,重点讲解模块化设计与组件化开发的实现思路。内容包括项目模块拆分策略、公共组件封装、依赖管理优化、路由通信机制以及大型项目的工程化管理方法。通过真实项目案例分析,帮助开发者构建结构清晰、易扩展且维护成本低的 Android 应用架构体系,提升团队协作效率与项目迭代速度。

83

2026.03.09

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

97

2026.03.06

Rust内存安全机制与所有权模型深度实践
Rust内存安全机制与所有权模型深度实践

本专题围绕 Rust 语言核心特性展开,深入讲解所有权机制、借用规则、生命周期管理以及智能指针等关键概念。通过系统级开发案例,分析内存安全保障原理与零成本抽象优势,并结合并发场景讲解 Send 与 Sync 特性实现机制。帮助开发者真正理解 Rust 的设计哲学,掌握在高性能与安全性并重场景中的工程实践能力。

223

2026.03.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

458

2026.03.04

AI安装教程大全
AI安装教程大全

2026最全AI工具安装教程专题:包含各版本AI绘图、AI视频、智能办公软件的本地化部署手册。全篇零基础友好,附带最新模型下载地址、一键安装脚本及常见报错修复方案。每日更新,收藏这一篇就够了,让AI安装不再报错!

169

2026.03.04

Swift iOS架构设计与MVVM模式实战
Swift iOS架构设计与MVVM模式实战

本专题聚焦 Swift 在 iOS 应用架构设计中的实践,系统讲解 MVVM 模式的核心思想、数据绑定机制、模块拆分策略以及组件化开发方法。内容涵盖网络层封装、状态管理、依赖注入与性能优化技巧。通过完整项目案例,帮助开发者构建结构清晰、可维护性强的 iOS 应用架构体系。

246

2026.03.03

C++高性能网络编程与Reactor模型实践
C++高性能网络编程与Reactor模型实践

本专题围绕 C++ 在高性能网络服务开发中的应用展开,深入讲解 Socket 编程、多路复用机制、Reactor 模型设计原理以及线程池协作策略。内容涵盖 epoll 实现机制、内存管理优化、连接管理策略与高并发场景下的性能调优方法。通过构建高并发网络服务器实战案例,帮助开发者掌握 C++ 在底层系统与网络通信领域的核心技术。

34

2026.03.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 6万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1万人学习

React核心原理新老生命周期精讲
React核心原理新老生命周期精讲

共12课时 | 1.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号