插件开发指南

开发前准备

开发环境

  • ubuntu18.04
  • GCC 7.3.1
  • Nodejs 22.0+
  • MindStudio Insight头文件(本项目下plugin_core/include目录)

插件框架

MindStudio Insight整体采用前后端分离+模块化的设计。其中后端采用C++实现,MindStudio Insight主体加载插件,采取动态库加载符号的方式实现。因此插件应当提供以下产物:1.前端产物 2.后端动态库文件(继承MindStudio Insight的基础类)。

下图为整体MindStudio Insight 和插件的架构图

插件加载 这里面介绍几个重要的步骤和模块: 主程序:MindStudio Insight主体,负责几个重要的功能:1.网络通信 2.加载动态库,模块管理

PluginsManager: 插件管理类,负责加载管理MindStudio Insight中所有的插件的生命周期

BasePlugin: 插件基类,所有插件继承自此

LoadPlugins(): 加载插件逻辑

RegisterPlugin(): 向主程序注册插件

GetMoudleConfig(): 通知主程序前端资源文件位置

我们的开发思路也逐渐清晰, 通过继承MindStudio Insight中的插件基类, 开发属于自己的插件后端和前端。MindStudio Insight启动时会调用LoadPlugins()函数加载插件后端程序。后端程序通过RegisterPlugin()向主程序的PluginsManager注册插件基本信息。 主程序中又通过GetMoudleConfig()获取前端资源文件位置。到此,主程序已经成功加载了插件的前端和后端信息,接下来主程序负责进行网络通信(Http/websocket),并为插件提供一些基础服务(文件导入、记忆功能)。

插件开发示例

此章节会详细介绍两种通信方式插件的前后端如何搭建Demo项目,项目源码均位于本项目的Example目录下。

