不带React的Redux-Vanilla JavaScript中的状态管理

本文由Vildan Softic进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

俄罗斯方块形状的饼干在烤盘上

我是喜欢从头开始做事情并了解一切工作原理的开发人员之一。 尽管我知道自己所从事的(不必要的)工作,但它无疑可以帮助我理解和理解特定框架,库或模块的本质。

最近,我又遇到了其中之一,开始使用Redux开发 Web应用程序,除了香草JavaScript之外,别无其他 。 在本文中,我想概述一下如何构建应用程序,检查一些较早的(最终是不成功的)迭代,然后再查看我确定的解决方案以及在此过程中学到的知识。

设置

您可能已经听说过流行的React.jsRedux结合使用最新的前端技术来构建快速而强大的Web应用程序。

由Facebook制作的React是一个基于组件的开源库,用于构建用户界面。 尽管React只是一个视图层 (不是完整的框架,例如Angular或Ember) ,但是Redux可以管理应用程序的状态。 它起可预测状态容器的作用 ,整个状态存储在单个对象树中,并且只能通过发出所谓的操作来进行更改。 如果您是该主题的新手,建议您阅读这篇说明性文章

对于本文的其余部分,并不需要成为Redux的专家,但肯定有助于至少对其概念有基本的了解。

没有React的Redux-Scratch的应用程序

Redux之所以如此出色,是因为它迫使您提前思考并尽早了解应用程序设计。 您开始定义应实际存储的内容,可以更改的数据以及可以访问存储的组件。 但是由于Redux只关心状态,所以我发现自己对如何构造和连接其余应用程序有些困惑。 React可以很好地指导您完成所有事情,但是如果没有它,我就不得不找出最有效的方法。

该应用程序是第一个移动优先的俄罗斯方块克隆,它具有几个不同的视图。 实际的游戏逻辑在Redux中完成,而脱机功能由localStorage和自定义视图处理提供。 可以在GitHub上找到该存储库,尽管该应用程序仍在积极开发中,我在撰写本文时正在写这篇文章。

定义应用架构

我决定采用Redux和React项目中常见的文件结构。 这是一个逻辑结构,适用于许多不同的设置。 这个主题有很多 变体 ,大多数项目在做事上有些不同,但是总体结构是相同的。

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/ ,以便在我的代码库中保持一致的体系结构。

src / markup /

layouts/
└── default.html
partials/
├── back-button.html
└── meta.html
pages/
├── about.html
├── settings.html
└── ...
index.html

管理和访问商店

要访问存储,需要一次创建它,并将其传递给应用程序的所有实例。 大多数框架都使用某种类型的依赖项注入容器,因此,作为框架的用户,我们不必提出自己的解决方案。 但是在滚动自己的解决方案时,如何使所有组件都可以访问它?

我的第一次迭代有点被炸了。 我不知道为什么我会认为这是一个好主意,但是我将商店放在了自己的模块( scripts/store/index.js )中,然后可以由我的应用程序的其他部分导入。 我最终为此感到后悔,并很快地处理了循环依赖 。 问题是,当组件尝试访问商店时,商店没有得到正确的初始化。 我整理了一个图表来演示我正在处理的依赖流:

没有React的Redux-失败的依赖树

应用程序的入口点是初始化所有组件,然后直接或通过辅助函数(在此称为connect )在内部使用商店。 但是由于未明确创建存储,而只是在其自己的模块中作为副作用,因此组件最终在创建存储之前就使用了存储。 无法控制何时组件或辅助功能首次调用商店。 太混乱了。

存储模块如下所示:

scripts/store/index.js (不好)

import { createStore } from 'redux'
import reducers from '../reducers'

const store = createStore(reducers)

export default store
export { getItemList } from './connect'

如上所述,创建商店是一种副作用,然后将其导出。 助手功能也需要存储。

scripts/store/connect.js /store/connect.js(不好)

import store from './'

export function getItemList () {
  return store.getState().items.all
}

