體驗了一把華為的 OpenInula,談談使用感受
華為在今年開源了一款類似于 React 的前端框架, openInula。他的宣傳語上面,把 openInula 與大語言模型、前端 AI 賦能結(jié)合在一起,主打一個高性能、全場景、智能化。
果然遙遙領先在宣傳語的設計上還是有點水平的。然后我就去了解了一下這個框架。
一、無縫遷移
我想先試一下能不能真的做到無縫切換。如果真的能做到的話,我們就可以非常方便的使用 React 的生態(tài)直接搞 openinula 項目了。
然后我在 vite 上隨便搞了一個項目,把 openinula 跑了起來。能運行官方文檔首頁的 demo。
然后我在項目中引入了一個 react 生態(tài)中最常用的 react-router。
yarn add react-router-dom
然后寫了一個很小的 demo 想看看能不能跑起來。
function ReactiveApp() {
return (
<Routes>
<Route path='/' element={Index} />
<Route path='child' element={Child} />
</Routes>
);
}
結(jié)果不出所料。跑不起來。
然后嘗試修改了一下,發(fā)現(xiàn)要改的地方太多了,算了,就算最后改成功,也不是我想要的那種無縫切換的效果,還是比較麻煩。所以想要順利把 React 生態(tài)的東西直接用到 openinula 上也并不簡單,需要調(diào)整和修改內(nèi)容。
react 的底層模塊區(qū)分了 react 和 react-dom ,就導致了區(qū)別還是比較大。
無縫切換:GG
但是他確實在兼容 React API 上做得比較好,幾乎所有常用的 api 都有支持。所以如果只是基于這些 api 寫出來的東西應該切換起來難度還是不高的。
二、響應式 API
openInula 還支持了一個響應式 API:useReactive
響應式 API 其實就是當監(jiān)聽的數(shù)據(jù)發(fā)生變化時,組件函數(shù)不需要重新執(zhí)行。通過這樣的方式減少函數(shù)執(zhí)行范圍,可以比 diff 少一些邏輯執(zhí)行。
function ReactiveApp() {
const renderCount = ++useRef(0).current;
const data = useReactive({ count: 0 });
const countText = useComputed(() => {
return `計時: ${data.count.get()}`;
});
setInterval(() => {
data.count.set((c) => c + 1);
}, 1000);
return (
<div>
<div>{countText}</div>
<div>組件渲染次數(shù):{renderCount}</div>
</div>
);
}
export default ReactiveApp;
這個 api 比較有意思的他的 getter 和 setter 的設計。
data.count.get()
data.count.set(() => c + 1)
項目經(jīng)驗豐富,對可維護性很重視的同學應該能想得通為什么要設計成這樣。因為看上去使用比較麻煩,沒有直接像 Vue 那樣,通過 Proxy 劫持來省略掉顯示的調(diào)用 get/set ,所以肯定會給人帶來一些疑惑和不解。
data.count
data.count += 1
這樣又簡潔又舒適,有什么不好。
與 React 非常相似的 Solid.js 也沒有這樣做。而是選擇了另外一種方式
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const interval = setInterval(
() => setCount(count => count + 1),
1000
);
onCleanup(() => clearInterval(interval));
return <div>Count value is {count()}</div>;
};
一個最主要的原因是,當項目變得龐大和久遠,我們在重新閱讀項目或者修改 bug 時,或者閱讀別人的項目時,無法在代碼邏輯中快速區(qū)分普通數(shù)據(jù)和響應式數(shù)據(jù),從而增加了維護成本。
如下例所示,我們只有追溯到數(shù)據(jù)最初聲明的地方,才能分清他到底是響應式數(shù)據(jù)還是普通數(shù)據(jù)。
data.count
data.count += 1
result.count
result.count++
綜合來看,從語法上我更喜歡 openinula 的 api 設計。
// openInula
data.count.get()
data.count.set((v) => v + 1)
// solid
count()
setCount(count => count + 1)
openInula 還有一個比較重要的問題,就是 React API 和 響應式 API 共存的問題。也就是說,響應式 API 使用的一個很重要的前提,就是函數(shù)組件不會重新執(zhí)行。也就意味著,他們的混用,特別是當 useState 存在于父級中時,會出現(xiàn)嚴重的混亂。
function ReactiveApp() {
const [index, setIndex] = useState(0)
return (
<div>
<div notallow={() => setIndex(index + 1)}>index: {index}</div>
<Child />
</div>
);
}
function Child() {
const counter = useReactive({ count: 1 })
const p = ++useRef(0).current
const timer = useRef
useEffect(() => {
setInterval(() => {
counter.count.set((c) => c + 1)
}, 1000)
}, [])
return (
<>
<div>Child 執(zhí)行次數(shù):{p}</div>
<div>記時:{counter.count.get()}</div>
</>
)
}
也就意味著,他們的共存在使用時一定要非常小心。在這種情況下,useReactive 的存在與 useState 有點犯沖,顯得格格不入?;蛘呖梢栽陧椖恐?,盡量避免使用 useState,具體效果如何,還要深度使用之后才能體會到。
三、遷移我的 React 組件庫
我在 React 中有一些積累的組件庫,然后我把一些常用的遷移到 openInula 中來,經(jīng)過簡單的修改,遷移成功。使用語法沒有任何變化。
<Icon type='search' color='red' />
<Button type='primary'>hello world</Button>
這樣來看的話,確實能夠快速將 React 的生態(tài)遷移到 openInula 上面來。但是由于我大多數(shù)組件都是基于 useState 來編寫的,因此,想要使用 useReactive 的話,只能全部替換掉。
- const [display, setDisplay] = useState(false)
+ const display = useReactive({ show: false })
替換掉之后功能基本上沒什么毛病。但是在最佳實踐的摸索上還存在一些疑問。比如當我想要將一個響應式數(shù)據(jù)傳遞給子組件時,下面哪種方式更好一些呢?我還沒有一個定論,還需要進一步的體會和摸索。
<Dialog show={data.open.get()}}>hello</Dialog>
<Dialog show={data.open}>hello</Dialog>
第一種方式會更加契合解耦方面的思考,但書寫稍微繁瑣了一點,第二種方式呢,會對子組件邏輯造成更大的干擾。想到這里,突然之間明白了在 arkUI 里的狀態(tài)設計,如果從父組件里傳遞一個響應式數(shù)據(jù)給子組件時,子組件必須使用 @Prop
裝飾來接收這個狀態(tài)。
這樣在子組件中,我們就能夠清晰的知道這個數(shù)據(jù)類型的特性到底是怎么回事了。從而降低了維護成本。這樣一想的話,arkUI 在組件狀態(tài)的設計上,確實有點東西。
@Component
struct ChildComponent {
@Prop
private count: number
build() {
Text(`Child Count: ${this.count}}`)
}
}
四、意外之喜
當我試圖使用解構(gòu)的方式來拆解 useReactive 時,居然不會失去響應性。
const {count, open} = useReactive({
count: 0,
open: false
});
const countText = useComputed(() => {
return `計時: ${count.get()}`;
});
setInterval(() => {
count.set((c) => c + 1);
}, 1000);
這可就解決了大問題了!當數(shù)據(jù)變得龐大,它的繁瑣的程度將會大大的降低。所以在使用上會比 solid.js 方便許多。
我了解到的 Vue3 和 Solid 實際上在這一點上都做得不是很好,解構(gòu)之后,Vue3 的狀態(tài)會失去響應性。
// 直接使用 count 無法具備響應性
const {count} = reactive({ count: 0 })
Solid 的 API 設計,又無法做到把顆粒度細分到每個子屬性
const [count, setCount] = createSignal({n: 1});
function clickHandler() {
setCount({ n: count().n + 1 })
}
所以,當需要更細的屬性時,Vue3 可能會更多的使用 ref 來做,而 solid 則與 useState 一樣,單獨聲明這個屬性。
這么橫向一對比,openInula 的響應式 API 就有點厲害了。在設計上充分體現(xiàn)了自己的獨創(chuàng)性和先進性,如果其他方面不出什么問題的話,應該會受到一大批程序員的喜愛。
不愧是遙遙領先。
五、總結(jié)
openInula 的使用體驗與 React 幾乎一樣。與 React 不同的是,他增加了一個響應式 API。因此能夠增加一些不同的開發(fā)體驗。也正是由于這個響應式 API 的存在,讓 openInula 在 API 設計上有了自己的獨創(chuàng)性。
與其他響應式框架相比,我更喜歡 openInula 的 API 設計,在開發(fā)體驗與維護體驗的綜合考慮上目前是做得最好的,雖然為了考慮維護體驗犧牲了一些開發(fā)體驗,不過我完全能接受。由于接觸了幾款華為的框架,可以感受到,他們在設計 API 時,會把可維護性的重要性看得比開發(fā)體驗更高。
當然,svelte 我還沒有怎么了解過,不過有聽到坊間傳言說是模仿 Vue3 的,那估計設計模式跟 Vue3 差別不算大。
var { count, a, b, c } = useReactive({
count: 1,
a: 1,
b: 1,
c: 1
})
count.set((v) => v + 1)
count.get()
a.set((v) => v + 1)
a.get()
b.set((v) => v + 1)
b.get()
c.set((v) => v + 1)
c.get()