[Architect (Part 23)] Rendering of canvas area components developed by editors

Continue to create, accelerate growth! This is the third day of my participation in the "Nuggets Daily New Plan · June Update Challenge", click to view the details of the event

data structure

component data structure

  • props : Properties of the component, including style properties and some other properties, such as url, actionetc.
  • id : The idunique identifier of the component, uuidgenerated .
  • name : the name of the component, the :isproperty

Editor data structure

  • components : List of components, which components are added to the current canvas.
  • currentElement : The active component, representing the component currently being edited.

Other knowledge points

  • Modulevuex : Provide a type for the module, the first parameter is the type of the current module, and the second parameter is storethe type of the entire .
import type { Module } from 'vuex';
import type { GlobalStore } from './index';
import { v4 as uuidv4 } from 'uuid';

// 组件数据结构
export interface ComponentData {
  // 这个元素的属性
  props: { [key: string]: unknown };
  // id, uuid  v4 生成
  id: string;
  // 业务组件库的名称 l-text , l-image 等,动态组件渲染的组件名称。
  name: string;
}

// 编辑器数据结构
export interface EditorStore {
  // 供中间编辑器渲染的数据
  components: ComponentData[];
  // 当前编辑的是哪一个元素 , uuid
  currentElement: string;
}

// 测试数据
const testComponents: ComponentData[] = [
  {
    id: uuidv4(),
    name: 'l-text',
    props: {
      text: 'hello',
      fontSize: '20px',
      tag: 'div',
    },
  },
  {
    id: uuidv4(),
    name: 'l-text',
    props: {
      text: 'hello2',
      fontSize: '14px',
      tag: 'div',
      color: 'red',
    },
  },
  {
    id: uuidv4(),
    name: 'l-text',
    props: {
      text: 'hello3',
      tag: 'div',
      fontSize: '12px',
      fontWeight: '800',
      actionType: 'url',
      url: 'http://www.baidu.com',
    },
  },
];

const editorStore: Module<EditorStore, GlobalStore> = {
  state: {
    // 组件列表
    components: testComponents,
    // 当前操作的组件
    currentElement: '',
  },
};

export default editorStore;
复制代码

Basic layout

Use ant-design-vuefor basic layout, including header, component list area, canvas component editing area, component property editing area.

<template>
  <div class="editor"
       id="editor-layout-main">
    <!-- header -->
    <a-layout :style="{ background: '#fff' }">
      <a-layout-header class="header">
        <div class="page-title"
             :style="{ color: '#fff' }">
          慕课乐高
        </div>
      </a-layout-header>
    </a-layout>
    <a-layout>
      <!-- 左侧组件列表 -->
      <a-layout-sider width="300"
                      style="background:yellow">
        <div class="sidebar-container">
          组件列表
        </div>
      </a-layout-sider>
      <!-- 中间画布编辑区域 -->
      <a-layout style="padding:0 24px 24px">
        <a-layout-content class="preview-container">
          <p>画布区域</p>
          <!-- 组件列表 -->
          <div class="preview-list"
               id="canvas-area">
            <!-- 使用动态组件进行渲染 -->
            <component v-for="component in components"
                       :key="component.id"
                       :is="component.name"
                       v-bind="component.props"></component>
          </div>
        </a-layout-content>
      </a-layout>
      <!-- 右侧组件属性编辑 -->
      <a-layout-sider width="300"
                      style="background:purple"
                      class="setting-container">
        组件属性
      </a-layout-sider>
    </a-layout>
  </div>
</template>

<script  lang="ts">
import { computed, defineComponent } from 'vue';
import { useStore } from 'vuex';
import { GlobalStore } from '../store/index'
import LText from '../components/LText.vue'
export default defineComponent({
  components: {
    LText
  },
  setup() {
    // 从 store 里获取数据,使用泛型以获得类型
    const store = useStore<GlobalStore>()
    // 从 store 里回组件列表
    const components = computed(() => store.state.editor.components)
    return {
      components
    }
  }
})
</script>

<style scoped>
.editor {
  width: 100%;
  height: 100%;
}
.ant-layout-has-sider {
  height: calc(100% - 64px);
}
.preview-list {
  background: #fff;
  position: relative;
}
</style>
复制代码

Using option Apiboth and Composition API, I think this is not a good way, so I plan to use setupSyntactic sugar to rewrite, but when using dynamic components :is, if you do not register components explicitly, the final rendered result is a custom tag, rather than the components we wrote.

//  template
 <component v-for="component in components"
                       :key="component.id"
                       :is="component.name"
                       v-bind="component.props"></component>

//  使用 setup 语法糖时
<script  lang="ts" setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
import { GlobalStore } from '../store/index'
// 只是引用而没有显式的注册
import LText from '../components/LText.vue'
// 从 store 里获取数据,使用泛型以获得类型
const store = useStore<GlobalStore>()
// 从 store 里回组件列表
const components = computed(() => store.state.editor.components)
</script>
复制代码

