Salesforce LWC學習(二十一) Error淺談

zero.zhang發表於2020-08-24

本篇參考:https://developer.salesforce.com/docs/component-library/documentation/en/lwc/data_error

https://developer.salesforce.com/docs/atlas.en-us.uiapi.meta/uiapi/ui_api_errors.htm

在salesforce lwc開發的時候,我們在進行正常的業務處理基礎上,也需要考慮捕捉異常系,對異常的內容根據正確的業務進行跳轉到不同頁面或者展示不同的報錯資訊等處理。通過上面的連線我們可以看到salesforce status code有幾種,常用的頁面開發的報錯資訊可能三種: 400/404/500。對以下的報錯進行列印解析。

 status code : 400

1)使用 wire adapter的 getRecord 搜尋不存在或者FLS沒有訪問許可權的欄位

{
    "status": 400, 
    "headers": {}, 
    "body": {
        "message": "INVALID_FIELD: \nSystemModstamp, Owner.SystemModstamp, Test_No_Access_Field__c, Account.LastModifiedDate\n ^\nERROR at Row:1:Column:347\nNo such column 'Test_No_Access_Field__c' on entity 'Contact'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.", 
        "errorCode": "INVALID_FIELD", 
        "statusCode": 400
    }
}

2)使用wire adapter的 updateRecord,必填欄位或者 restrict等場景導致更新失敗的報錯資訊

{
    "status": 400, 
    "headers": {}, 
    "body": {
        "message": "An error occurred while trying to update the record. Please try again.", 
        "output": {
            "fieldErrors": {
                "Active__c": [
                    {
                        "errorCode": "INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST", 
                        "field": "Active__c", 
                        "duplicateRecordError": null, 
                        "fieldLabel": "Active", 
                        "message": "Active: bad value for restricted picklist field: xx", 
                        "constituentField": null
                    }
                ]
            }, 
            "errors": []
        }, 
        "statusCode": 400, 
        "enhancedErrorType": "RecordError"
    }
}

3. 使用wire adapter的update record時,validation rule / trigger 觸發

{
    "status": 400, 
    "headers": {}, 
    "body": {
        "message": "An error occurred while trying to update the record. Please try again.", 
        "output": {
            "fieldErrors": {}, 
            "errors": [
                {
                    "errorCode": "FIELD_CUSTOM_VALIDATION_EXCEPTION", 
                    "field": null, 
                    "duplicateRecordError": null, 
                    "fieldLabel": null, 
                    "message": "Annual Revenue must over 0", 
                    "constituentField": null
                }
            ]
        }, 
        "statusCode": 400, 
        "enhancedErrorType": "RecordError"
    }
}
{
    "status": 400, 
    "headers": {}, 
    "body": {
        "message": "An error occurred while trying to update the record. Please try again.", 
        "output": {
            "fieldErrors": {
                "AnnualRevenue": [
                    {
                        "errorCode": "FIELD_CUSTOM_VALIDATION_EXCEPTION", 
                        "field": "AnnualRevenue", 
                        "duplicateRecordError": null, 
                        "fieldLabel": "Annual Revenue", 
                        "message": "Annual Revenue must over 0", 
                        "constituentField": null
                    }
                ]
            }, 
            "errors": []
        }, 
        "statusCode": 400, 
        "enhancedErrorType": "RecordError"
    }
}

status code: 404

請求不存在的資源 

{
    "status": 404, 
    "headers": {}, 
    "body": {
        "message": "The requested resource does not exist", 
        "errorCode": "NOT_FOUND", 
        "statusCode": 404
    }
}

static code 500

1)apex方式 validation rule / trigger針對欄位或者表新增報錯資訊

{
    "status": 500, 
    "headers": {}, 
    "body": {
        "fieldErrors": {
            "xxField__c": [
                {
                    "message": "xx field validation", 
                    "statusCode": "FIELD_CUSTOM_VALIDATION_EXCEPTION"
                }
            ]
        }, 
        "pageErrors": [
            {
                "message": "test page level message", 
                "statusCode": "FIELD_CUSTOM_VALIDATION_EXCEPTION"
            }
        ], 
        "index": null, 
        "duplicateResults": []
    }
}

 2)apex方式 null pointer,除零等程式報錯

