ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Dynamics 365设备类型判断:getFormFactor与getClient实战指南

Dynamics 365设备类型判断:getFormFactor与getClient实战指南 1. 为什么设备类型是 Dynamics 365 二次开发绕不开的“隐形参数”如果你做过几年 Dynamics 365 的定制开发大概率遇到过这类需求客户要求手机端打开单据时某些字段不显示或者显示成下拉框PC 端则保持原样又比如移动办公场景里表单保存前要做一次轻量级校验平板和手机的交互习惯不一样校验规则也不能一刀切。这些需求本质上是同一个问题——当前到底运行在什么设备上很多开发者的第一反应是看 UserAgent在 JavaScript 里写一段 navigator.userAgent 正则匹配一下“Android”“iPhone”“iPad”。这种做法在普通网页里没什么问题放到 Dynamics 365 里就容易翻车。原因主要有两个一是 Dynamics 365 客户端不一定只跑在浏览器里它还可能跑在平板 App、手机 App、Outlook 客户端甚至通过软电话/自定义客户端嵌入UserAgent 在这些环境下的表现差异很大二是 UserAgent 本身是可以伪造的企业环境里常有统一的浏览器代理、安全网关改写请求头你拿到的那段 UA 字符串未必能代表真实设备形态。那该怎么办Dynamics 365 早就考虑过这个场景它在官方客户端 API 里暴露了两组非常明确的信息客户端类型Client Type和设备尺寸因子Form Factor。这两组信息是平台在运行时自己感知的比你在页面里拿 UserAgent 猜要靠谱得多。这篇文章要讲的核心就是怎么用这套官方 API 准确判断设备类型并且在不同场景下正确落地。如果你在做表单脚本、PCF 组件、自定义页面或者遇到“插件里能不能拿到设备类型”这类问题这篇内容应该能帮上忙。先说一个容易混淆的前提Dynamics 365 的“官方 API”并不仅仅指 Web APIOData 接口。Web API 是用来操作数据的它本身不知道你是在手机上发请求还是在 PC 上发请求真正能拿到设备类型的是客户端 APIClient API也就是我们在表单脚本、命令栏按钮、Ribbon 工作台里常用的 Xrm 对象。这篇文章会重点讲客户端 API 的用法同时也会把 Web API 和插件层面的边界说清楚免得你查代码的时候走错方向。2. Xrm.client 是获取设备类型的正牌入口getClient 和 getFormFactor 的准确语义先来看一段最基础的代码。在 Dynamics 365 的模型驱动应用里任何表单、字段事件脚本、Ribbon 命令、自定义按钮的 JavaScript 中都可以通过 Xrm.Utility.getGlobalContext().client 拿到一个 client 对象。这个对象上有两个方法直接关系到设备类型getClient()返回当前宿主程序的客户端类型取值为 Web、Outlook、Mobile、UnifiedServiceDesk。getFormFactor()返回当前设备形态取值为 0未知、1手机、2平板、3PC/桌面电脑。很多中文资料把这两个方法混在一起讲实际区别非常大。getClient 告诉你的是“你在哪类应用里”比如你在电脑浏览器上打开返回 Web你在手机 App 里打开返回 Mobile你在桌面版 Outlook 里使用 Dynamics 365 插件可能返回 Outlook。getFormFactor 回答的才是“屏幕尺寸和交互形态”它不看应用外壳是什么只看设备本身的物理类型。这里有个经常让人疑惑的点getClient() Web 并不等于你用的是 PC。举个例子你用手机浏览器打开 Dynamics 365 的 Web 地址如果这个站点没有自动跳转到移动 App浏览器里的客户端类型仍然是 Web但 getFormFactor() 会返回 1手机。反过来你在 PC 上安装了 Dynamics 365 平板 AppTurbine 模式下客户端类型可能是 Mobile但 form factor 是 3。所以如果你要写“手机端做一套交互、PC 端做另一套交互”的逻辑判断标准应该优先用 getFormFactor()而不是 getClient()。我再补充一个概念getClientState()。它返回的是客户端状态比如是否处于在线状态取值为 Online 或 Offline。状态信息偶尔和设备判断有关联但通常不会直接派上用场。我做过的项目里唯一需要用它的情况是当客户端离线时禁用某些依赖服务端字段的 UI 逻辑——这跟设备类型是两码事别混在一起。下面这张表是我在实际排查问题时常用的速查表建议你收藏场景getClient()getFormFactor()典型用途PC 浏览器打开模型驱动应用Web3正常桌面表单、复杂网格操作手机浏览器打开 Web 应用Web1移动 Web 适配手机安装 Dyn365 手机 AppMobile1手机 App 表单优化平板安装 Dyn365 平板 AppMobile2平板布局优化电脑上通过 Outlook 使用Outlook3Outlook 客户端里的记录操作统一服务台USDUnifiedServiceDesk3客服工作台集成从这个表能看出如果你只判断 getClient()最多只能区分“是不是移动 App”但区分不了手机和平板。而 getFormFactor() 正好补齐了这个粒度。所以官方文档实际上也是建议需要区分设备时优先用 getFormFactor()需要区分宿主类型时用 getClient()。两者结合就是完整的设备上下文。还有一个小知识点getFormFactor() 在最新版本中如果是在不可靠环境下调用可能返回 0。比如你在 Dynamics 365 外部页面无 Xrm 上下文的独立 HTML里强行引用客户端 API拿到的就是一个不确定值。所以后面我会讲到怎么构造安全的判断函数避免在无上下文环境里抛出异常。3. 表单脚本落地一个可以直接抄的安全判断函数与 UI 调整示例知道了方法接下来就是最实际的问题——怎么写进表单脚本里。我见过很多同事直接在 OnLoad 事件里这样写if (Xrm.Utility.getGlobalContext().client.getFormFactor() 1) { Xrm.Page.getControl(new_phone).setVisible(false); }这段代码有两个问题。第一Xrm.Page 是老客户端 API在新版模型驱动应用里可能可用但它标记已过时官方建议用 formContext第二没有做任何存在性检查如果脚本被某些自定义页面试图提前执行Xrm 都没准备好直接报错会导致表单加载中断。我推荐的写法是这样的既能判断设备类型又能安全降级function getDeviceFactor() { var formFactor 0; // 默认未知 try { if (typeof Xrm ! undefined Xrm.Utility Xrm.Utility.getGlobalContext) { var client Xrm.Utility.getGlobalContext().client; if (client client.getFormFactor) { formFactor client.getFormFactor(); } } } catch (e) { formFactor 0; } return formFactor; } function getClientType() { var clientType ; try { if (typeof Xrm ! undefined Xrm.Utility Xrm.Utility.getGlobalContext) { var client Xrm.Utility.getGlobalContext().client; if (client client.getClient) { clientType client.getClient(); } } } catch (e) { clientType ; } return clientType; }有了这两个函数你可以在表单 OnLoad 里拿到设备形态然后做对应的 UI 控制。比如我想在手机端把“备注”字段从多行文本框改成只读摘要在 PC 端保持可编辑function onLoad(executionContext) { var formContext executionContext.getFormContext(); var factor getDeviceFactor(); var notesControl formContext.getControl(new_notes); if (!notesControl) return; if (factor 1) { // 手机 notesControl.setVisible(true); formContext.getAttribute(new_notes).setValue( (formContext.getAttribute(new_notes).getValue() || ).substring(0, 50) ... ); // 只读处理避免用户在手机上误编辑超级长文本 notesControl.setDisabled(true); } else { notesControl.setDisabled(false); } }这段逻辑虽然简单但它展示了设备类型在真实业务里的核心价值——同一个表单可以有不同的交互深度。手机屏幕小信息要精炼操作要防误触平板屏幕大可以用更复杂的分栏PC 屏幕最大可以做全功能编辑。用 getFormFactor 做分支比用 CSS 媒体查询更准确因为 Dynamics 365 的移动 App 里网页 CSS 的视口宽度和设备真实物理尺寸并不总是一一对应。我在实际项目里还发现一个细节如果你在表单的 OnLoad 里判断设备类型最好等表单主要字段完成初始化后再操作也就是在一个 setTimeout 或者 afterRender 事件里执行否则某些自定义字段控件还没加载完setVisible/setDisabled 可能不生效。当然如果你的逻辑只是根据设备类型配置某些变量的值不直接操作控件那就无所谓。注意在 Dynamics 365 的标准化脚本里不要依赖 window.outerWidth/outerHeight 这种浏览器宽度来判断设备类型。移动 App 里可能会有自己的导航栏和工具栏浏览器视口宽度不能反映真实设备形态官方 API 才是稳定入口。4. PCF 组件和自定义页面里的设备类型获取同源不同路如果你不是在做表单脚本而是在开发 PCF 组件Power Apps Component Framework或者在做自定义页面Canvas 页面嵌入模型驱动应用获取设备类型的方式略有不同但底层依然是同一个官方数据源。先说 PCF。PCF 组件运行在模型驱动应用中时它的上下文对象context直接暴露了client属性。在你的组件控制类里可以这样拿public updateView(context: ComponentFramework.ContextIInputs): void { const clientType context.client.getClient(); const formFactor context.client.getFormFactor(); // 根据设备类型调整组件内部渲染 if (formFactor 1) { this.container.classList.add(device-phone); } else if (formFactor 2) { this.container.classList.add(device-tablet); } else { this.container.classList.add(device-desktop); } }这里要注意PCF 的 context.client 和表单脚本里的 Xrm.client 本质上是同一套客户端上下文。但 PCF 的类型定义里client接口的getClient方法返回的是字符串getFormFactor返回的是数字类型定义在 types/dynamics-client-api 或官方 npm 包里都有。PCF 组件的好处是可以用真正的响应式 CSS 做布局同时又能拿到设备形态做逻辑分支所以我在开发多端统一组件时经常把设备判断放在组件内部而不是依赖外部脚本。再说自定义页面Custom Page。模型驱动应用中的自定义页面本质上是一个 Canvas 画布它的逻辑运行在宿主应用里但有时候没有像表单脚本那样直接的Xrm全局对象。不过在自定义页面中Power Apps 通过App.RuntimeUtility和组件属性能间接拿到设备信息。一个比较可靠的方式是在模型驱动应用的自定义页面里通过SetProperty或者变量从主应用接收客户端上下文。如果你是在自定义页面的 OnStart 里想判断设备可以尝试读取Param(ClientType)或组件属性绑定但这种方法建设性比较差我后面会讲一个更通用的解法。我这里更推荐的方案是把设备类型判断封装成一个公共 JavaScript 库放在 CDN 上表单脚本和自定义页面里的 Web 资源都引用同一个库函数。这样维护一套判断逻辑任何宿主环境都能用还能统一处理版本迭代。不过你要注意自定义页面里的 Web 资源引用需要借助 PCF 或 iframe不能直接用 HTML 里的script标签引入外部库这是 Dynamics 365 的环境限制。还有一个容易踩的坑在 PCF 组件的测试环境中context.client可能是一个 mock 对象它的getFormFactor()始终返回 0。如果你在组件初始化时就拿这个值做分支测试环境下可能会走到“未知设备”分支。我的习惯是组件里先做降级处理formFactor 为 0 时继续用浏览器 UserAgent 做辅助判断比如function getDeviceFactorWithFallback(context: ComponentFramework.Contextany): number { let factor 0; if (context.client context.client.getFormFactor) { factor context.client.getFormFactor(); } if (factor 0 navigator.userAgent) { const ua navigator.userAgent; if (/(Android|iPhone|iPod)/i.test(ua)) factor 1; if (/(iPad|Tablet)/i.test(ua)) factor 2; } return factor; }这种降级并不可耻——官方 API 没给确定性答案时UserAgent 至少是个不错的参考。但注意官方 API 是主UserAgent 是辅不要颠倒主次。5. 插件和 Power Automate 拿不到设备类型先搞清楚边界再动手很多开发者问得最多的问题之一是我在 Web API 请求里能不能带上设备类型插件里能不能判断当前调用是来自手机还是 PC这里我直接给结论从 Web API 服务端层面你拿不到设备类型。如果你一定想知道必须由客户端显式地把信息传到服务端比如通过自定义请求头、查询参数或者写在实体的某个字段里。这不是说 Web API 没用而是要用对姿势。我们来看一个我处理过的真实场景客户要求移动端提交商机时在服务端插件里检查某个必填字段PC 端允许暂存。实现方式很简单前端在调用 Web API 创建商机时加一个自定义请求头fetch(/api/data/v9.2/opportunities, { method: POST, headers: { Content-Type: application/json; charsetutf-8, Accept: application/json, X-DeviceFormFactor: String(getDeviceFactor()), // 自定义请求头 OData-MaxPageSize: 50 }, body: JSON.stringify({ name: Test Opp }) })然后服务端插件里你可以这样读取请求头public class OpportunityPreCreatePlugin : IPlugin { public void Execute(IServiceProvider serviceProvider) { var context (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); if (context.MessageName ! Create || context.Stage ! 20) return; var deviceFactorHeader string.Empty; if (context.InputParameters.ContainsKey(DeviceFormFactor)) deviceFactorHeader context.InputParameters[DeviceFormFactor]?.ToString(); // 如果你的环境不支持从 InputParameters 拿到请求头可以考虑扩展 HTTP 头 } }不过你很快会发现直接读取自定义请求头在实际环境里并没有想象中那么简单。Dynamics 365 的 Web API 请求在进入插件上下文前自定义请求头并不会自动映射到 InputParameters。更稳妥的做法是不用自定义请求头而是在业务实体里加一个“设备类型”字段客户端创建记录时直接写入字段值服务端插件读取实体字段进行校验。这样完全绕开了请求头映射的坑。我当时的做法是在商机实体上创建一个字段new_deviceformfactor前端在创建前用getDeviceFactor()填进去服务端插件读这个字段做分支。这是最透明、最容易跨端维护的方案。别看它土业务排错时一眼就能看懂比藏在请求头里强得多。至于 Power Automate情况更明确。Power Automate 的 HTTP 请求只能拿到 HTTP 请求头的标准信息像User-Agent偶尔能拿到但一般也不建议依赖。如果你的自动化要从 Dynamics 365 请求里判断设备类型同样需要前端的业务实体字段或者查询参数来传递。另外如果你还在用 Organization ServiceSOAP 端点同样面临这个边界问题。SOAP 请求的Request对象里没有设备形态。所以结论不变设备类型是客户端上下文独有的信息服务端要拿必须由客户端显式传递。6. 踩坑记录UA 模拟、iframe 包装和 Xrm 缺失时的真实误判链路讲完正确用法我分享三个我踩过、也帮别人排查过的坑。这些坑存在的共同背景是你以为你拿到的设备类型是真的但实际采集路径已经被环境篡改了。第一个坑是浏览器开发者工具模拟移动端但 getFormFactor() 不变。有些同行为了调试移动端布局会在 Chrome DevTools 里切换到 iPhone 模拟。表面上看 UserAgent 变成了 iPhone窗口尺寸也变成了 375x812但 Dynamics 365 客户端依然会认为设备是桌面端getFormFactor() 返回 3。为什么因为模型驱动应用的客户端不仅仅是看 UA它还会结合宿主容器、交互事件、触摸能力等综合判断。真正的移动 App 会给平台发送不同的宿主信号而浏览器模拟器没有这些信号。所以遇到“比例看起来对但逻辑走的是 PC 分支”的问题不用怀疑代码只需要检查你是在模拟器里开的还是真机打开的。第二个坑是iframe 嵌套时的 Xrm 对象来源问题。很多项目会在 Dynamics 365 表单里嵌一个 iframe这个 iframe 里可能是一个独立的 HTML Web 资源。如果你在 iframe 内部写Xrm.Utility.getGlobalContext().client.getFormFactor()可能能工作也可能拿不到值这取决于 iframe 的跨域情况和 Dynamics 365 是否允许在 iframe 中访问父级上下文。我遇到的情况是iframe 和表单同域时能拿到跨域嵌入第三方系统时什么也拿不到。这个坑的根源是设备类型是宿主应用的属性iframe 里的内容本质上是一个独立的浏览器环境宿主应用并不保证会把客户端上下文注入进去。解决方案只有一个别在 iframe 里直接判断设备而是把表单脚本判断出的结果用 URL 参数或 postMessage 传给 iframe。比如在表单 OnLoad 里var factor getDeviceFactor(); var frame formContext.getControl(IFRAME_new_area); if (frame) { frame.getSrc(); // 假设你的 iframe 地址支持 device_type 参数 frame.setSrc(frame.getSrc().split(?)[0] ?device_type factor); }iframe 内部再从 URL 参数读取设备类型。这样做有明确的单向数据流不会出现两边各自猜、结果不一致的情况。第三个坑是Xrm 对象在自定义页面或独立 HTML Web 资源中直接缺失。如果你在无模型驱动上下文的页面里比如一个单独的 HTML 文件部署为 Web 资源然后直接在浏览器里访问这个资源Xrm对象是 undefined所有客户端 API 调用都会报错。这时候你要么检查是否在模型驱动应用的navBar范围内打开要么就用我前面给的那个 try/catch 安全函数做降级。在 Dynamics 365 里官方 API 只保证在受支持的主机上下文中存在不要在外部环境强行依赖它。最后一个结合实际的经验当我需要排查设备判断异常时我会在表单 OnLoad 里临时放一段诊断脚本把客户端类型、form factor、UserAgent 三个值都打印到控制台function diag() { var info { clientType: getClientType(), formFactor: getDeviceFactor(), userAgent: navigator.userAgent }; console.log(DeviceContext: JSON.stringify(info)); }打出来的日志能帮你快速确认当前环境返回的设备形态到底是多少。很多时候不是代码逻辑错而是你测试的设备形态和你预期的不同。这个诊断脚本既能在字段脚本中用也能在 PCF 组件里用排查效率极高。如果你正准备实施类似的判断逻辑我的建议是先把getFormFactor当作主力把getClient当作辅助区分宿主类型把 UserAgent 当作最后的兜底。不要只依赖单一来源更不要为了取巧直接读 UA。Dynamics 365 的客户端 API 既然给我们提供了标准答案就用标准答案出问题也能找到明确的责任边界。
返回列表