Mendix,在开发组件之前,需要了解的部分知识

环境安装

  • node 环境,建议安装v18以上版本
    • nodejs官网下载安装包,或通过nvm安装
    • node -v
  • 全局安装yo
    • npm install -g yo
  • 全局安装 @mendix/generator-widget
    • npm install -g @mendix/generator-widget

使用

  • 初始化项目
    • yo @mendix/widget myWidget
  • 进入组件目录并打包,将组件更新到mendix本地工程中
    • cd myWidget && npm run build

组件目录说明

go 复制代码
- dist
- node_modules
- src
    components
      HelloWorldSample.tsx   // 默认生成的子组件
    ui
      xxx.css   // 可修改为 scss文件,
    package.xml
    xxx.editorConfig.ts   // xml配置信息调整,比如某个xml字段的显示隐藏等操作
    xxx.editorPreview.tsx // mendix预览模式下的展示内容
    xxx.tsx  // 组件渲染的内容文件
    xxx.xml  // 配置组件需要用到的参数信息
- typings
    xxx.d.ts  // xml文件配置的字段类型,自动生成的

xml配置项说明

xml 复制代码
<?xml version="1.0" encoding="utf-8" ?>
<widget
    id="mendix.xxx.xxx"
    pluginWidget="true"
    needsEntityContext="true"
    offlineCapable="true"
    supportedPlatform="Web"
    xmlns="http://www.mendix.com/widget/1.0/"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../node_modules/mendix/custom_widget.xsd"
>
    <name>xxx</name>
    <description>My widget description</description>
    <icon />
    <properties>
        <propertyGroup caption="General">
            <!-- Data source分组 -->
            <propertyGroup caption="Data source">
                <property key="myDatasource" type="datasource" isList="true">
                    <caption>Data Source</caption>
                    <description>关联数据源</description>
                </property>
                <property key="mode" type="enumeration" defaultValue="undefined">
                    <caption>Mode</caption>
                    <description>枚举类型</description>
                    <enumerationValues>
                        <enumerationValue key="undefined">Single</enumerationValue>
                        <enumerationValue key="multiple">Multiple</enumerationValue>
                        <enumerationValue key="tags">Tags</enumerationValue>
                    </enumerationValues>
                </property>
                <property key="showCode" type="boolean" defaultValue="false">
                    <caption>Show Prefix</caption>
                    <description>布尔值</description>
                </property>
                <property key="code" type="attribute" dataSource="myDatasource">
                    <caption>Option Prefix</caption>
                    <description>从myDatasource中选取某个字段,需要指定类型</description>
                    <attributeTypes>
                        <!-- 根据字段类型显示字段 -->  
                        <attributeType name="String" />  
                        <attributeType name="AutoNumber" />  
                        <attributeType name="Boolean" />  
                        <attributeType name="DateTime" />  
                        <attributeType name="Decimal" />  
                        <attributeType name="Enum" />  
                        <attributeType name="Integer" />  
                        <attributeType name="Long" />
                    </attributeTypes>
                </property>
                <property key="labelName" type="attribute" dataSource="myDatasource">
                    <caption>Label Name</caption>
                    <description>从myDatasource中选取某个字段,需要指定类型</description>
                    <attributeTypes>
                        <attributeType name="String" />
                    </attributeTypes>
                </property>
                <property key="placeholder" type="string" defaultValue="placeholder">
                    <caption>PlaceHolder</caption>
                    <description>mendix配置的字符串</description>
                </property>
                <property key="isRequired" type="expression" required="true" defaultValue="false">
                    <caption>Is Required</caption>
                    <description>条件表达式,定义返回数据类型</description>
                    <returnType type="Boolean" />
                </property>
            </propertyGroup>
            <!-- Events分组 -->
            <propertyGroup caption="Events">
                <property key="onChangeAction" type="action" required="false">
                    <caption>Value Change</caption>
                    <description>事件</description>
                </property>
            </propertyGroup>
        </propertyGroup>
    </properties>
</widget>

更多文档内容,可参考Mendix组件文档

xml配置项的显示/隐藏方式

  • 修改 xxx.editorConfig.ts 文件

    typescript 复制代码
    import { xxxProps } from "../typings/xxx";
    import { hidePropertyIn } from "@mendix/pluggable-widgets-tools"; // 新增依赖    
    // ...
    // ...
    // ...    
    export function getProperties(_values: xxxProps, defaultProperties: Properties): Properties {
        // 主要代码
        if (!_values.showCode) {
            hidePropertyIn(defaultProperties, _values, "code");
        }
        if (!_values.isRequired) {
            hidePropertyIn(defaultProperties, _values, "errorText");
        }
        return defaultProperties;
    }

