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

林明道發表於2019-02-18

基於svg寫了一個塗鴉元件,說專案之前先附上幾張效果圖:

專案地址:https://github.com/linmingdao/SVGraffiti

效果預覽:

image
image
image

功能演示:

image

由於篇幅問題,本文先總體介紹一下專案的大概情況,重點介紹一下元件間的通訊方式。

一、專案說明

image

該專案是基於webpack@3.x.x構建的多頁應用,使用ES6開發,以元件的方式組織程式碼。 git clone專案後(文末附上該專案github倉庫地址),npm i安裝相關依賴,npm run dev執行專案,預設會開啟應用的首頁,也就是上面的效果預覽對應的介面。開發過程會單獨地為一些功能編寫一些測試程式碼,所以該專案提供了不同的頁面對應於不同的功能,比如:

color picker元件測試頁:

image

元件訊息通訊框架測試頁:

image

svg底層繪製api測試頁:

image

二、元件間通訊

image

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”等等與本專案相關的文章,寫得不好求親噴。

專案地址:https://github.com/linmingdao/SVGraffiti

感興趣的同學們歡迎star一起交流。

相關文章