1

image.png

前言

最近项目想集成drawio,由于官方并没有提供NPM包引入方案,所以最终选择了官网提供的嵌入式方案来集成drawio,并且集成过程中自定义了菜单,下面具体说说如何操作。

这里首先说明一下,因为我使用的是next.js所以我在网上找到一个已经集成了drawio的嵌入模式的npm包react-drawio,在使用的过程中我发现这个包并不满足我的需要,所以我参考了它的代码一步步实现我想要的效果。

还有在使用drawio的二次开发过程中,我参考了这篇系列文章draw.io 网页版二次开发

需求

每个人在准备开发drawio前总有自己的需求和目的,所以我的需求就是:

1、使用drawio的iframe嵌入模式
2、添加和修改下拉菜单并且调用自己网站的弹出框逻辑

操作

带着需求我们来具体的操作:

1、下载drawio代码
官网地址在这

2、运行代码及访问

我们首先安装一下serve包:

sudo npm install -g serve

安装好之后,我们到webapp目录执行启动命令:

serve

image.png

执行之后,我们在url中输入如下地址来模拟嵌入模式:

http://localhost:61286/?embed=1&proto=json&ui=min&dark=0&spin=0&noExitBtn=1&noSaveBtn=1&saveAndExit=0&dev=1

image.png

上面url参数说明:

embed:是否是嵌入模式
ui:主题 'min' | 'atlas' | 'kennedy' | 'dark' | 'sketch' | 'simple'
dark:是否是暗色主题
dev:开发模式
noExitBtn:不显示退出按钮
noSaveBtn:不显示保存按钮
saveAndExit:是否显示保存和退出按钮

3、修改菜单及添加自定义事件
image.png
上面就是我想要实现的效果,打开我的文件,保存到我的文件,打开这三个是我新添加的,导出是使用原有的,还有就是隐藏不需要的菜单,比如系统自带的导入功能,我就觉得不好用,就隐藏了。

修改webapp/js/diagramly/Menus.js

