资讯专栏INFORMATION COLUMN

设计一个基于svg的涂鸦组件(一)

cartoon / 3240人阅读

摘要:基于写了一个涂鸦组件,说项目之前先附上几张效果图项目地址由于篇幅问题,本文先总体介绍一下项目的大概情况,重点介绍一下组件间的通信方式。一项目说明该项目是基于构建的多页应用,使用开发,以组件的方式组织代码。

基于svg写了一个涂鸦组件,说项目之前先附上几张效果图:

项目地址:SVGraffiti

由于篇幅问题,本文先总体介绍一下项目的大概情况,重点介绍一下组件间的通信方式。

一、项目说明

该项目是基于webpack@3.x.x构建的多页应用,使用ES6开发,以组件的方式组织代码。
git clone项目后(文末附上该项目github仓库地址),npm i安装相关依赖,npm run dev运行项目,默认会打开应用的首页,也就是上面的效果预览对应的界面。开发过程会多带带地为一些功能编写一些测试代码,所以该项目提供了不同的页面对应于不同的功能,比如:

color picker组件测试页:

组件消息通信框架测试页:

svg底层绘制api测试页:

二、组件间通信

1、组件间为了实现最大程度的封装与解耦,不直接进行互相通信,而是通过“消息订阅/发布管理中心”(以下简称“消息中心”)进行间接通信。组件通过声明自己为不同的角色从而拥有对应的通信能力:

组件声明为订阅者(Subscriber)并通过@Topics注解的形式从“消息中心”订阅自己感兴趣的主题消息,对应的消息会通过notify接口告知组件;

组件声明为发布者(Publisher),可以通过Publisher角色注入的publish方法发布主题消息;

组件声明为发布/订阅者(SubScatterer),同时拥有订阅者和发布者的通信能力。

这里以项目中的中间区域的画板组件为例,因为画板组件只是接收Toolbar组件发来的切换绘制能力、清空绘制内容以及Settings组件发来的设置绘制参数信息,所以该组件只是一个消息订阅者角色,编码设计如下:

首先导入对应的角色类:

import Subscriber from "../../supports/pubsub/base/subscriber";
import Topics from "../../supports/pubsub/base/topics";

编写对应的组件:

// 通过@Topics的形式订阅感兴趣的消息类型
@Topics(["function", "resident_function", "set_preference"])
export default class Sketchpad extends Subscriber {
    // 构造器
    constructor(sketchpad) {
        super();
        this.sketchpad = sketchpad;
        // ...
    }
    
    /**
     * 该接口由【PubSub消息管理中心】负责调用,画板组件在此接口处理接收到的消息类型
     * 1、处理Toolbar组件发送的 “切换画板绘制状态” ,对应的消息类型为:“function”
     * 2、处理Toolbar组件发送的 “清空绘制内容” ,对应的消息类型为:“resident_function”
     * 3、处理Settings组件发送的 “设置画板绘制参数” ,对应的消息类型为:“set_preference”
     * @param {String} topic 消息主题标识
     * @param {Object} entity 消息实体对象
     */
    notify(topic, entity) {
       // 在此处理接收到的消息
    }
}

注:@Topics是静态的,若有些主题是需要运行时订阅也可以调用Subscriber角色提供的subscribe方法动态订阅消息。

2、PubSub(消息订阅/发布管理中心)的实现
既然是底层通用能力就一定要实现的不带任何具体的业务,无论是在命名规范还是编码实现上都要保证它是一个通用模块

PubSub的实现:

/**
 * 主题订阅发布中心
 */
export default class PubSub {

    // 缓存主题和主题的订阅者列表
    static topics = {};

    /**
     * 发布主题消息
     * @param {String} topic 主题
     * @param {*} entity 消息体 
     */
    static publish(topic, entity) {
        if (!PubSub.topics[topic]) return;

        // 获取该主题的订阅者列表
        const subscribers = PubSub.topics[topic];

        // 向所有该主题的订阅者发送主题消息
        for (let subscriber of subscribers) {
            subscriber.notify && subscriber.notify(topic, entity);
        }
    }

