首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
【架构师(第一篇)】整体需求分析和架构设计
2
【架构师(第二篇)】脚手架架构设计和框架搭建
3
【架构师(第三篇)】脚手架开发之掌握Lerna操作流程
4
【架构师(第四篇)】脚手架开发之Lerna源码分析
5
【架构师(第五篇)】脚手架之import-local执行流程及简历设计
6
【架构师(第六篇)】脚手架之需求分析和架构设计
7
【架构师(第七篇)】脚手架之准备阶段编写
8
【架构师(第八篇)】脚手架之 commander 框架使用方法
9
【架构师(第九篇)】如何让 Node 环境支持 ES Module
10
【架构师(第十篇)】脚手架之注册命令及架构优化
11
【架构师(第十一篇)】脚手架之命令注册和执行过程开发
12
【架构师(第十二篇)】脚手架之命令行交互工具 inquirer.js 使用方法
13
【架构师(第十三篇)】脚手架之创建项目准备阶段开发
14
【架构师(第十四篇)】脚手架之 egg.js 和 mongodb 的使用
15
【架构师(第十五篇)】脚手架之创建项目模板开发
16
【架构师(第十六篇)】脚手架之创建项目模板的下载与更新
17
【架构师(第十七篇)】脚手架之 ejs 和 glob 的使用
18
【架构师(第十八篇)】脚手架之项目模板的安装
19
【架构师(第十九篇)】脚手架之组件库模板开发
20
【架构师(第二十篇)】脚手架之自定义模板及第一阶段总结
21
【架构师(第二十一篇)】编辑器开发之需求分析和架构设计
22
【架构师(第二十二篇)】编辑器开发之项目整体搭建
23
【架构师(第二十三篇)】编辑器开发之画布区域组件的渲染
24
【架构师(第二十四篇)】编辑器开发之添加模版到画布
25
【架构师(第二十五篇)】编辑器开发之属性编辑区域表单渲染
26
【架构师(第二十六篇)】编辑器开发之属性编辑同步渲染
27
【架构师(第二十七篇)】前端单元测试框架 Jest 基础知识入门
28
【架构师(第二十八篇)】 测试工具 Vue-Test-Utils 基础语法
29
【架构师(第二十九篇)】Vue-Test-Utils 触发事件和异步请求
30
【架构师(第三十篇)】Vue-Test-Utils 全局组件和第三方库 vuex | vue-router
31
【架构师(第三十一篇)】前端测试之 TDD 的开发方式
32
【架构师(第三十二篇)】 通用上传组件开发及测试用例
33
【架构师(第三十三篇)】 Vue 中的实例及本地图片预览
34
【架构师(第三十四篇)】 业务组件库开发之 vue3 的插件系统
35
【架构师(第三十五篇)】 业务组件库开发之使用 Rollup 进行打包
36
【架构师(第三十六篇)】 业务组件库开发之发布到 NPM
37
【架构师(第三十七篇)】 服务端开发之后端框架与数据库技术选型
38
【架构师(第三十八篇)】 服务端开发之本地安装最新版 MySQL 数据库
39
【架构师(第三十九篇)】 服务端开发之连接 MySQL 数据库
40
【架构师(第四十篇)】 服务端开发之连接 Mongodb 数据库
41
【架构师(第四十一篇)】 服务端开发之安装并连接 Redis数据库
42
【架构师(第四十二篇)】 服务端开发之常用的登录鉴权方式
43
【架构师(第四十三篇)】 服务端开发之单元测试和接口测试
44
【架构师(第四十四篇)】 服务端开发之 pm2 和 nginx 介绍
45
【架构师(第四十五篇)】 服务端开发之认识 Github actions
46
【架构师(第四十六篇)】 服务端开发之安装 Docker
47
【架构师(第四十七篇)】 服务端开发之认识 Docker
48
【架构师(第四十八篇)】 服务端开发之 Dockerfile
49
【架构师(第四十九篇)】 服务端开发之认识 Docker-compose
50
【架构师(第五十篇)】 服务端开发之自动发布到测试机
清单首页架构文章详情

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


数据结构

组件数据结构

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

编辑器数据结构

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

其他知识点

  • Module: 给 vuex 模块化提供类型,第一个参数是当前模块的类型,第二个参数是整个 store 的类型。
代码语言:javascript
复制
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,组件列表区域,画布组件编辑区域,组件属性编辑区域。

代码语言:javascript
复制
<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 时,如果不显式的注册组件,最后渲染出来的结果就是一个自定义标签,而不是我们书写的组件。

代码语言:javascript
复制
//  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 结构会渲染成这样,这显然不是我们想要的。

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

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

代码语言:javascript
复制
//  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 组件的特有默认属性。

代码语言:javascript
复制
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 ,挑选出样式属性,并返回一个点击事件处理函数。

代码语言:javascript
复制
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' &amp;&amp; props.url) {
      window.location.href = props.url;
    }
  };
  return {
    styleProps,
    handleClick,
  };
};

export default useComponentCommon;
  • 这里偷了个懒,没有去使用 setup 语法糖进行改写。
  • :is 绑定的 tag 属性是渲染后的标签类型。
代码语言:javascript
复制
<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 组件就渲染到画布区域了,有点丑,但是重点又不是样式,不重要了。

下一篇
举报
领券