组件参数说明

  • 组件默认接收一个对象,默认参数在mendix中可配置且,包含有:

    • name
    • class
    • style:非必传
    • tabIndex:非必传
  • 用法

    typescript 复制代码
    import { ReactElement, createElement } from "react";
    import classNames from "classnames";
    import { xxxProps } from "../typings/xxxProps";
    import "./ui/xxx.scss";
    
    export function xxx(props: xxxProps): ReactElement {
        const { name, class: cls, style, tabIndex } = props;
    
        return (
            <div className={classNames(cls, "v-test-wrap")} style={style}>
                <p>Container</p>
            </div>
        );
    }

如需要用到mendix挂载在window上的mx方法,可在 typings 目录下添加 client.d.ts

typescript 复制代码
declare namespace mx {
    namespace ui {
        type OpenForm2Function = ((
            page: string,
            dh: DisposeCallback,
            title: string,
            currentForm: any,
            option: Option,
            numberOfPagesToClose: number
        ) => Promise<any>) & { ["_tabRouter"]: boolean };
        interface DisposeCallback {
            [key: string]: { unsubscribe: () => void };
        }
        interface Option {
            location: "content" | "popup" | "node";
            domNode?: Element;
        }
        let openForm2: OpenForm2Function;

        let getContentForm: any;
    }
    namespace data {
        type saveDocument = (
            guid: string,
            name: string,
            params: any,
            blob: File,
            callback: () => void,
            errorCallback: (error: any) => void
        ) => Promise<void>;

        type remove = (params: RemoveType) => Promise<void>;

        interface RemoveType {
            guid: string;
            callback: () => void;
            errorCallback: (error: any) => void;
        }

        type action = (
            actionname: any,
            applyto: any,
            guids: any[],
            params: any,
            callback: (result: any) => void,
            errorCallback: (error: any) => void
        ) => Promise<void>;

        type callNanoflow = (
            nanoflow: any,
            context: mx.lib.MxContext,
            origin: mx.lib.form._FormBase,
            callback: (result: any) => void,
            errorCallback: (error: any) => void
        ) => Promise<void>;

        type create = (
            entity: string,
            callback: (guid: string) => void,
            errorCallback: (error: any) => void
        ) => Promise<void>;

        const action = action;
        const callNanoflow = callNanoflow;
        const saveDocument = saveDocument;
        const remove = remove;
        const create = create;
    }
}
declare namespace mendix {
    namespace lib {
        class MxContext {
            setTrackObject(obj: any): void;
        }
    }
    interface Lang {
        getUniqueId(): string;
    }
    let lang: Lang;
}

declare namespace dijit {
    function getUniqueId(id: string): string;
}

declare namespace window {
    namespace vRequestManager {
        function request(url: string, options: any, requestId: string): Promise<any>;
        function vCreateObjFn(obj: any): Promise<any>;
        function cancelRequest(id: string): Promise<void>;
    }
}

