解决OwnCloud的No input file specified.错误

临关机睡觉前,总觉得不升级点什么睡不踏实。比如手机应用,电脑操作系统,IDE和浏览器插件之类的,都要点一圈保证是最新的。
结果今天发现没有可以更新的。。。不行,难受。那就看看Godday的host的PHP有没有新版本吧。

由于租用时间较早,当年还是PHP5.2的天下。结果PHP7都快过时了,服务器可选择的PHP版本仍然停留在PHP5。俗话说,世界上最好的PHP版本是PHP6(并没有6,直接从5到7),我退而求其次,升级到7还不行?毕竟在性能上有着天差地别。

于是乎满怀期待的点开了Program Language Version。呦,提供了
新版本 PHP5.6。行吧,总比没有强。于是痛快的选择了这个稍微高一点点的版本。

很快升级就完成了。按流程,依次点开各个服务进行回归测试。博客,正常。论坛,正常…………网盘,“No input file specified.”。糟糕了。

网盘用的是老牌私有云盘OwnCloud,网上能搜出的信息不算太多。
于是乎冷静下来,分析目前的情况。
首先,变化的只有PHP的版本,而且不是大版本变动。服务本身没有变动。
其次,其他服务,比如Wordpress等皆运行正常。

所以,推测是OwnCloud的PHP设置问题。
由于Godaddy为Shared Host,所以并不能直接修改PHP设置,而是要通过.htcaccess文件去实现基于路径的调整。随之,我找到了网盘的根目录,打开了.htcaccess。
留意到了如下片段

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
…………
</IfModule>

咦?我怎么记得RewriteEngine下面应该有个RewriteBase啊。
这小问题,于是顺手加上了“RewriteBase /”。保存。

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteBase /
  RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
…………
</IfModule>

刷新之后,还是 “No input file specified.” 。哎呀?Wordpress的配置文件就是这么写的啊。然后又开始疯狂搜索信息,发现了很多在Godaddy上部署了Owncloud的受害者遇到过相同的问题,但是都没有找到答案。

看来还是得靠自己。经过一番努力地思考,难道因为是SharedHost,所以必须用绝对路径?而且由于当年不会给子域名加SSL,所以作为早期架起的服务,Owncloud并没有分配子域名,所以不该写根目录?
于是根据Goddady的Control Panel提供的绝对路径地址,拼上Owncloud的所在路径,写在了RewriteBase中。

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteBase /XXXX/XXXX/XXX/99999999/html/cloud/owncloud5/
  RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
…………
</IfModule>

再次访问OwnCloud的URL,恢复正常。

总结,
1. RewriteEngine后面要接RewriteBase
2. 如果不是域名的根目录,要写全路径。
3. 如果相对路径不好用,尝试绝对路径。

PHP7什么时候才提供啊。。。GoDaddy!

关于Promise

自从Saleforce进入Lightning时代,Promise这个在前端领域耳熟能详的名词,如当年鸦片战争的坚船利炮一般,强行闯入了Salesforce开发者的世界。

也不想谈太多的技术细节,因为Promise作为ES6的新特性,汉语的,英语的,日语的资料也是林林总总的非常详细。比如阮一峰老师的《ECMAScript 6 入门》中的此章节

其实Promise理解起来也很简单,就是按字面意思,我许诺,这事我答应你了去办。
事儿我解决了,就是resolve,没办成,就是reject。事儿办成以后咱看看下一步怎么办,就是then。如果中间出岔子了,后面的不管了,直接去找谁谁谁,就是catch。不管事儿办没办成,最后告诉我一声,就是finally。

Promise的出现可以说是为了解决Callback Hell,回调地狱。甚至还有一个网站来介绍回调地狱是什么。http://callbackhell.com/
在前端开发的上古时代,回调地狱是非常常见。在代码缩略图上
能明显的看到代码的形状是>型。
顾名思义,回调,就是我找你办事,我托你办事的时候同时也嘱咐你这件事儿办完了做什么。就是我把你处理完成时要调用的方法先传给你。
一般回调地狱发生的重灾区是多步骤的AJAX。因为AJAX调用是异步的。比如,先发AJAX请求获取一个token,然后发AJAX请求获取一个user的Id,然后再发一个AJAX请求去获得这个user的数据等等。每一步的下一步要在发出AJAX请求之前就作为回调设好。强行的将一堆异步的处理变成严格的按照先后顺序执行。

那么Promise的意义就在于,把>型的代码嵌套变成了流线型的new Promise(getToken()).then(value => getUser(value);).then(value => getRecord(value)).catch(e => console.error(e)); 在感官上更贴合“
将一堆异步的处理变成严格的按照先后顺序执行。 ”的目标。

