## 一、异步解决了什么问题?🚀
1. **释放 UI 线程**
提升用户体验,避免页面卡顿。
![效果展示](./assets/测试onchange和保存事件.gif)
2. **优化代码结构**
减少冗余代码,逻辑更加清晰。
![效果展示](./assets/代码结构优化.png)
3. **缩短加载时间**
初始加载事件(如 `Onload` 和按钮 `Enable`)可同时执行,加快窗体加载速度。
## 二、使用的场景
按钮的显隐事件,onchange事件,onload事件,onsave事件,覆盖目前标准窗体缺少异步的场景。
## 三、使用代码示例
REST2.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 = 120000return 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 和超时 Promisetry {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 和超时 Promisetry {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 和超时 Promisetry {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;} })();
调用示例:
/*** REST2调用示例* @auth : YuanZhui* @date : 2024.1125*/var executionContext; var Example = Example || {}; Example.saveFlag = false; // 全局保存标识var REST; // 初始加载事件 Example.Onload = async (context) => {executionContext = context;// 引入REST2.jsif (!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 = falsereturn;}// 普通校验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; }
## 四、实践中遇到的问题和解决方案
### 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. **声明保存标识**:
![保存标识代码]
---
/**
* 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;
}