mendix工程,默认是不会生成 index.html 文件的,需要手动生成,方式如下

  • mendix 菜单栏 App -> Show App Directory in Explorer

  • xxx/theme/web 目录下,就是项目启动的入口文件所在位置

  • index.html 文件内容

    html 复制代码
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
        <meta http-equiv="Pragma" content="no-cache" />
        <meta http-equiv="Expires" content="0" />
        <meta name="referrer" content="no-referrer" />
        <title>Mendix</title>
        <script type="text/javascript">
          // 定义全局 hash
          globalThis.hash = Math.floor(Math.random() * 0xffffff).toString(16);
          try {
            eval("async () => {}");
          } catch (error) {
            var homeUrl = window.location.origin + window.location.pathname;
            var appUrl = homeUrl.slice(0, homeUrl.lastIndexOf("/") + 1);
            window.location.replace(appUrl + "unsupported-browser.html");
          }
        </script>
      </head>
    
      <body>
        <noscript>To use this application, please enable JavaScript.</noscript>
        <div id="content"></div>
      </body>
    
      <script type="text/javascript">
        dojoConfig = {
          isDebug: false,
          useCustomLogger: true,
          async: true,
          baseUrl: "mxclientsystem/dojo/",
          cacheBust: globalThis.hash,
          rtlRedirect: "index-rtl.html",
        };
        // 需要添加script标签的地方
        const jsSrcList = [ "mxclientsystem/mxui/mxui.js" ];
        for (const src of jsSrcList) {
          const scriptEl = document.createElement("script");
          scriptEl.src = `${src}?s=${globalThis.hash}`;
          document.body.appendChild(scriptEl);
        }
        // 加载 link 标签
        const iconList = [
          // manifest
          {
            rel: "manifest",
            href: "manifest.webmanifest",
            crossorigin: "use-credentials",
          },
          // main.scss 编译之后文件
          { rel: "stylesheet", href: "theme.compiled.css" },
          // icon
          {
            rel: "apple-touch-icon",
            href: "apple-touch-icon.png",
            sizes: "180x180",
          },
          { rel: "icon", href: "icon-32.png", sizes: "32x32" },
          { rel: "icon", href: "icon-16.png", sizes: "16x16" },
          // apple-touch-startup-image
          {
            custorm: true,
            width: 1024,
            height: 1366,
            devicePixelRatio: 2,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 1366,
            height: 1024,
            devicePixelRatio: 2,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 834,
            height: 1194,
            devicePixelRatio: 2,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 1194,
            height: 834,
            devicePixelRatio: 2,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 834,
            height: 1112,
            devicePixelRatio: 2,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 1112,
            height: 834,
            devicePixelRatio: 2,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 810,
            height: 1080,
            devicePixelRatio: 2,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 1080,
            height: 810,
            devicePixelRatio: 2,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 768,
            height: 1024,
            devicePixelRatio: 2,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 1024,
            height: 768,
            devicePixelRatio: 2,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 428,
            height: 926,
            devicePixelRatio: 3,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 926,
            height: 428,
            devicePixelRatio: 3,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 390,
            height: 844,
            devicePixelRatio: 3,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 844,
            height: 390,
            devicePixelRatio: 3,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 360,
            height: 780,
            devicePixelRatio: 3,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 780,
            height: 360,
            devicePixelRatio: 3,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 414,
            height: 896,
            devicePixelRatio: 3,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 896,
            height: 414,
            devicePixelRatio: 3,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 414,
            height: 896,
            devicePixelRatio: 2,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 896,
            height: 414,
            devicePixelRatio: 2,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 375,
            height: 812,
            devicePixelRatio: 3,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 812,
            height: 375,
            devicePixelRatio: 3,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 375,
            height: 667,
            devicePixelRatio: 2,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 667,
            height: 375,
            devicePixelRatio: 2,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 414,
            height: 736,
            devicePixelRatio: 3,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 736,
            height: 414,
            devicePixelRatio: 3,
            orientation: "landscape",
          },
          {
            custorm: true,
            width: 320,
            height: 568,
            devicePixelRatio: 2,
            orientation: "portrait",
          },
          {
            custorm: true,
            width: 568,
            height: 320,
            devicePixelRatio: 2,
            orientation: "landscape",
          },
        ];
        for (const items of iconList) {
          const link = document.createElement("link");
          if (items.custorm) {
            link.rel = "apple-touch-startup-image";
            link.href = `img/startup-image-${
              items.width * items.devicePixelRatio
            }x${items.height * items.devicePixelRatio}.png?${globalThis.hash}`;
            link.media = `screen and (device-width: ${items.width}px) and (device-height: ${items.height}px) and (-webkit-device-pixel-ratio: ${items.devicePixelRatio}) and (orientation: ${items.orientation})`;
          } else {
            link.rel = items.rel;
            if (items.sizes) {
              link.sizes = items.sizes;
            }
            if (items.crossorigin) {
              link.crossorigin = items.crossorigin;
            }
            link.href = `${items.href}?l=${globalThis.hash}`;
          }
          document.head.appendChild(link);
        }
    
        // 处理登录页面的 cookie
        if (!document.cookie || !document.cookie.match(/(^|;) *originURI=/gi)) {
          const url = new URL(window.location.href);
          const subPath = url.pathname.substring(0, url.pathname.lastIndexOf("/"));
          document.cookie = `originURI=${subPath}/login.html${
            window.location.protocol === "https:" ? ";SameSite=None;Secure" : ""
          }`;
        }
      </script>
    </html>
相关推荐
Pedantic2 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘2 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
YFF菲菲兔3 小时前
调度系统和调和系统的桥梁
react.js
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆3 小时前
VSCode自动格式化三要素
前端
爱勇宝4 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen5 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518137 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端