华为在今年开源了一款类似于 React 的前端框架, openInula。他的宣传语上面,把 openInula 与大语言模型、前端 AI 赋能结合在一起,主打一个高性能、全场景、智能化。
果然遥遥领先在宣传语的设计上还是有点水平的。然后我就去了解了一下这个框架。
一、无缝迁移
我想先试一下能不能真的做到无缝切换。如果真的能做到的话,我们就可以非常方便的使用 React 的生态直接搞 openinula 项目了。
然后我在 vite 上随便搞了一个项目,把 openinula 跑了起来。能运行官方文档首页的 demo。
然后我在项目中引入了一个 react 生态中最常用的 react-router。
yarn add react-router-dom
然后写了一个很小的 demo 想看看能不能跑起来。
function ReactiveApp() {
return (
<Routes>
<Route path='/' element={Index} />
<Route path='child' element={Child} />
</Routes>
);
}
结果不出所料。跑不起来。
然后尝试修改了一下,发现要改的地方太多了,算了,就算最后改成功,也不是我想要的那种无缝切换的效果,还是比较麻烦。所以想要顺利把 React 生态的东西直接用到 openinula 上也并不简单,需要调整和修改内容。
react 的底层模块区分了 react 和 react-dom ,就导致了区别还是比较大。
无缝切换:GG
但是他确实在兼容 React API 上做得比较好,几乎所有常用的 api 都有支持。所以如果只是基于这些 api 写出来的东西应该切换起来难度还是不高的。
二、响应式 API
openInula 还支持了一个响应式 API:useReactive
响应式 API 其实就是当监听的数据发生变化时,组件函数不需要重新执行。通过这样的方式减少函数执行范围,可以比 diff 少一些逻辑执行。
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>组件渲染次数:{renderCount}</div>
</div>
);
}
export default ReactiveApp;
这个 api 比较有意思的他的 getter 和 setter 的设计。
data.count.get()
data.count.set(() => c + 1)
项目经验丰富,对可维护性很重视的同学应该能想得通为什么要设计成这样。因为看上去使用比较麻烦,没有直接像 Vue 那样,通过 Proxy 劫持来省略掉显示的调用 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 时,或者阅读别人的项目时,无法在代码逻辑中快速区分普通数据和响应式数据,从而增加了维护成本。
如下例所示,我们只有追溯到数据最初声明的地方,才能分清他到底是响应式数据还是普通数据。
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 使用的一个很重要的前提,就是函数组件不会重新执行。也就意味着,他们的混用,特别是当 useState 存在于父级中时,会出现严重的混乱。
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 执行次数:{p}</div>
<div>记时:{counter.count.get()}</div>
</>
)
}
也就意味着,他们的共存在使用时一定要非常小心。在这种情况下,useReactive 的存在与 useState 有点犯冲,显得格格不入。或者可以在项目中,尽量避免使用 useState,具体效果如何,还要深度使用之后才能体会到。
三、迁移我的 React 组件库
我在 React 中有一些积累的组件库,然后我把一些常用的迁移到 openInula 中来,经过简单的修改,迁移成功。使用语法没有任何变化。
<Icon type='search' color='red' />
<Button type='primary'>hello world</Button>
这样来看的话,确实能够快速将 React 的生态迁移到 openInula 上面来。但是由于我大多数组件都是基于 useState 来编写的,因此,想要使用 useReactive 的话,只能全部替换掉。
- const [display, setDisplay] = useState(false)
+ const display = useReactive({ show: false })
替换掉之后功能基本上没什么毛病。但是在最佳实践的摸索上还存在一些疑问。比如当我想要将一个响应式数据传递给子组件时,下面哪种方式更好一些呢?我还没有一个定论,还需要进一步的体会和摸索。
<Dialog show={data.open.get()}}>hello</Dialog>
<Dialog show={data.open}>hello</Dialog>
第一种方式会更加契合解耦方面的思考,但书写稍微繁琐了一点,第二种方式呢,会对子组件逻辑造成更大的干扰。想到这里,突然之间明白了在 arkUI 里的状态设计,如果从父组件里传递一个响应式数据给子组件时,子组件必须使用 @Prop
装饰来接收这个状态。
这样在子组件中,我们就能够清晰的知道这个数据类型的特性到底是怎么回事了。从而降低了维护成本。这样一想的话,arkUI 在组件状态的设计上,确实有点东西。
@Component
struct ChildComponent {
@Prop
private count: number
build() {
Text(`Child Count: ${this.count}}`)
}
}
四、意外之喜
当我试图使用解构的方式来拆解 useReactive 时,居然不会失去响应性。
const {count, open} = useReactive({
count: 0,
open: false
});
const countText = useComputed(() => {
return `计时: ${count.get()}`;
});
setInterval(() => {
count.set((c) => c + 1);
}, 1000);
这可就解决了大问题了!当数据变得庞大,它的繁琐的程度将会大大的降低。所以在使用上会比 solid.js 方便许多。
我了解到的 Vue3 和 Solid 实际上在这一点上都做得不是很好,解构之后,Vue3 的状态会失去响应性。
// 直接使用 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 就有点厉害了。在设计上充分体现了自己的独创性和先进性,如果其他方面不出什么问题的话,应该会受到一大批程序员的喜爱。
不愧是遥遥领先。
五、总结
openInula 的使用体验与 React 几乎一样。与 React 不同的是,他增加了一个响应式 API。因此能够增加一些不同的开发体验。也正是由于这个响应式 API 的存在,让 openInula 在 API 设计上有了自己的独创性。
与其他响应式框架相比,我更喜欢 openInula 的 API 设计,在开发体验与维护体验的综合考虑上目前是做得最好的,虽然为了考虑维护体验牺牲了一些开发体验,不过我完全能接受。由于接触了几款华为的框架,可以感受到,他们在设计 API 时,会把可维护性的重要性看得比开发体验更高。
当然,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()