使用Firefox访问Lex下的VF页面时csv下载失败的解决办法

众所周知,如果打算做一个纯前端的下载CSV的功能,可以使用如下代码。

let content = "test,test1\nabc,123";
let blob = new Blob([content], { type: 'text/csv' });
let csvUrl = URL.createObjectURL(blob);
let elementLink = document.createElement('a');
elementLink.href = csvUrl;
elementLink.target = '_blank';
elementLink.download = 'test.csv';
elementLink.click();

在DOM中新建了临时的a标签,并执行点击动作。之后就会把content的内容作为csv文件下载下来。
无论是在VF里还是Lightning Component中皆可使用。

在VF页面中嵌入这段代码之后,使用Chrome在Classic与Lex下使用都是正常的。
但是使用Firefox在Classic下正常,在Lex下却会报错失败。
比如在我的实验环境,错误信息为如下。

Content Security Policy: The page’s settings blocked the loading of a resource at blob:https://snrenv2-dev-ed–c.ap4.visual.force.com/016d4701-1e33-47a1-8099-eaec82c7ff1d (“frame-src”).

唔。。。Content Security Policy。(关于什么是CSP,可以参考阮一峰老师的文章或者MDN文档(什么是内容安全策略CSP用法)。
而且是来自于frame-src的CSP设定。

那么就一定存在iframe。
还记得在Classic时代,VF页面的主域名和标准页面的主域名是不同的。
VF:https://snrenv2-dev-ed–c.ap4.visual.force.com/
非VF:https://snrenv2-dev-ed.my.salesforce.com/

Lightning时代可能为了解决这个问题,在Lex的时候,居然将VF嵌套进了iframe里???

图例

这样的话,让我们来看看Salesforce的CSP是怎么设定的。F12->网络->respones header。

Content-Security-Policy:
default-src ‘self’; script-src ‘self’ ‘nonce-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx’ chrome-extension: ‘unsafe-inline’ ‘unsafe-eval’ *.canary.lwc.dev *.ap4.visual.force.com https://ssl.gstatic.com/accessibility/; object-src ‘self’ https://snrenv2-dev-ed–c.ap4.content.force.com; style-src ‘self’ blob: chrome-extension: ‘unsafe-inline’ *.ap4.visual.force.com https://snrenv2-dev-ed–c.ap4.content.force.com; img-src ‘self’ http: https: data: blob: *.ap4.visual.force.com; media-src ‘self’ *.ap4.visual.force.com https://snrenv2-dev-ed–c.ap4.content.force.com blob:; frame-ancestors ‘self’; frame-src https: mailto: *.ap4.visual.force.com; font-src ‘self’ https: data: *.ap4.visual.force.com; connect-src ‘self’ https://api.bluetail.salesforce.com https://staging.bluetail.salesforce.com https://preprod.bluetail.salesforce.com *.ap4.visual.force.com https://snrenv2-dev-ed–c.ap4.content.force.com https://ap4.salesforce.com

可以看到针对iframe的设定是允许https, mailto, vf域名。这明明vf域名是合法的啊。
按照最上面的写法,最后生成的url是blob:https://snrenv2-dev-ed–c.ap4.visual.force.com/016d4701-1e33-47a1-8099-eaec82c7ff1d,甚至将https://snrenv2-dev-ed–c.ap4.visual.force.com/016d4701-1e33-47a1-8099-eaec82c7ff1d直接拷贝到浏览器地址栏可以看到csv内容本身。那么为什么Firefox就根据CSP拦截了,而Chrome却没有拦截呢?

其实互联网的世界,并不是那么风平浪静。
以前在一篇文章里谈到过,计算机的世界其实并不是精确如一尘不染的实验室,而是建立在一系列没那么精确,不那么清晰,不是很靠谱的机制上。能跑就行。
虽然对于各种互联网协议的定义都有专门的组织去管理去发展。但是就如法律条文,协议与规范写的再精确也难免因为理解偏差而出现南辕北辙的情况。
而且从当年微软的IE带头不遵守规范伊始,各家浏览器的兼容性问题就始终困扰着广大开发者。就像你规定了IE,Firefox,Chrome都必须戴帽子出门,但是你会发现IE把帽子戴膝盖上了,Firefox斜着戴,Chrome反着戴——————谁让你没规定怎么戴。

这里关于CSP如何拦截我推测也是类似的情况(查阅浏览器代码实在力不从心),Chrome觉得blob: + 合法域名 = 合法,而Firefox觉得blob: 没有在名单里 = 不合法。

所以为了解决这个问题,我们只能干掉Firefox。我们只能绕开CSP,这是一种自己hack自己的行为,也许在某次Firefox更新或者salesforce更新之后就失效了。慎用。
如果说,VF所在的iFrame收到主页面的CSP策略约束,那么,我在iFrame中再造一个iFrame,外层iFrame没有设定CSP,在我造的iFrame里就可以为所欲为了。
代码如下

 let iframe = document.createElement('iframe');
 document.body.append(iframe);
 iframe.style.display = 'none';
 iframe.addEventListener('load', ()=> {
     let content = "test,test1\nabc,123";
     let blob = new Blob([content], { type: 'text/csv' });
     let csvUrl = URL.createObjectURL(blob);
     let elementLink = document.createElement('a');
     elementLink.href = csvUrl;
     elementLink.target = '_blank';
     elementLink.download = 'test.csv';
     elementLink.click();
 });

原理为,凭空捏造一个iFrame嵌到当前iFrame,然后监听load事件,将下载CSV的代码放入其中。这样当iFrame创建好了之后就会自动开始下载。

慎用。。。慎用。。。慎用

如何写出容易阅读的代码

一. 开篇废话

很多年前,我写过一篇文章叫《关于阅读代码 》。是在2014年写的,那时候刚开始写博客。根据后台统计数据显示,这篇文章这些年来也没什么人看。

其实文章分两种,一种是给自己看的,一种是给别人看的。就像写代码一样,有的代码你自己写的爽就行了,也没打算给别人看;还有一种,写出来的目的就是为了给别人看的。

所以回到《关于阅读代码》这篇文章,当时写的时候更多的是因为随着职业经历的积累自己有一些了感悟,觉得自己有一些绝妙的想法,想记录下来。这篇文章也一样。

作为程序员,工作职责中,可能写代码只占很小的一部分,不过因人而异。如果作为Salesfoce从业者,工作职责中80%以上都是写代码的话,我倒是觉得这其实是一件挺可悲的事情。应该考虑换一个平台或者试试别的角色。

但是,单就开发这部分来说,由于标准功能的存在,代码在Salesforce开发中的比重也不再如传统开发的100%。可以说我们的目标就是要干掉所有代码。

虽然说我们要干掉代码,但是代码却极其重要。毕竟所有标准功能无法实现的功能还是要代码去实现。这时候“对人友善”的代码在Salesforce开发活动中就格外的重要。毕竟在梳理业务流程的时候,谁也不想面对方便面一样的代码皱眉头。或者,被问到头上的时候,只能╮(╯▽╰)╭说:“这个功能好用但我不知道为什么。”

二. 不容易阅读的代码怎么来的

这些年我发现一个现象。习惯疯狂抽象分层的Java程序员,在转Salesforce开发之后往往更容易写出散文诗式流水账代码。然后再进行几次需求变动,代码就变成了方便面。

那么,为什么一个优秀的coder做salesforce之后会写出方便面代码?我觉得原因是接受到的信息的不同。

在Java时代,代码写手在动手之前接受到的信息为具体的功能模块,已经有架构师或同等角色的人将功能完全拆解或者绘制好蓝图。代码写手要做的是实现规划好的功能模块,将自己的模块成功嵌入到大框架内。而Salesforce代码写手接受的信息往往是业务描述——只是说了要做哪些事情,而不是严谨的功能模块划分。

举个例子,就像做意大利面,非Salesforce的厨师被提供了一个精确的食谱和严格的验收标准,要做的是用规定的食材严格按照食谱在指定地点把意大利面做好就行。而Salesforce的厨师只是被告知要做一盘什么味道的意大利面,可能连食材都还没买,到做好上桌之前都要自己搞定。

如果有自由发挥的空间,就像在饭店点蛋炒饭,每个厨师都会做出不同的味道。

继续阅读“如何写出容易阅读的代码”

Javascript版的String.format()

Salesforce的Apex语言作为Java的高度豪华定制版。不仅提供了很多Java原生就有的很好的方法与语法。同时也对Java进行了纷纷总总的简化。
比如Apex的String.format方法。
Apex的format方法提供了一个简单的方式可以对字符串模板进行关键字替换。
下面是官方文档的例子。

String template = '{0} was last updated {1}';
List<Object> parameters = new List<Object> {'Universal Containers', DateTime.newInstance(2018, 11, 15) };
String formatted = String.format(template, parameters);
System.debug ('Newly formatted string is:' + formatted);

如此一来,我们在custom label里做好带{x}字样的字符串模板,就可以随心所欲的动态生成内容了。

但是,随着进入Lightning时代,在Lightning Component前端承担的业务和内容越来越多,随之而来的就是出现了在前端直接获取字符串模板并动态生成内容的需求。
而Aura库和原生Javascript都没有提供类似与Apex的String.format方法。

俗话说,自己动手丰衣足食。那就自己写一个吧。
首先在Helper.js里面添加如下format方法。

    },    //.......... Other Helper Method
    format: function(label, args) {
        for (var k in args) {
            label = label.replace("{" + k + "}", args[k]);
        }
        return label;
    },
  // .......... Other Helper Method

