当 Vue 遇上鸿蒙:我的声明式 UI 实验笔记
1. 前言
最近在学习鸿蒙开发,接触到 ArkUI 的声明式开发范式,不禁联想到 Vue 是否也能以类似的方式构建 UI,像 Flutter 和 ArkUI 那样优雅地描述界面。于是,带着这份好奇与探索欲,我决定动手实践一番。经过一番折腾,终于捣鼓出了一个小玩具,不仅加深了对 Vue 的理解,也让我对声明式 UI 的魅力有了更深的体会。
在这篇博文中,我将分享这段有趣的探索之旅,希望能为同样对声明式开发感兴趣的你带来一些启发和灵感。如果你也喜欢折腾技术,不妨一起踏上这段奇妙的旅程吧!🚀
2. 基础搭建和样式库
基础库使用vite搭建,并导入样式库naive-ui (题主公司最近的项目用的这个,就用这个作为示例,也可以使用其他的)且安装jsx插件@vitejs/plugin-vue-jsx
//按流程创建模板工程
npm create vite@latest
//导入样式库
npm i -D naive-ui
//安装jsx插件
npm i @vitejs/plugin-vue-jsx -D
3. 封装工厂类
核心思想就是实现一个类进行链式调用即可。下面举俩个例子,完整示例请看文章末尾。
-
定义一个
ButtonFactory工厂类。import { NButton } from "naive-ui"; import { getSlotsDom } from "./utils"; import type { ButtonProps } from "naive-ui"; import type { BaseComponentType } from "./types"; import type { HTMLAttributes } from "vue"; export type ButtonFactoryConstructorType = { props?: ButtonProps; attrs?: HTMLAttributes; defaultSlot?: BaseComponentType; iconSlot?: BaseComponentType; }; export class ButtonFactory { private defaultSlot: BaseComponentType = null; private iconSlot: BaseComponentType = null; private props: ButtonProps = {}; private attrs: HTMLAttributes = {}; constructor(param?: ButtonFactoryConstructorType) { if (param?.props) this.setProps(param.props); if (param?.defaultSlot) this.setDefault(param.defaultSlot); if (param?.iconSlot) this.setIcon(param.iconSlot); if (param?.attrs) this.setAttrs(param.attrs); } setAttrs(attrs: HTMLAttributes) { this.attrs = attrs; return this; } setProps(props: ButtonProps) { this.props = props; return this; } setDefault(component: BaseComponentType) { this.defaultSlot = component; return this; } setIcon(component: BaseComponentType) { this.iconSlot = component; return this; } create() { return ( <NButton {...this.attrs} {...this.props}> {{ default: () => getSlotsDom(this.defaultSlot), icon: () => getSlotsDom(this.iconSlot), }} </NButton> ); } } -
定义
CardFactory工厂类import { NCard } from "naive-ui"; import type { CardProps } from "naive-ui"; import type { BaseComponentType } from "./types"; import type { HTMLAttributes } from "vue"; import { getSlotsDom } from "./utils"; export type CardFactoryConstructorType = { props?: CardProps; defaultSlot?: BaseComponentType; coverSlot?: BaseComponentType; headerSlot?: BaseComponentType; headerExtraSlot?: BaseComponentType; footSlot?: BaseComponentType; actionSlot?: BaseComponentType; attrs?: HTMLAttributes; }; export class CardFactory { private defaultSlot: BaseComponentType = null; private coverSlot: BaseComponentType = null; private headerSlot: BaseComponentType = null; private headerExtraSlot: BaseComponentType = null; private footSlot: BaseComponentType = null; private actionSlot: BaseComponentType = null; private props: CardProps = {}; private attrs: HTMLAttributes = {}; constructor(param?: CardFactoryConstructorType) { if (param?.props) this.setProps(param.props); if (param?.defaultSlot) this.setDefault(param.defaultSlot); if (param?.coverSlot) this.setCover(param.coverSlot); if (param?.attrs) this.setAttrs(param.attrs); if (param?.headerSlot) this.setHeader(param.headerSlot); if (param?.headerExtraSlot) this.setHeaderExtra(param.headerExtraSlot); if (param?.footSlot) this.setFoot(param.footSlot); if (param?.actionSlot) this.setAction(param.actionSlot); } setAttrs(attrs: HTMLAttributes) { this.attrs = attrs; return this; } setProps(props: CardProps) { this.props = props; return this; } setDefault(component: BaseComponentType) { this.defaultSlot = component; return this; } setHeader(component: BaseComponentType) { this.headerSlot = component; return this; } setHeaderExtra(component: BaseComponentType) { this.headerExtraSlot = component; return this; } setCover(component: BaseComponentType) { this.coverSlot = component; return this; } setFoot(component: BaseComponentType) { this.footSlot = component; return this; } setAction(component: BaseComponentType) { this.actionSlot = component; return this; } create() { return ( <NCard {...this.attrs} {...this.props}> {{ default: () => getSlotsDom(this.defaultSlot), cover: () => getSlotsDom(this.coverSlot), header: () => getSlotsDom(this.headerSlot), "header-extra": () => getSlotsDom(this.headerExtraSlot), foot: () => getSlotsDom(this.footSlot), action: () => getSlotsDom(this.actionSlot), }} </NCard> ); } }
4. 具体使用
import { defineComponent } from "vue";
import { ButtonFactory } from "./factory/ButtonFactory";
import { ButtonGroupFactory } from "./factory/ButtonGroupFactory";
import { AvatarFactory } from "./factory/AvatarFactory";
import { AvatarGroupFactory } from "./factory/AvatarGroupFactory";
import type { AvatarGroupOption } from "naive-ui";
import { CardFactory } from "./factory/CardFactory";
export default defineComponent({
name: "App",
setup() {
return () =>
new CardFactory()
.setHeader(new AvatarFactory().setDefault("张三").create())
.setHeaderExtra(
new AvatarGroupFactory()
.setProps({
options: [
{
name: "张三",
src: "https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg",
},
{
name: "李四",
src: "https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg",
},
{
name: "王五",
src: "https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg",
},
{
name: "赵六",
src: "https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg",
},
{
name: "孙七",
src: "https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg",
},
] as unknown as Array<AvatarGroupOption>,
max: 3,
})
.setAvatar((v) =>
new AvatarFactory().setProps({ src: v?.option.src }).create()
)
.setRest((v) =>
new AvatarFactory().setDefault("+" + v?.rest).create()
)
.create()
)
.setAction([
new ButtonFactory().setDefault("btn1").create(),
new ButtonFactory().setDefault("btn2").create(),
])
.setDefault([
new ButtonGroupFactory()
.setDefault(() => [
new ButtonFactory().setDefault("btn1").create(),
new ButtonFactory().setDefault("btn2").create(),
new ButtonFactory().setDefault("btn3").create(),
])
.setProps({
vertical: true,
})
.create(),
])
.create();
},
});
实际运行效果:
5. 总结
通过这次从鸿蒙 ArkUI 到 Vue 的声明式 UI 探索,我深刻体会到声明式开发的魅力所在。无论是 ArkUI 的简洁优雅,还是 Flutter 的强大灵活,亦或是 Vue 的轻量易用,它们都在用不同的方式诠释着“声明式”这一核心理念。而这次实践,不仅让我对 Vue 的能力有了新的认识,也让我意识到,前端开发的边界远比我们想象的更加广阔。
当然,这个小玩具只是一个起点,未来还有很多可以优化的地方,比如性能优化、功能扩展,甚至是跨框架的兼容性探索。如果你也对声明式 UI 感兴趣,不妨动手试试,或许你会有更多有趣的发现!
最后,感谢你阅读这篇博文,希望我的分享能为你带来一些启发。如果你有任何想法或建议,欢迎在评论区交流讨论。让我们一起探索前端开发的无限可能吧!🚀
完整示例 希望这个封装思路能够帮助到大家,也可以动动小手指,给作者点个小星星~