面向对象在前端的应用最佳实践(实战)

如何写出高质量的前端代码》学习笔记

低代码编辑器实现教程

1. 项目概述

1.1 需求分析

这是一个简单的低代码大屏编辑器项目,主要功能包括:

  • 物料区:展示预制的图形元素(圆形、矩形、三角形等),支持拖拽图形到画布,每个图形都有默认的样式配置
  • 画布区:使用 canvas 作为绘制容器,支持选中图形(点击选中),支持移动图形(拖拽移动),支持删除图形(键盘删除),选中图形时显示高亮边框
  • 属性配置区:展示选中图形的属性,支持实时编辑属性(位置、大小、颜色等),属性修改后画布实时更新
  • 数据存储:画布基础信息:宽度、高度、背景色,节点列表:存储所有图形的信息

1.2 数据结构设计

画布数据采用 JSON 格式存储:

js 复制代码
const graphData = {
    width: 300, // 画布宽度
    height: 300, // 画布高度 
    backgroundColor: '#fff', // 画布背景色
    nodes: [
        {
            id: 'node1', // 节点唯一标识
            type: 'circle', // 节点类型:circle/rect/triangle
            attributes: {
                x: 10, // 左上角x坐标
                y: 10, // 左上角y坐标
                radius: 20, // 圆形半径
                // 其他属性...
            }
        },
        // 更多节点...
    ]
}

2. 核心类设计

2.1 Node 基类

Node 类是所有图形节点的基类,定义了共同的属性和方法:

  • 定义所有图形共有的属性(id、type等)

  • 提供属性操作方法(get/set)

  • 定义图形操作接口(由子类实现):

    • 判断点是否在图形内
    • 绘制图形
    • 获取外围盒子
    • 绘制选中边框
    • 移动图形
js 复制代码
class Node {
    constructor({graphView, dataModel, id, attributes={}}) {
        this.graphView = graphView;  // 画布引用
        this.id = id;
        this.type = 'node';   // 子类需重写类型
        this.attributes = attributes;
    }
    
    // 属性操作
    getAttributes() { /* ... */ }
    getAttribute(key) { /* ... */ }
    setAttributes(attributes) { /* ... */ }
    setAttribute(key, value) { /* ... */ }
    
    // 图形操作(子类实现)
    containsPoint(pointX, pointY) { /* 判断点是否在图形内 */ }
    draw() { /* 绘制图形 */ }
    getBoundingBox() { /* 获取外围盒子 */ } 
    drawBoundingBox() { /* 绘制选中边框 */ }
    move(dx, dy) { /* 移动图形 */ }
}

2.2 具体图形节点类

以圆形节点为例:

对每个子类,通过多态实现了containsPointdrawgetBoundingBoxmove方法的重写。

js 复制代码
class CircleNode extends Node {
    constructor(graphView, id, {x, y, radius}) {
        super(graphView, id, {x, y, radius});
        this.type = 'circle';
    }

    // 判断点击是否在圆内
    containsPoint(pointX, pointY) {
        const {x, y, radius} = this.attributes;
        const distance = Math.sqrt(
            Math.pow(pointX - x, 2) + 
            Math.pow(pointY - y, 2)
        );
        return distance <= radius;
    }

    // 绘制圆形
    draw() {
        const ctx = this.graphView.getCtx();
        const {x, y, radius} = this.attributes;
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2);
        ctx.fill();
        ctx.stroke();
    }
    
    // 其他方法实现...
}

2.3 画布类(GraphView)

负责 canvas DOM 操作和事件处理:

  • 创建canvas的DOM
  • 绘制编辑器的背景
  • 监听canvas上的鼠标事件,比如点击、拖动等
  • 监听键盘事件,比如支持delete或backspace删除选中的节点
js 复制代码
class GraphView {
    constructor(dom, options = {}) {
        this.dom = dom;
        this.canvas = null;
        this.ctx = null;
        this.width = options.width;
        this.height = options.height;
        this.backgroundColor = options.backgroundColor;
        this.scale = window.devicePixelRatio;
        this.observer = {};
        this.init();
    }

