likes
comments
collection
share

Vue3一种仿有赞微页面的实现方案

作者站长头像
站长
· 阅读数 10

前言

最近答应教弟弟用Vue3+ts写业务代码,拿商城练手。 牵涉到类似有赞微页面的功能,随手把实现方案分享一下。 demo

需求

设计功能组件,按需布局,可控参数,多端复用。下图 Vue3一种仿有赞微页面的实现方案

分析

图中视图区域分WidgetsPageAction。 分别为组件列表、渲染区域、组件配置。 用户根据需要拖拽左侧Widget至Page区域渲染,右侧显示组件配置项。 Action区域参数变动,Page区域渲染

实现方案

根据图片视图布局分析,Layout需要暴露Api挂载Widget供用户选择。 考虑到维护和扩展,Layout只负责Widget挂载、视图渲染,不做其它扩展功能。 单Widget包含Page区域和Action区域渲染,并联动。

type WidgetNode = ReturnType<typeof defineComponent> | VNode | Component;

interface Widget {
  title: string;
  key: string;
  component: WidgetNode,
  data?: Object;
}

interface PageWidget extends Widget {
  id: number;
}

export default defineComponent({
  name: 'LayoutCore'
  
  setup () {
    // 左侧列表
    const asideWidgets = ref<Widget[]>([]);
    // Page
    const pageWidgetsRef = ref<PageWidget[]>([]);
    // Action
    const actionRender = ref<WidgetNode>();
  }
  
  render () {
    return (
      <div class='layout'>
        <div>
          // 渲染asideWidgets
        </div>
        
        <div>
          // 渲染pageWidgetsRef
        </div>
        
        <div>
          // 渲染actionRender
        </div>
      </div>
    )
  }
})

考虑到WidgetAction部分跟Page区域牵涉到数据联动,首先想到单Widget渲染区域跟Action区域共用一个VNode实例。省去维护数据流操作。

// Widget结构
interface TitleTextProps {
  title: String;
}

const titleTextProps = {
  data: Object as PropType<TitleTextProps>
}

export default defineComponent({
  name: 'TitleText',

  props: titleTextProps,
  
  setup () {
    const model = ref({
      title
    });
    const modelUnrf= unref(model)
    
    // 焦点id
    const currentPageWidgetId = ref<number>();
    
    function renderAction () {
      return (
        // 渲染Action
        ...
        <input v-model:value={modelUnrf.title} />
      )
    }
    
    // 这里返回model, renderAction
    return {
      model,
      renderAction
    }
  },
  
  render () {
    const {
      model
    } = this
    return (
      // 渲染模板
      ...
      <div>
        {model.title}
      </div>
    )
  }
})

这里Widget暴露modelrenderAction,供Layout渲染Page区域和Action区域。

用户选中拖拽左侧Widgets区域组件至Page区域,Layout获取当前Widget push到pageWidgetsRef

 // 添加Page widget
  function handleAddPageWidget (widget: AsideWidget<any>) {
    // 生成唯一id
    const id = generatorPageWidgetId(pageWidgetsRef.value);
    // 插入渲染区域数据
    pageWidgetsRef.value.push({
      ...widget,
      id
    });

    // 设置焦点id
    handleSetCurrentPageId(id);
  }

Page区域渲染Widget

interface WidgetsRef {
  [key: number]: Ref<WidgetNode>;
}

...
setup () {
  ...
  const widgetsRef = ref<WidgetsRef>({})
  
  // 设置page widgets ref用于获取实体
  // 对应关系 id => widget
  function handleSetRefs (node: any, id: number) {
    widgetsRefs.value[id] = node;
  }
  
  return {
    pageWidgets: pageWidgetsRef
  }
},

render () {
  const {
    pageWidgets
  } = this

  return (
    ...
    {pageWidgets.map((widget) => {
      return (
        <widget.component {...{
          id: widget.id,
          widgetKey: widget.key,
          // 默认数据(记得深拷贝)
          data: _.cloneDeep(widget.data) || {},
        }} ref={(e) => handleSetRefs(e, widget.id)} />
      )
    })}
    ...
  )
}

渲染Page区域时我们拿到当前Widget的句柄ref, 通过handleSetRefs(e, widget.id)保存当前组件渲染实例。 我们拿到焦点id, 通过currentPageWidgetId获取当前Page区域焦点Widgetref实例句柄渲染renderAction:

const renderAction = computed<WidgetNode>(() => {
  return pageWidgetsRef.value[currentPageWidgetId].renderAction
})

...
<renderAction />
...

上边只是简单原理实现,主要功能在ref={(e) => handleSetRefs(e, widget.id)}获取渲染Widget的实例句柄,获取暴露的renderAction渲染。

上述的Widget都包含渲染视图和renderAction, 有种情况,Page区域不需要渲染,只渲染renderAction。这时候拿不到Widget实例,解决方案也很简单。单独维护一份列表隐式渲染获取Widget实例句柄。

附上github free-core