{
    "status": 500, 
    "headers": {}, 
    "body": {
        "message": "Divide by 0", 
        "isUserDefinedException": false, 
        "exceptionType": "System.MathException", 
        "stackTrace": "Class.xxx.xxx: line xx, column 1"
    }
}

對報錯資訊的結構進行簡單瞭解以後,接下來考慮如何進行公用元件封裝變成一個通用的元件。首先需要考慮的是,哪些是我們需要捕獲的error資訊,然後展示到畫面上,哪些是應該跳轉到ERROR共通畫面的,比如如果呼叫後臺產生了 null pointer等錯誤資訊,毫無疑問應該跳轉到一個公用的訪問錯誤的頁面。不同的專案設計不同的需求有不同的實現。篇中的內容實現如下:

trigger / validation rule / lookup filter等 DML錯誤認為是自定義異常,需要展示在畫面,告訴使用者這些訊息,以便讓他們知道更好的去運算元據。資料許可權以及後臺程式處理的報錯跳轉到共通頁面,聯絡管理員通過debug log去排查。接下來考慮自定義的處理。自定義處理有兩種方式,一種是無表單DML操作,展示toast資訊。另一種是有表單,在頭部或者欄位處展示錯誤資訊。根據這些簡單資訊進行強化。

一. 實裝校驗是否有Error的工具類

這裡errorCheckUtils元件封裝了以下的功能:

  • isSystemOrCustomError:校驗當前的錯誤是屬於系統異常還是屬於自定義異常。這裡的判斷方式其實也比較曖昧。我們在這裡宣告的自定義的異常為 validation rule / trigger或者是restrict或者是有 lookup filter的型別的欄位,其他型別的異常我們歸為系統異常,將會跳轉到自定義error頁面;
  • getPageCustomErrorMessageList:獲取頁面級別的錯誤。這種通常有兩種情況,一個是validation rule中的error location為page級別的,另外一種是trigger中具體的sObject的addError操作;
  • getFieldCustomErrorMessageList:獲取欄位級別的錯誤。返回型別為:[{'key1':'value1','keyn','valuen'}]. 其中 key為表欄位的api 名字,value為具體的報錯。這種通常有兩種情況,一個是validation rule中的error location為field級別,另外一種是trigger中的具體的sObject的某個欄位的addError操作。
  • getPageAndFieldCustomErrorMessageList:獲取頁面和欄位級別總計的錯誤資訊。

我們在看上面的連結可以看出來,errorItem的body可能返回出來一個陣列,這裡進行了簡單的操作,直接獲取了第一個操作。

const isSystemOrCustomError = (errorItem) => {
    let errorBody;
    let isSystemError = false;
    if (Array.isArray(errorItem.body)) {
        errorBody = errorItem.body[0];
    } else {
        errorBody = errorItem.body;
    }

    if(errorBody.pageErrors || errorBody.fieldErrors || errorBody.output.errors || errorBody.output.fieldErrors) {
        isSystemError = false;
    } else {
        isSystemError = true;
    }

    return isSystemError;
}

const getPageCustomErrorMessageList = (errorItem) => {
    let pageErrorMessages = [];
    let errorBody;
    if (Array.isArray(errorItem.body)) {
        errorBody = errorItem.body[0];
    } else {
        errorBody = errorItem.body;
    }

    if(errorBody.pageErrors && Array.isArray(errorBody.pageErrors) && errorBody.pageErrors.length > 0) {
        errorBody.pageErrors.forEach(field => {
            pageErrorMessages.push(field.message);
        });
    } else if(errorBody.output && errorBody.output.errors && Array.isArray(errorBody.output.errors) && errorBody.output.errors.length > 0) {
        errorBody.output.errors.forEach(field => {
            pageErrorMessages.push(field.message);
        });
    }

    return pageErrorMessages;
}