然后在Controller.js中使用的时候,如同Apex版,第一个参数传入模板,第二个参数为按模板参数顺序传入的字符串数组。

let template = "{0} was last updated {1}";
let myLabel = helper.format(template , ["Universal Containers", "2018/11/15"]);

如何在使用e.force:createRecord创建数据之后阻止跳转到数据详细页面

有一天,小明同学和他的同伙们在实现一个需求。

三天前我路过他座位的时候简单的了解了下,感觉功能都实现的差不多了,怎么这会又开始一堆人聚一起叽叽喳喳的乱叫什么”CreateRecord事件”啦,什么“自定义Modal”啦,还让不让人静一静了。

“喂喂喂,怎么了怎么了?”。我实在忍不住上去扒拉开了最外面的几个。
被围在最里面的可怜人回头看到了我,立马趁机脱离开了包围圈。扯了扯衣角,开始介绍起情况来了。“是这样的,这个需求是我几天前给你介绍过的,就是一个Lightning Component上有一个按钮,然后点一下弹出一个新建数据的页面。”

“嗯嗯,然后呢”。我点了点头,心想着果然nothing new under sunshine,还是那个需求。
“然后我决定使用createRecord事件,就是e.force:createRecord。”,说着他扒拉开站在他身后的人,俯身用鼠标高亮了$A.get(“e.force:createRecord”)那行代码。“然后我们发现,居然在EditForm点保存之后,就直接跳到了新建数据的详细页面!”
在我开头之前,他立马切到了e.force:createRecord官方文档的页面,指在上面赶紧补充到,“我看了官方文档,上面也没有可以使用的参数或者选项。所以现在这几个人主张让我自己写一个modal出来,虽然这样一切都可控,但是工程量太大了。所以我们还在争论这件事。”

