前端加载非公开第三方文件及接口数据

前言

有时候可能需要从一些非公开的接口或者地址去获取数据或者文件,这个听起来像是一个爬虫过程,不过相比单纯的脚本爬虫,前端爬虫除了单纯的HTTP请求,也许还多了一些选择;

注:以下探讨只是针对GET请求;

可能的问题

看到非公开&第三方的字眼就马上会想到:请求地址一端完全不可控;所以很有可能会有下面的问题:

  • 跨域请求
  • 请求头参数匹配

解决跨域的方法

对于跨域请求,目前确实存在许多解决办法,比如:

  • CORS代理
  • fetch no-cors mode
  • JSONP
  • crossorigin属性

上面的跨域解决办法都不是完美的,都有各自的局限性,因此需要根据具体场景选择合适的方法;

CORS代理

一般的跨域都是由于服务端的响应头设置了特定的CORS参数,比如限定了允许跨域的域名范围已经请求参数类型等等,这种时候请求头的某些参数就不会匹配,因此被断定为跨域请求,然后被中止请求;

所谓的CORS参数就是指Access-Control-*这类;

由于第三方的服务端代码不受控,因此只能利用代理服务器来返回一个允许跨域的CORS响应头;这里有一个比较常用的CORS代理网站:

https://cors-anywhere.herokuapp.com/

使用方法很简单,就是网址 + 要代理的网址https://cors-anywhere.herokuapp.com/https://example.com/abc

一般来说,只要是由于响应头CORS参数设置造成的跨域,这种代理都可以解决;

fetch

fetch是新推出的http请求api,风格为Promise式的;

Promise fetch(input[, init]);[1]

其中第二个参数可以设置一些请求相关的参数,比如请求头设置和缓存策略等等;其中有个mode的参数:

  • same-origin只允许同站的请求,其它请求一律拒绝;
  • cors:在same-origin的基础上还允许拥有正确CORS响应头的请求;
  • no-cors:发出的请求不会进行CORS检测,但同时得到的响应头是不透明(opaque[2],即会丢失很多响应信息。

从上面不难看出,使用fetchno-cors mode可以避免跨域:

1
2
3
4
5
fetch('https://example.com/abc', {
mode: 'no-cors'
}).then(res => {
return res.json()
})

但是,这种方式的跨域有个致命的副作用,那就是获取到响应体正文是空的,也就是说跨域请求成功了,但是由于得到是不透明响应体,响应正文的解析触发时机出现了问题

1
2
3
4
5
6
fetch('https://example.com/abc', {
mode: 'no-cors'
}).then(res => {
// 此时res.body为空,导致解析不正确
return res.json()
})

img

这个在请求媒体文件时尤为明显,跨域请求成功后,其Promiseresolve触发时机并不是我们所期待的资源加载完成后,而是响应报文返回后立马触发,导致得到的二进制数据字节数为0;这一点MDN[3]也有提及:

img

因此利用fetchno-cors mode进行跨域在实际应用上基本行不通;

JSONP

JSONP跨域的原理就是script标签天然支持跨域,然后将数据包裹在外部JS代码中进行回调执行然后解析;因此这种跨域方法需要第三方接口本身就支持返回JSONP的数据,否则不能使用,比如:

img

因此只需要配合返回的JS代码,提供一个相应的解析函数即可;

crossorigin属性

这是videoaudioimg等一些元素用于设置跨域策略的属性,有以下几个值:

  • anonymous:不会检测CORS请求;
  • use-credentialsCORS请求会要求带上相应的凭证;
  • "":空字符串,等同于anonymous

当然,一般来说直接给video这些媒体元素设置第三方地址,也不会引起跨域错误(不过有些网址可能会设置了防盗功能);因此这个属性反而很多时候是来解决一些H5 API转码的时候出现的跨域报错,比如canvas绘制第三方来源的视频和图片时;

关于请求头参数匹配

在请求第三方地址时,可能好不容易解决了跨域问题,但是又会出现其他莫名其妙的报错,那很有可能是其请求头增加了内部专用的一些参数设置,要想完全给出匹配的请求头这时候完全看人品了……

简单的总结

img

解析数据的一些思路

前端らしい

既然前面说了前端面对这种类似爬虫的需求也许有更多的选择,那么我就想到了一些很前端的思路:

  • Q:已知像video/audio/img这类标签本身就支持跨域加载,那么能不能通过创建标签加载数据,然后在onload事件中获取标签的二进制数据呢?
  • A:不能。虽然这类标签可以很自然地跨域加载数据,且能够正确地触发load/loadeddata事件,但是目前还没发现DOM有暴露获取这个标签对应的二进制buffer数据的API,或者将相应的DOM类型转化为Blob/File之类的API,因此走不通;(canvas/webGL/webAudio这类更底层的API还没尝试过,有时间可以验证下)
  • Q:已知URL.createObjectURL()方法可以接收MediaSource对象,而MediaSource可以通过ArrayBuffer进行构造,那么video/audio/img这类标签能够转化成ArrayBuffer吗?
  • A:不能。同样也没找到相应的API
  • Q:<script>标签 + innerText/innerHtml
  • A:不能,因为innerText/innerHtml方法无法获取到script内部源码

经过一些折腾,发现通过前端直接获取媒体元素的二进制数据,从而完全绕开跨域拉取数据进而上传是很有难度的,这也许是浏览器处于安全因素的考量,也可能是对深层次的DOM API并不熟悉;

fetch

通过fetch API获取到的响应体是一个Response对象,这个对象已经自带了几种常用的数据解析方法[4]

img

解析成功的前提自然是响应体正文符合对应解析方法的格式;

相关文档


  1. WorkerOrGlobalScope.fetch() - Web API 接口参考 | MDN ↩︎

  2. https://developers.google.com/web/updates/2015/03/introduction-to-fetch ↩︎

  3. Body.blob() - Web APIs | MDN ↩︎

  4. Response - Web API 接口参考 | MDN ↩︎