在React.JS上创建数据驱动的应用程序的基本模式问题

为了创建接口,React 建议使用组合和状态管理库来构建组件层次结构。 但是,使用复杂的组合模式会出现问题:


  1. 需要不必要地构造子元素
  2. 或将它们作为道具传递,这会使代码的可读性,语义和结构变得复杂

对于大多数开发人员而言,问题可能并不明显,他们将其置于状态管理级别。 在React文档中也对此进行了讨论


如果要摆脱将某些道具传递到多个级别的麻烦,通常组件组成比上下文更简单。
反应文档 上下文。

如果我们点击链接,我们将看到另一个参数:


在Facebook上,我们在成千上万的组件中使用React,并且没有发现建议创建组件继承层次结构的任何情况。
React文档 组成与继承。

当然,如果每个使用该工具的人都阅读了文档并认可了作者的权限,那么本出版物将不是这样。 因此,我们分析了现有方法的问题。


模式1-直接控制的组件



我从此解决方案开始,因为Vue框架推荐此方法 。 在表单的情况下,我们采用来自支持或设计的数据结构。 转发到我们的组件-例如,到电影卡:


const MovieCard = (props) => { const {title, genre, description, rating, image} = props.data; return ( <div> <img href={image} /> <div><h2>{title}</h2></div> <div><p>{genre}</p></div> <div><p>{description}</p></div> <div><h1>{rating}</h1></div> </div> ) } 

停下 我们已经知道组件需求的不断扩展。 标题突然有一个指向电影评论的链接? 以及类型-从中获得最佳影片? 现在不添加:


 const MovieCard = (props) => { const {title: {title}, description: {description}, rating: {rating}, genre: {genre}, image: {imageHref} } = props.data; return ( <div> <img href={imageHref} /> <div><h2>{name}</h2></div> <div><p>{genre}</p></div> <div><h1>{rating}</h1></div> <div><p>{description}</p></div> </div> ) } 

因此,我们将在将来保护自己免受问题困扰,但为零错误打开大门。 最初,我们可以直接从后面抛出结构:


 <MovieCard data={res.data} /> 

现在,每次您需要复制所有信息时:


 <MovieCard data={{ title: {res.title}, description: {res.description}, rating: {res.rating}, image: {res.imageHref} }} /> 

但是,我们忘记了这种类型-该组件下降了。 而且,如果未设置错误限制器,则整个应用程序都可以使用。


TypeScript可以解救。 我们通过重构卡及其使用元素来简化方案。 幸运的是,所有内容都在编辑器或汇编过程中突出显示:


 interface IMovieCardElement { text?: string; } interface IMovieCardImage { imageHref?: string; } interface IMovieCardProps { title: IMovieCardElement; description: IMovieCardElement; rating: IMovieCardElement; genre: IMovieCardElement; image: IMovieCardImage; } ... const {title: {text: title}, description: {text: description}, rating: {text: rating}, genre: {text: genre}, image: {imageHref} } = props.data; 

为了节省时间,我们仍将“按任意方式”或“按IMovieCardProps”滚动数据。 结果如何? 我们已经描述了三种数据结构(如果在一个地方使用)。 那我们有什么呢? 仍然无法修改的组件。 一个有可能使整个应用程序崩溃的组件。


现在该重用此组件了。 不再需要评分。 我们有两个选择:


在需要评级的地方放置道具而无需评级


 const MovieCard = ({withoutRating, ...props}) => { const {title: {title}, description: {description}, rating: {rating}, genre: {genre}, image: {imageHref} } = props.data; return ( <div> <img href={imageHref} /> <div><h2>{name}</h2></div> <div><p>{genre}</p></div> { withoutRating && <div><h1>{rating}</h1></div> } <div><p>{description}</p></div> </div> ) } 

速度很快,但是我们堆积了道具并建立了第四个数据结构。


使IMovieCardProps中的评级为可选。 不要忘记将其默认设置为空对象


 const MovieCard = ({data, ...props}) => { const {title: {text: title}, description: {text: description}, rating: {text: rating} = {}, genre: {text: genre}, image: {imageHref} } = data; return ( <div> <img href={imageHref} /> <div><h2>{name}</h2></div> <div><p>{genre}</p></div> { data.rating && <div><h1>{rating}</h1></div> } <div><p>{description}</p></div> </div> ) } 

比较棘手,但是很难阅读代码。 同样, 第四次重复。 对组件的控制并不明显,因为它不受数据结构的控制。 假设我们被要求将臭名昭著的评级链接起来,但并非到处都是:


  rating: {text: rating, url: ratingUrl} = {}, ... { data.rating && data.rating.url ? <div>><h1><a href={ratingUrl}{rating}</a></h1></div> : <div><h1>{rating}</h1></div> } 

在这里,我们遇到了指示不透明数据结构的复杂逻辑。


模式2-具有自己状态和减速器的组件



同时,一种奇怪且流行的方法。 我开始使用React时使用了它,而Vue中缺少JSX功能。 我不止一次在mitaps上听到开发人员的消息,这种方法使您可以跳过更通用的数据结构:


  1. 一个组件可以采用许多数据结构;布局保持不变
  2. 接收数据时,它将根据所需方案处理它们。
  3. 数据以组件状态存储,以免每次渲染时都启动reducer(可选)