    init() {
        // 创建canvas
        this.canvas = document.createElement('canvas');
        this.canvas.width = this.width * this.scale;
        this.canvas.height = this.height * this.scale;
        this.dom.appendChild(this.canvas);
        
        // 初始化context
        this.ctx = this.canvas.getContext('2d');
        this.ctx.scale(this.scale, this.scale);
        
        // 绑定事件
        this.bindEvents();
    }

    bindEvents() {
        // 鼠标事件
        this.canvas.addEventListener('mousedown', this.handleMouseDown);
        this.canvas.addEventListener('mousemove', this.handleMouseMove);
        this.canvas.addEventListener('mouseup', this.handleMouseUp);
        
        // 键盘事件
        document.addEventListener('keydown', this.handleKeyDown);
    }
    
    // 其他方法...
}

2.4 数据模型类(DataModel)

DataModel应该是整个项目的核心,它负责调度各个模块,有节点列表nodes,提供方法操作节点列表,查询节点,根据id查询节点,查询整个画布JSON格式数据,初始化将JSON格式的节点数据变成图形节点。

js 复制代码
class DataModel {
    constructor(options = {}) {
        this.nodes = [];
        this.dirty = true;
        this.initNodes(options.nodes);
        this.startDirtyCheck();
    }

    // 初始化节点
    initNodes(nodesData) {
        nodesData?.forEach(data => {
            const node = this.createNode(data);
            this.nodes.push(node);
        });
    }

    // 创建节点
    createNode(data) {
        switch(data.type) {
            case 'circle':
                return new CircleNode(this.graphView, data.id, data.attributes);
            case 'rect':
                return new RectNode(this.graphView, data.id, data.attributes);
            // ...其他类型
        }
    }

    // 脏检查机制
    startDirtyCheck() {
        requestIdleCallback(() => {
            if(this.dirty) {
                this.render();
                this.dirty = false; 
            }
            this.startDirtyCheck();
        });
    }
    
    // 其他方法...
}

2.5 选择器类(Selector)

管理选中状态:

  • 添加选中的节点
  • 查询选中的节点
  • 清空当前选中节点
  • 选择事件变化后要能够通知外部监听者
js 复制代码
class Selector {
    constructor() {
        this.selectedNodes = [];
        this.observers = [];
    }

    add(node) {
        if(!this.selectedNodes.includes(node)) {
            this.selectedNodes.push(node);
            this.notifyObservers();
        }
    }

    clear() {
        this.selectedNodes = [];
        this.notifyObservers();
    }

    notifyObservers() {
        this.observers.forEach(cb => cb(this.selectedNodes));
    }
    
    // 其他方法...
}

3. 编辑器组装

将以上组件组装成完整编辑器:

  • GraphView:负责画布的绘制、事件监听等

  • DataModel:负责画布上的节点数据管理

  • Selector:负责画布上的选中节点管理

  • Node: 图形节点

    • CircleNode:圆形节点
    • RectNode:矩形节点
    • PolyNode:多边形节点
js 复制代码
function createEditor(dom, graphData = {}) {
    // 初始化组件
    const graphView = new GraphView(dom, {
        width: graphData.width || 300,
        height: graphData.height || 300,
        backgroundColor: graphData.backgroundColor || '#fff',
    });
    
    const selector = new Selector();
    const dataModel = new DataModel(graphData, graphView, selector);

    // 事件处理
    let mouseDown = false;
    
    graphView.addEventListener('mousedown', (event, point) => {
        mouseDown = true;
        const clickedNode = dataModel.getClickedNode(point);
        if (clickedNode) {
            selector.add(clickedNode);
        } else {
            selector.clear();
        }
    });

    graphView.addEventListener('mousemove', (event, point) => {
        if (mouseDown) {
            selector.getSelectedNodes().forEach(node => 
                node.move(event.movementX, event.movementY)
            );
        }
    });

    // 返回编辑器实例
    return {
        graphView,
        dataModel,
        selector,
    }
}

通过面向对象构造的类,通过面向过程方式来调度,所以编程范式并不存在谁高级谁低级的问题,每个范式都有自己的适用场景。

4. 拖动图形到画布

4.1 物料区实现

js 复制代码
<template>
  <div class="material-select-wrapper">
    <div v-for="item in materialList"
         :key="item.type"
         class="material-item"
         draggable
         @dragstart="onDragStart($event, item)">
      <img :src="item.icon" :alt="item.label">
      <span>{{ item.label }}</span>
    </div>
  </div>
