Aura中调用服务器端方法的通用方法

// In helper
callServerAction : function(component, action) {
	return new Promise(function(resolve, reject) {
		action.setCallback(this, function(response) {
			let _state = response.getState();
                	if (_state === "SUCCESS") {
                    		resolve(response.getReturnValue());
                	} else {
                    		let errors = response.getError();
                    		let message = "Error";
                    		if (errors && Array.isArray(errors) && errors.length > 0) {
                        		message = errors[0].message;
                    		}
                		reject(new Error(message));
                	}
            	});
            	$A.enqueueAction(action);
	});
},

// In Controller
doMethod : function(component, event, helper) {
        let myAction = component.get("c.doSomething");
        myAction.setParams({
            "param1": param1
        });
        let myAcitonPromise = helper.callServerAction(component, myAction);
        myAcitonPromise.then(function(_returnValue) {
             // DO SOMETHINT OR CONTINUE PROMISE
	}).catch(function(_error) {
    		console.error(_error);
	}).finally(function() {
	});

}

Aura中Javascript版的String.replace()

作为Java的高度定制豪华版,Apex的String对象提供了三个与字符串替换相关的函数,replace(), replaceAll(), replaceFirst()。其中replace负责用字符串进行替换,replaceAll()负责用正则表达式进行替换,replaceFisrt负责用正则表达式替换第一个匹配到的结果。

在Apex范畴,这三个方法已经足够日常使用。但是在Javascript领域状况有所不同。
在Javascript中也有个函数叫做replace(),但却复杂很多。

首先引用来自官方文档的例子

const p = 'The quick brown fox jumps over the lazy dog. If the dog reacted, was it really lazy?';

const regex = /dog/gi;

console.log(p.replace(regex, 'ferret'));
// expected output: "The quick brown fox jumps over the lazy ferret. If the ferret reacted, was it really lazy?"

console.log(p.replace('dog', 'monkey'));
// expected output: "The quick brown fox jumps over the lazy monkey. If the dog reacted, was it really lazy?"

Javascipt的replace同时支持字符串替换与正则表达式替换,并且支持特殊替换符等高级功能。
但是,此replace却非彼replace。
Apex的replace使用字符串替模式会更加直白一些,直接使用 a.replace(b,c); 就能将a中的b替换成c,注意,是所有的b都被替换c。
而Javascript的字符串替换模式则仅替换第一个匹配项。与Apex的replaceFirst方法效果等同。

这样的话,在Lightning Component中如果打算实现与Apex中的replace一样的效果,就需要启用正则表达式模式。

在Helper添加如下方法:

    replace : function(content, reg, replacement, isCaseSensitive) {
        let mode = 'g';
        if(!isCaseSensitive) {
            mode = mode + 'i';
        }  
        let re = new RegExp(reg, mode);
        if(content !== null && content !== undefined && content !== "") {
            content = content.replace(re, replacement);
        }
        return content;
    }

虽然无法像原生方法一样直接写成a.replace(b,c); 但好在仍然是一行代码调用。

	init : function(component, event, helper) {
            const p = 'The quick brown fox jumps over the lazy dog. If the dog reacted, was it really lazy?';
            const reg = 'Dog';
            const replacement = 'ferret';
            let result = helper.replace(p, reg, replacement);
            console.log('result', result);
            // result The quick brown fox jumps over the lazy ferret. If the ferret reacted, was it really lazy?
        }

如果需要打开大小写敏感模式,则直接增加第四个参数 helper.replace(p, reg, replacement, true); 即可。

Winter’21升级导致Aura的createRecordEvent中Datetime赋值行为变化

总所周知,Winter‘21在发布此文时已经正式上线。
其中,由于对Aura使用的Apex的权限采取了更严格的标准,从而导致了大批功能崩溃的惨剧。

除此之外,还有另一个Aura的底层变化也值得注意。
在最新的文档中,force:createRecord这个event的文档下面增加了一行小字

Date and time field values must use the ISO 8601 format. For example:

Date: 2017-07-18
Datetime: 2017-07-18T03:00:00Z
While the create record panel presents datetime values in the user’s local time, you must convert datetime values to UTC to prepopulate the field.

