使用React 18 hydrateRoot或其他设计模式渲染慢速组件



我有一个相当大的web应用程序,我正在从React 16升级到React 18,并对其进行彻底检修,它使用.NET 6。目前,它没有使用任何水合SSR技术。我已经研究了很长一段时间,但没有找到任何坚实的例子来帮助我完成这项技术。我认为这可能是一个很好的线程,为未来的开发人员可能会做同样的事情。基本上,我的页面上有一组"小部件"(React Components),其中一些组件需要加载比其他组件更多的数据,有些不需要任何数据。问题是,所有的数据都是通过API调用从C#控制器加载的,并被放入一个大模型/对象中,该模型/对象包含页面上每个小部件的数据,因此在收集并从控制器传入所有数据之前,不会加载小部件。举例说明如何使用可用的React技术使其发挥最佳效果,我相信可能有几个很棒的设计。我想把事情安排好,这样我至少可以做两种技术中的一种,第一种是首选方法:

  1. 在收集数据时,将数据从C#控制器直接加载到每个单独的React组件,并在那时渲染单独的小部件。然后,当数据准备好用于另一个小部件时,它会被传递到特定的组件并在屏幕上呈现。这将使任何组件都不依赖于其他组件需要加载的数据。
    • 我想避免的事情:一些"页面">当前使用来自JSX文件的Fetch(GET/POST)调用从API加载数据,而不是在单击页面链接时从控制器加载数据。这在过去造成了问题,因为不同的小部件加载同时进行了太多的获取调用(冲突等),这只会导致它们崩溃很多,所以我一直在修改所有这些小部件的数据,以便事先从控制器加载
  2. 显示一个"加载"屏幕,甚至只显示窗口小部件的容器html,直到所有数据通过数据模型从控制器传递

我目前如何设置一切:

->在主页上,用户单击链接转到位于"Account\Index\userId"的帐户配置文件

->当前AccountController.cs索引调用示例:

[HttpGet]
public async Task<ActionResult> Index(int? userId) {
var model = new AccountViewModelContract();
model.NameData = await ApiClient1.GetNameViewModelAsync(userId);
model.AccountSecurityData = await ApiClient2.GetAccountSecurityViewModelAsync(userId);
model.EmailData = await ApiClient2.GetEmailViewModelAsync(userId);
model.PhoneData = await ApiClient2.GetPhoneViewModelAsync(userId);
model.AddressData = await ApiClient3.GetAddressViewModelAsync(userId);
model.LoadedFromController = true;
return View(model);
}

->活期账户视图示例,Index.chtml

@using App.Models
@model AccountViewModelContract
<input id="accountPageData" type='hidden' value="@System.Text.Json.JsonSerializer.Serialize(Model)" />
<div id="accountDash" style="height:100%;"> </div>
@section ReactBundle{
<script src="@Url.Content("~/Dist/AccountDashboard.bundle.js")" type="text/javascript"></script>
}

->当前AccountDashboard.jsx:示例

import React from 'react';
import { createRoot } from 'react-dom/client';
import BootstrapFooter from 'Shared/BootstrapFooter';
import AccountSecurity from './AccountSecurity/AccountSecurity';
import Address from 'Account/Address/Address';
import EmailSettings from 'Account/Email/EmailSettings';
import Name from 'Account/Name/Name';
import Phone from 'Account/Phone/Phone';
import PropTypes from 'prop-types';
import BootstrapHeaderMenu from '../Shared/BootstrapHeaderMenu';
class AccountDashboard extends React.Component {
constructor(props) {
super(props);
const data = JSON.parse(props.accountPageData);
this.state = { data };
}
render() {
const { data } = this.state;
return (
<div className="container">
<div className="row">
<BootstrapHeaderMenu/>
</div>
<div>My Account</div>
<div className="row">
<div className="col">
<Name value={data.NameData} />
</div>
<div className="col">
<Phone value={data.PhoneData} />
</div>
<div className="col">
<Address value={data.AddressData} />
</div>
<div className="col">
<AccountSecurity value={data.AccountSecurityData} />
</div>
<div className="col">
<EmailSettings value={data.EmailData} />
</div>
</div>
<div className="row">
<BootstrapFooter/>
</div>
</div>
);
}
}
AccountDashboard.propTypes = {
accountPageData: PropTypes.string.isRequired
};
createRoot(document.getElementById('accountDash')).render(<AccountDashboard accountPageData={document.getElementById('accountPageData').value} />);

->一个Widget(Name.jsx)的示例,压缩代码使不复杂

