本篇參考:
https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch
salesforce零基礎學習(一百零三)項目中的零碎知識點小總結(五)
https://jeremyliberman.com/2019/02/11/fetch-has-been-blocked-by-cors-policy.html
我們在學習LWC的時候,使用 wire adapter特別爽,比如 createRecord / updateRecord,按照指定的格式,在前端就可以直接將數據的創建更新等操作搞定了,lwc提供的wire adapter使用的是 User Interface API來實現。當然,人都是很貪婪的,當我們對這個功能使用起來特別爽的時候,也在疑惑為什么沒有批量的創建和更新的 wire adapter,這樣我們針對一些簡單的數據結構,就不需要寫apex class,這樣也就不需要維護相關的test class,不需要考慮部署的時候漏資源等等。那么,針對批量數據的場景,是否有什么方式可以不需要apex,直接前台搞定嗎?當然可以,我們可以通過調用標准的rest api接口去搞定。
ContactController.cls
public with sharing class ContactController { @AuraEnabled(cacheable=true) public static List<Contact> getContacts() { return [ SELECT AccountId, Id, FirstName, LastName, Title, Phone, Email FROM Contact limit 10 ]; } @AuraEnabled public static string updateContacts(Object data) { List<Contact> contactsForUpdate = (List<Contact>) JSON.deserialize( JSON.serialize(data), List<Contact>.class ); try { update contactsForUpdate; return 'Success: contacts updated successfully'; } catch (Exception e) { return 'The following exception has occurred: ' + e.getMessage(); } } }
datatableUpdateExample.html
<template> <lightning-card title="Datatable Example" icon-name="custom:custom63"> <div class="slds-m-around_medium"> <template if:true={contact.data}> <lightning-datatable key-field="Id" data={contact.data} columns={columns} onsave={handleSave} draft-values={draftValues}> </lightning-datatable> </template> <template if:true={contact.error}> <!-- handle Apex error --> </template> </div> </lightning-card> </template>
datatableUpdateExample.js
import { LightningElement, wire, api } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import updateContacts from '@salesforce/apex/ContactController.updateContacts';
const COLS = [
{ label: 'First Name', fieldName: 'FirstName', editable: true },
{ label: 'Last Name', fieldName: 'LastName', editable: true },
{ label: 'Title', fieldName: 'Title' },
{ label: 'Phone', fieldName: 'Phone', type: 'phone' },
{ label: 'Email', fieldName: 'Email', type: 'email' }
];
export default class DatatableUpdateExample extends LightningElement {
columns = COLS;
draftValues = [];
@wire(getContacts)
contact;
async handleSave(event) {
const updatedFields = event.detail.draftValues;
await updateContacts({data: updatedFields})
.then(result => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Contact updated',
variant: 'success'
})
);
// Display fresh data in the datatable
refreshApex(this.contact).then(() => {
this.draftValues = [];
});
}).catch(error => {
console.log(JSON.stringify(error));
if(error.body) {
console.log(JSON.stringify(error.body));
} else if(error.detail) {
console.log(JSON.stringify(error.detail));
}
this.dispatchEvent(
new ShowToastEvent({
title: 'Error updating or refreshing records',
//message: error.body.message,
variant: 'error'
})
);
});
}
}
結果展示:

點擊以后