“主张做modal的先散了吧。”。接下来我做到了座位上,找出了相关的一个Idea 《Allow redirect after creating a new record using force:createRecord》。

小明难以置信的瞪大了眼睛,说:“这上面明明写的是没有这个功能啊。。。”
“上了这么多年的网,还不知道评论才是精华!?”

俗话说,文档上没写的不一定就没有。

——沃·兹几硕德

评论里提到了两个属性,一个是panelOnDestroyCallback,在Summer’19之后单独使用已经无效。另一个是navigationLocation有个固定写法”navigationLocation”:”LOOKUP”。

一起服用才有效果。

var createRecordEvent = $A.get("e.force:createRecord");
createRecordEvent.setParams({
	"entityApiName": "Opportunity",
	"navigationLocation": "LOOKUP",
	"panelOnDestroyCallback": function(event) {
		// Refesh current cmp
		$A.get('e.force:refreshView').fire();
		/* OR direct to anywhere
		let urlDirectEvent = $A.get("e.force:navigateToURL");
		urlDirectEvent.setParams({
			"url": "/lightning/n/XXXX"
		});
		urlDirectEvent.fire();
		*/
	}
});
createRecordEvent .fire();

但是,由于此属性从未出现在官方文档中,所以有可能就像Summer’19失效那次一样,在某一次升级之后又不好用了,使用还需慎重。

