前端异步在CRM窗体中的使用方式
## 一、异步解决了什么问题?🚀
1. **释放 UI 线程**
提升用户体验,避免页面卡顿。

2. **优化代码结构**
减少冗余代码,逻辑更加清晰。

3. **缩短加载时间**
初始加载事件(如 `Onload` 和按钮 `Enable`)可同时执行,加快窗体加载速度。
## 二、使用的场景
按钮的显隐事件,onchange事件,onload事件,onsave事件,覆盖目前标准窗体缺少异步的场景。
## 三、实践中遇到的问题和解决方案
### 1. 异步方法加载顺序问题
**问题**:即使按顺序引用异步方法,脚本加载顺序不固定,导致后续脚本偶尔无法访问 `REST2` 对象。
**解决方案**:
- 在需要的地方使用 `async/await` 主动加载文件:
```javascript
// 引入 REST2.js
if (!top.REST2) {
varclientUrl = awaitXrm.Page.context.getClientUrl();
awaitloadScript(`${clientUrl}//WebResources/new_REST2.js`);
}
```
- **优点**:`loadScript` 确保异步函数按顺序加载,避免窗体直接引用脚本时可能出现的 `REST2 is not defined` 错误。
---
### 2. `loadScript` 加载的脚本作用域问题
**问题**:使用 `loadScript` 加载的文件与当前脚本不在同一 `window` 层,无法访问 `REST2` 对象。
**解决方案**:
1. 使用初始加载方法并声明 `async`:
```javascript
(asyncfunction () {
// your code
})();
```
2. 将类对象挂载到顶层 `window`:
```javascript
if (!top.REST2) {
top.REST2 = REST2;
}
```
### 3. 异步按钮事件与脚本加载的先后顺序问题
**问题**:即使 `Onload` 中已执行 `loadScript`,但按钮的 `Enable` 异步事件可能先于脚本生成。
**解决方案**:
- 封装 `waitForREST2` 方法,确保 `REST2` 加载完成后再使用。
---
### 4. 在 `Onsave` 事件中使用 `async/await` 导致直接保存
**问题**:使用 `await` 时,`Onsave` 事件直接触发保存,可能导致数据错误。
**解决方案**:
1. **阻止保存**:在使用 `await` 前先阻止保存,满足条件后再调用保存:
```javascript
Example.saveFlag = true; // 全局变量设为 true
```
![保存示例]:

2. **声明保存标识**:
![保存标识代码]![]()

## 四、代码与使用示例
REST.js(异步方法库)
/*================================================================== 2024-11-22 Zhui.Yuan -------------------------------------------------------------------- REST v2.0 ===================================================================*/ (async function () { const ORG_VERSION = "9.2"; function REST2(url) { this.Heads = { 'OData-MaxVersion': '4.0', 'OData-Version': '4.0', 'Accept': 'application/json', 'Prefer': 'odata.include-annotations=*', 'Content-Type': 'application/json; charset=utf-8' }; this.ServerUrl = url || (window.Xrm ? Xrm.Page.context.getClientUrl() : ""); this.ApiUrl = `/api/data/v${ORG_VERSION}/`; } REST2.prototype.createTimeoutPromise = function (timeout) { if(!timeout) timeout = 120000 return new Promise((_, reject) => setTimeout(() => reject(new Error('请求超时,请检查网络设置')), timeout) ); }; REST2.prototype.Send = async function (url, method, data) { const fullUrl = this.ServerUrl + this.ApiUrl + url; var timeoutPromise = this.createTimeoutPromise(); // 使用 Promise.race 来并行执行 fetch 和超时 Promise try { const response = await Promise.race([ fetch(fullUrl, { method, headers: this.Heads, body: data ? JSON.stringify(data) : null }), timeoutPromise // 超时的 Promise ]); // 如果请求不成功,抛出错误 if (!response.ok) { const errorData = await response.json(); throw new Error(`Error: ${response.status} - ${errorData.error.message || response.statusText}`); } // 处理没有内容的响应 return response.status !== 204 ? await response.json() : null; } catch (error) { // 捕获并处理超时和其他错误 console.error(`Error in Send [${method}]:`, error.message); throw error; } }; REST2.prototype.create = async function (entitySet, data) { return await this.Send(entitySet, 'POST', data); }; REST2.prototype.update = async function (entitySet, id, data) { const requestURL = `${entitySet}(${id.replace(/{|}/g, '')})`; return await this.Send(requestURL, 'PATCH', data); }; REST2.prototype.del = async function (entitySet, id) { const requestURL = `${entitySet}(${id.replace(/{|}/g, '')})`; return await this.Send(requestURL, 'DELETE'); }; REST2.prototype.get = async function (url) { return await this.Send(url, 'GET'); }; REST2.prototype.execFetchXml = async function (entitySet, fetchXml) { const url = `${this.ServerUrl}${this.ApiUrl}${entitySet}?fetchXml=${encodeURI(fetchXml)}`; var timeoutPromise = this.createTimeoutPromise(); // 使用 Promise.race 来并行执行 fetch 和超时 Promise try { const response = await Promise.race([ fetch(url, { method: "GET", headers: this.Heads }), timeoutPromise // 超时的 Promise ]); if (!response.ok) { const errorData = await response.json(); throw new Error(`Error: ${response.status} - ${errorData.error.message || response.statusText}`); } return response.json(); } catch (error) { console.error("Error in execFetchXml:", error.message); throw error; } }; REST2.prototype.excuteAction = async function (actionName, object) { const fullUrl = this.ServerUrl + this.ApiUrl + actionName; var timeoutPromise = this.createTimeoutPromise(); // 使用 Promise.race 来并行执行 fetch 和超时 Promise try { const response = await Promise.race([ fetch(fullUrl, { method: "POST", headers: this.Heads, body: JSON.stringify(object) }), timeoutPromise // 超时的 Promise ]); if (!response.ok) { const errorData = await response.json(); throw new Error(`Error: ${response.status} - ${errorData.error.message || response.statusText}`); } return response.json(); } catch (error) { console.error("Error in excuteAction:", error.message); throw error; } }; // 挂载到顶层window对象 if (!top.REST2) { top.REST2 = REST2; } })();
Example.js (使用示例)
/** * REST2调用示例 * @auth : YuanZhui * @date : 2024.1125 */ var executionContext; var Example = Example || {}; Example.saveFlag = false; // 全局保存标识 var REST; // 初始加载事件 Example.Onload = async (context) => { executionContext = context; // 引入REST2.js if (!top.REST2) { var clientUrl = await Xrm.Page.context.getClientUrl(); await loadScript(`${clientUrl}//WebResources/new_REST2.js`); } if (!REST) REST = await waitForREST2(); var queryCurrency = "new_currencies?$select=new_currencyid,new_name&$filter=new_name eq 'CNY' and statecode eq 0"; const value1 = await REST.get(queryCurrency); if (value1.length > 1) { //币种 SetLookupValue("new_currency", "new_currency", value1[0].new_currencyid, value1[0].new_name, executionContext); } var queryBusinessunit = "businessunits?$select=name,new_unique_socialcode,new_address&$filter=name eq '日立电梯(中国)有限公司'"; const value = await REST.get(queryBusinessunit); if (value.length > 0) { //申请人名称 SetLookupValue("new_nameofapplicant", "businessunit", value[0].businessunitid, value[0].name, executionContext); //新建申请单时申请人相关信息默认赋值 SetValue(executionContext, "new_applicantscc", value[0].new_unique_socialcode); SetValue(executionContext, "new_applicantaddress", value[0].new_address); } //合同如发生变化,带出相关值 var contract = Xrm.Page.getAttribute("new_contract"); contract.addOnChange(Example.ContractOnchange); }; // 保存事件 Example.Onsave = async function (context) { if (!Xrm.Page.data.entity.getIsDirty()) { // 有更改才进入保存校验 return; } // 为true时跳过验证,下一次保存恢复验证 if (Example.saveFlag) { Example.saveFlag = false return; } // 普通校验 var new_nameofcontract = GetValue(executionContext, "new_nameofcontract"); var new_contractsigndate = GetValue(executionContext, "new_contractsigndate"); var isCheck = new_nameofcontract == null || new_contractsigndate == null; if (isCheck) { CrmDialog.alert("WARNING", "当前保函为履约保函,请填入相应合同名称和合同签订日期"); context.getEventArgs().preventDefault(); return } // 若受益人名称变更,则校验【受益人名称】和客户.【客户名称】是否相同,若不相同弹出提示。若确认无误,可点击确认按钮强制保存 var new_beneficiary = GetValue(executionContext, "new_beneficiary"); // 受益人名称字段值有变化时 才进行校验 var accountName = ""; var account = GetValue(executionContext, "new_account"); if (account != null) { var accountId = account[0].id.replace("{", "").replace("}", ""); //查询客户 var queryAccount = "accounts(" + accountId + ")?$select=name"; // 异步校验 context.getEventArgs().preventDefault(); // 在使用await前先阻止保存,满足后再调用保存 Xrm.Utility.showProgressIndicator("保存校验"); var queryAccount = "accounts(" + accountId + ")?$select=name"; var responseAccount = await REST.get(queryAccount); // 模拟多个请求等待时长 var responseAccount1 = await REST.get(queryAccount); var responseAccount2 = await REST.get(queryAccount); //统一社会信用代码 Xrm.Utility.closeProgressIndicator(); if (responseAccount) { //统一社会信用代码 accountName = responseAccount["name"]; } } if (new_beneficiary != accountName) { context.getEventArgs().preventDefault(); CrmDialog.confirm("提示", "受益人与客户不同,请检查并确认受益人统一社会信用代码与受益人地址!若确认无误,可点击确认按钮保存当前申请单", () => { Example.saveFlag = true; // 将全局变量设为true 下一次保存时直接跳过验证 Xrm.Page.data.save(); }, () => { }) } }; // onchange事件 Example.ContractOnchange = async ()=> { // 合同清空后,所有相关赋值的字段都需要清空 SetValue(executionContext, "new_appointmentformat", null); SetValue(executionContext, "new_applydesc", null); var contract = GetValue(executionContext, new_contract); if (contract != null) { var contractId = contract[0].id.replace("{", "").replace("}", ""); //查询合同 var queryContract = "new_contracts(" + contractId + ")?$select=_new_keyaccount_r1_value,new_totalamount,_new_opportunity_r1_value,_new_account_r1_value,_new_businessunit_r1_value,new_contractdate,new_othercontnum"; var responseText = await REST.get(queryContract); //合同签订日期 if (IsOk(responseText.new_contractdate)) { SetValue(executionContext, "new_contractsigndate", new Date(responseText["new_contractdate"])); } //商机 if (IsOk(responseText._new_opportunity_r1_value)) { SetLookupValue("new_opportunity", "opportunity", responseText._new_opportunity_r1_value, responseText["_new_opportunity_r1_value@OData.Community.Display.V1.FormattedValue"], executionContext); //商机相关字段 var queryOpportunity = "opportunities(" + responseText._new_opportunity_r1_value + ")?$select=name,new_number,new_style"; var responseOpportunityText = await REST.get(queryOpportunity, executionContext); //项目名称 if (IsOk(responseOpportunityText.name)) { SetValue(executionContext, "new_projectname", responseOpportunityText.name); } } } }; // 按钮点击事件调用Action Example.ExcuteActionBtn = async () => { var formContext = executionContext.getFormContext(); var entityId = formContext.data.entity.getId().replace("{", "").replace("}", ""); var parameters = {}; parameters.entityId = entityId; Xrm.Utility.showProgressIndicator("同步中"); var response = await REST.excuteAction("new_Action", parameters); Xrm.Utility.closeProgressIndicator(); if (response.OutPutResult) { var data = JSON.parse(response.OutPutResult); if (data.issuccess == true) { CrmDialog.alert("INFO", "同步成功!"); Xrm.Page.data.refresh(); } else { CrmDialog.alert("WARNING", data.errMsg); } } }; // 按钮点击事件调用execFetchXml Example.TestAsyncBtn = async () => { var contract = GetValue(executionContext, "new_contract"); if (!contract) { alert("合同号为空") return; } Xrm.Utility.showProgressIndicator("开始查询"); var fetchXml = `<fetch> <entity name="new_contract"> <attribute name="new_contractid" /> <attribute name="new_other2" /> <attribute name="new_other2name" /> <attribute name="new_creditgrade" /> <attribute name="new_revisedcontent" /> <attribute name="new_revisedcontentname" /> <attribute name="new_modify_quantity" /> <filter type="and"> <condition attribute="new_contractid" operator="eq" value="${contract[0].id}" /> </filter> </entity> </fetch>`; var response = await REST.execFetchXml("new_contracts", fetchXml); console.log(response); Xrm.Utility.closeProgressIndicator(); } // 按钮异步显隐事件 Example.TestAsyncBtnEnable = async () => { var queryCurrency = "new_currencies?$select=new_currencyid,new_name&$filter=new_name eq 'CNY' and statecode eq 0"; const value1 = await REST.get(queryCurrency); if (value1) { //币种 return true; } return false; }

浙公网安备 33010602011771号