如果说,Promise只是从>型变成了|型或者—型,好处还不够多。更重要的一点,与回调地狱写法相比,Promise提供了很好的异常处理机制。只要then链中任何一环抛出异常,都会直奔catch集中处理。

但是Promise也非完美,在极复杂情况下,比如根据上一步的返回值,要执行完全不同的业务流程,promise链就会略显无力。与此同时整条Promise链也会变的及其冗长,优点损失殆尽。还有异常处理的一些坑等有时间在讲。

所以在代码结构设计与业务流程上应尽量规避极其复杂与冗长的回调链。毕竟代码还是需要人来阅读。

Salesforce开发者也应该多去了解前端技术,看看这个世界已经发展到了什么样子,而不是固步自封在SFDC的小圈子里。

关于在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的规则”

关于Javascript的Proxy

我只是想开橘子罐头,你却要卖我一把瑞士军刀Plus。为了用这把瑞士军刀Plus开罐头,我需要先折叠锤子,收起剪子,掰开螺丝刀……

一个只是想吃橘子罐头的人

某天,我要写一个显示某服务实时状态的页面。
动手之前感觉这个需求还是挺简单的。不就是新建一组HTML元素,然后用javascript对其中的元素赋值。So easy。

一边哼着小曲,一边就把HTML元素整整齐齐的码好了。接来下,让我看看有几个信息来源会更新HTML的值。1,2,3,4,5,6……什么?三十个模块?OK,OK,让我一个一个写,只是体力活而已。

继续阅读“关于Javascript的Proxy”

关于window.postMessage

很久没写东西了,偶尔会对着本文编辑器发呆,不知道自己想写什么,不知道应该写什么。又觉得什么都不应该写,什么也都不值得写。能静下心坐在电脑前写点什么是越来越奢侈的事情。也许,too many mind。

我自己说的

我相信每一个接触过前端开发的程序员都被跨域问题折磨过。

那么什么是跨域问题?
经历过的人脱口而出,“跨域问题就是处于安全考虑,当前域名不允许访问另一个域名的资源”。
这样说其实并不正确。仔细回忆一下,在曾经访问过的网页中,是否包含非本域名的CSS,JS和图片?
实际上获取这些CSS,JS和图片的请求已经跨域了,可是并没有出错。

CORS定义 点这里。严谨点说,是只有通过脚本发出的跨域请求才会被禁止,而通过标签发出的资源请求则可以顺利完成。并且进行拦截动作的,是浏览器本人。
原因显而易见————脚本运行在浏览器,而不是服务器。所以通过服务器,或者非浏览器的客户端(或者魔改版浏览器),可以随意的向任意域名发出请求。
为了绕开这个机制,各路神仙也是各显神通,开了各种各样的脑洞。

继续阅读“关于window.postMessage”

SOQL For Loop的效率问题

一天,有人和我抱怨说系统越跑越慢。
说当年系统刚开发出来的时候,页面唰的一下就打开了。
而现在同样的页面却要转圈转个800年。
客户天天向他抱怨。

听到这个现象我瞬间来了兴趣,随即放下了手中的工作。
这位兄台,借一步说话。

千里迢迢赶到了案发现场。兄台亲手做了演示。
确实,那个号称唰一下就打开的页面,现在要转个许久才行。
从上到下的仔细观察了一下页面(是个VF)的内容,发现有个带翻页的表格。
除了这个表格之外,其他的内容都只是简简单单的字段内容显示。
总体来讲,一眼看上去也没有什么非常不对劲的东西。

Talk is cheap, Show me the code.
兄台麻利儿的打开了VF和Controller的代码。
大概扫了一遍,格式工整,命名规范,结构清晰,注释齐备,没有任何奇葩写法。
那就奇怪了,如果是代码写法问题,那么应该从一开始就慢才对。

如果说有什么东西是随着时间不断积累的,不一定是经验和阅历,也有可能是数据量。
所以我又扫了一遍Controller里的SOQL。
发现只有在初始化表格的SOQL中使用了LIMIT 10000。
而其他的SOQL大都直接指定了ID作为检索条件。

我指了指那条SOQL,与那位兄台确认了一下眼神,不过兄台一脸迷茫。
“咳。。。我觉得是这里的问题,系统刚开始用的时候没有数据,所以这里初始化做的很快。但系统用久了之后,数据越来越多,这里要处理的数据也越来越多,所以就慢了。”
“哦。。。但是,才10000条数据,应该不至于啊。。。”

我竟一时语塞。
盯着这段代码半晌。

....................................
for(tom__c t: [SELECT Id, Name FROM tom__c WHERE RecordTypeId = 'XXXXXXXXXXXXXX' LIMIT 10000]) {
tableRowList.add(new tableRow(t));
}
.................................

