- 原文地址:Introducing SpaceAce, a new kind of front-end state library
- 原文做者:Jon Abrams
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:Noah Gao
- 校对者:Hopsken
开发前端应用的你们都知道,状态管理是开发中最重要,最具挑战性的一部分。目前流行的基于组件的视图库,如 React,包括功能齐全的(最基本的)状态管理能力。它们使应用中的每一个组件都可以管理本身的状态。这对于小型应用程序来讲足够了,但你很快就会感到挫败。由于决定哪些组件具备状态以及如何在组件之间共享来自每一个状态的数据将会成为一个挑战。最后还要弄清楚状态是如何或为什么被改变。javascript
为了解决面向组件状态的上述问题,Redux 一类的库被引入。它们将该状态集中到一个集中的“store”中,每一个组件均可以读写它。为了维护顺序,他们将改变状态的逻辑集中到应用程序的中心部分,称为 reducer,使用 actions 调用它们,并使其产生新的状态副本。它很是有效,但学习曲线很高,须要大量的样板代码,并强迫你将更新状态的代码与渲染视图的代码分开。前端
SpaceAce 是一个新的库,它具备 Redux 的全部优势,例如集中的 store,不可变状态,单向数据流,明肯定义的 actions,它 还 极大地简化了代码更新 store 中状态的方式。java
咱们已经在 Trusted Health 的主 React 应用上用 SpaceAce 来管理状态将近一年了,取得了巨大的成功。咱们的工程师团队相对较小(只有三我的),它在不加大代码复杂度和牺牲可测试性的基础上,加速了咱们的功能开发。react
SpaceAce 提供一个状态管理的 store 叫作一个 space。一个 space 包括只读(不可变)的状态,还有一些用于更新它的工具集。可是这个 store 里面不仅是 有 状态,而是它自己就 是 状态。同时,他还提供了不少方法来生成新版本的状态。怎么作到?是一些带有属性的函数!不少 JS 开发者不知道 JS 函数也是对象。只是它能执行而已,因此它也能有一些属性,就像对象同样(由于它就是个对象!)。android
每一个 space 都是一个有属性的不可变对象,可是只能被读取,不能直接写入。每一个 space 也是 一个函数,可以建立应用改动后的状态副本。ios
最后,放个例子:git
import Space from 'spaceace';
const space = new Space({
appName: "SpaceAce demoe",
user: { name: 'Jon', level: 9001 }
});
const newSpace = space({ appName: "SpaceAce demo" });
console.log(`Old app name: ${space.appName}, new app name: ${newSpace.appName}`);
复制代码
将会输出:“Old app name: SpaceAce demoe, new app name: SpaceAce demo”github
上面的例子展现了如何建立一个 space 并经过调用它将一个对象合并到状态来直接“更改”它。这和 React 的 setState 很像,应用了一次浅合并。记住,本来的 space 并无变化,只是被一个应用了改动的副本给替换了。redux
然而,这对应用在有新状态时须要进行从新渲染的场景来讲,没用。为了让解决这个场景更简单,一个 subscribe 函数被提供出来。它能在相关 space 被“改动”时去调用回调:后端
import Space, { subscribe } from 'spaceace';
const space = new Space({
appName: "SpaceAce demoe",
user: { name: 'Jon', level: 9001 }
});
subscribe(space, ({ newSpace, causedBy }) => {
console.log(`State updated by ${causedBy}`);
ReactDOM.render(
<h1>{newSpace.appName}</h1>,
document.getElementById('app')
);
});
// 将使 React 从新渲染
space({ appName: "SpaceAce demo" });
复制代码
大多数状况下,状态都是由于用户作的事情而发生变化。好比,他们单击一个复选框、从下拉列表中选择一个选项或填入一个字段。SpaceAce 经过这些简单的交互来更新状态 很是简单。若是使用字符串调用 space,它将生成并返回处理函数:
export const PizzaForm = ({ space }) => (
<form>
<label>Name</label>
<input
type="text"
value={space.name || ''}
onChange={space('name')} // 当用户输入时,`space.name` 会被更新
/>
<label>Do you like pizza?</label>
<input
type="checkbox"
checked={space.pizzaLover || false}
onChange={space('pizzaLover')} // 分配 true 或 false 给 `space.pizzaLover`
/>
</form>
);
复制代码
虽然大多数应用只有许多简单的交互,但它们有时也会包含一些复杂的 action。SpaceAce 容许你自定义 action,全部 action 都与组件在同一文件中。调用时,会为这些 action 提供一个对象,其中包含用于更新状态的便捷函数:
import { fetchPizza } from '../apiCalls';
/* handleSubmit 是一个自定义 action。 第一个参数由 SpaceAce 提供。 其他参数是须要传入的, 在这个案例中由 React 的事件对象组成。 */
const handleSubmit = async ({ space, merge }, event) => {
event.preventDefault();
// merge 函数将进行浅合并,容许一次分配多个属性
merge({ saving: true }); // 当即更新 space,将触发从新渲染
const { data, error } = await fetchPizza({ name: space.name });
if (error) return merge({ error: errorMsg, saving: false });
merge({
saving: false,
pizza: data.pizza // 期待获得 'Pepperoni'
});
};
/* handleReset 是另外一个自定义 action。 这个函数能够用来将 space 的全部属性抹除, 将它们用另外一些替换掉。 */
const handleReset = ({ replace }) => {
replace({
name: '',
pizzaLover: false
});
};
export const PizzaForm = ({ space }) => (
<form onSubmit={space(handleSubmit)}> {/* ... 一些 input 元素 */} <p className="error">{space.errorMsg}</p> {space.pizza && <p>You’ve been given: {space.pizza}</p>} <button disabled={space.saving} type="submit">Get Pizza</button> <button disabled={space.saving} type="button" onClick={space(handleReset)}>Reset</button> </form>
);
复制代码
你可能会注意到,全部这些改变 space 状态的方式都会假定状态相对较浅,但若是每一个应用程序只有一个 space,那怎么可能呢?不可能的!每一个 space 均可以有任意数量的 sub-space,它们也只是 space,但它们有父级。每当更新其中一个 sub-space 时,改动会冒泡,一旦更改到达根 sapce,就会触发应用的从新渲染。
有关子 space 最棒的地方在于,你不用特意去制造它,它将在你··访问 space 中的对象或是数组时,自动被建立出来:
const handleRemove = ({ remove }, itemToBeRemoved) => {
// `remove` 将在数组型 space 中可用,
// 它将为每一个元素运行回调。
// 若是回调的结果是 true,元素将被删除。
remove(item => item === itemToBeRemoved);
};
/* 一个购物车的 space 将是一个物品的数组, 每一个物品都是对象,它也将是一个 space。 */
export const ShoppingCart = ({ space }) => (
<div>
<ul>
{space.map(item => (
<li key={item.uuid}>
<CartItem
space={item}
onRemove={space(handleRemove).bind(null, item)}
/>
</li>
)}
</ul>
</div>
);
const CartItem = ({ space, onRemove }) => (
<div>
<strong>{space.name}</strong>
<input
type="number"
min="0"
max="10"
onChange={space('count')}
value={space.count}
/>
<button onClick={onRemove}>Remove</button>
</div>
);
复制代码
还有不少功能能够继续探索,我很快就会分享这些有趣的技巧。请继续关注个人下一篇文章!
与此同时,你能够在 Github 上的代码和文档 中了解更多信息,也能够 让我知道你的想法!
感谢 Zivi Weinstock 的付出。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。