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

ThinkerNoah發表於2019-02-14

開發前端應用的大家都知道,狀態管理是開發中最重要,最具挑戰性的一部分。目前流行的基於元件的檢視庫,如 React,包括功能齊全的(最基本的)狀態管理能力。它們使應用中的每個元件都能夠管理自己的狀態。這對於小型應用程式來說足夠了,但你很快就會感到挫敗。因為決定哪些元件具有狀態以及如何在元件之間共享來自每個狀態的資料將會成為一個挑戰。最後還要弄清楚狀態是如何或為何被改變。

為了解決面向元件狀態的上述問題,Redux 一類的庫被引入。它們將該狀態集中到一個集中的“store”中,每個元件都可以讀寫它。為了維護順序,他們將改變狀態的邏輯集中到應用程式的中心部分,稱為 reducer,使用 actions 呼叫它們,並使其產生新的狀態副本。它非常有效,但學習曲線很高,需要大量的樣板程式碼,並強迫你將更新狀態的程式碼與渲染檢視的程式碼分開。

SpaceAce 是一個新的庫,它具有 Redux 的所有優點,例如集中的 store,不可變狀態,單向資料流,明確定義的 actions,它 極大地簡化了程式碼更新 store 中狀態的方式。

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

SpaceAce 是什麼?

SpaceAce 提供一個狀態管理的 store 叫做一個 space。一個 space 包括只讀(不可變)的狀態,還有一些用於更新它的工具集。但是這個 store 裡面不只是 狀態,而是它本身就 狀態。同時,他還提供了很多方法來生成新版本的狀態。怎麼做到?是一些帶有屬性的函式!很多 JS 開發者不知道 JS 函式也是物件。只是它能執行而已,所以它也能有一些屬性,就像物件一樣(因為它就是個物件!)。

每個 space 都是一個有屬性的不可變物件,但是隻能被讀取,不能直接寫入。每個 space 也是 一個函式,能夠建立應用改動後的狀態副本。

最後,放個例子:

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”

上面的例子展示瞭如何建立一個 space 並通過呼叫它將一個物件合併到狀態來直接“更改”它。這和 React 的 setState 很像,應用了一次淺合併。記住,原本的 space 並沒有變化,只是被一個應用了改動的副本給替換了。

然而,這對應用在有新狀態時需要進行重新渲染的場景來說,沒用。為了讓解決這個場景更簡單,一個 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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章