求锤得锤,用实锤说话。
换测试环境,开Debug Log!
为了看到底是哪里拖慢了页面速度,我在每块处理前后都加了system.debug();
虽然做了万全之准备,结果却仍让人始料未及。
1. Debug Log的每一步的执行时间居然丢失了毫秒细节!

令人不得不吐槽的是,在Developer Console里面打开同样的一条Debug Log,执行时间的毫秒细节居然是有的!

2.由于Log内容太多,输出内容被无情截断。

所以只好换一个打法。
不再尝试执行完整transaction,
而是将每一块代码都改造成独立的代码块,
前后加上system.debug(datetime.now() + ‘ —- ‘ + datetime.now().millisecond());
逐一在匿名块中执行。

经过如同人生般漫长而又枯燥的实验后。
终于发现,耗时最长的确实是LIMIT 10000的那块代码。

那我就奇怪了,就是循环个10000条数据,也不至于那么慢啊。
难道是因为SOQL For Loop。
于是,立即动手做了下面的实验。
1. 准备一个干净的DE
2. 准备2000条Account(再多放不下)
3. 分别执行下面的匿名块

system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());
for(Account acc : [Select Id, Name from Account]){
String a = acc.Name;
}
system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());
// --------------WoShiFenGeXian----------------
system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());
List<Account> accList = [Select Id, Name from Account];
for(Account acc : accList){
String a = acc.Name;
}
system.debug(datetime.now() + ' ---- ' + datetime.now().millisecond());

4.调换顺序多次执行,以确保结果公平。因为最先跑的没有缓存,索引等数据库优化,会吃亏。所以为了严谨要排除前几轮之后取平均值。

结果如下。

SOQL For Loop写法,2061条数据循环,427-324=103毫秒。

先查询后循环写法,2061条数据循环,995-924=71毫秒。

速度提升 (103-71)/103*100%约等于31%

先查询后循环写法大比分胜出。
由于我的实验的循环内容几乎是没有任何负载,所以循环处理的消耗可以忽略不计。
但是,如果每次循环都很耗时(很不幸,这位兄台的系统就是如此),
这个百分比还会扩大。

那么为什么SOQL For Loop的写法会慢呢。
SOQL For Loop的官方文档介绍

原来SOQL For Loop的写法不仅仅是节省代码行数,为了美观。
而是改变了底层原理。
SOQL For Loop不再是将检索结果一起从数据库里取出来之后,使用迭代器一条一条处理。
而是改用SOAP API,使用Query与QueryMore从数据库里拿一批处理一批。
所以SOQL For Loop因此具有了额外的特性——————可以选择200条循环一次。

到最后就变成了一个经典的困境。
在生活中,你只能用时间去换钱,或者用钱换时间。
在算法里,必须用空间换时间,或者用时间换空间。

先查询后循环的方式,由于要先把整个结果集放到内存里,所以要使用很大的一块空间,对于viewState紧张的页面,或者已经占用了大量内存的代码,可能会成为压死骆驼的最后一根稻草。
而SOQL For Loop拿到一批,处理一批,释放一批的做法,不会占用大量的内存空间。但代价是要承担这部分性能消耗所造成的额外时间开销。
如果开启更详细的Debug log,则可以看到SOQL For Loop在循环的时候在疯狂的进行内存操作。

那么瓶颈找到了。
我们是不是把SOQL For Loop改成先查询后循环的方式就结案了呢。
不,我们最后把这个表格改成了异步加载。。。。

Dataloader与Timezone

此处本没有文章。催更的人多了,便有了文章。

最近一直沉迷于做另一款浏览器插件,所以写文章的事情又耽搁了下来。
人成长的过程就是不断的完善自己的过程,就如同维护系统一样。
很多问题以前想不清楚,说不定什么机缘巧合就清楚了。

最近(4个月前)有人跑来找我,说Dataloader疯了。
我就好奇啊,好端端的Dataloader怎么就说疯就疯了呢。
那人说了,他遇到的一个问题,
就是导入日期的时候,发现总是差了一天。
比如,tom__c上有一个日期字段date__c,看数据tom1的详细画面,date__c的值为2018-06-29。
然后他把tom1的date__c的值export出来,再update到jerry1的日期字段date__c。
结果在jerry1的详细画面上,date__c的日期少了一天,变成了2018-06-28。
而另一个日期时间型字段datetime__c则没有出现此问题。

肯定事出有因啊。
我就让他看看export的csv文件,里面的日期是啥样的。

咦?date__c是与详细画面一样2018-06-29,而datetime__c则变成了2018-06-28。
然后再update回去之后,在详细画面上看,反而date__c是2018-06-28,datatime__c却是2018-06-29。