这是我的组件最终相互递归的确切时刻。 辅助函数需要store起作用,并同时从商店初始化文件中导出,以使应用程序的其他部分可以访问它们。 您看到已经听起来很混乱吗?

解决方案

现在似乎很明显,花了我一段时间才能理解。 我通过将初始化移到应用程序入口点scripts/index.js )并将其传递给所有必需的组件来解决了这个问题。

再次,这非常类似于React实际上使商店可访问(检查源代码 )的方式 。 他们在一起工作如此之好是有原因的,为什么不从其概念中学习呢?

没有React的Redux-Redux流程成功

应用程序入口点首先创建商店,然后将其向下传递给所有组件。 然后,组件可以与存储和调度动作连接 ,订阅更改或获取特定数据。

让我们来看看这些变化:

脚本 /store/configureStore.js(✓好)

import { createStore } from 'redux'
import reducers from '../reducers'

export default function configureStore () {
  return createStore(reducers)
}

我保留了该模块,但是导出了一个名为configureStore的函数,该函数在我的代码库的其他地方创建了商店。 注意,这只是基本概念; 我还利用Redux DevTools扩展并通过localStorage加载持久状态。

scripts / store / connect.js (✓好)

export function getItemList (store) {
  return store.getState().items.all
}

connect helper函数基本上是不变的,但是现在需要将存储作为参数传递。 刚开始我犹豫要使用此解决方案,因为我认为“那么辅助函数的作用是什么?” 。 现在,我认为它们很好并且足够高级,可以使所有内容更具可读性。

脚本/ index.js

import configureStore from './store'
import { PageControls, TetrisGame } from './components'

const store = configureStore()
const pageControls = new PageControls(store)
const tetrisGame = new TetrisGame(store)

// Further initialization logic.

这是应用程序的入口点。 创建store ,并将其传递给所有组件。 PageControls将全局事件侦听器添加到特定的操作按钮,而TetrisGame是实际的游戏组件。 在将存储移动到这里之前,它看起来基本相同,但是没有将存储单独传递到所有模块。 如前所述,组件可以通过我失败的connect方法访问商店。

组件

我决定使用两种组件: presentationalcontainer组件
呈现组件除了纯DOM处理外没有其他作用。 他们不知道这家商店。 另一方面,容器组件可以调度操作或订阅更改。

丹·阿布拉莫夫(Dan Abramov)撰写了一篇有关React组件的出色文章 ,但是该方法也可以应用于任何其他组件体系结构。

对我而言,也有例外。 有时,一个组件实际上很少,只能做一件事。 我不想将它们分解为上述模式之一,因此我决定将它们混合使用。 如果组件增加并获得更多逻辑,我将其分离。

脚本/组件/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))
    )
  }
}

上面的示例是这些组件之一。 它具有元素列表(在本例中为所有具有data-action属性的元素),并根据属性内容在单击时分派操作。 没有其他的。 然后,其他模块可能会监听存储中的更改并相应地更新自身。 如前所述,如果组件也进行DOM更新,我将其分开。

现在,让我向您展示两种组件类型的清晰区分。

更新DOM

在启动项目时,我最大的问题之一是如何实际更新DOM。 React使用称为虚拟DOM 的DOM快速内存表示形式将DOM更新保持在最低限度。

我实际上是在考虑做同样的事情,如果我的应用程序应该变得更大并且DOM越来越重,我很可能会切换到Virtual DOM ,但是现在我可以进行经典的 DOM操作,并且可以在Redux上正常工作。

基本流程如下:

  • 初始化容器组件的新实例,并将其传递给store以供内部使用
  • 组件订阅商店中的更改
  • 并使用其他呈现组件在DOM中呈现更新

注意:对于JavaScript中与DOM相关的任何内容,我都喜欢$符号前缀。 您可能已经猜到它是从jQuery的$ 因此,纯演示文稿文件名以美元符号为前缀。

脚本/ index.js

import configureStore from './store'
import { ScoreObserver } from './components'

const store = configureStore()
const scoreObserver = new ScoreObserver(store)

scoreObserver.init()

