Salesforce Picklist翻译无法填写的另一种可能原因

抛开权限等常见原因,如果你狂点翻译却无法进入编辑状态,有一种可能性是被翻译控制台上世纪的UI设计坑害了。

当待翻译的内容超过一屏的时候,这个古老的UI的表格Title不会跟随屏幕滚动浮动,并且列之间没有分割线,导致看起来一整行只有一列。

而实际上却包含三列

只有点击在翻译列区域才可以进入编辑状态。

P.S. for 跳了坑的小伙伴

Opportuinty的Quote Relatedlist上看不到New button的另一种可能性/One more possibility of missing new button on Opportunity’s Quote Related List

如果看不到quote的新建按钮,原因是多种多样的。
There’re so many reasons cause Quote’s new button missing.

其中最普遍的原因不过如下:
The most common reasons as below:

  1. 没有Quote表的创建权限。/You didn’t have Create permission on Quote Object.
  2. Opportunity Pagelayout上的Quote RelatedList的new button没有勾选。/You didn’t check the new button on Quote RelatedList of Opportunity’s Pagelayout.
  3. 没有开启Quote模块。/You didn’t activate Quote module.

但是,之前在项目上遇到了另外一个原因,在上述三点都做到的前提下,也会导致Quote的New Button消失。
Thus, there is one more reason to cause Quote’s new button not visible although all above conditions had achieved.

这个原因就是Opportunity的Account字段权限。如果profile没有赋予Opportunity的Account字段任何权限,Quote的new button将不会显示。
It’s not grant field permission to Opportunity’s Account field. This will cause the missing of Quote’s new button globally.

关于Profile的部署

去年有人问了我一个问题————为什么清理干净的Profile部署到新环境会多出很多东西。比如说Custom Application Setting, Tab Settings与Object Permissions。明明在源环境相关权限都已经移除,结果到了新环境又阴魂不散的出现了。

这个问题确实略为复杂,这也是为何拖了一年才动笔讨论这个问题。

众所周知,Metadata的部署有两种行为模式,一种是覆盖,一种增量。本文这里不做展开。
相对的,Metadata的取得也有两种模式,一种是独立内容,一种是关联内容。独立内容是指在package.xml中只包含元素自己的时候能将所有内容取出;关联内容是指在package.xml中除了元素本身之外,必须包含关联元素,才能将与其他元素有所关联的内容取出。独立内容的典型代表为ApexClass,CustomField;关联内容的典型代表Object与Profile。如此同时,这两种Metadata也同时存在独立内容。
具体来说,Profile的Metadata内容可以一分为二,第一种是Profile独立内容;第二种是与其他Metadata关联的内容。
✳️具体哪些内容是独立内容,哪些是关联内容,以及关联内容的Retrieve方法参考此思维导图。

部分信息表格版如下:

Category Source Metadata Type Support *?
Profile Basic Infomation Profile Include N/A
Page Layouts Assignment RecordType+Layout Yes+Yes
Field-Level Security CustomField Yes
Custom App Settings CustomApplication Yes
Connected App Access ConnectedApp Yes
Tab Settings CustomTab+CustomObject Custom Yes
Record Type Settings RecordType Yes
Permissions Profile Include N/A
Object Permissions CustomObject Custom Yes
Session Settings Independent Metadata Type => ProfileSessionSetting Yes
Password Policies Independent Metadata Type => ProfilePasswordPolicy Yes
Login Hours Profile Include N/A
Login IP Ranges Profile Include N/A
Apex Class Access ApexClass Yes
Visualforce Page Access ApexPage Yes

所以如果package.xml中只包含profile本身的话,那么在profile metadata中仅能取出profile基本信息,权限信息,ip range等有限信息。只有将其余关联metadata都包含进package.xml才能取出完整的profile metadata内容。不过,这时候喜欢动手实践的同学会发现,就算是把关联的metadata添加完整,仍然有些profile信息是取不下来的,比如没有任何增删改查的Object Permission。你只能取下来有访问权限的部分,application部分同理。这也是部署到新环境之后希望杀死的权限又死灰复燃的原因之一。
那么有些同学会有疑问,就算是没能将无权限的部分取得下来,那么部署到新环境的时候作为新建的profile,权限是怎么出现的呢?这个默认值是哪里出现的?
所谓实践出真知,如果设计下列实验,就很容易得到答案:
1. 随便找一个环境。
2. package.xml中只包含profile「Minimum Access – Salesforce」。因为此profile为官方提供的权限最少的profile。
3. 取得步骤2的profile,拷贝metadata文件,并改成其他名字,将文件中的false改成true,保存。
4. 将package.xml中的profile名称改为步骤3新建profile的名字,部署。如果使用的是vscode,可以在profile上直接右键deploy。
5. 在环境中打开新建的profile与「Minimum Access – Salesforce」。肉眼可见,在object permissio部分非常不同。
6. 新建的profile与「Standard User」进行对比,发现object permission部分完全相同。
7. 修改「Standard User」的Object Permission,重复步骤1到4再新建一个profile,取得后再次与「Standard User」进行对比,会发现新建的profile与修改后的「Standard User」相同。

