普通视图

发现新文章,点击刷新页面。
昨天以前首页

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

组件目录说明

- 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 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 文件
    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:非必传
  • 用法
    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

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 文件内容
    <!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>
    

Three.js实现低代码开发的两种模式

作者 答案answer
2025年6月3日 16:52

前言

加载和渲染3D模型是 three.js 的核心功能之一,在许多和3D相关的业务中许多的功能都会围绕着3d模型进行实现

传统的业务开发过程中我们对three.js3D场景内容(材质,位置,缩放,相机,灯光,场景)都是通过代码设置参数值的方式来实现的。

通过手动修改代码参数值的方式来修改three.js3D场景内容,也就意味着需要花费更多的时间成本和维护代价

本篇以作者个人的角度给大家分享一下如何使用作者自己开发的three.js3D编辑器工具,实现基于three.js3D模型和3D场景的低代码开发模式

模式一:使用three.js3D模型可视化编辑系统

项目在线链接:

three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/threejs-3dm…

git地址:gitee.com/ZHANG_6666/…

首先对需要编辑的模型内容进行在线修改

这里我们对three.js的场景、模型材质、灯光、动画、标签内容进行了修改和调整

image.png

然后点击 “嵌入代码” 按钮 将弹框内的代码复制粘贴到自己的项目中去

image.png

这里我们新建一个 .html文件,然后将复制的内容添加到 body 标签中去

image.png

