当 Vue 遇上鸿蒙:我的声明式 UI 实验笔记

280 阅读4分钟

当 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. 封装工厂类

核心思想就是实现一个类进行链式调用即可。下面举俩个例子,完整示例请看文章末尾。

  1. 定义一个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>
        );
      }
    }
    
    
    
  2. 定义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();
  },
});

实际运行效果:

微信图片_20250121161652.png

5. 总结

通过这次从鸿蒙 ArkUI 到 Vue 的声明式 UI 探索,我深刻体会到声明式开发的魅力所在。无论是 ArkUI 的简洁优雅,还是 Flutter 的强大灵活,亦或是 Vue 的轻量易用,它们都在用不同的方式诠释着“声明式”这一核心理念。而这次实践,不仅让我对 Vue 的能力有了新的认识,也让我意识到,前端开发的边界远比我们想象的更加广阔。

当然,这个小玩具只是一个起点,未来还有很多可以优化的地方,比如性能优化、功能扩展,甚至是跨框架的兼容性探索。如果你也对声明式 UI 感兴趣,不妨动手试试,或许你会有更多有趣的发现!

最后,感谢你阅读这篇博文,希望我的分享能为你带来一些启发。如果你有任何想法或建议,欢迎在评论区交流讨论。让我们一起探索前端开发的无限可能吧!🚀

完整示例 希望这个封装思路能够帮助到大家,也可以动动小手指,给作者点个小星星~