设计一个基于svg的涂鸦组件(一)
基于svg写了一个涂鸦组件,说项目之前先附上几张效果图:
项目地址:SVGraffiti
由于篇幅问题,本文先总体介绍一下项目的大概情况,重点介绍一下组件间的通信方式。
一、项目说明
该项目是基于[email protected]构建的多页应用,使用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一起交流。