設計一個基於svg的塗鴉組件(一)

基於svg寫了一個塗鴉組件,說項目以前先附上幾張效果圖:webpack

項目地址:SVGraffitigit

curve

rect

polygon

因爲篇幅問題,本文先整體介紹一下項目的大概狀況,重點介紹一下組件間的通訊方式。github

1、項目說明
目錄結構說明web

該項目是基於webpack@3.x.x構建的多頁應用,使用ES6開發,以組件的方式組織代碼。
git clone項目後(文末附上該項目github倉庫地址),npm i安裝相關依賴,npm run dev運行項目,默認會打開應用的首頁,也就是上面的效果預覽對應的界面。開發過程會單獨地爲一些功能編寫一些測試代碼,因此該項目提供了不一樣的頁面對應於不一樣的功能,好比:npm

color picker組件測試頁:
圖片描述api

組件消息通訊框架測試頁:
圖片描述緩存

svg底層繪製api測試頁:
圖片描述框架

2、組件間通訊
組件間通訊ide

一、組件間爲了實現最大程度的封裝與解耦,不直接進行互相通訊,而是經過「消息訂閱/發佈管理中心」(如下簡稱「消息中心」)進行間接通訊。組件經過聲明本身爲不一樣的角色從而擁有對應的通訊能力:svg

  • 組件聲明爲訂閱者(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消息管理中心】負責調用,畫板組件在此接口處理接收到的消息類型
     * 一、處理Toolbar組件發送的 「切換畫板繪製狀態」 ,對應的消息類型爲:「function」
     * 二、處理Toolbar組件發送的 「清空繪製內容」 ,對應的消息類型爲:「resident_function」
     * 三、處理Settings組件發送的 「設置畫板繪製參數」 ,對應的消息類型爲:「set_preference」
     * @param {String} topic 消息主題標識
     * @param {Object} entity 消息實體對象
     */
    notify(topic, entity) {
       // 在此處理接收到的消息
    }
}

注:@Topics是靜態的,如有些主題是須要運行時訂閱也能夠調用Subscriber角色提供的subscribe方法動態訂閱消息。

二、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一塊兒交流。

相關文章
相關標籤/搜索