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

利用Gitlab CI将代码部署至FTP服务器

作为Github的拥趸,最初这篇文章的标题是《利用Github Travis CI将代码部署至FTP服务器》。本来盘算的很好,利用这篇文章,一来是记录一下自己解决的问题,二来是宣传一下Github是多好用。
众所周知,Github等一众在线同性交友代码托管平台随着开发方法论的不断进化,已经演变成了CI/CD平台。哪家平台要是说自己不支持CI/CD,都不好意思出来打招呼。而Gitbub原来并没有自己的CI/CD功能,Travis CI充当了这一角色。最近,Github也推出了自己的Gitbub Actions来弥补这一块空缺,目前仍然在beta阶段。我也申请了测试资格,但目前仍然没有轮到我。

作为个人立场,很喜欢使用Github来托管代码。以前免费账号只能托管公开仓库,而收费账户又舍不得银子,无奈只能把私有仓库分散在Bitbucket和Gitlab上。
后来大家都知道了,Github被微软收购之后,免费账号也可以托管私有仓库了。我就琢磨着把所有repo都集中到Github上吧,结果猜怎么着?Travis CI免费版仍然只能用在公开仓库上。只好作罢。

当然,这次放弃Github+Travis CI组合的原因之中,Repo必须公开是其中一方面。另一方面是在使用Travis CI时遇到了无法解决的问题。

俗话说,先有问题才有的工具,而不是先有的工具而后产生的问题。首先说这次要解决的问题是什么。
其实这个问题很简单。平时在写文章或者写插件的过程中会遇到一些不懂的web技术,然后就需要做一些小demo或者简单的概念验证放到我自己的服务器上。由于有多设备切换开发的需求,任何代码都要放在远程git上做版本管理与同步。这样的话,就会出现一个稍许繁琐的操作————改完代码之后,要先commit到本地仓库,然后再push到远程仓库,然后将本地文件通过FTP工具上传到服务器,之后在浏览器打开网站进行测试,如果有问题则循环本套操作。

如果在我将代码提交到远程仓库之后,能够自动部署到FTP上该多好啊~!
很久很久以前,在SFDX还没发布的那个年代,我曾经利用GitHub+Travis CI做过一个Salesforce StandardSetController的小Demo的自动部署。每次提交完改动,就能自动部署到指定环境。我觉得部署到FTP怎么也得比部署到Salesforce Org还要简单吧!

说干就干,由于对Travis CI还算熟悉,立马查了一下文档,自信满满的写下了如下的yml文件。

# .travis.yml
language: node_js
script:
- echo "skipping tests"
after_success:
- curl --ftp-create-dirs
       -T index.html
       ftp://${FTPUSER}:${FTPPASSWORD}@${FTPENDPOINT}

语言无所谓,测试跳过,直接将文件通过ftp传送到服务器上。
结果执行之后,发现不对劲了。正常传送一个文件不说是瞬间完成,也不至于10分钟都传不完,导致被Travis强行把任务中止吧?然后去FTP上瞧了一眼,文件是创建出来了,但是大小是0KB。这肯定不对劲啊,查查吧。结果查到一篇Travis的官方博客,解释他们的NAT因为什么什么原因,导致FTP的被动模式失败之类的。总之就是ftp命令不能用了。
那行,我改用sftp命令。结果这次直接报错了,说不支持sftp命令。Execuse me? 然后搜了大半天,也没找到什么靠谱的解决方案。这就让人觉得不可思议了,对于CI工具来讲,FTP上传不比连接AWS、Heroku之类的简单多了?

正在感到弱小无助之际,我想起了一直在默默当备胎的Gitlab。
Gitlab也有自己的CI/CD工具,并且是免费,且可以用在私有Repo上的。
但是Gitlab的yml文件语法与Travis不同,只好硬着头皮速读了一下Gitlab CI的yml写法。
经过不断的研究与尝试,终于写下了如下的yml文件。(其实是从sample yml文件改造出来的)

# .gitlab-ci.yml
# This file is a template, and might need editing before it works on your project.
# Full project: https://gitlab.com/pages/plain-html
pages:
  stage: deploy
  script:
    - mkdir .public
    - cp -r * .public
    - mv .public public
    - apt-get update -qq && apt-get install -y -qq lftp
  artifacts:
    paths:
      - public
  only:
    - master
  after_script:
    - lftp -c "set ftp:ssl-allow no; open -u $USERNAME,$PASSWORD $HOST; mirror -Rev public/ ./ --ignore-time --parallel=10 --exclude-glob .git* --exclude .git/"

任务名称随意,artifact就是本次部署的全部文件归档,这里文件夹名字叫public。为了使用lftp命令,在脚本中提前安装lftp命令。
在CI/CD的setting页面提前配置好USERNAME, PASSWORD, HOST三个变量可以避免将敏感信息放到仓库文件中。
接下来在Gitlab仓库中启用CI/CD,就可以享受了。

其实同理,既然能用Gitlab CI部署代码到FTP,也能部署代码到Salesforce。之后还会写一篇Salesforce的兄弟篇。而这篇文章呢,也不想写CI/CD是什么,也不想谈论DevOps的优势,毕竟这种东西用关键字一搜索,满大街都是,感兴趣的人在看到文章头两个自然段之后早就自己去搜了。毕竟我说它千好万好,不如举一个实际的,好上手的例子让大家看到实实在在的东西来的实惠。

// 后记

也很长时间没有更新了。有人问我哪去了。其实。。。这期间发生了很多事情,也经历了很多事情。素材也是越攒越多。奇怪的是素材攒的越多越觉得没什么值得写的东西。可能,大家都在进步,对于Salesforce平台的基本认知已经都跨越了那道门槛,现在更需要的是Salesforce这款产品以外的,更关乎于计算机世界的、网络世界的基础知识,软件工程的基本概念。还有对于各个行业是如何运转的行业基本了解。

随着年龄增加,进入到了人生的不同阶段,会发现你完全属于你自己的时候越来越少。你逐渐被必须要承担的各种角色完全瓜分。以前争不到的东西现在更加的争不到,原来拥有的东西反而越丢越多。Whatever,要不然又能怎样呢,要不然选择随波逐流,要不然选择自己受的苦自己咽下去,你敢选第三条路吗?(摊手)