自然地,不透明性问题(1)由逻辑过载(2)问题和最终组件的状态添加(3)补充。


最后(3)由对象的内部安全性决定。 也就是说,我们通过lodash.isEqual深入检查对象。 如果事件为高级事件或JSON.stringify,则一切都刚刚开始。 您还可以添加时间戳记,并检查是否丢失了所有内容。 无需保存或记录,因为由于计算的复杂性,优化可能比精简器更复杂。


带有脚本名称(通常是字符串)的数据被抛出:


 <MovieCard data={{ type: 'withoutRating', data: res.data, }} /> 

现在编写组件:


 const MovieCard = ({data}) => { const card = reduceData(data.type, data.data); return ( <div> <img href={card.imageHref} /> <div><h2>{card.name}</h2></div> <div><p>{card.genre}</p></div> { card.withoutRating && <div><h1>{card.rating}</h1></div> } <div><p>{card.description}</p></div> </div> ) } 

逻辑:


 const reduceData = (type, data) = { switch (type) { case 'withoutRating': return { title: {data.title}, description: {data.description}, rating: {data.rating}, genre: {data.genre}, image: {data.imageHref} withoutRating: true, }; ... } }; 

此步骤存在几个问题:


  1. 通过添加一层逻辑,我们最终失去了数据与显示之间的直接联系
  2. 每种情况下的逻辑重复意味着,在所有卡都需要使用年龄等级的情况下,将需要在每个异化器中进行注册。
  3. 步骤1中还有其他问题

模式3-将显示逻辑和数据传输到状态管理



在这里,我们放弃了用于构建接口的数据总线,即React。 我们将技术与我们自己的逻辑模型结合使用。 尽管该指南警告您不要以这种方式使用上下文,但这可能是在React上构建应用程序的最常见方法。


在React无法提供足够工具的地方使用类似的工具-例如,在路由中。 最有可能您正在使用react-router。 在这种情况下,使用上下文而不是转发来自每个顶级路由组件的回调将更有意义将会话转发到所有页面。 除了Java语言所提供的功能外,React对于异步操作没有单独的抽象。


似乎有一个好处:我们可以在应用程序的未来版本中重用该逻辑。 但这是一个骗局。 一方面,它与API绑定,另一方面与应用程序的结构绑定,并且逻辑提供了这种连接。 更改零件之一时,需要重写。


解决方案:样式4-组成



如果遵循以下原则(除了“ 设计模式”中的类似方法),则组合方法是显而易见的:


  1. 前端开发-用户界面开发-使用HTML进行布局
  2. Javascript用于接收,传输和处理数据。

因此,应尽早将数据从一个域传输到另一个域。 React使用JSX抽象来模板化HTML,但实际上它使用了一组createElement方法。 也就是说,同样是JSX元素的JSX和React组件应被视为一种显示和行为的方法,而不是必须在单独级别上进行的数据转换和处理。


在此步骤中,许多人使用上面列出的方法,但是它们没有解决扩展和修改组件显示的关键问题。 文档中显示了如何根据库的创建者进行操作:


 function SplitPane(props) { return ( <div className="SplitPane"> <div className="SplitPane-left"> {props.left} </div> <div className="SplitPane-right"> {props.right} </div> </div> ); } function App() { return ( <SplitPane left={ <Contacts /> } right={ <Chat /> } /> ); } 

即,作为参数,而不是字符串,数字和布尔类型,将传递现成的,已制成的组件。


las,这种方法也变得不灵活。 原因如下:


  1. 这两个道具都是必需的。 这限制了组件的重用
  2. 可选将意味着用逻辑重载SplitPane组件。
  3. 嵌套和复数在语义上不是很明显。
  4. 对于每个接受道具的组件,都必须重写此映射逻辑。

结果,即使在相当简单的情况下,这样的解决方案也会变得越来越复杂:


 function SplitPane(props) { return ( <div className="SplitPane"> { props.left && <div className="SplitPane-left"> {props.left} </div> } { props.right && <div className="SplitPane-right"> {props.right} </div> } </div> ); } function App() { return ( <SplitPane left={ contacts.map(el => <Contacts name={ <ContactsName name={el.name} /> } phone={ <ContactsPhone name={el.phone} /> } /> ) } right={ <Chat /> } /> ); } 

在文档中,对于高阶组件(HOC)和渲染道具,类似的代码称为 包装鬼”。 每增加一个新元素,代码的可读性就会变得更加困难。


Web组件技术和Vue框架中提供了对此问题的一个答案-插槽。 但是,在这两个地方都有限制:首先,插槽不是由符号定义的,而是由字符串定义的,这使重构变得复杂。 其次,插槽的功能受到限制,并且无法控制其自身的显示,无法将其他插槽转移到子组件或无法在其他元素中重用。


简而言之,我们将其称为模式5-slot


 function App() { return ( <SplitPane> <LeftPane> <Contacts /> </LeftPane> <RightPane> <Chat /> </RightPane> </SplitPane> ); } 

我将在下一篇文章中讨论有关React中插槽模式的现有解决方案以及我自己的解决方案。

Source: https://habr.com/ru/post/zh-CN475170/


All Articles