感兴趣的同学也可以直接将这段代码复制到你自己的项目中去,试试预览效果

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=`, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <iframe
      width="700"
      height="400"
      src="https://three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/threejs-3dmodel-edit/modelIframe?modelIframe?modelConfig={'background':{'visible':true,'type':3,'image':'https://three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/threejs-3dmodel-edit/static/jpg/model-bg-3.jpg','viewImg':'https://three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/threejs-3dmodel-edit/static/png/view-14.png','color':'#000','blurriness':1,'intensity':1},'material':{'materialType':'','meshList':[{'meshName':'Object_13','color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_14','color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_16','meshFrom':89,'color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_18','color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_20','color':'rgb(255,255,255)','opacity':1,'depthWrite':true,'wireframe':false,'visible':true,'type':'MeshStandardMaterial'},{'meshName':'Object_22','color':'rgb(255,255,255)','opacity':0.066063921416024,'depthWrite':false,'wireframe':false,'visible':true,'type':'MeshPhysicalMaterial'}]},'animation':{'visible':true,'animationName':'Armature|ON Gun Move','loop':'LoopRepeat','timeScale':1,'weight':1,'rotationVisible':true,'rotationType':'y','rotationSpeed':1.44},'attribute':{'visible':true,'gridHelper':false,'x':0,'y':-0.59,'z':-0.1,'positionX':0,'positionY':-1,'positionZ':0,'divisions':18,'size':6,'color':'#FFFFFF','axesHelper':false,'axesSize':1.8,'rotationX':0,'rotationY':401.7855999999079,'rotationZ':0},'light':{'ambientLight':true,'ambientLightColor':'#fff','ambientLightIntensity':0.8,'directionalLight':false,'directionalLightHelper':true,'directionalLightColor':'#fff','directionalLightIntensity':5,'directionalHorizontal':-1.26,'directionalVertical':-3.85,'directionalSistine':2.98,'directionShadow':true,'pointLight':false,'pointLightHelper':true,'pointLightColor':'#1E90FF','pointLightIntensity':10,'pointHorizontal':-4.21,'pointVertical':-4.1,'pointDistance':2.53,'spotLight':false,'spotLightColor':'#00BABD','spotLightIntensity':900,'spotHorizontal':-3.49,'spotVertical':-4.37,'spotSistine':4.09,'spotAngle':0.5,'spotPenumbra':1,'spotFocus':1,'spotCastShadow':true,'spotLightHelper':true,'spotDistance':20},'stage':{'meshPositionList':[{'name':'Object_13','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_14','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_16','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_18','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_20','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}},{'name':'Object_22','rotation':{'x':0,'y':0,'z':0},'scale':{'x':1,'y':1,'z':1},'position':{'x':0,'y':0,'z':0}}],'glow':false,'threshold':0.05,'strength':0.6,'radius':1,'decompose':0,'transformType':'translate','manageFlag':false,'toneMappingExposure':2,'color':''},'tags':{'dragTagList':[{'uuid':'003d8b55-05bf-48bc-8338-bb889b310afa','positionX':1.5823441552419992,'positionY':0.7765179838633216,'positionZ':0.8692105695891963,'innerText':'3D模型可视化编辑系统','width':60,'height':40,'fontSize':6,'backgroundColor':'rgba(0,127,127,0.45787380294404106)','color':'#ffffffbf','iconColor':'#fff','iconSize':12,'iconName':'AlarmClock','hover':false},{'uuid':'794bd956-c849-44fa-b21f-7f15ac69d639','positionX':-1,'positionY':0.9,'positionZ':2.1,'innerText':'3D直升机模型','width':60,'height':20,'fontSize':7.5,'backgroundColor':'#1F93FF','color':'#ffffffbf','iconColor':'#fff','iconSize':12,'iconName':'AlarmClock','hover':false},{'uuid':'576a8d9c-a751-44fb-854b-6984a64755ac','positionX':0.9562807420406231,'positionY':1.7154177438803084,'positionZ':0.9978499692275414,'innerText':'低代码开发','width':60,'height':18,'fontSize':10,'backgroundColor':'#90EE90','color':'#ffffffbf','iconColor':'#fff','iconSize':5.3,'iconName':'Basketball','hover':false},{'uuid':'71df0bc3-1b4d-4810-b47b-d309cca5aeff','positionX':-0.57,'positionY':1.6,'positionZ':1.7,'innerText':'作者:answer             \n项目地址:https://three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/threejs-3dmodel-edit/','width':88,'height':40,'fontSize':6,'backgroundColor':'rgba(0,127,127,0.43406815604267823)','color':'#FFFFFF','iconColor':'#fff','iconSize':12,'iconName':'Bell','hover':false}]},'camera':{'x':1.089360064112835,'y':1.9688846519393688,'z':6.265629858039992},'fileInfo':{'name':'直升机','key':'aircraft','fileType':'glb','id':17,'animation':true,'scale':0.2,'filePath':'threeFile/glb/glb-17.glb','icon':'https://three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/threejs-3dmodel-edit/static/png/15.png'}}"
      allowfullscreen
    ></iframe>
  </body>
</html>

预览效果

image.png

ok,这样一个3D模型的编辑内容效果就可以直接展示在你的项目中去了

模式二:使用three.js 场景编辑器

模式一主要是通过将 three.js 的编辑参数以json对象这样的数据格式存储起来,然后在three.js内容加载完成后在通过调用一个特定的方法将这些编辑参数手动设置回显的方式来实现的

但是如果我们要在一个场景中加载多个模型时,如果使用模式一这种方式的话整个页面的初始化加载的渲染性能就会变得十分的差

在 three.js 场景编辑器项目中为了解决这一问题,也是对three.js编辑数据的存储方案做了另一种方法的实现

这里主要是通过将three.js scene 数据内容通过 .toJSON 方法转换为 json 数据格式,然后在将数据内容存储在 indexedDB 中(ps:这种方案也同样适用于将数据存储在后端服务中)

three.js场景编辑器项目在线链接:

three3d-0gte3eg619c78ffd-1301256746.tcloudbaseapp.com/threejs-mod…

这里我们添加多个模型和多个灯光等内容,并且对其内容进行在线编辑调整

image.png

点击“场景”按钮下的“保存场景”按钮

这里保存作者是保存在浏览器的 indexedDB 中的(懒得写服务端接口了),同样也适用于将数据内容通过接口保存到后端

保存成功后我们就可以通过在预览页面查看完整编辑的内容了

image.png

当然如果你不想直接将编辑的数据存储在indexedDB 或者服务端 中,也可以通过 “场景”按钮下的“导出场景(.json)” 按钮和 “导入场景(.json)”按钮 来实现

image.png

image.png

结语

以上就是作者在开发three.js中实践和总结出的两种基于3D模型的低代码开发模式,减少我们在three.js开发过程中针对3D模型操作的复杂度

如果你有更好的实现方案,欢迎留言交流沟通

❌
❌