没有反应的redux
本文经Vildan Softic同行评审。感谢所有SitePoint的同行评审员,让SitePoint的内容尽善尽美!
我属于那种喜欢从零开始,并了解一切工作原理的开发者。虽然我知道这给自己带来了(不必要的)工作量,但这确实帮助我欣赏和理解特定框架、库或模块背后的机制。
最近,我又经历了这样的时刻,开始使用Redux和纯JavaScript开发一个Web应用程序。在本文中,我想概述我的应用程序结构,检查我早期(最终失败的)迭代,然后看看我最终选择的解决方案以及在此过程中学到的知识。
关键要点
- Redux可以不依赖React,使用纯JavaScript管理应用程序状态,展示了其在不同UI层中进行状态管理的灵活性。
- 正确初始化和管理Redux存储至关重要;应在应用程序入口点创建存储,并将其传递给组件,以避免循环依赖等问题。
- 在仅使用Redux的设置中,组件可以类似于React的组件结构,分为表现层和容器层,这有助于分离关注点并明确角色定义。
- 与React的虚拟DOM(根据状态更新自动处理UI更改)不同,在使用纯JavaScript和Redux时,需要手动更新DOM。
- 建议实现用例驱动的存储,确保仅存储必要的数据,这可以通过避免不必要的状态持久化来提高性能和用户体验。
设置
您可能听说过流行的React.js和Redux组合,它使用最新的前端技术构建快速而强大的Web应用程序。
React是由Facebook创建的用于构建用户界面的基于组件的开源库。虽然React只是一个视图层(不是像Angular或Ember这样的完整框架),但Redux管理应用程序的状态。它充当可预测的状态容器,其中整个状态存储在一个单一的对象树中,并且只能通过发出所谓的action来更改。如果您完全不了解这个主题,我建议您阅读这篇文章。
对于本文的其余部分,不需要成为Redux专家,但至少对它的概念有一定的了解会有所帮助。
无React的Redux——从零开始的应用程序
Redux的优点在于它迫使您提前思考并尽早了解应用程序的设计。您开始定义实际应该存储什么,哪些数据可以并且应该更改,以及哪些组件可以访问存储。但由于Redux只关注状态,我发现自己有点困惑如何构建和连接应用程序的其余部分。React在引导您完成所有步骤方面做得很好,但如果没有它,就需要我弄清楚什么方法最有效。
所讨论的应用程序是一个针对移动设备优先的俄罗斯方块克隆,它有几个不同的视图。实际的游戏逻辑在Redux中完成,而离线功能由localStorage和自定义视图处理提供。存储库可以在GitHub上找到,尽管该应用程序仍在积极开发中,并且我是在开发过程中撰写这篇文章的。
定义应用程序架构
我决定采用在Redux和React项目中常见的文件夹结构。这是一个逻辑结构,适用于许多不同的设置。这个主题有很多变体,大多数项目都略有不同,但整体结构相同。
src/scripts/
<code>actions/ ├── game.js ├── score.js └── ... components/ ├── router.js ├── pageControls.js ├── canvas.js └── ... constants/ ├── game.js ├── score.js └── ... reducers/ ├── game.js ├── score.js └── ... store/ ├── configureStore.js ├── connect.js └── index.js utils/ ├── serviceWorker.js ├── localStorage.js ├── dom.js └── ... index.js worker.js</code>
我的标记被分隔到另一个目录中,最终由单个index.html文件呈现。该结构类似于scripts/,以便在整个代码库中保持一致的架构。
src/markup/
<code>layouts/ └── default.html partials/ ├── back-button.html └── meta.html pages/ ├── about.html ├── settings.html └── ... index.html</code>
管理和访问存储
要访问存储,需要创建一次并将其传递给应用程序的所有实例。大多数框架都使用某种依赖注入容器,因此我们作为框架的用户不必自己想出解决方案。但是,当我使用自己的解决方案时,如何才能让它对我的所有组件都可用呢?
我的第一次迭代失败了。我不知道为什么我认为这是一个好主意,但我将存储放在它自己的模块(scripts/store/index.js)中,然后可以由应用程序的其他部分导入。我最终后悔了,并很快处理了循环依赖。问题是,当组件尝试访问存储时,存储没有正确初始化。我制作了一个图表来演示我正在处理的依赖关系流程:
应用程序入口点正在初始化所有组件,然后通过直接或通过辅助函数(此处称为connect)内部使用存储。但是,由于存储不是显式创建的,而只是在其自身模块中的副作用,组件最终在存储创建之前就使用了存储。无法控制组件或辅助函数第一次调用存储的时间。这很混乱。
存储模块如下所示:
scripts/store/index.js (☓ bad)
import { createStore } from 'redux' import reducers from '../reducers' const store = createStore(reducers) export default store export { getItemList } from './connect'
如上所述,存储是作为副作用创建的,然后导出。辅助函数也需要存储。
scripts/store/connect.js (☓ bad)
import store from './' export function getItemList () { return store.getState().items.all }
这正是我的组件最终相互递归的时刻。辅助函数需要存储才能运行,并且同时从存储初始化文件中导出,以便使它们可以访问应用程序的其他部分。您看到这听起来有多乱吗?
解决方案
现在看来很明显的事情,我花了一段时间才理解。我通过将初始化移动到我的应用程序入口点(scripts/index.js),并将其传递给所有必需的组件来解决此问题。
同样,这与React实际使存储可访问的方式非常相似(查看源代码)。它们一起工作得如此之好是有原因的,为什么不学习它的概念呢?
应用程序入口点首先创建存储,然后将其传递给所有组件。然后,组件可以连接到存储并调度操作、订阅更改或获取特定数据。
让我们来看一下更改:
scripts/store/configureStore.js (✓ good)
<code>actions/ ├── game.js ├── score.js └── ... components/ ├── router.js ├── pageControls.js ├── canvas.js └── ... constants/ ├── game.js ├── score.js └── ... reducers/ ├── game.js ├── score.js └── ... store/ ├── configureStore.js ├── connect.js └── index.js utils/ ├── serviceWorker.js ├── localStorage.js ├── dom.js └── ... index.js worker.js</code>
我保留了该模块,但改为了导出一个名为configureStore的函数,该函数在代码库中的其他地方创建存储。请注意,这只是基本概念;我还使用了Redux DevTools扩展程序并通过localStorage加载持久化状态。
scripts/store/connect.js (✓ good)
<code>layouts/ └── default.html partials/ ├── back-button.html └── meta.html pages/ ├── about.html ├── settings.html └── ... index.html</code>
connect辅助函数基本上没有改变,但现在需要将存储作为参数传递。起初我犹豫是否要使用此解决方案,因为我认为“那么辅助函数有什么意义呢?”。现在我认为它们很好而且足够高级,使一切更易于阅读。
scripts/index.js
import { createStore } from 'redux' import reducers from '../reducers' const store = createStore(reducers) export default store export { getItemList } from './connect'
这是应用程序入口点。存储被创建,并传递给所有组件。PageControls为特定操作按钮添加全局事件侦听器,TetrisGame是实际的游戏组件。在将存储移到这里之前,它看起来基本相同,但没有将存储分别传递给所有模块。如前所述,组件可以通过我失败的连接方法访问存储。
组件
我决定使用两种组件:表现层和容器组件。表现层组件除了纯DOM处理之外什么也不做;它们不知道存储。另一方面,容器组件可以调度操作或订阅更改。
Dan Abramov已经为React组件写了一篇很棒的文章,但这套方法也可以应用于任何其他组件架构。
不过,对我来说也有例外。有时组件非常小,只做一件事情。我不想将它们分成上述模式之一,所以我决定将它们混合使用。如果组件增长并获得更多逻辑,我将对其进行分离。
scripts/components/pageControls.js
import store from './' export function getItemList () { return store.getState().items.all }
上面的示例就是其中一个组件。它有一个元素列表(在本例中是所有具有data-action属性的元素),并根据属性内容在单击时调度操作。仅此而已。然后,其他模块可能会侦听存储中的更改并相应地更新自身。如前所述,如果组件还进行了DOM更新,我将对其进行分离。
现在,让我向您展示这两种组件类型的清晰分离。
更新DOM
在我开始该项目时,我遇到的一个更大的问题是如何实际更新DOM。React使用称为虚拟DOM的DOM的快速内存中表示来最大限度地减少DOM更新。
我实际上是在考虑做同样的事情,如果我的应用程序变得更大且DOM更繁重,我可能会切换到虚拟DOM,但就目前而言,我进行经典DOM操作,这与Redux配合得很好。
基本流程如下:
- 初始化容器组件的新实例并传递存储以供内部使用
- 组件订阅存储中的更改
- 并使用不同的表现层组件在DOM中呈现更新
注意:对于JavaScript中与DOM相关的任何内容,我都是$符号前缀的粉丝。正如您可能猜到的那样,它取自jQuery的$。因此,纯表现层组件文件名以美元符号为前缀。
scripts/index.js
<code>actions/ ├── game.js ├── score.js └── ... components/ ├── router.js ├── pageControls.js ├── canvas.js └── ... constants/ ├── game.js ├── score.js └── ... reducers/ ├── game.js ├── score.js └── ... store/ ├── configureStore.js ├── connect.js └── index.js utils/ ├── serviceWorker.js ├── localStorage.js ├── dom.js └── ... index.js worker.js</code>
这里没有什么花哨的东西。导入、创建和初始化容器组件ScoreObserver。它究竟做了什么?它更新所有与分数相关的视图元素:高分列表和游戏期间的当前分数信息。
scripts/components/scoreObserver/index.js
<code>layouts/ └── default.html partials/ ├── back-button.html └── meta.html pages/ ├── about.html ├── settings.html └── ... index.html</code>
请记住,这是一个简单的组件;其他组件可能具有更复杂的逻辑和需要处理的事情。这里发生了什么?ScoreObserver组件保存对存储的内部引用,并创建新实例的两个表现层组件以供以后使用。init方法订阅存储更新,并在每次存储更改时更新$label组件——但前提是游戏实际上正在运行。
updateScoreBoard方法在其他地方使用。每次发生更改时更新列表是没有意义的,因为视图无论如何都是不活动的。还有一个路由组件,它在每次视图更改时更新或停用不同的组件。它的API大致如下所示:
import { createStore } from 'redux' import reducers from '../reducers' const store = createStore(reducers) export default store export { getItemList } from './connect'
注意:$(和$$)不是jQuery引用,而是document.querySelector的便捷实用程序快捷方式。
scripts/components/scoreObserver/$board.js
import store from './' export function getItemList () { return store.getState().items.all }
同样,这是一个基本示例和一个基本组件。updateBoard()方法获取一个数组,对其进行迭代,并将内容插入到分数列表中。
scripts/components/scoreObserver/$label.js
import { createStore } from 'redux' import reducers from '../reducers' export default function configureStore () { return createStore(reducers) }
此组件与上面的ScoreBoard几乎完全相同,但只更新单个元素。
其他错误和建议
另一个重要点是实现用例驱动的存储。我认为只存储对应用程序必不可少的内容很重要。一开始,我几乎存储了一切:当前活动视图、游戏设置、分数、悬停效果、用户的呼吸模式等等。
虽然这可能与一个应用程序相关,但与另一个应用程序无关。存储当前视图并在重新加载时继续在完全相同的位置可能很好,但在我的情况下,这感觉像是糟糕的用户体验,而且比有用的更烦人。您也不想存储菜单或模态的切换,对吧?用户为什么要回到那个特定状态?在较大的Web应用程序中,这可能是有意义的。但在我的小型移动游戏重点游戏中,回到设置屏幕只是因为我从那里离开,这相当烦人。
结论
我已经使用和不使用React完成了Redux项目,我的主要收获是,应用程序设计中的巨大差异并非必要。React中使用的大多数方法实际上都可以适应任何其他视图处理设置。我花了一段时间才意识到这一点,因为我一开始认为我必须做不同的事情,但我最终发现这没有必要。
然而,不同的是您初始化模块、存储的方式,以及组件对整体应用程序状态的了解程度。概念保持不变,但实现和代码量完全适合您的需求。
Redux是一个很棒的工具,它有助于以更周到的方式构建您的应用程序。单独使用,没有任何视图库,一开始可能会非常棘手,但一旦您克服了最初的困惑,就没有任何东西可以阻止您了。
您如何看待我的方法?您是否独自使用Redux和不同的视图处理设置?我很乐意收到您的反馈并在评论中讨论它。
如果您想了解更多关于Redux的信息,请查看我们的课程《重写和测试Redux以解决设计问题》迷你课程。在本课程中,您将构建一个Redux应用程序,该应用程序通过websocket连接接收按主题组织的推文。为了让您了解即将发生的事情,请查看下面的免费课程。
加载播放器……关于无React的Redux的常见问题解答(FAQ)
使用Redux与React和不使用React的主要区别是什么?
Redux是一个适用于JavaScript应用程序的可预测状态容器,可以与任何UI层一起使用。使用Redux与React和不使用React之间的主要区别在于UI层与Redux存储交互的方式。当与React一起使用时,Redux可以利用React的基于组件的架构及其生命周期方法来自动处理状态更改时组件的更新。如果没有React,您需要手动订阅存储并在状态更改时处理UI的更新。
如何在没有React的情况下处理Redux中的异步操作?
Redux中的异步操作通常使用Redux Thunk或Redux Saga等中间件来处理。这些中间件允许您调度函数(thunk)或更复杂的异步操作(saga),而不是普通的对象。即使没有React,您仍然可以在Redux存储中使用这些中间件。您只需要在使用Redux的applyMiddleware函数创建存储时应用中间件即可。
我可以在没有React的情况下使用Redux DevTools吗?
是的,Redux DevTools不依赖于React,可以与任何使用Redux的UI层一起使用。您可以通过在创建Redux存储时将其添加为中间件来将Redux DevTools集成到您的应用程序中。这将允许您实时检查应用程序的状态和操作,即使没有React。
如何在我的UI组件连接到Redux存储而无需React?
如果没有React及其connect函数,您需要手动订阅Redux存储并在状态更改时更新UI组件。您可以使用store.subscribe方法订阅存储,该方法采用一个侦听器函数,该函数将在每次调度操作时被调用。在此侦听器函数中,您可以使用store.getState获取存储的当前状态并相应地更新UI组件。
我可以将Redux与其他库或框架(如Vue或Angular)一起使用吗?
是的,Redux不依赖于React,可以与任何UI层一起使用。对于其他库和框架(如Vue和Angular),都提供了提供与React的connect函数类似功能的绑定。这些绑定允许您轻松地将UI组件连接到Redux存储,并在状态更改时处理组件的更新。
如何在没有React的情况下测试我的Redux代码?
在没有React的情况下测试Redux代码类似于使用React测试它。您可以使用任何JavaScript测试框架(如Jest或Mocha)为您的action creators和reducers创建单元测试。对于测试异步操作,您可以使用模拟存储来模拟Redux存储。
如何在没有React的情况下处理Redux中的副作用?
Redux中的副作用通常使用Redux Thunk或Redux Saga等中间件来处理。这些中间件允许您调度具有副作用的函数或更复杂的异步操作,例如进行API调用。即使没有React,您仍然可以在Redux存储中使用这些中间件。
我可以将Redux与纯JavaScript一起使用吗?
是的,Redux可以与纯JavaScript一起使用。您可以创建一个Redux存储,向其调度操作,并仅使用纯JavaScript订阅状态中的更改。但是,如果没有像React这样的库或框架来处理UI的更新,您需要在状态更改时手动更新UI组件。
如何在没有React的情况下构建Redux代码?
Redux代码的结构并不取决于您是否使用React。您仍然可以遵循构建Redux代码的相同最佳实践,例如将操作、reducers和selectors分离到不同的文件或文件夹中,并以规范化和模块化的方式组织您的状态。
我可以在没有React的情况下使用Redux中间件吗?
是的,Redux中间件不依赖于React,可以与任何使用Redux的UI层一起使用。Redux中的中间件用于处理副作用和异步操作,等等。您可以使用Redux的applyMiddleware函数将中间件应用于您的Redux存储,无论您是否使用React。
以上是没有反应的redux的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