this.put('diagram', new Menu(mxUtils.bind(this, function (menu, parent) {
  var file = editorUi.getCurrentFile();

  if (Editor.currentTheme != 'simple') {
      editorUi.menus.addMenuItems(menu, ['-', 'importFromSeaurl','saveFromSeaurl', 'importFile'], parent);
      this.addSubmenu('exportFile', menu, parent);
      menu.addSeparator(parent);
      editorUi.menus.addSubmenu('extras', menu, parent, mxResources.get('settings'));
      menu.addSeparator(parent);
  }

...

// 导出文件我使用了原有的代码,下拉复制到这就完成了
this.put('exportFile', new Menu(mxUtils.bind(this, function (menu, parent) {
    if (editorUi.isExportToCanvas()) {
        this.addMenuItems(menu, ['exportPng'], parent);

        if (editorUi.jpgSupported) {
            this.addMenuItems(menu, ['exportJpg'], parent);
        }

        if (editorUi.webpSupported) {
            this.addMenuItems(menu, ['exportWebp'], parent);
        }
    }

    // Disabled for standalone mode in iOS because new tab cannot be closed
    else if (!editorUi.isOffline() && (!mxClient.IS_IOS || !navigator.standalone)) {
        this.addMenuItems(menu, ['exportPng', 'exportJpg'], parent);
    }

    this.addMenuItems(menu, ['exportSvg', '-'], parent);

    // Redirects export to PDF to print in Chrome App
    if (editorUi.isOffline() || editorUi.printPdfExport) {
        this.addMenuItems(menu, ['exportPdf'], parent);
    }
    // Disabled for standalone mode in iOS because new tab cannot be closed
    else if (!editorUi.isOffline() && (!mxClient.IS_IOS || !navigator.standalone)) {
        this.addMenuItems(menu, ['exportPdf'], parent);
    }

    if (!mxClient.IS_IE && (typeof (VsdxExport) !== 'undefined' || !editorUi.isOffline())) {
        this.addMenuItems(menu, ['exportVsdx'], parent);
    }

    this.addMenuItems(menu, ['-', 'exportHtml', 'exportXml', 'exportUrl'], parent);

    if (!editorUi.isOffline()) {
        menu.addSeparator(parent);
        this.addMenuItem(menu, 'export', parent).firstChild.nextSibling.innerHTML = mxResources.get('advanced') + '...';
    }

    if (!mxClient.IS_CHROMEAPP && !EditorUi.isElectronApp && Editor.currentTheme == 'min') {
        this.addMenuItems(menu, ['publishLink'], parent);
    }

    if (editorUi.mode != App.MODE_ATLAS && urlParams['extAuth'] != '1' &&
        (Editor.currentTheme == 'simple' || Editor.currentTheme == 'sketch' ||
            Editor.currentTheme == 'min')) {
        menu.addSeparator(parent);
        editorUi.menus.addSubmenu('embed', menu, parent);
    }
})));

上面的importFromSeaurlsaveFromSeaurlimportFileexportFile就是我添加的四个菜单项。其中exportFile导出按钮功能,已经在上面代码实现了。

修改webapp/js/grapheditor/Actions.js

添加如下代码

this.addAction('importFromSeaurl', function() {
    ui.importFromSeaurl()
}, null, null, '')

this.addAction('saveFromSeaurl', function() {
    ui.saveFromSeaurl()
}, null, null, '')

this.addAction('importFile', function() {
    ui.importLocalFile(true);
}, null, null, '');

注意:importLocalFile是系统自带的功能,这样就完成了打开文件的菜单项功能。而上面的importFromSeaurlsaveFromSeaurl是自己自定义的,所以我们还需要改下面的代码:

修改webapp/js/diagramly/EditorUi.js

EditorUi.prototype.importFromSeaurl = function(){
  var parent = window.opener || window.parent;

  parent.postMessage(JSON.stringify({
  event: 'importFromSeaurl'
  }), '*');
};

EditorUi.prototype.saveFromSeaurl = function(){
  var parent = window.opener || window.parent;

  var xml = this.getFileData(true);
  var msg = this.createLoadMessage('saveFromSeaurl');
  msg.xml = xml;
  parent.postMessage(JSON.stringify(msg), '*');
};

这里你会看到,我使用了iframe的postMessage功能,将消息发送给父窗体,这样就达到了触发事件效果,所以到时候我们只要到父窗体组件中接收这个消息处理自己的逻辑即可。

4、打包docker镜像并发布到k8s

这里我参考了docker-drawio的代码实现打包和发布功能,还有一点就是我使用了github actions来实现打包和部署,你们当然也可以使用了jenkins或者gitlab ci/cd来实现,这里我就不赘述了。

在/etc/docker目录下添加文件

1、添加DockerFile

FROM openjdk:11-jdk-slim AS build

RUN apt-get update -y && \
# this solves some weird issue with openjdk-11-jdk-headless
# https://github.com/nextcloud/docker/issues/380
    mkdir -p /usr/share/man/man1 && \
    apt-get install -y \
        ant \
        git

RUN cd /tmp && \
    git clone --depth 1 https://github.com/zhangwei900808/seaurl-drawio.git && \
    cd /tmp/seaurl-drawio/etc/build/ && \
    ant war

FROM tomcat:9-jre11

LABEL maintainer="seaurl" \
      org.opencontainers.image.authors="Seaurl Ltd" \
      org.opencontainers.image.url="https://www.seaurl.com" \
      org.opencontainers.image.source="https://github.com/zhangwei900808/seaurl-drawio"

ENV RUN_USER            tomcat
ENV RUN_GROUP           tomcat

RUN apt-get update -y && \
    apt-get install -y --no-install-recommends \
        certbot \
        curl \
        xmlstarlet \
        unzip && \
    apt-get autoremove -y --purge && \
    apt-get clean && \
    rm -r /var/lib/apt/lists/*

COPY --from=build /tmp/seaurl-drawio/build/draw.war /tmp

# Extract draw.io war & Update server.xml to set Draw.io webapp to root
RUN mkdir -p $CATALINA_HOME/webapps/draw && \
    unzip /tmp/draw.war -d $CATALINA_HOME/webapps/draw && \
    rm -rf /tmp/draw.war /tmp/seaurl-drawio && \
    cd $CATALINA_HOME && \
    xmlstarlet ed \
        -P -S -L \
        -i '/Server/Service/Engine/Host/Valve' -t 'elem' -n 'Context' \
        -i '/Server/Service/Engine/Host/Context' -t 'attr' -n 'path' -v '/' \
        -i '/Server/Service/Engine/Host/Context[@path="/"]' -t 'attr' -n 'docBase' -v 'draw' \
        -s '/Server/Service/Engine/Host/Context[@path="/"]' -t 'elem' -n 'WatchedResource' -v 'WEB-INF/web.xml' \
        conf/server.xml

# Copy docker-entrypoint
# 注意:当你使用 COPY --from=build 时,Docker 知道它需要从构建阶段(build)中提取文件
COPY --from=build /tmp/seaurl-drawio/etc/docker/docker-entrypoint.sh /
RUN chmod 755 /docker-entrypoint.sh
# Add a tomcat user
RUN groupadd -r ${RUN_GROUP} && useradd -g ${RUN_GROUP} -d ${CATALINA_HOME} -s /bin/bash ${RUN_USER} && \
    chown -R ${RUN_USER}:${RUN_GROUP} ${CATALINA_HOME}

USER ${RUN_USER}

WORKDIR $CATALINA_HOME

EXPOSE 8080 8443

ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["catalina.sh", "run"]

2、添加k8s-deploy.yaml文件

apiVersion: v1
kind: Namespace
metadata:
  name: drawio
---
apiVersion: v1
kind: Service
metadata:
  name: drawio
  namespace: drawio
  labels:
    app: draw.io
spec:
  # 使用ip访问,所以设置成NodePort
  type: NodePort
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 3600
  ports:
    - name: http
      port: 8080
  selector:
    app: draw.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: drawio
  namespace: drawio
spec:
  replicas: 2
  selector:
    matchLabels:
      app: draw.io
  template:
    metadata:
      labels:
        app: draw.io
    spec:
      containers:
        - image: xxx:<tag>
          imagePullPolicy: IfNotPresent
          name: drawio
          ports:
            - containerPort: 8080
---

3、添加docker-entrypoint文件

#!/bin/bash
#set -e

LETS_ENCRYPT_ENABLED=${LETS_ENCRYPT_ENABLED:-false}
PUBLIC_DNS=${PUBLIC_DNS:-'draw.example.com'}
ORGANISATION_UNIT=${ORGANISATION_UNIT:-'Cloud Native Application'}
ORGANISATION=${ORGANISATION:-'example inc'}
CITY=${CITY:-'Paris'}
STATE=${STATE:-'Paris'}
COUNTRY_CODE=${COUNTRY_CODE:-'FR'}
KEYSTORE_PASS=${KEYSTORE_PASS:-'V3ry1nS3cur3P4ssw0rd'}
KEY_PASS=${KEY_PASS:-$KEYSTORE_PASS}

echo "Init PreConfig.js"
#Add CSP to prevent calls to draw.io
echo "(function() {" > $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "  try {" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "        var s = document.createElement('meta');" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
if [[ -z "${DRAWIO_GITLAB_ID}" ]]; then
    echo "        s.setAttribute('content', '${DRAWIO_CSP_HEADER:-default-src \'self\'; script-src \'self\' https://storage.googleapis.com https://apis.google.com https://docs.google.com https://code.jquery.com \'unsafe-inline\'; connect-src \'self\' https://*.dropboxapi.com https://api.trello.com https://api.github.com https://raw.githubusercontent.com https://*.googleapis.com https://*.googleusercontent.com https://graph.microsoft.com https://*.1drv.com https://*.sharepoint.com https://gitlab.com https://*.google.com https://fonts.gstatic.com https://fonts.googleapis.com; img-src * data:; media-src * data:; font-src * about:; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com; frame-src \'self\' https://*.google.com;}');" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
else
    echo "        s.setAttribute('content', '${DRAWIO_CSP_HEADER:-default-src \'self\'; script-src \'self\' https://storage.googleapis.com https://apis.google.com https://docs.google.com https://code.jquery.com \'unsafe-inline\'; connect-src \'self\' $DRAWIO_GITLAB_URL https://*.dropboxapi.com https://api.trello.com https://api.github.com https://raw.githubusercontent.com https://*.googleapis.com https://*.googleusercontent.com https://graph.microsoft.com https://*.1drv.com https://*.sharepoint.com https://gitlab.com https://*.google.com https://fonts.gstatic.com https://fonts.googleapis.com; img-src * data:; media-src * data:; font-src * about:; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com; frame-src \'self\' https://*.google.com;}');" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
fi
echo "        s.setAttribute('http-equiv', 'Content-Security-Policy');" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "         var t = document.getElementsByTagName('meta')[0];" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "      t.parentNode.insertBefore(s, t);" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "  } catch (e) {} // ignore" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "})();" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
#Overrides of global vars need to be pre-loaded
if [[ "${DRAWIO_SELF_CONTAINED}" ]]; then
    echo "window.EXPORT_URL = '/service/0'; //This points to ExportProxyServlet which uses the local export server at port 8000. This proxy configuration allows https requests to the export server via Tomcat." >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
    echo "window.PLANT_URL = '/service/1';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
elif [[ "${EXPORT_URL}" ]]; then
    echo "window.EXPORT_URL = '/service/0';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
fi
#DRAWIO_SERVER_URL is the new URL of the deployment, e.g. https://www.example.com/drawio/
#DRAWIO_BASE_URL is still used by viewer, lightbox and embed. For backwards compatibility, DRAWIO_SERVER_URL is set to DRAWIO_BASE_URL if not specified.
if [[ "${DRAWIO_SERVER_URL}" ]]; then
    echo "window.DRAWIO_SERVER_URL = '${DRAWIO_SERVER_URL}';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
    echo "window.DRAWIO_BASE_URL = '${DRAWIO_BASE_URL:-${DRAWIO_SERVER_URL:0:$((${#DRAWIO_SERVER_URL}-1))}}';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
else
    echo "window.DRAWIO_BASE_URL = '${DRAWIO_BASE_URL:-http://localhost:8080}';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
    echo "window.DRAWIO_SERVER_URL = window.DRAWIO_BASE_URL + '/';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
fi
#DRAWIO_VIEWER_URL is path to the viewer js, e.g. https://www.example.com/js/viewer.min.js
echo "window.DRAWIO_VIEWER_URL = '${DRAWIO_VIEWER_URL}';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
#DRAWIO_LIGHTBOX_URL Replace with your lightbox URL, eg. https://www.example.com
echo "window.DRAWIO_LIGHTBOX_URL = '${DRAWIO_LIGHTBOX_URL}';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "window.DRAW_MATH_URL = 'math/es5';" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
#Custom draw.io configurations. For more details, https://www.drawio.com/doc/faq/configure-diagram-editor
echo "window.DRAWIO_CONFIG = ${DRAWIO_CONFIG:-null};" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
#Real-time configuration
echo "urlParams['sync'] = 'manual'; //Disable Real-Time" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js

#Disable unsupported services
echo "urlParams['db'] = '0'; //dropbox" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "urlParams['gh'] = '0'; //github" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
echo "urlParams['tr'] = '0'; //trello" >> $CATALINA_HOME/webapps/draw/js/PreConfig.js

#Google Drive
if [[ -z "${DRAWIO_GOOGLE_CLIENT_ID}" ]]; then
    echo "urlParams['gapi'] = '0'; //Google Drive"  >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
else
    #Google drive application id and client id for the editor
    echo "window.DRAWIO_GOOGLE_APP_ID = '${DRAWIO_GOOGLE_APP_ID}'; " >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
    echo "window.DRAWIO_GOOGLE_CLIENT_ID = '${DRAWIO_GOOGLE_CLIENT_ID}'; " >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
    echo -n "${DRAWIO_GOOGLE_CLIENT_ID}" > $CATALINA_HOME/webapps/draw/WEB-INF/google_client_id
    echo -n "${DRAWIO_GOOGLE_CLIENT_SECRET}" > $CATALINA_HOME/webapps/draw/WEB-INF/google_client_secret
    #If you want to use the editor as a viewer also, you can create another app with read-only access. You can use the same info as above if write-access is not an issue.
    if [[ "${DRAWIO_GOOGLE_VIEWER_CLIENT_ID}" ]]; then
        echo "window.DRAWIO_GOOGLE_VIEWER_APP_ID = '${DRAWIO_GOOGLE_VIEWER_APP_ID}'; " >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
        echo "window.DRAWIO_GOOGLE_VIEWER_CLIENT_ID = '${DRAWIO_GOOGLE_VIEWER_CLIENT_ID}'; " >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
        echo -n "/:::/${DRAWIO_GOOGLE_VIEWER_CLIENT_ID}" >> $CATALINA_HOME/webapps/draw/WEB-INF/google_client_id
        echo -n "/:::/${DRAWIO_GOOGLE_VIEWER_CLIENT_SECRET}" >> $CATALINA_HOME/webapps/draw/WEB-INF/google_client_secret
    fi
fi

#Microsoft OneDrive
if [[ -z "${DRAWIO_MSGRAPH_CLIENT_ID}" ]]; then
    echo "urlParams['od'] = '0'; //OneDrive"  >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
else
    #Google drive application id and client id for the editor
    echo "window.DRAWIO_MSGRAPH_CLIENT_ID = '${DRAWIO_MSGRAPH_CLIENT_ID}'; " >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
    echo -n "${DRAWIO_MSGRAPH_CLIENT_ID}" > $CATALINA_HOME/webapps/draw/WEB-INF/msgraph_client_id
    echo -n "${DRAWIO_MSGRAPH_CLIENT_SECRET}" > $CATALINA_HOME/webapps/draw/WEB-INF/msgraph_client_secret

    if [[ "${DRAWIO_MSGRAPH_TENANT_ID}" ]]; then
        echo "window.DRAWIO_MSGRAPH_TENANT_ID = '${DRAWIO_MSGRAPH_TENANT_ID}'; " >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
    fi
fi

#Gitlab
if [[ -z "${DRAWIO_GITLAB_ID}" ]]; then
    echo "urlParams['gl'] = '0'; //Gitlab"  >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
else
    #Gitlab url and id for the editor
    echo "window.DRAWIO_GITLAB_URL = '${DRAWIO_GITLAB_URL}'; " >> $CATALINA_HOME/webapps/draw/js/PreConfig.js
    echo "window.DRAWIO_GITLAB_ID = '${DRAWIO_GITLAB_ID}'; " >> $CATALINA_HOME/webapps/draw/js/PreConfig.js

    #Gitlab server flow auth (since 14.6.7)
    echo -n "${DRAWIO_GITLAB_URL}/oauth/token" > $CATALINA_HOME/webapps/draw/WEB-INF/gitlab_auth_url
    echo -n "${DRAWIO_GITLAB_ID}" > $CATALINA_HOME/webapps/draw/WEB-INF/gitlab_client_id
    echo -n "${DRAWIO_GITLAB_SECRET}" > $CATALINA_HOME/webapps/draw/WEB-INF/gitlab_client_secret
fi

cat $CATALINA_HOME/webapps/draw/js/PreConfig.js

echo "Init PostConfig.js"

#null'ing of global vars need to be after init.js
echo "window.VSD_CONVERT_URL = null;" > $CATALINA_HOME/webapps/draw/js/PostConfig.js
echo "window.ICONSEARCH_PATH = null;" >> $CATALINA_HOME/webapps/draw/js/PostConfig.js
echo "EditorUi.enableLogging = false; //Disable logging" >> $CATALINA_HOME/webapps/draw/js/PostConfig.js

#This requires subscription with cloudconvert.com
if [[ -z "${DRAWIO_CLOUD_CONVERT_APIKEY}" ]]; then
    echo "window.EMF_CONVERT_URL = null;"  >> $CATALINA_HOME/webapps/draw/js/PostConfig.js
else
    echo "window.EMF_CONVERT_URL = '/convert';" >> $CATALINA_HOME/webapps/draw/js/PostConfig.js
    echo -n "${DRAWIO_CLOUD_CONVERT_APIKEY}" > $CATALINA_HOME/webapps/draw/WEB-INF/cloud_convert_api_key
fi

if [[ "${DRAWIO_SELF_CONTAINED}" ]]; then
    echo "EditorUi.enablePlantUml = true; //Enables PlantUML" >> $CATALINA_HOME/webapps/draw/js/PostConfig.js
fi

#Treat this domain as a draw.io domain
echo "App.prototype.isDriveDomain = function() { return true; }" >> $CATALINA_HOME/webapps/draw/js/PostConfig.js

cat $CATALINA_HOME/webapps/draw/js/PostConfig.js

if ! [ -f $CATALINA_HOME/.keystore ] && [ "$LETS_ENCRYPT_ENABLED" == "true" ]; then
    echo "Generating Let's Encrypt certificate"

    keytool -genkey -noprompt -alias tomcat -dname "CN=${PUBLIC_DNS}, OU=${ORGANISATION_UNIT}, O=${ORGANISATION}, L=${CITY}, S=${STATE}, C=${COUNTRY_CODE}" -keystore $CATALINA_HOME/.keystore -storepass "${KEYSTORE_PASS}" -KeySize 2048 -keypass "${KEY_PASS}" -keyalg RSA -storetype pkcs12

    keytool -list -keystore $CATALINA_HOME/.keystore -v -storepass "${KEYSTORE_PASS}"

    keytool -certreq -alias tomcat -file request.csr -keystore $CATALINA_HOME/.keystore -storepass "${KEYSTORE_PASS}"

    certbot certonly --csr $CATALINA_HOME/request.csr --standalone --register-unsafely-without-email --agree-tos

    keytool -import -trustcacerts -alias tomcat -file 0001_chain.pem -keystore $CATALINA_HOME/.keystore -storepass "${KEYSTORE_PASS}"
fi

if ! [ -f $CATALINA_HOME/.keystore ] && [ "$LETS_ENCRYPT_ENABLED" == "false" ]; then
    echo "Generating Self-Signed certificate"

    keytool -genkey -noprompt -alias selfsigned -dname "CN=${PUBLIC_DNS}, OU=${ORGANISATION_UNIT}, O=${ORGANISATION}, L=${CITY}, S=${STATE}, C=${COUNTRY_CODE}" -keystore $CATALINA_HOME/.keystore -storepass "${KEYSTORE_PASS}" -KeySize 2048 -keypass "${KEY_PASS}" -keyalg RSA -validity 3600 -storetype pkcs12

    keytool -list -keystore $CATALINA_HOME/.keystore -v -storepass "${KEYSTORE_PASS}"
fi

# Update SSL port configuration if it does'nt exists
#
UUID="$(cat /dev/urandom | tr -dc 'a-zA-Z' | fold -w 1 | head -n 1)$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 7 | head -n 1)"
VAR=$(cat conf/server.xml | grep "$CATALINA_HOME/.keystore")

if [ -f $CATALINA_HOME/.keystore ] && [ -z $VAR ]; then
     echo "Append https connector to server.xml"

    xmlstarlet ed \
        -P -S -L \
        -s '/Server/Service' -t 'elem' -n "${UUID}" \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'port' -v '8443' \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'protocol' -v 'org.apache.coyote.http11.Http11NioProtocol' \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'SSLEnabled' -v 'true' \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'maxThreads' -v '150' \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'scheme' -v 'https' \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'secure' -v 'true' \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'clientAuth' -v 'false' \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'sslProtocol' -v 'TLS' \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'KeystoreFile' -v "$CATALINA_HOME/.keystore" \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'KeystorePass' -v "${KEY_PASS}" \
        -i "/Server/Service/${UUID}" -t 'attr' -n 'defaultSSLHostConfigName' -v "${PUBLIC_DNS:-'draw.example.com'}" \
        -s "/Server/Service/${UUID}" -t 'elem' -n 'SSLHostConfig' \
        -i "/Server/Service/${UUID}/SSLHostConfig" -t 'attr' -n 'hostName' -v "${PUBLIC_DNS:-'draw.example.com'}" \
        -i "/Server/Service/${UUID}/SSLHostConfig" -t 'attr' -n 'protocols' -v 'TLSv1.2' \
        -s "/Server/Service/${UUID}/SSLHostConfig" -t 'elem' -n 'Certificate' \
        -i "/Server/Service/${UUID}/SSLHostConfig/Certificate" -t 'attr' -n 'certificateKeystoreFile' -v "$CATALINA_HOME/.keystore" \
        -i "/Server/Service/${UUID}/SSLHostConfig/Certificate" -t 'attr' -n 'certificateKeystorePassword' -v "${KEY_PASS}" \
        -r "/Server/Service/${UUID}" -v 'Connector' \
    conf/server.xml
fi


exec "$@"
添加github actions文件

添加.github/workflow/docker-image.yml文件

name: Docker Image CI

on:
  workflow_dispatch:

#  push:
#    branches:
#      - main
#    tags:
#      - 'v*'

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Log in to Alibaba Cloud Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ secrets.ALIYUN_REGISTRY_SERVER }}
          username: ${{ secrets.ALIYUN_REGISTRY_USERNAME }}
          password: ${{ secrets.ALIYUN_REGISTRY_PASSWORD }}

      - name: Build And Publish Docker Image
        run: |
          # Get draw.io current latest version
          wget https://raw.githubusercontent.com/zhangwei900808/seaurl-drawio/main/VERSION
          export VERSION=`cat VERSION`
          export TAG=`if [ "" == ${VERSION} ]; then echo "latest"; else echo ${VERSION} ; fi`
          docker build -f etc/docker/Dockerfile -t ${{ secrets.ALIYUN_REGISTRY_SERVER }}/com-seaurl/seaurl-drawio:${TAG} .
          docker push ${{ secrets.ALIYUN_REGISTRY_SERVER }}/com-seaurl/seaurl-drawio:${TAG}

      - name: Set up Kubernetes CLI
        uses: azure/setup-kubectl@v3
      #        with:
      #          version: '1.24.0' # Specify the kubectl version you need

      - name: Configure kubectl
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.KUBECONFIG }}" > ~/.kube/config

      - name: Deploy to Kubernetes
        run: |
          # Get draw.io current latest version
          wget https://raw.githubusercontent.com/zhangwei900808/seaurl-drawio/main/VERSION
          export VERSION=`cat VERSION`
          export TAG=`if [ "" == ${VERSION} ]; then echo "latest"; else echo ${VERSION} ; fi`
          sed -i "s/<tag>/${TAG}/g" etc/docker/k8s-deploy.yaml
          kubectl get namespace drawio || kubectl create namespace drawio
          kubectl apply -f etc/docker/k8s-deploy.yaml -n drawio
#          kubectl rollout status deployment/seaurl-drawio-deployment
运行github actions

image.png

运行成功之后,我们看看有没有成功

☁  seaurl-drawio-docker [main] ⚡  k get all -ndrawio
NAME                          READY   STATUS    RESTARTS   AGE
pod/drawio-86555c55f4-48t7x   1/1     Running   0          88s
pod/drawio-86555c55f4-pr525   1/1     Running   0          67s

NAME             TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/drawio   NodePort   192.168.49.196   <none>        8080:30128/TCP   3d6h

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/drawio   2/2     2            2           3d6h

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/drawio-54c59986f4   0         0         0       30h
replicaset.apps/drawio-59fbbd8dfd   0         0         0       5h17m
replicaset.apps/drawio-69cfff758d   0         0         0       28h
replicaset.apps/drawio-777c9485f5   0         0         0       32h
replicaset.apps/drawio-786d85d487   0         0         0       3d6h
replicaset.apps/drawio-86555c55f4   2         2         2       88s

说明已经成功了,这里我们获取到NODE_IP和PORT之后,我们复制到自己的项目中。

5、集成到Next.js项目中

下面是我的集成后的代码:

'use client'
import {useEffect, useRef, useState} from "react";
import {useSession} from "next-auth/react";
import {useRouter, useSearchParams} from "next/navigation";
import {withRouter} from "@/hoc/withRouter";
import {useDispatch, useSelector} from "react-redux";
import {addFileForText, queryUserAiTools, setDrawio, updateDt} from "@/lib/slices/aiSpaceSlice";
import {message, Spin} from "antd";
import Loading from "@/components/loading";
import SaveFileDlg from "@/components/dialog/saveFileDlg";
import SelectFileDlg from "@/components/dialog/selectFileDlg";

const Drawio = (props) => {
    const {data: session} = useSession()
    const query = useSearchParams()

    const router = useRouter()
    const isUpdate = useRef(false)
    const drawioRef = useRef(null);
    const [currentData, setCurrentData] = useState()
    const {drawio, aiToolsWillChangeFunc} = useSelector(state => state.aiSpace)
    const {initData} = drawio
    const dispatch = useDispatch()
    const [isLoading, setIsLoading] = useState(true);
    const gotoRef = useRef(null)
    const {platFormTheme} = useSelector(state => state.system)

    const [dlg, setDlg] = useState({
        title: '',
        show: false
    })
    const [selectFileDlg, setSelectFileDlg] = useState({
        title: '',
        show: false
    })

    useEffect(() => {
        // 默认加载空的画板
        if(drawioRef.current){
            drawioRef.current?.contentWindow?.postMessage(JSON.stringify({
                action: 'load',
                xml: ''
            }), "*")
        }
        // 第一次进来的时候选择模板
        if (drawioRef.current && !isLoading && !initData) {
            // 打开template窗口
            drawioRef.current?.contentWindow?.postMessage(JSON.stringify({
                action: 'template'
            }), "*")
        }

        // 如果有initData说明有数据缓存,则加载这个缓存数据到drawio中
        if (drawioRef.current && initData){
            drawioRef.current?.contentWindow?.postMessage(JSON.stringify({
                action: 'load',
                xml: initData
            }), "*")
        }
    }, [isLoading])

    // 打开我的文件
    function openMyFile() {
        ...
    }

    // 保存到我的文件
    function saveMyFile(xml) {
        ...
    }

    // 监听drawio的消息
    function getMessageFromDrawio(event) {
        try {
            const data = JSON.parse(event?.data);
            console.log('event data=', data)
            if (data.event === "clickDrawio"){
                globalUpdate()
            }
            if (data.event === 'importFromSeaurl'){
                openMyFile()
            }
            if (data.event === "saveFromSeaurl"){
                saveMyFile(data.xml)
            }
            // 页面初始化完成
            if (data?.event === 'init') {
                setIsLoading(false)
            }
            // 用户发起了导出命令
            if (data?.event === "export"){
                setCurrentData(data.xml)
            }
            if (event.origin === "*") {
                console.log("Message received from iframe:", event.data);
            }
        } catch (e) {

        }
    }

    useEffect(() => {
        // 父窗口中监听从 iframe 返回的消息
        window.addEventListener("message", getMessageFromDrawio);
        return () => {
            window.removeEventListener("message", getMessageFromDrawio);
        }
    }, [])

    return <div className={'w-full h-[calc(100vh-56px)] relative'} onClick={globalUpdate}>
        {isLoading ?
            <div className={'flex items-center justify-center h-full w-full absolute top-0 left-0 right-0 bottom-0 z-20 bg-white dark:bg-[#141414]'}>
                <Loading/>
            </div> : null}
        <iframe ref={drawioRef}
                className={'w-full h-full min-h-[400px] min-w-[400px] border-0'}
                src={`http://xxx.xxx.xxx.xxx:xxxx/?embed=1&proto=json&ui=min&${platFormTheme.background === 'light' ? "dark=0":"dark=1"}&spin=0&noExitBtn=1&noSaveBtn=1&saveAndExit=0`}
                title="seaurl.com"></iframe>
        <SaveFileDlg dlg={dlg}/>
        <SelectFileDlg dlg={selectFileDlg}/>
    </div>
};

export default withRouter(Drawio);

上面代码中我们使用window.addEventListener来监听drawio菜单返回来的消息,这样你就可以打开自己项目中的窗体或者执行相关逻辑了。
image.png

总结

1、这里最难的点应该算修改drawio的菜单项的新增与修改了,所以需要多阅读drawio的代码才能实现自己想要的效果。
2、因为使用了iframe的嵌入模式,所以需要来回的发送和接收消息,我觉得drawio应该出个自己的NPM包,这样开发者集成更方便一点
3、打开页面的时候应该默认xml:'',否则打开就是空白,拖拽图标到空白处不起作用
4、react中,useEffect中的return方法是在组件销毁后执行的,所以当你在return方法中使用ref来获取组件相关数据时是获取不到的,报null的错
5、如果你们觉得有啥问题可以多看看react-drawio这个npm包源码,我也是参考它的。

引用

draw.io 网页版二次开发(2):开始修改代码


Awbeci
3.1k 声望212 粉丝

Awbeci