前端异步在CRM窗体中的使用方式

## 一、异步解决了什么问题?🚀

1. **释放 UI 线程**  
   提升用户体验,避免页面卡顿。
![效果展示](./assets/测试onchange和保存事件.gif)

2. **优化代码结构**  
   减少冗余代码,逻辑更加清晰。
![效果展示](./assets/代码结构优化.png)

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;
}

 

 
 
posted @ 2025-01-16 13:32  KleinBlue_克莱因蓝  阅读(30)  评论(0)    收藏  举报