我們在上一篇講述了標准的rest api,那OK,我們可以嘗試不適用后台apex方式去搞定,而是在前台通過rest api去玩一下,說到做到,開弄。后台 apex增加獲取session的方法
public with sharing class ContactController { @AuraEnabled(cacheable=true) public static String getSessionId() { return UserInfo.getSessionId(); } @AuraEnabled(cacheable=true) public static List<Contact> getContacts() { return [ SELECT AccountId, Id, FirstName, LastName, Title, Phone, Email FROM Contact limit 10 ]; } }
前端 html / javascript也同樣的改造一下
import { LightningElement, wire, api, track } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
import { refreshApex } from '@salesforce/apex';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import updateContacts from '@salesforce/apex/ContactController.updateContacts';
import getSessionId from '@salesforce/apex/ContactController.getSessionId';
const COLS = [
{ label: 'First Name', fieldName: 'FirstName', editable: true },
{ label: 'Last Name', fieldName: 'LastName', editable: true },
{ label: 'Title', fieldName: 'Title' },
{ label: 'Phone', fieldName: 'Phone', type: 'phone' },
{ label: 'Email', fieldName: 'Email', type: 'email' }
];
export default class DatatableUpdateExample extends LightningElement {
columns = COLS;
draftValues = [];
@track isShowSpinner = false;
@track sessionId;
@wire(getContacts)
contact;
handleSave(event) {
this.isShowSpinner = true;
const updatedFields = event.detail.draftValues;
updatedFields.forEach(item => {
item.attributes = {"type" : "Contact"};
});
let requestBody = { "allOrNone": false, "records": updatedFields };
console.log(JSON.stringify(updatedFields));
getSessionId()
.then(result => {
this.sessionId = result;
fetch('/services/data/v50.0/composite/sobjects/',
{
method: "PATCH",
body: JSON.stringify(requestBody),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + this.sessionId
}
}).then((response) => {
//TODO 可以通過 status code判斷是否有超時或者其他異常,如果是200,則不管更新成功失敗,至少response回來
console.log(response.status);
return response.json(); // returning the response in the form of JSON
})
.then((jsonResponse) => {
console.log('jsonResponse ===> '+JSON.stringify(jsonResponse));
if(jsonResponse) {
jsonResponse.forEach(item => {
if(item.success) {
console.log(item.id + 'update success');
} else {
console.log(item.id + 'update failed');
}
})
}
refreshApex(this.contact).then(() => {
this.draftValues = [];
});
this.isShowSpinner = false;
})
.catch(error => {
console.log('callout error ===> '+JSON.stringify(error));
this.isShowSpinner = false;
})
})
.catch(error => {
//TODO
console.log('callout error ===> '+JSON.stringify(error));
this.isShowSpinner = false;
})
}
}
對應html
<template> <lightning-card title="Datatable Example" icon-name="custom:custom63"> <div class="slds-m-around_medium"> <template if:true={contact.data}> <lightning-datatable key-field="Id" data={contact.data} columns={columns} onsave={handleSave} draft-values={draftValues}> </lightning-datatable> </template> <template if:true={contact.error}> <!-- handle Apex error --> </template> </div> </lightning-card> <template if:true={isShowSpinner}> <lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner> </template> </template>
運行展示:通過下圖可以看到報錯了CORS相關的錯誤,因為跨域進行了請求,這種情況的處理很單一也不麻煩,只需要 setup去配置相關的CORS以及CSP trust site肯定沒有錯

下圖是配置的CSP 以及CORS


但是很遺憾的是,即使配置了這些內容,還是不可以。也征集了群里大神的各種建議意見,各種嘗試擴充了 request header,發現還是不行。因為准備備考integration,所以也就暫時擱置了這個嘗試。周末時間相對充裕,不太甘心的忽然想到了一個事情,不要只看 console的報錯,查看一下network是否有什么有用的信息。
通過這個截圖我們可以看出來,這個http 操作有三次的請求,第一次是跨域的檢查,request method是option,感興趣的可以自己查看

進行了錯誤的這次請求的展開,將 response內容展開,發現了問題

好家伙,盡管console報錯是CORS,但是其實這個問題的rootcause是 請求返回的code是401未授權,打開 rest api 文檔查看一下

破案了,后台通過 UserInfo.getSessionId獲取的session信息無法用於REST API的授權,這里就會有一個疑問,因為艾總發過來了一個VF的demo,是可以通過rest去調用的,難道是vf / lex哪里有區別,或者session有區別?
然后我就做了一個vf去打印一下session信息以及通過apex在lex展示session信息,發現visualforce page通過 GETSESSIONID或者 {!$Api.Session_ID}獲取的session id信息和apexclass獲取的session id不一致,並且 vf 獲取的是可用的。OK,找到了解決方案以后,進行demo的bug fix。
GenerateSessionId.page
<apex:page contentType="application/json"> {!$Api.Session_ID} </apex:page>
ContactController: 只需要修改 getSessionId方法即可
@AuraEnabled(cacheable=true) public static String getSessionId() { return Page.GenerateSessionId.getContent().toString().trim(); }
驗證:搞定

總結:篇中只展示了一下通過 REST API去批量操作數據的可行性,僅作為一個簡單的demo很多沒有優化,異常處理,錯誤處理等等。而且對數據量也有要求,200以內。如果感興趣的小伙伴歡迎自行去進行優化,希望以后有相關需求的小伙伴可以避免踩坑。篇中有錯誤的地方歡迎指出,有不懂歡迎留言。