domThe structure will render like this, which is obviously not what we want.

image.png

In this regard, the official website gives an explanation, let's take a look.

image.png

So when we use setupsyntactic sugar, there is no way to explicitly register components ( maybe I don't know the method ), so we can only use the second way to bind an imported component object, which requires more Write a mapping table of component objects and component names, and this will solve the problem.

//  template
<component v-for="component in components"
           :key="component.id"
           :is="componentMap[component.name]"
           v-bind="component.props"></component>
           
//  使用 setup 语法糖时
<script  lang="ts" setup>
//  组件实例映射表的类型
export interface ComponentMap {
  [key: string]: Component;
}
import { computed, Component } from 'vue';
import { useStore } from 'vuex';
import { GlobalStore } from '../store/index'
import LText from '../components/LText.vue'
// 从 store 里获取数据,使用泛型以获得类型
const store = useStore<GlobalStore>()
// 从 store 里回组件列表
const components = computed(() => store.state.editor.components)
//  组件实例映射表
const componentMap: ComponentMap = {
  'l-text': LText
}
</script>
复制代码

L-Text Components

Configure common default properties for components as well as l-textcomponent -specific default properties.

import { mapValues, without } from 'lodash-es';

// 通用的默认属性
export const commonDefaultProps = {
  // actions
  actionType: '',
  url: '',
  // size
  height: '',
  width: '318px',
  paddingLeft: '0px',
  paddingRight: '0px',
  paddingTop: '0px',
  paddingBottom: '0px',
  // border type
  borderStyle: 'none',
  borderColor: '#000',
  borderWidth: '0',
  borderRadius: '0',
  // shadow and opacity
  boxShadow: '0 0 0 #000000',
  opacity: '1',
  // position and x,y
  position: 'absolute',
  top: '0',
  left: '0',
  right: '0',
  bottom: '0',
};

// l-text 组件特有默认属性
export const textDefaultProps = {
  // basic props - font styles
  text: '正文内容',
  fontSize: '14px',
  fontFamily: '',
  fontWeight: 'normal',
  fontStyle: 'normal',
  textDecoration: 'none',
  lineHeight: '1',
  textAlign: 'left',
  color: '#000000',
  backgroundColor: '',
  ...commonDefaultProps,
};

// 排除非样式属性
export const textStylePropNames = without(
  Object.keys(textDefaultProps),
  'actionType',
  'url',
  'text',
);

// 转换成组件的props属性
export const transformToComponentProps = <T extends { [key: string]: any }>(
  props: T,
) => {
  return mapValues(props, (item) => {
    return {
      type: item.constructor,
      default: item,
    };
  });
};

复制代码

Wraps one hooks, picks out style properties, and returns a click event handler.

import { computed } from 'vue';
import { pick } from 'lodash-es';

// 使用 lodash 的 pick 方法挑选出样式属性,并返回一个点击事件处理函数
const useComponentCommon = <T extends { [key: string]: any }>(
  props: T,
  picks: string[],
) => {
  const styleProps = computed(() => pick(props, picks));
  const handleClick = () => {
    if (props.actionType === 'url' && props.url) {
      window.location.href = props.url;
    }
  };
  return {
    styleProps,
    handleClick,
  };
};

export default useComponentCommon;
复制代码
  • I was lazy here and didn't use setupsyntactic sugar to rewrite it.
  • :isThe bound tagproperty is the rendered label type.
<template>
  <!-- 使用动态组件进行渲染 -->
  <component :is="tag"
             :style="styleProps"
             @click="handleClick"
             class="l-text-component">
    {{ text }}
  </component>
</template>

<script  lang="ts">

import { defineComponent } from 'vue';
import { transformToComponentProps, textDefaultProps, textStylePropNames } from '../defaultProps'

import useComponentCommon from '../hooks/useComponentCommon'
const defaultProps = transformToComponentProps(textDefaultProps)
export default defineComponent({
  // 合并 props
  props: {
    tag: {
      type: String,
      default: 'div'
    },
    ...defaultProps
  },
  setup(props) {
    // 获取到样式属性
    const { styleProps, handleClick } = useComponentCommon(props, textStylePropNames)
    return {
      styleProps,
      handleClick
    }
  }
})
</script>

<style scoped>
h2.l-text-component,
p.l-text-component {
  margin-bottom: 0;
}

button.l-text-component {
  padding: 5px 10px;
  cursor: pointer;
}

.l-text-component {
  box-sizing: border-box;
  white-space: pre-wrap;
  position: relative !important;
}
</style>
复制代码

The final result is as follows, the l-textcomponent is rendered to the canvas area, which is a bit ugly, but the focus is not on the style, it is not important.

image.png

Guess you like

Origin juejin.im/post/7102211394224783373