不同JavaScript引擎在解析和执行JavaScript代码时,效果会有所不同,因为每个引擎的实现原理和优化策略各有差异。1.词法分析:将源码转换为词法单元。2.语法分析:生成抽象语法树。3.优化和编译:通过JIT编译器生成机器码。4.执行:运行机器码。V8引擎通过即时编译和隐藏类优化,SpiderMonkey使用类型推断系统,导致在相同代码上的性能表现不同。

Python更适合初学者,学习曲线平缓,语法简洁;JavaScript适合前端开发,学习曲线较陡,语法灵活。1.Python语法直观,适用于数据科学和后端开发。2.JavaScript灵活,广泛用于前端和服务器端编程。

JavaScript是现代Web开发的核心语言,因其多样性和灵活性而广泛应用。1)前端开发:通过DOM操作和现代框架(如React、Vue.js、Angular)构建动态网页和单页面应用。2)服务器端开发:Node.js利用非阻塞I/O模型处理高并发和实时应用。3)移动和桌面应用开发:通过ReactNative和Electron实现跨平台开发,提高开发效率。

本文展示了与许可证确保的后端的前端集成,并使用Next.js构建功能性Edtech SaaS应用程序。 前端获取用户权限以控制UI的可见性并确保API要求遵守角色库

我使用您的日常技术工具构建了功能性的多租户SaaS应用程序(一个Edtech应用程序),您可以做同样的事情。 首先,什么是多租户SaaS应用程序? 多租户SaaS应用程序可让您从唱歌中为多个客户提供服务

从C/C 转向JavaScript需要适应动态类型、垃圾回收和异步编程等特点。1)C/C 是静态类型语言,需手动管理内存,而JavaScript是动态类型,垃圾回收自动处理。2)C/C 需编译成机器码,JavaScript则为解释型语言。3)JavaScript引入闭包、原型链和Promise等概念,增强了灵活性和异步编程能力。