这就导致了原来直接向Datatime字段赋值Date值的功能报错。

 lang='javascript']
	let createRecordEvent = $A.get("e.force:createRecord");
        createRecordEvent .setParams({
            "entityApiName": "A__c",
            "defaultFieldValues": {
                'testDatetime__c' : '2020-10-21'
            }
        });
        createRecordEvent .fire();
// Error on Save : Value for field 'testDatetime__c' is not in ISO 8601 format, Value: 2020-10-21, Runtime class: java.lang.String

保存数据时会报如下错误

Error: Value for field ‘testDatetime__c’ is not in ISO 8601 format, Value: 2020-10-21, Runtime class: java.lang.String

如果有需要把date值写入datetime型字段的同学,可以在helper添加如下方法为date值填充时分秒,以此伪装成datatime值。

    // in helper
    paddingTime : function(date) {
        if(date != "" && date != null && date != undefined && date != {}) {
            date = date + "T00:00:00.000";
        }
        return date;
    }

    // in controller
    init : function(component, event, helper) {
        let createRecordEvent = $A.get("e.force:createRecord");
        createRecordEvent .setParams({
            "entityApiName": "A__c",
            "defaultFieldValues": {
                'testDatetime__c' : helper.paddingTime('2020-10-21')
            }
        });
        createRecordEvent .fire();
     }

另外需要值得注意的是,虽然aura的controller看起来是前端,但实际上createRecord事件的日期时间赋值行为等同于Apex————既作为UTC时间插入数据库,需要考虑显示时差问题。

那么相比之下,有同学会产生疑问,Apex受影响吗? 还能愉快的直接将date值写入datetime型字段吗?
答案是,放心,Apex一如既往。

// 后记
其实我并不确定官方文档里关于时期格式的描述一定是Winter’21后才加的,但是很确定Winter’21之前可以直接把date值赋给datetime型字段。毕竟aura event的文档无法查看历史版本,所以,就让我主观的视其为Winter’21吧。

班得瑞,查克·伯朗与钢琴

大概是在2019年1月份,理查德克莱德曼来到我所在的城市举办音乐会。可惜当时忙于生计,再次错过。为什么说是再次错过,因为学生时代理查得克莱德曼在隔壁大学举办过音乐会,400块钱的票价对于还是学生的我来说太奢侈了。

理查德克莱德曼和凯利金可以说是霸占了童年所有电视节目BGM的男人。有别于古典钢琴曲目,理查德克莱德曼引入了现代音乐元素,赋予了钢琴曲流行属性。对于那个年代的我来说,第一次知道了钢琴还可以这么玩。毕竟在听古典音乐都会被戳脊梁骨的说装高雅的环境里,钢琴成了古典音乐和高大上的代名词。当时就算是听理查德克莱德曼的《星空》和《梦中的婚礼》,都要偷偷的听。把磁带放到别的流行歌曲的盒子里,换掉上面的贴纸更是稀疏平常。对,那时候听古典音乐就是罪大恶极的事情一般,如果被发现就会招来无尽的嘲讽和孤立。

我喜欢听曲胜于听歌。歌有歌词,演唱者可以通过歌词辅助表达自己像讲述的东西,而曲不同,摆脱开了辞藻的束缚,演奏者必须使用更深层的更抽象的方式去表达自己的情绪。年幼时以为自己能理解古典大师要表达的东西,其实也只是跟着磁带附页上的文字说明自我附会而已。与伟大的文学作品一样,在没有达到接受作者信息的层次之前,其实根本不知道这部作品到底伟大在哪。

那时候与古典音乐相比,我更喜欢班得瑞的轻音乐。班得瑞的磁带封皮总是一张美丽的风景照片,然后附页也会堆叠很多高大上的词语。虽然现在听起来班得瑞的音乐不是很特别,但是在那个年代,班得瑞是突然多出来的另一种选择。在民俗音乐与高大上的古典音乐之间杀出来的第三者。虽然是纯音乐,但是没有交响乐般的凝重,是可以闭上眼睛躺在那里听的音乐。里面有流水,有鸟鸣,有风声。让你在压抑黑暗的环境中仿佛可以闻到大自然的味道的音乐。

当年我对班得瑞的专辑如数家珍,每当音像店有新专辑上架必收入囊中。《春野》《秘密花园》《蓝色天际》等自然不必说,有一张纯钢琴曲的专辑,叫《纯粹》,带给了我新的震撼。

