本篇参考: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 informationh4> <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的常用处理以及解析方式的简单实现。篇中有错误还请指出,有项目更优方案还请不吝赐教,有不懂欢迎留言。