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

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

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"]);

关于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的小圈子里。

Javascript如何生成Json

最近遇到个想复杂了的问题,就是如何用Javascript生成Json。
因为之前用的都是高级语言,已经封装好了数据结构或者类,直接赋值就好了。
之前想模拟前端向后台呼叫接口,需要POST方式传递JSON进来。然后就懵住了。

咦?我应该不用傻傻的拼接字符串吧。然后开始了脑洞。

脑洞第一版,模仿Salesforce制作List的方式

var dataArray = [];
var data = {};
data["id"] = "0010000000ABCD";
data["name"] = "Account";
dataArray.push(data);
var jsonString = JSON.stringify(dataArray);
var output = eval("(" + jsonString + ")");

感觉很怪。

脑洞第二版,用Javascript的类概念

var Account = new Object();
Account.Id = "0010000000ABCD";
Account.Name = "Account";
var output = JSON.stringify(Account);

感觉好多了,但感觉还是不够简单。毕竟Json是Javascirpt原生支持的啊。

然后查了半天,发现自己真的想复杂了

var output = {Id:"0010000000ABCD", Name:"Account"};

// Update 1
补充一下如何传两条数据

var output = [{Id:"0010000000ABCD", Name:"Account1"}, {Id:"0010000000ABCE", Name:"Account2"}];

Javascript的KeyCode

在前端开发中,经常使用onkeypress,onkeydown等event来做快捷键。
我之前就是打算使用“;”来做快捷键功能。
使用了如下代码

document.onkeydown = function(e) {
    e = e || window.event;
    var kcode = e.which || e.keyCode;
	if(String.fromCharCode(kcode) == ";") {
              // TODO Place your code here.
	}
};

结果。。。只有在Firefox好用,Chrome和IE都阵亡了。
我并没有做任何的浏览器判断,怎么会自动按浏览器区分功能呢?

原来,对于event的keycode,大部分的按键的keycode值在所有浏览器中都是一致的。
但是,有一小撮按键。。。。很不幸就有我选中的的“;”,在firefox和Chrome中是不一致的。
具体列表参照http://www.javascripter.net/faq/keycodes.htm

Javascript元素选择器的另一种选择

最近喜欢阅读代码,好的代码比好的文章还好看。

这次的起因是在看新的浏览器API叫做Push API,作用是作为服务器端可以向所有的订阅客户端推送消息。这个就厉害了,以前是没有方法进行主动推送的,都是在服务器端做轮询。不过还在起草阶段。其中有个Demo代码,我就下来看。

结果,发现了好东西querySelector()

以往我只会使用标准Js的getElementByXX与Jquery的$()。没想到新的Web API中居然自带了性能爆表的QuerySelector。语法与Jquery的选择器非常相似。

目前从官方文档看来,其他浏览器还好,IE系的话只支持IE8以上。下次决定试一下。