const getFieldCustomErrorMessageList = (errorItem) => {
    let resultMessageList = [];
    let errorBody;
    if (Array.isArray(errorItem.body)) {
        errorBody = errorItem.body[0];
    } else {
        errorBody = errorItem.body;
    }

    let fieldErrors;

    if(errorBody.fieldErrors || errorBody.output.fieldErrors) {
        if(errorBody.fieldErrors) {
            fieldErrors = errorBody.fieldErrors;
        } else {
            fieldErrors = errorBody.output.fieldErrors;
        }

        for(let key in fieldErrors) {
            if (fieldErrors.hasOwnProperty(key)) { // Filtering the data in the loop
                let fieldErrorMessages = fieldErrors[key];
                let errorMessage;
                if(Array.isArray(fieldErrorMessages) && fieldErrorMessages.length > 0) {
                    errorMessage = fieldErrorMessages[0].message;
                } else {
                    errorMessage = fieldErrorMessages.message;
                }
                resultMessageList.push({"key" : key,"value" : errorMessage});
            }
        }
    }
    return resultMessageList;
}

const getPageAndFieldCustomErrorMessageList = (errorItem) => {
    let pageErrorMessages = [];
    let errorBody;
    if (Array.isArray(errorItem.body)) {
        errorBody = errorItem.body[0];
    } else {
        errorBody = errorItem.body;
    }

    if(errorBody.pageErrors && Array.isArray(errorBody.pageErrors) && errorBody.pageErrors.length > 0) {
        errorBody.pageErrors.forEach(field => {
            pageErrorMessages.push(field.message);
        });
    }

    if(errorBody.output && errorBody.output.errors && Array.isArray(errorBody.output.errors) && errorBody.output.errors.length > 0) {
        errorBody.output.errors.forEach(field => {
            pageErrorMessages.push(field.message);
        });
    }

    let fieldErrors;

    if(errorBody.fieldErrors || errorBody.output.fieldErrors) {
        if(errorBody.fieldErrors) {
            fieldErrors = errorBody.fieldErrors;
        } else {
            fieldErrors = errorBody.output.fieldErrors;
        }
        for(let key in fieldErrors) {
            if (fieldErrors.hasOwnProperty(key)) { // Filtering the data in the loop
                let fieldErrorMessages = fieldErrors[key];
                let errorMessage;
                if(Array.isArray(fieldErrorMessages) && fieldErrorMessages.length > 0) {
                    errorMessage = fieldErrorMessages[0].message;
                } else {
                    errorMessage = fieldErrorMessages.message;
                }
                pageErrorMessages.push(errorMessage);
            }
        }
    }

    return pageErrorMessages;
}

export { isSystemOrCustomError, getPageCustomErrorMessageList, getFieldCustomErrorMessageList, getPageAndFieldCustomErrorMessageList};

二. 構築系統錯誤的公共跳轉頁面

1. 這裡我們封裝了一個公共的error跳轉的公用元件 navigationUtils,使用的是navigation,因為navigation沒法直接跳轉到lwc,只能先跳轉到aura,所以實現為aura套殼子來進行實現。這裡需要特別強調的一點,如果你的專案包含了community,需要為community進行一個定製,因為community不支援navigation 傳遞引數,所以以下的內容對community不適用。如何適應community這裡不做展示。因為這裡需要有跳轉操作,所以需要 import NavigationMixin

  • navigationErrorPage:跳轉到 commonErrorPageAura這個aura component,通常 maincomInstance為this;
  • navigationWhenErrorOccur:呼叫上面的方法。
import { NavigationMixin } from 'lightning/navigation';

const navigationErrorPage = (maincomInstance,errorMessage) => {
    maincomInstance[NavigationMixin.Navigate]({
        type: 'standard__component',
        attributes: {
            componentName : 'c__commonErrorPageAura',
        },
        state : {
            c__errorMessage : errorMessage
        }
    });
}

const navigationWhenErrorOccur = (maincomInstance, error) => {
    let errorBody;
    if (Array.isArray(error.body)) {
        errorBody = error.body[0];
    } else {
        errorBody = error.body;
    }
    navigationErrorPage(maincomInstance, errorBody.message);
}

export {navigationErrorPage,navigationWhenErrorOccur};

2. commonErrorPageAura實現

commonErrorPageAura.cmp:因為需要實現跳轉,所以這裡需要 implements="lightning:isUrlAddressable",將error資訊傳遞給子commonErrorPage元件。

<aura:component implements="lightning:isUrlAddressable" access="global">
    <aura:attribute name="errorMessage" type="String"/>
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" ></aura:handler>
    <c:commonErrorPage errorMessage="{!v.errorMessage}"></c:commonErrorPage>
