Redux without React — State Management in Vanilla JavaScript
关键要点
- Redux可以在没有React的情况下使用纯JavaScript管理应用状态,展示了在不同UI层中状态管理的灵活性
- 正确的Redux存储初始化和管理至关重要;存储应在应用入口点创建并传递给组件,以避免循环依赖等问题
- 在纯Redux设置中,组件可以分为展示型和容器型,类似于React的组件结构,有助于关注点分离和角色定义清晰
- 在纯JavaScript中使用Redux需要手动更新DOM,这与React的虚拟DOM自动处理基于状态更新的UI变化不同
- 建议实现用例驱动的存储,确保只存储必要数据,通过避免不必要的状态持久化来提升性能和用户体验
项目设置
你可能听说过流行的React.js和Redux组合,用于使用最新的前端技术构建快速强大的Web应用程序。
React是Facebook开发的一个基于组件的开源库,用于构建用户界面。虽然React只是一个视图层(不是像Angular或Ember这样的完整框架),但Redux管理你的应用状态。它作为一个可预测的状态容器,整个状态存储在单个对象树中,只能通过发出所谓的操作来更改。
纯JavaScript中的Redux应用
Redux的伟大之处在于它迫使你提前思考并尽早了解应用设计。你开始定义应该实际存储什么,哪些数据可以且应该更改,以及哪些组件可以访问存储。但由于Redux只关注状态,我对自己如何构建和连接应用的其余部分感到有些困惑。
该应用是一个移动优先的俄罗斯方块克隆,有几个不同的视图。实际的游戏逻辑在Redux中完成,而离线功能由localStorage和自定义视图处理提供。
定义应用架构
我决定采用Redux和React项目中常见的文件结构。这是一个逻辑结构,适用于许多不同的设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
src/scripts/
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
|
我的标记被分离到另一个目录中,最终由单个index.html文件渲染。
管理和访问存储
要访问存储,需要创建一次并传递给应用的所有实例。大多数框架都使用某种依赖注入容器,因此我们作为框架用户不必提出自己的解决方案。
错误方案
我的第一次迭代失败了。我不知道为什么认为这是个好主意,但我将存储放在自己的模块(scripts/store/index.js)中,然后可以由应用的其他部分导入。
1
2
3
4
5
6
7
8
|
// scripts/store/index.js (☓ 错误)
import { createStore } from 'redux'
import reducers from '../reducers'
const store = createStore(reducers)
export default store
export { getItemList } from './connect'
|
辅助函数也需要存储:
1
2
3
4
5
6
|
// scripts/store/connect.js (☓ 错误)
import store from './'
export function getItemList () {
return store.getState().items.all
}
|
正确方案
我通过将初始化移动到应用入口点(scripts/index.js)并将存储传递给所有需要的组件来解决这个问题。
1
2
3
4
5
6
7
|
// scripts/store/configureStore.js (✓ 正确)
import { createStore } from 'redux'
import reducers from '../reducers'
export default function configureStore () {
return createStore(reducers)
}
|
1
2
3
4
|
// scripts/store/connect.js (✓ 正确)
export function getItemList (store) {
return store.getState().items.all
}
|
1
2
3
4
5
6
7
8
9
|
// scripts/index.js
import configureStore from './store'
import { PageControls, TetrisGame } from './components'
const store = configureStore()
const pageControls = new PageControls(store)
const tetrisGame = new TetrisGame(store)
// 进一步的初始化逻辑
|
组件
我决定使用两种类型的组件:展示型组件和容器型组件。
展示型组件除了纯DOM处理外什么都不做;它们不知道存储。另一方面,容器型组件可以分发操作或订阅更改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// scripts/components/pageControls.js
import { $$ } from '../utils'
import { startGame, endGame, addScore, openSettings } from '../actions'
export default class PageControls {
constructor ({ selector, store } = {}) {
this.$buttons = [...$$('button, [role=button]')]
this.store = store
}
onClick ({ target }) {
switch (target.getAttribute('data-action')) {
case 'endGame':
this.store.dispatch(endGame())
this.store.dispatch(addScore())
break
case 'startGame':
this.store.dispatch(startGame())
break
case 'openSettings':
this.store.dispatch(openSettings())
break
default:
break
}
target.blur()
}
addEvents () {
this.$buttons.forEach(
$btn => $btn.addEventListener('click', this.onClick.bind(this))
)
}
}
|
更新DOM
我开始项目时的一个较大问题是如何实际更新DOM。React使用称为虚拟DOM的快速内存中DOM表示来将DOM更新保持在最低限度。
基本流程如下:
- 容器组件的新实例被初始化并传递存储供内部使用
- 组件订阅存储中的更改
- 并使用不同的展示型组件来渲染DOM中的更新
1
2
3
4
5
6
7
8
|
// scripts/index.js
import configureStore from './store'
import { ScoreObserver } from './components'
const store = configureStore()
const scoreObserver = new ScoreObserver(store)
scoreObserver.init()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// scripts/components/scoreObserver/index.js
import { isRunning, getScoreList, getCurrentScore } from '../../store'
import ScoreBoard from './$board'
import ScoreLabel from './$label'
export default class ScoreObserver {
constructor (store) {
this.store = store
this.$board = new ScoreBoard()
this.$label = new ScoreLabel()
}
updateScore () {
if (!isRunning(this.store)) {
return
}
this.$label.updateLabel(getCurrentScore(this.store))
}
// 在不同地方使用
updateScoreBoard () {
this.$board.updateBoard(getScoreList(this.store))
}
init () {
this.store.subscribe(this.updateScore.bind(this))
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// scripts/components/scoreObserver/$board.js
import { $ } from '../../utils'
export default class ScoreBoard {
constructor () {
this.$board = $('.tetrys-scoreboard')
}
emptyBoard () {
this.$board.innerHTML = ''
}
createListItem (txt) {
const $li = document.createElement('li')
const $span = document.createElement('span')
$span.appendChild(document.createTextNode(txt))
$li.appendChild($span)
return $li
}
updateBoard (list = []) {
const fragment = document.createDocumentFragment()
list.forEach((score) => fragment.appendChild(this.createListItem(score)))
this.emptyBoard()
this.$board.appendChild(fragment)
}
}
|
其他错误和建议
另一个重要点是实现用例驱动的存储。在我看来,只存储对应用必要的内容很重要。
结论
我在有React和没有React的情况下都从事过Redux项目,我的主要收获是应用设计不需要有巨大差异。React中使用的大多数方法实际上可以适应任何其他视图处理设置。
然而,不同的是你初始化模块、存储的方式,以及组件对整体应用状态的感知程度。概念保持不变,但实现和代码量正好适合你的需求。
常见问题解答
在带React和不带React的情况下使用Redux的主要区别是什么?
Redux是一个用于JavaScript应用的可预测状态容器,可以与任何UI层一起使用。在带React和不带React的情况下使用Redux的主要区别在于UI层与Redux存储交互的方式。当与React一起使用时,Redux可以利用React基于组件的架构及其生命周期方法,在状态更改时自动处理组件的更新。没有React,你需要手动订阅存储并在状态更改时处理UI的更新。
我可以在没有React的情况下使用Redux DevTools吗?
是的,Redux DevTools不依赖于React,可以与任何使用Redux的UI层一起使用。你可以通过将其作为中间件添加到创建Redux存储时将其集成到你的应用中。
我可以在没有React的情况下使用Redux中间件吗?
是的,Redux中间件不依赖于React,可以与任何使用Redux的UI层一起使用。Redux中的中间件用于处理副作用和异步操作等。无论你是否使用React,都可以使用Redux的applyMiddleware函数将中间件应用于你的Redux存储。