SFDX Create Project with Manifest模式下建议的.gitignore模板

俗话说,标题不长没人看。
不过这篇只是做一个存档,方便分享传阅,并没有故事。

如果使用VS Code选择了SFDX Create Project with Manifest,并且使用git进行代码版本管理,建议使用如下.gitignore

/*
!.gitignore
!anonymous.apex
!/force-app
!/manifest

此配置只追踪代码所在文件夹和package.xml所在文件夹。能有效的加快git处理速度。

// Update
解释:
Line 1 忽略此路径下所有文件夹,所属文件,及子文件夹及所属文件
Line 2 不忽略自己(.gitignore)
Line 3 VScode支持apex后缀的文件作为匿名块。所以你可以配和sfdx的命令愉快的储存匿名块重复使用了。公用的匿名块放在anonymous.apex同步到repo,私用的新建一个新的不同步到repo。
Line 4 不忽略代码所在文件夹及所属文件,及子文件夹与所属文件
Line 5 不忽略Package.xml所在文件夹及所属文件,及子文件夹与所属文件

Salesforce执行顺序小口诀

  1. 页面后台两不同,
  2. 布局规则最优先,
  3. 格式长度和必填。
  4. Before Trigger触发前,
  5. 后台只将外键检。
  6. 批量插入有例外,
  7. 验证规则提前验。
  8. Before之后做验证,
  9. 自定规则和必填,
  10. 系统规则不二遍。
  11. 验证之后跑去重,
  12. 存入DB不提交。
  13. After Trigger触发后,
  14. 分配/回复/工作流,
  15. 如果字段有更新,
  16. 验证/去重不再做,
  17. Trigger仅再跑一次。
  18. PB/Flow依次跑,
  19. 数据操作从头走,
  20. Case规则在随后。
  21. 父表汇总此时算,
  22. 工作流把父表更,
  23. 共享规则重计算。
  24. 数据DB提交后,
  25. 后续邮件才发送。

// 逐句解释更新中……
解释:
行1,行2,行3:与使用apex或者api新建或者更新数据相比, 通过Page Layout新建或者更新数据的行为会少许不同。通过Page Layout的时候会根据layout的字段属性配置提前进行一次字段格式(比如邮箱类型,电话号码格式等)/字段长度/字段是否必填等检查,才会进行后续的Trigger处理。
行4,行5:对于通过Apex或者API等方式后台创建/更新数据,虽然不会提前做一次系统级检查,但会对外键做一次检查,确保没有发生引用自己的情况。
行6,行7 :按理说接下来无论是用过Page Layout还是通过后台,下一步都应该进入Trigger处理了吧?还不一定。这里有一个例外形况,就是如果发生了例如Opportunity Line Item或者Quote Line Item的批量创建,则会提前执行一次自定义的Validation Rule。

官方文档地址: https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_order_of_execution.htm

关于CANNOT_CASCADE_PRODUCT_ACTIVE错误

某日,一个悠闲的午后,我泡的一杯毛尖正在阳光下散发着热气。嗯,接到一个小活儿。平平稳稳,不需要经历大风大浪,不需过五关斩六将的小活儿——-导数据。

这次需要更新的数据是Product。就是那个标准SObject,Product2,三位prefix是01t。人们常说用标准SObject用多了总会遇见坑,但人生又何尝不是如此。不是
完全控制在自己手里的东西,只能去了解,去接受,最后习以为常。

果然,这次就掉坑里了。工具执行结束之后,一看Log。在1000条数据里面,有50多条报了如下的错误。
StatusCode: CANNOT_CASCADE_PRODUCT_ACTIVE
Error Message: Cannot update associated pricebook entries without the Manage Price Books permission
然后看了下报错的数据,也没什么特别明显的特征。 貌似是无差别报错。

唔。。。万事不决查文档。
很快就在《SOAP API Developer Guide》定位到了这个Status Code。
https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_concepts_core_data_objects.htm


CANNOT_CASCADE_PRODUCT_ACTIVE
An update to a product caused by a cascade can’t be done because the associated product is active.

《SOAP API Developer Guide》

咦?怎么详描述的与系统返回的信息不一样?


系统反馈给我的信息是“由于没有Manage Price Books Permission, 所以无法更新关联的Pricebook”。只是说由于我没有Manage Price Books权限才报错,但是我更新Product为什么又会在意我有没有Manage Price Books的权限?

而文档给的信息是“由于product是active,所以无法完成由联级更新引起的被关联的product。 ”这写的是啥啥啥?什么叫caused by a cascade,谁是发出动作的人?是我的话我又cascade了谁?谁又Associated了我?

既然《SOAP API Developer Guide》的描述完全不知所云,那么就从系统直接反馈的错误信息入手吧。查了一下使用的账号,Profile和Permission上确实都没有PriceBook的CRUD权限。不过。。。。如果是因为没有PriceBook的权限的话,为什么绝大部分数据还更新成功了?如果真是这样,由于我没有权限,应该一条都不会成功才对。

结合两条截然不同的信息来看。线索指向了两个关键点。
1. PriceBook的权限。
2. Product的active状态。

于是做了一个试验,在没有PriceBook编辑权限的前提下,尝试去变更Product的active状态。果然报出的相同的错误信息。
就是说,如果想更新product的active状态,需要Pricebook的编辑权限。
唔。。。。


故事当然没有到此结束,不甘心的我突发奇想的在本地硬盘搜索了一下这个status code,结果在一本上古文档里发现了这样一段描述。

CANNOT_CASCADE_PRODUCT_ACTIVE

You cannot activate or deactivate this product without also being able to edit pricebooks.

某上古文档

原来错误信息还有第三个版本。这个版本明确的指出了问题的所在。

接下来是无责任脑洞推理没有任何实锤和Evidence。

在Salesforce中,Product和Price Book是多对多的关系。一个product可以有多个Price Book,而Price Book也可以包含多个Product。Price Book Entry就是二者的Junction Object。
如果用workbench查看Price Book Entry上关联Product与Price Book的字段的关系属性,会发现该关系具有联级删除属性(cascadeDelete: true)。
所以在此推定Product与Price Book Entry,Price Book与Price Book Entry之间的关系皆为Master-Detail。
在表权限层级。

在Profile中,如果想要拥有子表的编辑权限,那么至少要拥有父表的View权限。反过来,如果连父表的view权限都没有,则子表没有任何权限。

所以,由于我没有Price Book的任何权限,导致失去Price Book Entry的所有权限。
而根据观察,Product在active状态变化的时候,会同步active状态到所有旗下的Product Book Entry。
在此推测Salesforce在Product上加了一个Trigger/Workflow/Flow/Process Builder用来用来同步active状态,而且在这个处理中特意做了权限检查。如果发现没有Price Book Entry的权限,则去检查Price Book的权限,如果也没有权限,就报错。

这也正是系统直接返回的错误信息的由来:

Cannot update associated pricebook entries without the Manage Price Books permission

至于为何错误信息出现这么多版本,我推测是由于与Product有多对多关系的不仅仅只有Price Book,所以同一个StatusCode从单一情景变为也会复用在其他的情景。所以错误信息在文档里就改成了更加泛指的版本。而这次我遇见的错误是一个很具体的场景,则出现的信息又由泛指的版本变为更加具体的版本。

脑洞完毕。我去加权限了。。

Dataloader与External Id

某日,又见某童鞋双手合十,指尖杵在下巴上,做福尔摩斯状。
忍不住凑了过去 。
此童鞋用眼角瞄到有人过来了,嘴角抽动了一下,欲言又止。

“别难为自己了,说说吧。”团结才是力量,对不对。
“好吧。。。”他长长的吐了一口气,手也放了下来。“事情是这样的,我有一张表叫做order。这个order有个字段叫product,会去lookup一个叫product的表。”。说着,他打开了SObject的详细页面。“现在我需要导入历史订单数据,量很大。”

我点了点头,示意他继续,因为目前为止听起来没什么。
“问题是原始数据的product字段并不是product的id,而是product的code。这样的话我没法直接导入系统。” 在我开口之前他赶紧抢先又说道,“product的种类和order的数据量都实在太大了,实在没办法用vlookup处理。已经死机好几次了。”

我弹开了他的手,接过鼠标打开了product表的定义页面。在自定义字段中找寻那个叫code的字段。唔。。。。External Id勾选了,Unique勾选了。不禁扬起了嘴角。

“看,Product的code是external Id。”我指着屏幕。
“可是我们要导入数据的是Order啊,不是Product。”
“那你先说说看,External Id在Dataloader里怎么用?”
“就是Upsert的时候,可以指定External Id,如果匹配上了就update,没匹配上就insert。”他自信的回答道。
“没错。但是还有一种用法。”
“啊?”

众所周知,lookup字段实际储存的值为ID。这也就意味着,在insert或者update数据的时候,我们要将Id写在lookup字段上。如果写的不是Id,或者Id不存在,Dataloader会告诉我们数据写入失败。
一般的情况下,如果我们只有被lookup的表中一个字段的值(Product Code),就不得不先根据这个值(Product的Code)去查出被lookup表的id(Product的Id),然后放在待更新数据的lookup字段上(Order的Product字段),才能继续执行下去(Order)。

不过,老天关上了你的门,一定会打开你的窗,毕竟你才是你生活的主角,所以有主角光环。

事实上,如果你拥有的值(Product Code)是被Lookup表的External Id(Product的Code),那么在插入数据时,可以让Dataloader根据External Id自动的将lookup字段填入对应的Id。这样就不用人工的去查找替换了。

做法如下。
1. 首先,一如往常,准备好csv,点insert,输入账号密码,选择Order,选择csv路径,下一步。
2. 然后,Create or Edit a Map, Auto-Match Fields to Columns。
3. 接下来就不一样了。在匹配完字段时候,点击Save Mapping。选择路径,填入文件名。保存sdl文件。
4. 找到刚刚保存的sdl文件,用文本编辑器打开。修改如下。保存。

#Mapping values
#Wed Apr 24 19:13:21 CST 2019
# Old 
#PRODUCT__C=product__c
# New 
PRODUCT__C=product__r\:code__c
.........

5. 回到Datalaoder,关掉Mapping Dialog窗口,这次点击Choose an Exsiting Map,选择刚刚保存的sdl文件。
6. 一路next。
7. 恭喜插入成功。(如果Product Code存在的话)

“看,你与成功之间只差一个\:”。我拍了拍了他的肩旁。
“恩人慢走”。

总结:
1. 被lookup表需要有External ID。
2. 在SDL文件中,将lookup字段的__c变成__r(标准字段去掉Id),加上“\:”,加上External Id的API Name。insert或者update的时候使用此sdl。
3. 必须仅且只仅匹配到一条记录才可以成功写入。

参考官方文档:
https://help.salesforce.com/articleView?id=000002783&type=1

后记:
有人问我说,唉,明明写的技术相关的内容,文风为什么这么不正经?
其实原因不外乎两个。第一个是想看正八经儿的说明文就应该去看官方文档,而不是等着官方文档的翻译。Salesforce的文档和知识库是很强大和全面的。第二个是每一篇文章的灵感来源都是真实被提出过的问题,我觉得自古以来,最好的承载知识的方式就是带入情景,讲故事。官方文档似的说明文是告诉你怎么一步一步的用锤子钉钉子,而我是想用故事告诉你可以用锤子来解决钉钉子的问题。
总之,想要一个工具速查手册的话,应该去看官方文档知识库才对。

关于在Component中向Lightning Button传值的方法

某日,
打水途中看到某位同学眉头紧锁双手合十做沉思状,于是乎忍不住凑了过去。
哦。。。原来在写Lightning Component。隐隐约约看到密密麻麻的controller.js代码感觉不妙。
“咳。”为了避免吓到他,我轻咳了一下表示进场。
只见他先是略微抬起头,之后彻底滑落到了椅子上。
“唉,事情是这样的,” 他用沙哑的嗓音说道,“最开始他们要我做一个button,点了之后去后台更新一条数据,将其字段A__c更新为a。”
他把自己从椅子上重新支了起来,指着屏幕说,“这个很简单,我放了一个attribute来存需要更新的数据的id,然后在lightning button的onClick属性指定controller的方法,在controller的方法中cmp.get那个attribute的值,之后传给apex去更新这条数据。”
我微微点头表示赞同。“没毛病。然后呢?”

“然后,他们又说,要再加一个button,点这个button之后,要给这条数据的字段A__c更新为b。OK,我在cmp中加了又一个button,然后在controller里又为了新button新建了一个方法,与之前的方法不同的是,更新的值为空。”
“也不是不可以呀。“
“对,”接下来他双眼空洞的目视前方。“然后他们又让我加三个button,点了之后分别赋值b,c,d。这时候我意识到情况不对,打算在button的conlick事件把值传给controller中的方法,这样就不用每加一个button就新加一个方法了。结果……我发现……lightning button的onclick根本不能设参数,controller的方法参数也是固定的。我没办法了……”

“我明白你的意思了”。我按住了他的肩膀。

在常规前端开发中,在button的click事件中绑定的js函数传递不同的值是常见做法。如果点击button之后,处理相同,仅是需要处理的参数不同的情况应该对代码进行合理的复用。如下例。

let foo1 = function(param1) {
console.log(param1);
}
<div>
    <input type="button" name="Button" onClick="foo1('a')"/>
</div>

但是lightning button(官方文档)的onclick事件,并没有提供直接传递参数的方式。所以我们只能走曲线救国的路线。
其实很简单。
首先,在官方文档里提到,

You can retrieve the button that’s clicked by using event.getSource(). For example, to retrieve the label on the button, use event.getSource().get("v.label").

说明我们可以在controller的方法中拿到event的触发者——被点击的button,然后获得此button上的属性——比如Label。

那么就好办了,首先我们在lightning button上光明正大的设置value属性

<lightning:button label="button1" title="create" onclick="{!c.updateRecord}" value="a"/>

然后在controller的方法中,去get这个button的value属性,接下来正常执行业务逻辑就可以了。

({   
    updateRecord : function (component, event, helper) {
        let targetValue = event.getSource().get("v.value");
        // YOUR STATEMENT HERE ......
    }
})

好了,水凉了,还得重新接一杯。

Salesforce判断字符串是否为合法Id的规则

在去年年初,我们曾经讨论过Salesforce 15位ID与18位ID的话题(《关于salesforce的15位id与18位id》)。
那时候简单介绍了Salesforce的Id的组成,比如头三位为prefix,4到6位为instance code,校检位,18位ID的特性是case safe而不是大小写不敏感,等等。
那么有没有想过,Salesforce是如何知道某个字符串是不是合法的ID的呢?

虽然Apex里有Id与String两种数据类型,但是两者之间的关系却是如此暧昧与纠缠不清。
很多时候本应当使用Id型变量储存Id值,我们用String型变量去储存,然而也不会出问题。
再比如说,在SOQL中,我们在where语句中可以这么写…WHERE Id = ‘0016F000035LFXo ‘…干脆就是用的字符串。
如果是在Apex中,我们也可以使用变量…WHERE Id =: myId …,这里的变量myId无论是Id型还是String型也都不会有问题。所以粗略的看,似乎当作是一回事也没关系。

但是,如果我将字符串”ABCDEFG”赋值给ID型的变量,会怎么样呢?
如我们所料,会报异常,“ System.StringException: Invalid id: ABCDEFG ”。
这说明ID数据类型还是会对Id做合法性检查。
所以引出了最开始的问题,Salesforce是如何知道这个字符串是不是合法的ID的呢?

继续阅读“Salesforce判断字符串是否为合法Id的规则”