</aura:component>

commonErrorPageAuraController.js:通過pageReference獲取到param資訊然後設定給errorMessage變數

({
    doInit : function(component, event, helper) {
        var myPageRef = component.get("v.pageReference");
        var errorMessage = myPageRef.state.c__errorMessage;
        component.set('v.errorMessage',errorMessage);
    }
})

3. commonErrorPage這個lwc component的實現

commonErrorPage.html

<template>
    <lightning-card>
        <div class='slds-grid slds-grid--vertical slds-align--absolute-center slds-container--large'>
            <div class='slds-align-middle slds-m-bottom--xx-large slds-m-top--xx-large'>
                ERROR picture set here   
            </div>   
            <h4 class='slds-text-align--center slds-text-heading--large slds-text-color--weak slds-m-bottom--small'>error information</h4>
            <p class='slds-text-align--center slds-text-heading--medium slds-text-color--weak'>
                {errorMessage}
            </p>   
        </div>
    </lightning-card>
    
</template>

commonErrorPage.js

import { LightningElement, api } from 'lwc';
export default class CommonErrorPage extends LightningElement {
    @api errorMessage;
}

三. 針對自定義異常的捕捉以及展示實現

這種展示實現不同專案有不同的要求,我們參考標準畫面以及具體的業務大概可以分成兩種展示形式: Toast展示具體錯誤資訊 & form表單中展示page level在頭部,error level在具體欄位資訊。篇幅原因這裡只展示 form表單方式。我們假設有一個edit form表單,要進行了update操作,針對update操作展示不同型別的錯誤資訊操作。

1. errorMessageModal實現:標準的UI錯誤資訊展示如下圖所示,我們扒了以下對應的css以及佈局效果,實現這個errorMessageModal

 

 這裡有三個變數, isShowErrorDiv用來判斷是否展示這個modal,isShowMessage用來判斷是否展示errorList詳細資訊,errorMessageList用來展示具體的page level錯誤資訊。

<template>
    <template if:true={isShowErrorDiv}>
        <div class="pageLevelErrors" tabindex="-1" >
            <div class="desktop forcePageError" aria-live="assertive" data-aura-class="forcePageError">
                <div class="genericNotification">
                    <span class="genericError uiOutputText" data-aura-class="uiOutputText">
                        Review the errors on this page.
                    </span>
                </div>
                <template if:true={isShowMessage}>
                    <ul class="errorsList">
                        <template for:each={errorMessageList} for:item="errorMessageItem">
                            <li key={errorMessageItem}>{errorMessageItem}</li>
                        </template>
                    </ul>
                </template>
            </div>
        </div>
    </template>
</template>

對應的js端展示

import { LightningElement,api,track } from 'lwc';
export default class ErrorMessageModal extends LightningElement {

    @api isShowErrorDiv = false;
    @api errorMessageList = [];
    @track isShowMessage = false;

    renderedCallback() {
        if(this.errorMessageList && this.errorMessageList.length > 0) {
            this.isShowMessage = true;
        } else {
            this.isShowMessage = false;
        }
    }
}

四. 做一個demo,將整體串起來。

 accountEditSample.html:此html用於展示欄位,點選儲存進行save操作

<template>
    <lightning-record-edit-form
        record-id={recordId}
        object-api-name="Account"
        onsubmit={handleSubmit}
        >
        <c-error-message-modal is-show-error-div={isShowErrorDiv} error-message-list={errorMessageList}></c-error-message-modal>
        <lightning-layout multiple-rows="true">
            <lightning-layout-item size="6">
                <lightning-input value={nameValue} label="name" name="accountName" class="accountName" onchange={handleInputChange}></lightning-input>
            </lightning-layout-item>
            <lightning-layout-item size="6">
                <lightning-input value={annualRevenueValue} label="annual revenue" class="accountRevenue" name="accountRevenue" onchange={handleInputChange}></lightning-input>
            </lightning-layout-item>
            <lightning-layout-item size="12">
                <div class="slds-m-top_medium">
                    <lightning-button class="slds-m-top_small" label="Cancel" onclick={handleReset}></lightning-button>
                    <lightning-button class="slds-m-top_small" type="submit" label="Save Record"></lightning-button>
                </div>
            </lightning-layout-item>
        </lightning-layout>
    </lightning-record-edit-form>