</template>

<script>
export default {
    data() {
        return {
            materialList: [
                {
                    type: 'circle',
                    label: '圆形',
                    icon: 'circle.png',
                    defaultAttributes: {
                        radius: 30,
                        backgroundColor: '#ff0000'
                    }
                },
                // 其他图形配置...
            ]
        }
    },
    methods: {
        onDragStart(event, item) {
            event.dataTransfer.setData('config', JSON.stringify(item));
        }
    }
}
</script>

4.2 画布区接收拖拽

js 复制代码
<template>
  <div class="canvas-wrapper" 
       @drop="onDrop"
       @dragover.prevent>
    <canvas ref="canvas"></canvas>
  </div>
</template>

<script>
export default {
    methods: {
        onDrop(event) {
            const config = JSON.parse(
                event.dataTransfer.getData('config')
            );
            
            // 创建新节点
            const node = this.createNode(config);
            
            // 设置节点位置
            const point = this.getCanvasPoint(event);
            node.setCenter(point.x, point.y);
            
            // 添加到画布
            this.dataModel.addNode(node);
            this.selector.select(node);
        }
    }
}
</script>

5. 属性编辑实现

js 复制代码
<template>
  <div class="attributes-panel">
    <template v-if="selectedNode">
      <el-form :model="attributes">
        <!-- 通用属性 -->
        <el-form-item label="背景色">
          <el-color-picker 
            v-model="attributes.backgroundColor"
            @change="updateAttribute('backgroundColor', $event)"
          />
        </el-form-item>
        
        <!-- 特定图形属性 -->
        <template v-if="selectedNode.type === 'circle'">
          <el-form-item label="半径">
            <el-input-number
              v-model="attributes.radius"
              @change="updateAttribute('radius', $event)"
            />
          </el-form-item>
        </template>
      </el-form>
    </template>
  </div>
</template>

<script>
export default {
    data() {
        return {
            selectedNode: null,
            attributes: {}
        }
    },
    mounted() {
        this.selector.addObserver(nodes => {
            this.selectedNode = nodes[0];
            this.attributes = {...this.selectedNode?.attributes};
        });
    },
    methods: {
        updateAttribute(key, value) {
            this.selectedNode?.setAttribute(key, value);
        }
    }
}
</script>

至此,一个简单的低代码大屏编辑器就实现完成了。

6. 最佳实践总结

  1. 职责分离

    • 每个类都有明确的单一职责
    • 通过接口而非直接访问进行通信
  2. 可扩展性

    • 基类抽象共性,子类实现特性
    • 新增图形只需继承 Node 类
  3. 性能优化

    • 使用脏检查机制避免频繁重绘
    • 事件委托处理用户交互
  4. 代码复用

    • 通用逻辑在基类中实现
    • 组件间通过观察者模式解耦
  5. 面向对象的优势

    • 代码组织清晰
    • 扩展维护方便
    • 复用性好

这个项目很好地展示了面向对象在前端中的应用价值,通过合理的抽象和封装,使得代码结构清晰,易于维护和扩展。

相关推荐
撸码到无法自拔4 分钟前
React:组件、状态与事件处理的完整指南
前端·javascript·react.js·前端框架·ecmascript
高山我梦口香糖4 分钟前
[react]不能将类型“string | undefined”分配给类型“To”。 不能将类型“undefined”分配给类型“To”
前端·javascript·react.js
代码cv移动工程师8 分钟前
HTML语法规范
前端·html
Elena_Lucky_baby30 分钟前
实现路由懒加载的方式有哪些?
前端·javascript·vue.js
Domain-zhuo30 分钟前
如何利用webpack来优化前端性能?
前端·webpack·前端框架·node.js·ecmascript
理想不理想v34 分钟前
webpack如何自定义插件?示例
前端·webpack·node.js
小华同学ai1 小时前
ShowDoc:Star12.3k,福利项目,个人小团队的在线文档“简单、易用、轻量化”还专门针对API文档、技术文档做了优化
前端·程序员·github
王解1 小时前
Vue CLI 脚手架创建项目流程详解 (2)
前端·javascript·vue.js
刘大浪1 小时前
vue.js滑动到顶便锁定位置
前端·javascript·vue.js
小金刚®1 小时前
构建简洁之美:我的第一个前端页面
前端