这里没有任何幻想。 容器组件ScoreObserver被导入,创建和初始化。 它实际上是做什么的? 它会更新所有与分数相关的视图元素:高分数列表,以及在游戏过程中当前分数信息。

脚本/组件/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))
  }

  // Used in a different place.
  updateScoreBoard () {
    this.$board.updateBoard(getScoreList(this.store))
  }

  init () {
    this.store.subscribe(this.updateScore.bind(this))
  }
}

请记住,这是一个简单的组件; 其他组件可能具有更复杂的逻辑和需要注意的事情。 这里发生了什么? ScoreObserver组件保存对store的内部引用,并创建两个演示组件的新实例以供以后使用。 init方法订阅商店更新,并在每次商店更改时更新$label组件-但前提是游戏确实在运行。

updateScoreBoard方法在其他地方使用。 每次发生更改时更新列表都是没有意义的,因为该视图始终处于不活动状态。 还有一个路由组件,该组件会在每次视图更改时更新或停用不同的组件。 其API大致如下所示:

// scripts/index.js

route.onRouteChange((leave, enter) => {
  if (enter === 'scoreboard') {
    scoreObserver.updateScoreBoard()
  }

  // more logic...
})

注意: $ (和$$ )不是jQuery引用,而是document.querySelector的便捷实用程序快捷方式。

脚本/组件/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)
  }
}

同样,一个基本示例和一个基本组成部分。 updateBoard()方法获取一个数组,对其进行迭代,然后将其内容插入得分列表。

脚本/组件/scoreObserver/$label.js

import { $ } from '../../utils'

export default class ScoreLabel {
  constructor () {
    this.$label = $('.game-current-score')
    this.$labelCount = this.$label.querySelector('span')
    this.initScore = 0
  }

  updateLabel (score = this.initScore) {
    this.$labelCount.innerText = score
  }
}

此组件的功能几乎与上面的ScoreBoard完全相同,但是仅更新了一个元素。

其他错误和建议

另一个重要点是实现用例驱动的存储。 我认为仅存储对于应用程序必不可少的内容很重要 。 在开始的时候,我几乎存储了所有内容:当前活动视图,游戏设置,得分,悬停效果, 用户的呼吸模式等。

尽管这可能与一个应用程序相关,但与另一个应用程序无关。 存储当前视图,并在重新加载时在完全相同的位置继续是很好的,但是在我的情况下,这感觉像是糟糕的用户体验,并且比有用的东西更烦人。 您也不想存储菜单或模式的切换,对吗? 用户为什么要回到该特定状态? 在较大的Web应用程序中,这可能很有意义。 但是在我的小型移动游戏中,仅仅因为我离开那里而回到设置屏幕是很烦人的。

结论

我在有和没有React的情况下都参与过Redux项目,而我的主要收获是,不需要在应用程序设计中有巨大的差异。 实际上,React中使用的大多数方法都可以适应任何其他视图处理设置。 我花了一段时间才意识到这一点,因为我开始以为我必须做不同的事情 ,但最终我意识到这是没有必要的。

什么不同的不过是你初始化模块,您的商店的方式,多少认识一个组件可以有整个应用程序的状态。 概念保持不变,但是代码的实现和数量完全适合您的需求。

Redux是一个很棒的工具,它可以帮助您以更深思熟虑的方式构建应用程序。 单独使用时,如果没有任何视图库,一开始可能会非常棘手,但是一旦您摆脱了最初的困惑,便无法阻止您。

您如何看待我的方法? 您是否一直在单独使用Redux和其他视图处理设置? 我希望得到您的反馈并在评论中进行讨论。


如果您需要有关Redux的更多信息,请查看我们的课程重写和测试Redux以解决设计问题迷你课程。 在本课程中,您将构建一个Redux应用程序,该应用程序通过Websocket连接接收按主题组织的推文。 为了让您尝尝店里的东西,请查看下面的免费课程。

正在加载播放器…

From: https://www.sitepoint.com/redux-without-react-state-management-vanilla-javascript/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值