Hydux: 一个 Elm-like 的 全功能的 Redux 替代品

#1

在学习和使用 Fable + Elmish 一段时间之后,对 Elm 架构有了更具体的了解, 和预料中的一样, Elm 风格的框架果然还是和强类型的 Meta Language 语言更搭,只有一个字: 爽。 但是呢,Fable 毕竟是一个小众语言,使用的 F# 语法而且还是来自“万恶”的微软,开发环境还需要依赖 dotnet, 就这几点恐怕在公司的一些正式项目中推行就有些难度。

刚好最近需要做一个答题小游戏的应用,不想再上 React + Redux 全家桶了,一是体积太大,二是无论配置还是写起来都太繁琐。忽然发现 hyperapp 让我眼前一亮,简洁的架构,elm 风格, 1kb 的体积,丰富的生态,简直小应用神器! 但是呢,在实际使用中就发现,hyperapp 破坏性更新太多,导致很多第三方库,比如 persist, Redux Devtools, hmr 都不能用了,虽然这些库实现都不复杂,但是一个个改太麻烦了,又不想用老版本,干脆自己重新造了个轮子 – Hydux.

Hydux 的语法和 hyperapp 差不多,抽离了 view 层,支持任意的 vdom 库,包括 react, 特点是 内置了 不依赖于 react-hot-loader 的热更新,logger, Redux Devtools 和 persist,依然是 1kb大小 (gzip, 不包括开发环境),完全无痛的开发环境,真正的一站式解决方案!

view 层内置了 1kb 的 picodom, 同时也有官方支持的 React 扩展 使用 React 来渲染.

说了这么多,还是上点代码:
首先我们有一个 counter 模块,代码和 Elm 的组织方式很类似,不需要像 Redux 在 Actions/Reducers/ActionTypes 中跳来跳去的

// Counter.js
export default {
  init: () => ({ count: 1 }), // 初始化状态
  actions: { // actions 改变状态
    down: () => state => ({ count: state.count - 1 }),
    up: () => state => ({ count: state.count + 1 })
  },
  view: (state: State) => (actions: Actions) => // view
    <div>
      <h1>{state.count}</h1>
      <button onclick={actions.down}>–</button>
      <button onclick={actions.up}>+</button>
    </div>
}

然后呢,我们可以像 Elm 一样 复用 模块, 以前在用 Redux 时总是会面临不知道怎么复用才好的问题,而实际上 Elm 的复用是超级简单和方便的。

import _app from 'hydux'
import withPersist from 'hydux/lib/enhancers/persist'
import withPicodom, { h, React } from 'hydux/lib/enhancers/picodom-render'
import Counter from './counter'

// let app = withPersist<State, Actions>({
//   key: 'my-counter-app/v1'
// })(_app)

// use built-in 1kb picodom to render the view.
let app = withPicodom()(_app)

if (process.env.NODE_ENV === 'development') {
  // built-in dev tools, without pain.
  const devTools = require('hydux/lib/enhancers/devtools').default
  const logger = require('hydux/lib/enhancers/logger').default
  const hmr = require('hydux/lib/enhancers/hmr').default
  app = logger()(app) // 内置的 logger 
  app = devTools()(app) // 内置的 Redux Devtools 扩展支持
  app = hmr()(app) // 内置的热更新模块
}

const actions = {
  counter1: Counter.actions,
  counter2: Counter.actions,
}

const state = {
  counter1: Counter.init(),
  counter2: Counter.init(),
}

const view = (state: State) => (actions: Actions) =>
    <main>
      <h1>Counter1:</h1>
      {Counter.view(state.counter1)(actions.counter1)}
      <h1>Counter2:</h1>
      {Counter.view(state.counter2)(actions.counter2)}
    </main>

export default app({
  init: () => state,
  actions,
  view,
})

然后就可以了!简单,可控,无痛的开发环境和代码组织。

在线 demo

异步使用的是类似 Elm 的副作用管理器风格, actions 可以是完全纯的函数,也可以是直接返回一个 promise: https://github.com/hydux/hydux#actions-with-cmd

官网: https://github.com/hydux/hydux

官方支持的 React 扩展: https://github.com/hydux/hydux-react
官方支持的 React-Router 扩展: https://github.com/hydux/hydux-react-router

2 Likes
#2

写的不错,能转载到我的知乎专栏去吗

#3

可以,感谢分享。