结果人家突然一拍大腿,说“我知道了!”,我就很惊诧,这就知道了?
他笃定的说:“这肯定是Dataloader有Bug!我在setting里设置的是东八区,这date型取出来没问题,datetime型取出来却是零时区。这肯定是Dataloder的Bug!我去提票了!”说罢起身就要走。
“且慢!”我赶紧叫住了他。
作为一个成熟的CRM系统,SFDC已经内置了成熟I18N解决方案,以此确保客户全球化的业务可以正常开展。
I18N除了语言的相互翻译,还有包括维护时间的一致性。比如,我在北京早上8点创建一条数据,洛杉矶的的同事应该看到创建时间是昨天的17点,而不是洛杉矶的早上8点。
为了实现这个功能,sfdc在DB中统一存放GMT时间,谁在页面上查看就换算成谁所在的时区。
但是,如果从DB查询(包括Dataloader)的话,是无视任何User个人及Dataloader的时区设定,取出来的是已经换算成零时区的时间。
相反的,不管你所在哪个时区,日期时间都会被换算成零时区之后,才会存进数据库。
所以,由于这个人设定的时区是东八区,CSV文件中datetime__c被减了八小时是正确的。
那么date__c为什么没减八小时呢?这里推测是因为在编辑页面保存的时候,系统会自动补上与时区相等的时分秒,以此保持日期不变。

“这么说的话,那就是update的时候Dataloader出bug了。要不然最后怎么差了一天。”他幽幽的说道。
我摇了摇头。
继续阅读“Dataloader与Timezone”

关于编辑Converted的Lead

Salesforce真的应该对已经反悔的设定,做一个文档召回机制。

最近想修改Converted的Lead上的信息。
由于使用的是上帝账号,所以直接用Dataloader进行Update,
结果出现了“Cannot reference converted Lead”的错误。
唔?在上古时期,确实Convert之后的Lead再也不能动了,
但我确实记得某次更新之后就可以了啊。

然后开始翻找与此相关的文档,
功夫不负有心人,终于找到了在Spring’16的Release Note上,有相关的描述。
Spring’16,开启了”Set Audit Fields upon Record Creation”与”Update Records with Inactive Owners”两个权限的话,
就有了编辑Converted Lead的能力。

不过,我很确定我有这两个权限。
由于标准的System Admin Profile无法编辑User Permission,所以我特意做了一个Permission Set,并且里面确实有我自己。
八成Salesforce又吃设定了。
果然,这篇Article中明确指出,

Since Spring ’17 ‘Set Audit Fields upon Record Creation’ and ‘Update Records with Inactive Owners’ no longer grants access to converted leads.

这两个权限不再负责Converted Lead的编辑。
而是增加了一个权限叫做’View and Edit Converted Leads.’

好吧。你赢了。
然后我打开了Permission Set——————————————————没有?
再三确认之后,我确定了,没错,在Permission Set里没有并没有这个权限!!!!
目测又是一个Bug。

最后没办法,用Ant Migration Tool手动增加了下面的metadata片段,Deploy回去。

<userPermissions>
    <enabled>true</enabled>
    <name>AllowViewEditConvertedLeads</name>
</userPermissions>

之后,再用Dataloader试一次,这次可以了。

目前针对这个问题进行搜索,排名靠前的搜索结果仍然是对Spring’16 Release Note的引用。
这不是第一次,也不会是最后一次。希望Salesforce能尽早考虑实现文档的召回机制。囧

Trigger的小口诀

insert没有old,delete没有new,
update俩都有,undelete不通用。
before改自己,after改别人,
flag防止死循环。
DML必须批量化,
循环数据整理完。
trigger不需要多建,
handler业务都实现。
预留开关可跳过,
运维人员笑呵呵。

解释:
行1:Trigger.old只能在update和delete中使用。Trigger.new只能在insert,update和undelete中使用。
行2:update里面可以同时用Trigger.new与Trigger.old,undetete只能用在部分Obj上(链接)。
行3:Trigger.new只能在before里更改,所以在after里等一切尘埃落定之后再去改别的表吧。
行4:Trigger中改别的表,很容易出现循环触发的情况。如果没有特殊的需求,应该加flag控制,防止Trigger之间形成死循环。
行5:不要在循环里DML,不要在循环里DML,不要在循环里DML。DML次数很宝贵的,要攒一起做。
行6:三部曲,获得数据->整理数据->插入数据。循环只用来整理数据。
行7:如果在同一个Obj上建立多个Trigger,SFDC并不保证执行顺序。所以只建一个Trigger就够了。
行8:在Trigger中不要直接写业务逻辑,应该调用handler,并安排好业务执行顺序。
行9:一定要设计某个user跳过Trigger执行的开关,除非你将来不维护它。(方法有很多,其中一个例子
行10:别让维护人员发现没有跳过Trigger开关的系统是你写的。。。如果是的话赶紧逃命吧!