HTTP通信插件后端开发

  1. 创建如下目录结构:
    .
    ├── CMakeLists.txt
    ├── include
    │   ├── ApiHandler.h
    │   ├── BasePlugin.h
    │   └── PluginsManager.h
    └── src
    
  • src: 插件源码目录
  • include: 包括了MindStudio Insight插件开发所必须的头文件
  1. 在CmakeLists.txt中添加如下内容

        cmake_minimum_required(VERSION 3.20)
        set(CMAKE_CXX_STANDARD  17)
        set(CMAKE_C_STANDARD 17)
        project("HttpPluginExample" LANGUAGES CXX)
        set(CMAKE_SKIP_RPATH  true)  # 设置通过默认路径或LD_LIBRARY_PATH路径加载动态库
        add_library(${PROJECT_NAME} SHARED
                src/HttpExamplePlugin.h
                src/HttpExamplePlugin.cpp
                src/ExampleHandler.h
        )
        set(LIBRARY_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/output/${PROJECT_NAME})
        target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
    
  2. 定义插件类 创建HttpExamplePlugin.h头文件,其中增加如下内容

        #include "BasePlugin.h"
        using namespace Dic::Core;
        namespace Insight::Example{
        class HttpExamplePlugin:public BasePlugin{
        public:
            HttpExamplePlugin();
            ~HttpExamplePlugin() override = default;
            std::map<std::string, std::shared_ptr<ApiHandler>> GetAllHandlers() override;
            std::vector<std::string> GetModuleConfig() override;
        private:
            std::map<std::string, std::shared_ptr<ApiHandler>> handlers_;
        };
    
  3. 实现插件方法 上面我们定义了自己的插件类,但是还没有实现其中的方法,接下来我们将实现插件所必须的函数。其余可由开发者自行探索 新增文件HttpExamplePlugin.cpp GetModuleConfig 正如上面提到的,该函数用于通知主程序前端资源文件的位置。它返回一个vector类型,数组内是插件的json字符串形式配置信息。在HttpExamplePlugin.cpp中添加如下内容:

    std::vector<std::string> HttpExamplePlugin::GetModuleConfig()
        {
            return {R"(
                {
                    "name":"HttpExamplePlugin",
                    "requestName":"HttpExample",
                    "attributes":{
                        "src":"./plugin/Scalar/index.html",
                        "isDefault":true
                    }
                    }
        )"};
        }
    

    ​ 以下是各个字段的详细说明:

name: 插件的名称,也用作后续路由,要求不同插件的名称不能重复

requestName: 插件请求名称,暂未使用,保留字段

attributes: 插件属性配置

src: 前端资源文件位置

isDefault: 默认是否展示

GetAllHandlers() ​ 该函数用于主程序获取插件所支持的接口, 返回一个map。在HttpExamplePlugin.cpp中添加如下内容: ​


    // 返回请求接口的map
    std::map<std::string, std::shared_ptr<ApiHandler>> HttpExamplePlugin::GetAllHandlers()
    {
        return {};
    }

  1. 插件注册

​ 前面已经实现了插件的各个方法,但是还没有将插件注册到主程序中,为了实现这个功能,在HttpExamplePlugin.cpp中添加如下内容:

动态库加载时便会初始化pluginRegister符号,并完成注册动作

  1. 接口实现

到目前为止,插件的基本功能和注册已经实现。但是,你可能已经注意到了GetAllHandlers返回的是一个空map,也就是说我们的插件现在还是光杆司令。接下来为了使插件真正发挥用武之地,我们向它添加一个接口,接口的功能很简单,将后端收到的消息再返回给前端 添加ExampleHandler.h头文件,在其中增加如下内容:

    class ExampleHandler:public PostHandler {
    public:
        bool run(std::string_view data, std::string& result) override
        {
            result = "Received request: " + std::string(data);
            return true;
        }
    };

WebSocket类型插件后端开发(推荐形式)

websocket通信的插件支持事件通知,对比Http类型的插件在某些持续性任务上存在较大优势,下面介绍该类型插件开发的步骤,与Http类型相同的步骤略过,读者可自行通过demo工程了解

  1. 创建插件声明

    增加WebSocketPluginExample.h,其中增加如下内容

    #include "BasePlugin.h"
    
    class WebSocketExamplePlugin:public BasePlugin {
    public:
        WebSocketExamplePlugin();
        std::vector<std::string> GetModuleConfig() override;
        std::unique_ptr<Dic::Module::BaseModule> GetModule() override;
        std::unique_ptr<Dic::Protocol::ProtocolUtil> GetProtocolUtil() override;
    };
    

    这里出现了两个新的函数:

    GetModule():获取WebSocket类型插件的请求路由信息,与Http类型插件中GetAllHandler()函数相当

    GetProtocolUtil(): 注册请求应答的序列化方式,WebSocket插件的序列化

  2. 补充插件定义

    增加插件请求处理Module和Protocol注册

    
    std::unique_ptr<Dic::Module::BaseModule> WebSocketExamplePlugin::GetModule()
    {
        return std::make_unique<WebSocketExampleModule>();
    }
    
    std::unique_ptr<Dic::Protocol::ProtocolUtil> WebSocketExamplePlugin::GetProtocolUtil()
    {
        return std::make_unique<WebSocketPluginExampleProtocol>();
    }
    

​ 下面详细介绍Module类和ProtocolUtil类的实现

  1. 增加一个Module类

    Module主要负责请求路由的注册和请求派发。

    增加WebSocketPluginExampleModule.h和WebSocketPluginExampleModule.cpp,分别增加以下内容

    class WebSocketExampleModule : public Dic::Module::BaseModule
    {
    public:
        WebSocketExampleModule();
        ~WebSocketExampleModule() override;
        void RegisterRequestHandlers() override;
        void OnRequest(std::unique_ptr<Dic::Protocol::Request> request) override;
    };
    

    注册请求和实现请求派发

    void WebSocketExampleModule::RegisterRequestHandlers()
    {
        requestHandlerMap.emplace("WebSocketExample/test", std::make_unique<WebSocketExampleHandler>());
    }
    
    void WebSocketExampleModule::OnRequest(std::unique_ptr<Dic::Protocol::Request> request)
    {
        BaseModule::OnRequest(std::move(request));
    }
    
    
  2. 为moudle实现一个Handler请求处理类

    Handler类主要负责对于请求的实际处理逻辑

    增加文件WebSocketExampleHandler.h 和WebSocketExampleHandler.cpp,并向文件中添加如下内容

    class WebSocketExampleHandler:public Dic::Module::ModuleRequestHandler {
    public:
        WebSocketExampleHandler();
        ~WebSocketExampleHandler() override = default;
        void HandleRequest(std::unique_ptr<Dic::Protocol::Request> requestPtr) override;
    };
    

    设置请求的路由和基本信息

    WebSocketExampleHandler::WebSocketExampleHandler()
    {
        moduleName = "WebSocketExample";
        command = "WebSocketExample/test";
        async = false;
    }
    

    Handler请求类的构造函数,其中command字段用于唯一标识某一种请求,建议用插件名+接口名的方式避免与其它插件冲突。async字段用于标识请求的异0步属性,true则请求为异步请求,false则为同步请求

    void WebSocketExampleHandler::HandleRequest(std::unique_ptr<Dic::Protocol::Request> requestPtr)
    {
        auto &request = dynamic_cast<WebSocketPluginExampleRequest&>(*requestPtr);
        auto responsePtr = std::make_unique<WebSocketPluginExampleResponse>();
        auto& response = *responsePtr;
        SetBaseResponse(request, response);
        response.message = request.message;
        SendResponse(std::move(responsePtr), true);
    }
    

    请求处理函数,再其中处理请求的整个流程。主要分为三步:1.请求结构体转换 2.设置response结构体 3.发送应答

  3. 为Handler提供自定义的序列化和反序列化功能

    增加请求/应答结构体定义

    #include "ProtocolUtil.h"
    struct WebSocketPluginExampleResponse : public Response
    {
        WebSocketPluginExampleResponse() : Response("WebSocketExampleHandler")
        {
        }
    
        std::string message;
    };
    
    struct WebSocketPluginExampleRequest : public Request
    {
        WebSocketPluginExampleRequest(): Request(std::string_view("WebSocketExample/test")) {}
        std::string message;
    };
    

    为请求和应答结构体增加序列化反序列化函数

    inline std::optional<document_t> ToWebSocketPluginExampleResponse(const Response& res)
    {
        const auto& response = dynamic_cast<const WebSocketPluginExampleResponse&>(res);
        document_t json(rapidjson::kObjectType);
        auto& allocator = json.GetAllocator();
        Protocol::ProtocolUtil::SetResponseJsonBaseInfo(response, json);
        json.AddMember(rapidjson::StringRef("message"),
                       json_t().SetString(response.message.c_str(), response.message.length(), allocator),
                       allocator);
        return std::optional<document_t>(std::move(json));
    }
    
    inline std::unique_ptr<Request> ToWebSocketPluginExampleRequest(const json_t& req, std::string& err)
    {
        auto request = std::make_unique<WebSocketPluginExampleRequest>();
        request->message = req.GetString();
        return request;
    }
    

    增加事件的序列化函数

    inline std::optional<document_t> ToWebSocketPluginExampleEvent(const Event& baseEvent)
    {
        document_t json(rapidjson::kObjectType);
        json.AddMember(rapidjson::StringRef("eventStr"), json_t().SetString(baseEvent.event.c_str(), baseEvent.event.length(), json.GetAllocator()), json.GetAllocator());
        return std::optional<document_t>(std::move(json));
    }
    

    将序列化/反序列化注册到ProtocolUtil中

    class WebSocketPluginExampleProtocol : public ProtocolUtil
    {
    public:
        WebSocketPluginExampleProtocol() = default;
        ~WebSocketPluginExampleProtocol() override = default;
    
    private:
        void RegisterEventToJsonFuncs() override
        {
            eventToJsonFactory.emplace("WebSocketExample/testEvent", ToWebSocketPluginExampleEvent);
        }
        void RegisterJsonToRequestFuncs() override
        {
        	jsonToReqFactory.emplace("WebSocketExample/test", ToWebSocketPluginExampleRequest);
    	}
        void RegisterResponseToJsonFuncs() override
        {
        	resToJsonFactory.emplace("WebSocketExample/test", ToWebSocketPluginExampleResponse);
    	}
    };
    

插件前端开发

前端开发主要围绕几个基本事件

  1. 挂载前端的组件到MindStudio Insight上

    App.tsx中增加挂载语句,发送挂载事件消息给MindStudio Insight

    window.parent.postMessage({ event: 'pluginMounted' }, '*');
    
  2. 注册唤醒组件事件

    App.tsx中增加对于window.onmessage的处理:

            window.onmessage = async (e) => {
                // 解析onmessage数据
                const { target, event, data, body } = typeof e.data === 'string' ? safeJSONParse(e.data) : e.data;
                if (target !== 'plugin') {
                    return;
                }
    			
                switch (event) {
                    case 'wakeupPlugin':
                        //唤醒插件
                        setBaseURL(data.url);
                        break;
                    default:
                        break;
                }
            };
    

    setBaseURL:获取前端链接的URL,设置对应的链接协议

  3. 文件导入功能实现

    文件导入功能依赖于MindStudio Insight提供的功能,在导入时会向插件广播一个前端事件,在上方的onmessage中增加处理逻辑

            window.onmessage = async (e) => {
              
                const { target, event, data, body } = typeof e.data === 'string' ? safeJSONParse(e.data) : e.data;
                if (target !== 'plugin') {
                    return;
                }
    
                switch (event) {
                    case 'wakeupPlugin':
                        //唤醒插件
                        setBaseURL(data.url);
                        setHasURL(true);
                        window.parent.postMessage({ event: 'getLanguage' }, '*');
                        window.parent.postMessage({ event: 'getTheme' }, '*');
                        break;
                    case 'remote/import':
                        //导入文件处理逻辑,用户自行实现即可
                        break;
                    default:
                        break;
                }
            };
    
  4. 额外事件(主题切换、语言切换)

    MindStudio Insight 除了上述两个前端事件外还有主题和语言切换两个事件消息为可选处理

            window.onmessage = async (e) => {
                const { target, event, data, body } = typeof e.data === 'string' ? safeJSONParse(e.data) : e.data;
                if (target !== 'plugin') {
                    return;
                }
    
                switch (event) {
                    case 'wakeupPlugin':
                        //唤醒插件
                        setBaseURL(data.url);
                        setHasURL(true);
                        window.parent.postMessage({ event: 'getLanguage' }, '*');
                        window.parent.postMessage({ event: 'getTheme' }, '*');
                        break;
                    case 'switchLanguage':
                        //切换语言
                        break;
                    case 'setTheme':
                        //设置主题色
                        if (body.isDark) {
                            // 设置主题逻辑
                        } else {
                            // 设置主题逻辑
                        }
                        break;
                    case 'remote/import':
                        //导入文件
                        break;
                    default:
                        break;
                }
            };
    

插件编译打包及安装使用

分别打包插件的前后端产物后,我们接下来需要将插件产物打包,并安装到MindStudio Insight中使用。

插件打包

为了简化安装流程,MindStudio Insight中提供了插件安装脚本,位于resource/profiler目录下,插件打包也要遵守相应的打包规范,以便于使用安装脚本一键安装 安装包规格: 总体为zip包,其中分为三个部分:配置文件(config.json)、前端文件(zip包)、后端文件(so/zip包) config.json

    {
        "pluginName":"xxxx",  // 插件名称
        "frontend":"xxx.zip",  // 前端产物
        "backend_${platfrom}_${machine}":"xx.so", //后端产物, ${platform}和${machine}为平台相关变量,通过python的platform模块获取
    }

前端文件:前端构建产物的zip包,解压后为前端产物 后端文件:后端构建产物的zip包,一般为对应平台和架构下的so文件,如果一个平台下存在多个so文件需要打包为一个zip包

插件安装

在MindStudio Insight安装目录下执行

    python resource/profile/plugin_install.py install --path=XXX_plugin.zip

插件使用

打开MindStudio Insight,导入对应数据即可,如果插件实现自己的唤醒逻辑,则依据实际情况。

完整示例代码

见本项目plugins/mindstudio-insight-plugins/Example目录下