#4

要不你投稿把,我不知道你知乎账号: -)

#5

可以,感谢分享。

好的,其实我在知乎也发过一篇。。

#6

不是 太明白 可以像 Elm 一样 复用 模块, 以前在用 Redux 时总是会面临不知道怎么复用才好的问题 这个地方是指的什么。 如果是我理解的话,Redux遵循的是flux构架,和你的elm构架是不同的思路。 Redux的源代码完全是函数式编程的。 针对大型的应用,异步数据和组件可以完全分开。
如果是值得组件的复用性, https://medium.com/@mirkomariani/functional-components-with-react-stateless-functions-and-ramda-e83e54fcd86b 不知道这篇文章是不是有点端倪?

子组件,父组件完全都可以函数化,也可用compose和pipeline函数链接起来,等待参数的传递。由于Redux是单向的数据流, rest data =>store=>{pipeline(parent Component->child Component)} 。柯理化的组件可以完全复用。 这里的复用是不是你所指的复用呢? 我觉得在组件这一块利用好函数式编程,外加Flux构架(redux)的组合威力才够大。

#7
{Counter.view(state.counter1)(actions.counter1)}

如果只这一部分复用,可以说,用Redux 完成一定问题都没有。 这里如果你要彻底使用函数式编程,代码会更简洁。 medium网站用 redux+ramda 搜索,还有两三篇文章,你一看就知道是怎么回事了。

你可以导入Ramda.js库看看, 函数式编程对于 reducer的改进余地也很大,也有相关的文章介绍。

#8

复用视图组件没啥特别的,React 本身就提供了,hydux 参考 hyperapp 用 curry 的函数也只是个风格问题,这个地方和 jsx 或者函数调用其实没啥区别, 问题还是对于状态和逻辑的复用,也就是 update/state (这里是 actions/state ) 的复用。

在 hydux/Elm 中 一个组件虽然包含了 状态/逻辑和视图,但是实际上这三个都是纯函数,而且互不依赖,你可以复用一个组件,也可以只复用这个组件的逻辑,视图自己造,只需要保证依赖的状态结构一致就好(复用 init),整个代码的结构非常清晰易懂,都是简单的函数调用,没有太多晦涩的概念。

对应于 Redux 就是 actions 和 reducers,我很少在实践中看到能正确且大量复用 actions 和 reducers 的,这一点可能和不会用有关,但是我觉得和框架的设计不够易用,让初学者摸不着头脑也有关。而且代码冗余的部分比较多,对于动态语言来说不符合 dry 原则,维护起来也不是很方便。。

https://www.zhihu.com/question/263928256 这里有一个知乎上的问答可以参考一下,有一个回答说 Redux 不是分形的,而 Elm 是分形的回答我觉得挺有道理的。

我也做了一个比较简单的回答,不过针对状态管理这一点可能没有回答的很详细,我感觉使用 Redux 和 Elm 在状态管理方面有一个区别就是 Redux 中 actions 和 reducers (包括状态)都是扁平的(至少官方 推荐的API 是这样的),而 Elm 的状态是一个 tree。 而你指出的那句话我其实是指觉得 tree 的结构更好复用一点,无论是直接复用状态引用还是只复用状态结构(拷贝状态)都很方便。

当然这里并不是说有哪个功能用 Redux 就一定实现不了,只有 Elm 才能实现,Redux 很灵活,也很容易扩展,就像你说的,多引入一个库,多看几篇文章,你就能把 Redux 改进的很好,问题就在于 它还需要 “改进”。就那么简单的一个库,百来行代码,写出了一本比代码还厚的 Redux book,而且看完之后依然用不好 Redux, 还需要去阅读更多的最佳实践,整合一堆社区框架,最后居然还要自己再“改进”一下,你用 Ramda 改进,他用 redux-saga 改进,我用 dva 改进,最后百花齐放,redux由一个框架变成了一个平台。。。

而相比之下 Elm 风格,除了 Elm 的函数式语法可能不习惯,你只需要很低的成本就能掌握类似 Elm 优雅的架构和清晰的代码,一个啥都不会的新手看下代码,就能知道是怎么回事,没有一大堆的第三方库和概念,因为整个架构中大部分都是简单的函数式调用,知道语法就能知道这个框架怎么用了,不需要另外去学习 Ramda.js, dva 或者 redux-saga。我觉得这也是 Hydux 最大的价值。