钢琴人们就叫它音乐王子,可是从来没听说过哪个王子这么百搭。既可以独奏,又可以合奏,还可以当陪衬。既可以登大雅之堂,又可以在小酒馆自娱自乐。毕竟钢琴在我小时候的生活环境里就是高大上的代名词,从来没听说过爵士之类的东西,印象里总觉得钢琴要不然就是穿个燕尾服挺着腰板举着下巴开口自称钢琴八级下手就来了一段奏鸣曲,要不然就是理查德克莱德曼那种伴随着节奏明星一般闪耀的律动弹奏。结果《纯粹》这张专辑不一样,太不一样了。

现在的我仍然能回忆起每首曲子的旋律,对于年幼的我,评价只有三个字,“真好听。”

专辑的名字叫《纯粹》,专辑里面的钢琴曲也确实纯粹,只有钢琴本身。以前写过古德尔弹奏的《哥德堡变奏曲》,你能感受到古德尔对巴赫的理解,而巴赫所在的年代与当时的环境,你很难再感同身受。而对于《纯粹》,年幼的我知道演奏者在讲述着一个故事,但是旋律却深深的印刻在脑海里了。一个你暂时无法理解,演奏者用钢琴给你讲述的,美丽的故事。

很多年过去了,音乐的种类开始繁多了起来,班得瑞慢慢的从我的歌单中消失了。后来偶然间看到网上有一个讨论——班得瑞到底存不存在。What?立马点开了讨论的内容。原来班得瑞并不是一个乐队,什么瑞士的阿尔卑斯山脉的低调的执着的音乐人都是不存在的。是瑞士一家叫做AVC的公司组织的音乐项目,然后经过台湾金革公司包装发行(就是那些玄之又玄的文案)风靡全国。这里有破案的全过程(知乎虾米豆瓣)。本来以为这个故事就这样结束了,但是未曾想还有转折。

当时有两张纯钢琴曲的专辑叫《纯粹》与《深呼吸》,结果资料告诉我,台湾金革没发行过这两张专辑,Excuse me?正式发行的只有《仙境》、《寂静山林》、《春野》、《蓝色天际》、《迷雾森林》、《日光海岸》、《梦花园》、《琉璃湖畔》 、《微风山谷》、《月光水岸》、《雾色山脉》。

还好我还记得演奏者的名字叫做查克伯朗,Chuck Brown。那么问题来了,查克博朗是谁?如果是钢琴大师,应该早听说过才对。于是我打开了谷歌,输入chuck brown。结果。

啥?是个黑人大叔,而且还是在美国东海岸唱R&B和灵魂乐的??这画风不大对啊。不是说黑人没有钢琴家,而是这造型不像是能讲出那温婉悠长故事的人啊。这位大叔2012年去世了,R.I.P。

但是,还是感觉不大对。就像你可以通过文字风格感受到作者是个什么样的人。用钢琴给我讲故事的肯定不是这位大叔。我就不甘心的又搜了下Chuck Brown piano。寻思这人肯定是个钢琴家,应该能找到的。

看到检索结果,这个网站映入眼帘,https://chuckbrownmusic.com/about/a-quick-bio/  一个络腮胡子的大叔。嗯。。。行吧。原来这位查克伯朗并不是专职的钢琴家,而是位作曲,并且还干过一段时间的Voice Over,住在俄亥俄州。

下面的链接是大叔放在自己网站的试听。

Solo Piano Music

其实,有人会说,有些事情还是保持住那份神秘感比较好,就像魔术,真相大白的时候一切乐趣也都消失无踪了。当我知道了那些在黑暗中陪伴我的音乐不是出自那个神秘的来自阿尔卑斯山脉的乐团,还会喜欢这些音乐吗?

我觉得这个问题是这样的。首先我要问自己,曲子好听吗?好听。陪伴你渡过很多时光吗?是的。那怎么,知道是谁演奏的,就不是钢琴曲了?旋律就变了?不会。

音乐是很单纯的美的表达方式,音乐本身没有太多的可升华的附加价值。不要给自己喜欢的事物附加太多不着边际的价值。也不要给没有尝试过的或者自己不喜欢的事物贴上太多负面的标签。不管班达瑞乐团存不存在,不管查克伯朗是不是钢琴家,音乐却是确实存在的。听你所喜欢听的音乐,这就够了。管他是古典/流行/说唱/鬼哭狼嚎/喊麦。

 

使用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