[譯] SpaceAce 瞭解一下,一個新的前端狀態管理庫

開發前端應用的你們都知道,狀態管理是開發中最重要,最具挑戰性的一部分。目前流行的基於組件的視圖庫,如 React,包括功能齊全的(最基本的)狀態管理能力。它們使應用中的每一個組件都可以管理本身的狀態。這對於小型應用程序來講足夠了,但你很快就會感到挫敗。由於決定哪些組件具備狀態以及如何在組件之間共享來自每一個狀態的數據將會成爲一個挑戰。最後還要弄清楚狀態是如何或爲什麼被改變。javascript

爲了解決面向組件狀態的上述問題,Redux 一類的庫被引入。它們將該狀態集中到一個集中的「store」中,每一個組件均可以讀寫它。爲了維護順序,他們將改變狀態的邏輯集中到應用程序的中心部分,稱爲 reducer,使用 actions 調用它們,並使其產生新的狀態副本。它很是有效,但學習曲線很高,須要大量的樣板代碼,並強迫你將更新狀態的代碼與渲染視圖的代碼分開。前端

SpaceAce 是一個新的庫,它具備 Redux 的全部優勢,例如集中的 store,不可變狀態,單向數據流,明肯定義的 actions,它 極大地簡化了代碼更新 store 中狀態的方式。java

咱們已經在 Trusted Health 的主 React 應用上用 SpaceAce 來管理狀態將近一年了,取得了巨大的成功。咱們的工程師團隊相對較小(只有三我的),它在不加大代碼複雜度和犧牲可測試性的基礎上,加速了咱們的功能開發。react

SpaceAce 是什麼?

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 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索