import React from 'react';
import BlueCard from 'Controls/BlueCard';
import LoadingEllipsis from 'Controls/LoadingEllipsis';
class Name extends React.Component {
constructor(props) {
super(props);
const data = props.value;
this.state = {
isLoaded: false,
.....
loadedFromController: props.value.LoadedFromController,
};
if (props.value.LoadedFromController) {
...set more state data  
} 
}
componentDidMount() {
if (!this.state.isLoaded && !this.state.loadedFromController) {
..//go to server to get data, widget sometimes loads this way in odd certain cases, pay no attention
} else {
this.setState({
isLoaded: true
});
}
render() {
let content = <LoadingEllipsis>Loading Name</LoadingEllipsis>;
const { ... } = this.state;
if (isLoaded === true) {
content = (<div>....fill content from state data</div>);
}
return (<BlueCard icon="../../Content/Images/Icon1.png" title="Name" banner={banner} content={content} />);
}
}
export default Name;

实现这些目标的一些方法、技术或设计模式是什么?并给出如何实现这些目标以及为什么它比替代方案更好的详细示例。在你的例子中使用上面的例子类名。

关于上述方法:

将数据从C#控制器加载到每个单独的React组件直接在收集数据时,让单独的小部件在那个时候呈现。然后,当数据准备好用于另一个小部件时,它会被传递到特定的组件并在屏幕上呈现。这将使任何组件都不依赖于其他组件需要加载的数据。

  • 为了创建这样的东西,需要在React中使用Fetch调用来呈现来自控制器的数据。您只需要获取react组件中每个小部件/组件的数据。不幸的是,将html从控制器直接注入视图违背了MVC和React模式。此外,根据从API调用加载数据的时间,无法动态地将浏览器的控制权移交给控制器,从而可以在不同的时间注入/返回多个单独的视图

显示某种"加载"屏幕,甚至只显示小部件容器html,直到所有数据从控制器。

  • 上述方法提到的问题是,当用户单击转到下一页时,会调用控制器来决定返回什么视图。当加载数据时,不能先返回快速视图,然后再返回另一个视图。当将控制权返回到视图时,控制器将失去作用

我采用的方法是:

我创建了一个到处使用的主视图,切换到路由(react router dom)、延迟加载,现在主要只使用一个主控制器。

示例:Main.cs

[Authorize]
[HttpGet]
public async Task<ActionResult> Index(int? id) {
if(User.Identity != null && User.Identity.IsAuthenticated) {
var model = await GetMainViewModel(id);
return View(model);
}
return View();
}
[HttpGet]
[Route("AccountDashboard")]
public async Task<ActionResult> AccountDashboard(int? id) {
if(User.Identity != null && User.Identity.IsAuthenticated) {
var model = await GetMainViewModel(id);
return View(model);
}
return View();
}
[HttpGet]
[Route("AnotherDashboard")]
public async Task<ActionResult> AnotherDashboard(int? id) {
if(User.Identity != null && User.Identity.IsAuthenticated) {
var model = await GetMainViewModel(id);
return View(model);
}
return View();
}

每个仪表板使用的示例Index.html:

@using Main.Models
@model MainViewModel
<input id="PageData" type='hidden' value="@System.Text.Json.JsonSerializer.Serialize(Model)" />
<div id="root"></div>
@section ReactBundle{
<script src="@Url.Content("~/Dist/Main.bundle.js")"></script>
}

示例Main.jsx:

import { createRoot } from 'react-dom/client';
import { Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import BootstrapHeader from "BootstrapHeader";
import BootstrapFooter from "BootstrapFooter";
import LandingPage from "./LandingPage";
const AccountDashboard = React.lazy(() => import('../AccountDashboard'));
const AnotherDashboard = React.lazy(() => import('../AnotherDashboard'));
class Main extends React.Component {
constructor(props) {
super(props);
var data = JSON.parse(props.PageData);
this.state = {
data: data,
};
}
render() {
return(
<BrowserRouter>
<div className={} style={{ minHeight: '100vh' }}>
<div style={{ minHeight: '11vh' }}>
<BootstrapHeader value={this.state.data} />
</div>
<div className="container-column" style={{ minHeight: '75vh' }}>                    
<Routes>
<Route exact path="/" element={<Suspense fallback={<div>Loading...</div>}><LandingPage PageData={this.state.data}/></Suspense>} />
<Route exact path="/AccountDashboard" element={<Suspense fallback={<div>Loading...</div>}><AccountDashboard AccountData={this.state.data.AccountModels} /></Suspense>} />                          
<Route exact path="/AnotherDashboard" element={<Suspense fallback={<div>Loading...</div>}><AnotherDashboard AnotherData={this.state.data.AnotherModels} /></Suspense>} />
</Routes>                                        
</div>
<div style={{ minHeight: '14vh' }}>
<BootstrapFooter value={this.state.data} />
</div>    
</div>
</BrowserRouter>
);
}
}
createRoot(document.getElementById('root')).render(<Main PageData={document.getElementById('PageData').value} />);

每个Dashboard都有自己的小部件,这些小部件依赖于主模型中的数据。基本上,所有的数据都是为每个用户,为整个网站预先获取的。我发现数据量值得提前获取,而不是在每个小部件加载之前。通过使用缓存技术,该应用程序现在运行得非常快。将Route名称作为方法放入Main.cs中是允许的,这样页面就可以重新加载而不会失败,这在路由中很常见。

最新更新