【架构师(第二十三篇)】编辑器开发之画布区域组件的渲染

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

数据结构

组件数据结构

  • props:组件的属性,包括样式属性和一些其他属性,比如 urlaction 等。
  • id:组件的 id,唯一标识,使用第三方库 uuid 生成。
  • name:组件的名称,用于动态组件渲染的 :is 属性

编辑器数据结构

  • components:组件列表,当前画布添加了哪些组件。
  • currentElement:激活的组件,表示当前正在编辑的组件。

其他知识点

  • Module: 给 vuex 模块化提供类型,第一个参数是当前模块的类型,第二个参数是整个 store 的类型。
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;
复制代码

基础布局

使用 ant-design-vue 进行基础的布局,包含 header,组件列表区域,画布组件编辑区域,组件属性编辑区域。

<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>
复制代码

课程里同时使用 option ApiComposition API ,我认为这不是一个好的方式,所以我打算使用 setup 语法糖进行改写,但是在使用动态组件 :is 时,如果不显式的注册组件,最后渲染出来的结果就是一个自定义标签,而不是我们书写的组件。

//  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>
复制代码

dom 结构会渲染成这样,这显然不是我们想要的。

image.png

对此,官网给出了解释,我们来看一下。

image.png

所以当我们使用 setup 语法糖的时候,就没有办法显式的注册组件(也可能是我不知道方法),那么就只能用第二种方式,绑定一个导入的组件对象,这样的话就要多写一个组件对象和组件名称的映射表,这样就解决问题了。

//  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 组件

配置组件的通用默认属性以及 l-text 组件的特有默认属性。

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,
    };
  });
};

复制代码

封装一个 hooks ,挑选出样式属性,并返回一个点击事件处理函数。

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;
复制代码
  • 这里偷了个懒,没有去使用 setup 语法糖进行改写。
  • :is 绑定的 tag 属性是渲染后的标签类型。
<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>
复制代码

最终结果如下,l-text 组件就渲染到画布区域了,有点丑,但是重点又不是样式,不重要了。

image.png

猜你喜欢

转载自juejin.im/post/7102211394224783373