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