    /**
     * 一次登记一个主题
     * @param {String} topic 
     */
    static registerTopic(topic) {
        const topics = PubSub["topics"];
        !topics[topic] && (topics[topic] = []);
    }

    /**
     * 同时登记多个主题
     * @param {Array} topics 
     */
    static registerTopics(topics = []) {
        topics.forEach(topic => {
            this.registerTopic(topic);
        });
    }

    /**
     * 添加主题订阅者
     * @param {String} topic 主题
     * @param {Object} subscriber 实现了notify接口的订阅者
     */
    static addSubscriber(topic, subscriber) {
        const topics = PubSub["topics"];
        !topics[topic] && (topics[topic] = []);

        // 将该主题的订阅者登记到对应的主题
        topics[topic].push(subscriber);
    }
    
    /**
     * 删除对应的订阅者
     * @param subscriber 
     */
    static removeSubscriber(subscriber) {
        const subs = [];
        // 遍历所有主题下的订阅者列表,将对应订阅者删除
        const topics = PubSub.topics;
        Object.keys(topics).forEach(topicName => {
            const topic = topics[topicName];
            for (let i = 0; i < topic.length; ++i) {
                if (topic[i] === subscriber) {
                    subs.push(topics[topic].splice(i, 1));
                    break;
                }
            }
        });
        return subs;
    }
}

Subscriber的实现:

import PubSub from "../pubsub";

const addSubscribe = (topics = [], context) => {
    topics.forEach(topic => {
        PubSub.addSubscriber(topic, context);
    });
}

/**
 * 主题订阅者
 */
export default class Subscriber {
    constructor() {
        addSubscribe(this.__proto__.constructor.topics, this);
    }

    subscribe(topic) {
        PubSub.addSubscriber(topic, this);
    }
}

为了方便订阅主题,再提供一个@Topics注解:

import PubSub from "../pubsub";

/**
 * 订阅者主题装饰器
 * @param {Array} topics
 */
export default function Topics(topics) {
    return target => {
        target.topics = topics;
        PubSub.registerTopics(topics);
    }
}

Publisher的实现:

import PubSub from "../pubsub";

/**
 * 主题消息发布者
 */
export default class Publisher {
    publish(topic, entity) {
        PubSub.publish(topic, entity);
    }
}

SubScatterer的实现:

import PubSub from "../pubsub";
import Subscriber from "./subscriber";

/**
 * 主题订阅者 and 主题消息发布者
 */
export default class SubScatterer extends Subscriber {
    publish(topic, entity) {
        PubSub.publish(topic, entity);
    }
}

本篇介绍了项目的大概情况,重点分析了如何以发布/订阅的形式实现组件间的通信,接下来还会抽时间写几个篇分别介绍“svg底层绘制能力的封装”、“画板不同绘制状态的实现与管理”、“如何开发一个通用的ColorPicker”等等与本项目相关的文章,写得不好求亲喷。

项目github地址:SVGraffiti

感兴趣的同学们欢迎star一起交流。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94972.html

相关文章

  • vue组件:canvas实现图片涂鸦功能

    摘要:方案背景需求需要对图片进行标注,导出图片。对应方案用实现涂鸦圆形矩形的绘制,最终生成图片编码用于上传大量图片批量上传很耗时间,为了提高用户体验,改为只实现圆形矩形绘制,最终保存成坐标,下次显示时根据坐标再绘制。 方案背景 需求 需要对图片进行标注,导出图片。 需要标注N多图片最后同时保存。 需要根据多边形区域数据(区域、颜色、名称)标注。 对应方案 用canvas实现涂鸦、圆形、...

    roland_reed 评论0 收藏0
  • STM32CubeMX学习教程之硬件I2C读取光照度

    摘要:使用库读写环境光照度传感器本文将教大家如何快速使用库读取光照度数据。五实验样机测试展示通过之前配置好的面板,通过涂鸦智能进行配网实时采集光照度传感器的数据。 使用STM32 HAL库读写环境光照度传感器(BH1750) 本文将教大家如何快速使用STM32HAL库读取光照度数据。 实现功能:通...

    tinylcy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<