Salesforce LWC学习(三十五) 使用 REST API实现不写Apex的批量创建/更新数据
本篇参考:
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以内。如果感兴趣的小伙伴欢迎自行去进行优化,希望以后有相关需求的小伙伴可以避免踩坑。篇中有错误的地方欢迎指出,有不懂欢迎留言。