由以上实验可以得出结论,通过部署方式新建profile的时候,对标手动创建时必须选择参照哪个profile的流程,系统会默认以「Standard User」作为新建模版,将部署的metadata内容以增量的方式作用到新profile上。未指定的部分以目标系统的「Standard User」设定为准。

那么原因知道了,该如何解决这个问题呢?
目前方式有三。
1. 擒贼先擒王,先人工干掉目标环境的「Standard User」上面所有的权限,让「Standard User」变成「Minimum Access – Salesforce」。这时候再部署的话就不会有额外的权限。
2. 打铁还须自身硬。会被「Standard User」影响本质上还是因为自己没有全面的指定好所有的权限,让它钻了空子。如果能够在profile metadata文件中人工编辑所有未取得的内容,也不会有问题。
3. 中庸方案,人工去目标环境新建好profile,参照profile选择「Minimum Access – Salesforce」,然后再部署。这样既不会影响「Standard User」,也不需要手动编辑profile metadata文件,又保持了新建profile的洁净。不过由于「Minimum Access – Salesforce」也不是绝对的Mininum,比如Custom Application Permission仍然带上了全部的app,所以针对此部分还是要在1与2中进行选择。

有同学又有疑问了,部署的方式有很多种,如果我用changeset还会有相同的问题吗?
答案是一样的,好奇的同学可以自己设计一个实验试一下。

Governor Limit小口诀

1.查询一百异步双
2.条数五万插万行
3.增删改查一百五
4.呼出一百就用光
5.请求累计两分整
6.发送邮件可十封
7.内存6兆异步倍
8.运算耗时十秒钟
9.异步升级六十秒
10.整体十分别嫌少
11.子查询使用要慎重
12.SOQL等待两分钟
13.batch可查五千万
14.查不出来也白干

解释:
官方文档
行1: SOQL最多100次,在异步环境200次,比如Batch,Future…
行2: SOQL查询最多返回5万条结果,DML操作最多1万条。
行3: DML操作做多是150次。
行4,5: Callout最多100次,累计等待时间120秒。
行6:无需解释
行7:Heapsize最大6MB,异步环境最大12MB。
行8,9:CPU使用时间最大10秒,异步环境一分钟。这里注意,查询或者HTTP请求不是CPU的运算时间。
行10: 所以是各种时间加一起才是事务消耗的总时间。最大十分钟。
行11: 子查询会额外消耗计算查询次数,累计查询次数等指标。能不用最好别用。
行12: SOQL查询最多等待2分钟,如果2分钟没有返回查询结果则异常。(子查询会增加查询复杂度)
行13,14: Batch的start方法里最多可以返回5千万条查询结果。如果数据太多查询执行超过了两分钟,batch也不会启动。

关于Trigger中的Return

某天,旁边的同事自顾自地嘀咕起来:“如果Trigger里也能用Return该多好啊。。。。”。
我一听这话忍不了啊,使劲儿拍了下他的桌子,“能啊!谁说不能的!”

Trigger中使用Return规则遵从代码块作用域规则。
举个例子,

(注:Return;皆加在Debug Log之前)
如果在位置0写Return;
那么整个Trigger都会被Return跳过去,Debug Log中没有任何输出。
如果在位置1写Return;
那么第一个handler中的内容都会被跳过,Debug Log的输出结果为0,4,5,6。
如果在位置2写Return;
那么第一个handler的第一个方法的内容都会被跳过,Debug Log的输出结果为0,1,3,4,5,6。
同理,如果在位置4写Return;
那么第二个handler被完整跳过,Debug Log的输出结果为0,1,2,3。
通俗的讲,就是从方法级别开始,只影响到花括号范围。

那么有同学肯定会问。虽然代码规范一般不允许一个sObject建多个Trigger。那么万一就是有两个Trigger呢。
然后在其中一个Trigger中使用Return,会影响另一个Trigger执行吗?

答案是:不会。

道理是一样的,把Trigger降级一层,外面再加一层运行环境,那么每个Trigger里的Return就如同Handler里的Return,也只能影响自己而已。

// Update
自从某次Release之后,如果Return; 语句后面还有可执行的语句,保存时会报错,说有unreachable的代码。
如果想做实验,需要使用如下写法

if(true) {
    return;
}

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 = JSON.stringify(errors);
                    		}
                		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值的功能报错。

	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创建好了之后就会自动开始下载。

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