更少的API - Crank.js
April 25, 2020
這週有一個新的 UI framework —— Crank.js 受到了前端開發者們的注意。這年頭前端 framework 已經發展得相當成熟了,市場已經 React, Angualr 跟 Vue 瓜分,新 framework 如果不是有過人之處,不會受到太大的關注了。這個 framework 不但 Dan Abramov 注意到了,在 reddit 有發文祝賀,處在資訊鍊的下游的我都收到訊息,更證明這個新東西不簡單。
Async!
這個 Crank.js 是什麼特別之處受到大家的注意呢?因為他直接使用 async / generator function 來處理各種狀況。例如一個典型的前端工作:拉後端資料顯示是這樣的:
async function IPAddress () {
const res = await fetch("https://api.ipify.org");
const address = await res.text();
return <div>Your IP Address: {address}</div>;
}
而用 React 做一樣的事情,要麻煩很多
function IPAddress() {
const [address, setAddress] = useState('');
useEffect(
() => {
fetch("https://api.ipify.org").then(res => res.text()).then(address => {
setAddress(address)
});
},
[]
);
return <div>address</div>;
}
當然你可以把這邊用到的 React Hooks 用一個 function包起來,讓自己以後不用那麼麻煩。但是 Crank 這邊的寫法真是讓人耳目一新,我自己就是因為這個範例而讓我想嘗試這個 Library。
Generator!
另外一個 Crank.js 主打的是用 generator 來寫 stateful component。一個簡單的範例:
function* Clicker() {
let count = 0;
const handleClick = () => {
count++;
this.refresh();
};
while (true) {
yield (
<div>
<button onclick={handleClick}>Clicked {count} times</button>
</div>
);
}
}
Crank.js 的寫法要比 React 的寫法多出一些 code。這是相當於 React:
function Clicker() {
const [count, setCount] = useState(0);
const handleClick = ev => {
setCount(count + 1);
};
return (
<div>
<button onClick={handleClick}>Clicked {count} times</button>
</div>
);
}
兩邊乍看之下差不多,都要呼叫一個 framework 的 api —— this.refresh
vs useState
,但是概念卻完全不一樣了。在 React,state 其實是來自 component 外面 的,雖然看起來像是個 local 變數。React 的 setState 做的事是改變一個在 componnet之外,React 系統之內的 state 變數,然後 React 知道這個 state 改變後,重新 render 相對應的 component。
也就是說 React 的 render 永遠是資料的改變驅動UI重畫,但是 Crank.js 的作法卻是由 component 通知核心自己要 refresh,方向可以說是相反的。
Async Generator!!
如果你想要 concurrent update 你的 UI,那就要用 async generator 了。你說你不到 concurrent update?其實如果你想要在 component 抓資料的同時顯示 loading,就是一種 concurrent update 了:
async function Loader({ wait }) {
await new Promise(resolve => setTimeout(resolve, wait));
return <div>loading...</div>;
}
async function Data({ wait }) {
await new Promise(resolve => setTimeout(resolve, wait));
return <div>Data loaded</div>;
}
async function* DataLoader(_) {
const dataDelay = 5000;
for await (_ of this) {
yield <Loader wait={1000} />;
yield <Data wait={dataDelay} />;
}
}
在這個範例中,如果 dataDelay
是小於 1000 的數值 Crank.js 會跳過 Loader
直接 render Data
,是不是很方便。如果有在跟 React 消息的人會知道,這個讓 concurrent render 的能力 Suspense 想要做的,但是 Suspense 在 JSConf Iceland 2018 發表後,已經難產到現在還沒正式推出…。
總而言之
Crank.js 的的 component 寫法真的非常有意思。相比 React,你要會 JavaScript generator,但是要學的 framework api 少很多。使用 generator 的著名 library/framework,好像只有 redux-saga 跟 koa,能再看到一個用 generator 的 framework 受到注目也是蠻令人高興的。
從設計理念上來說,這是把更多的事情搬到 component 來處理。這讓我回想起 React + Redux 的時代,當時大家瘋狂追求純粹,結果是 React component 很 pure,Redux reducer/action 很 pure,拉資料等等不乾淨的工作一路推推推推到 redux 的 middleware,像是 redux-saga 去。最終造成該做的工作沒有少,反而更麻煩了。當然現在把事情搬到 component 裡面,造成的影響也還是要長期實作才能知道了,如果有機會的話。
最後還是大力推薦一下大家玩一下這個 framework,它的 introduction 也值得一讀,寫得非常感人肺腑呀。