纯JavaScript中的Redux状态管理实践

本文详细介绍了如何在纯JavaScript项目中使用Redux进行状态管理,涵盖应用架构设计、Store管理、组件分类、DOM更新等核心技术要点,通过Tetris游戏实例展示完整实现方案。

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存储。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计