</template>

accountEditSample.js:用於載入資料,驗證資料以及儲存資料操作,篇中為了簡單展示效果,對ID使用了hard code,有一些寫法也不是優化的,僅供效果展示

import { LightningElement,track,api,wire } from 'lwc';
import { updateRecord,getRecord } from 'lightning/uiRecordApi';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { NavigationMixin } from 'lightning/navigation';
import { navigationWhenErrorOccur } from 'c/navigationUtils';
import {isSystemOrCustomError,getPageCustomErrorMessageList,getFieldCustomErrorMessageList} from 'c/errorCheckUtils';
import ACCOUNT_ID_FIELD from '@salesforce/schema/Account.Id';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';
import ACCOUNT_ANNUALREVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue';
const fields = [
    ACCOUNT_ID_FIELD,
    ACCOUNT_NAME_FIELD,
    ACCOUNT_ANNUALREVENUE_FIELD
];
export default class AccountEditSample extends NavigationMixin(LightningElement) {

    @api recordId = '0010I00002U8dBPQAZ';
    @track isShowErrorDiv = false;
    @track errorMessageList = [];

    @track nameValue;
    @track annualRevenueValue;

    @wire(getRecord, { recordId: '$recordId', fields })
    wiredAccount({ error, data }) {
        if(error) {
            navigationWhenErrorOccur(this,error);
        } else if(data) {
            if(data.fields) {
                this.nameValue = data.fields.Name.value;
                this.annualRevenueValue = data.fields.AnnualRevenue.value;
            }
        }
    }

    handleInputChange(event) {
        let eventSourceName = event.target.name;
        if(eventSourceName === 'accountRevenue') {
            this.annualRevenueValue = event.target.value;
        } else if(eventSourceName === 'accountName') {
            this.nameValue = event.target.value;
        }
    }

    handleSubmit(event) {
        event.preventDefault();
        const fields = {};
        fields[ACCOUNT_ID_FIELD.fieldApiName] = this.recordId;
        fields[ACCOUNT_NAME_FIELD.fieldApiName] = this.nameValue;
        fields[ACCOUNT_ANNUALREVENUE_FIELD.fieldApiName] = this.annualRevenueValue;
        const recordInput = { fields };
        this.errorMessageList = [];
        this.isShowErrorDiv = false;
        updateRecord(recordInput)
        .then(() => {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Success',
                    message: 'Account updated',
                    variant: 'success'
                })
            );
        }).catch(error => {
            let systemOrCustomError = isSystemOrCustomError(error);
            if(systemOrCustomError) {
                navigationWhenErrorOccur(this,error);
            } else {
                this.isShowErrorDiv = true;
                this.errorMessageList = getPageCustomErrorMessageList(error);
                console.log(JSON.stringify(this.errorMessageList));
                let errorList = getFieldCustomErrorMessageList(error);
                if(errorList && errorList.length > 0) {
                    errorList.forEach(field => {
                        this.reportValidityForField(field.key,field.value);
                    });
                }
            }
        });
    }

    reportValidityForField(fieldName,errorMessage) {
        if(fieldName === 'Name') {
            this.template.querySelector('.accountName').setCustomValidity(errorMessage);
            this.template.querySelector('.accountName').reportValidity();
        } else if(fieldName === 'AnnualRevenue') {
            this.template.querySelector('.accountRevenue').setCustomValidity(errorMessage);
            this.template.querySelector('.accountRevenue').reportValidity();
        }
    }

    handleReset(event) {
        const inputFields = this.template.querySelectorAll(
            'lightning-input'
        );
        if (inputFields) {
            inputFields.forEach(field => {
                field.reset();
            });
        }
    }
}

展示效果

1. 不包含許可權等需要跳轉到自定義error頁面,我們把AnnualRevenue的FLS移除,則當前沒有欄位訪問許可權會報錯

 

 2. 觸發validation或者trigger等效果

 

 總結:篇中簡單介紹了一下lwc中針對error的常用處理以及解析方式的簡單實現。篇中有錯誤還請指出,有專案更優方案還請不吝賜教,有不懂歡迎留言。

相關文章