最近有领导检查,上课不能愉快地玩手机和睡觉了。为了防止不省人事的情况,这家伙把几年前在二手东买的那本《DOM 启蒙》(其实我觉得应该和同系列的另一本书一样,译名为《DOM 启示录》,这样听起来比较厉害)带到了教室。嗯,虽然一直吐槽这本书的排版错误糟蹋了这本书,以至于给我这是盗版书的感觉,不过书的质量还是不错的。
花了一上午看完了整本书,算是把 DOM 整个又复习了一遍。回到寝室的路上突然想起了书中有一章提到可以获取样式表文件的属性,于是机(la)智(ji)的我想出了这么一个奇怪的问题:能不能用 CSS 实现跨域获取数据?
在 JavaScript 中如果需要获取一个样式表的 CSSStyleSheet 对象,可以使用这样的方式:
1 |
document.getElementById('#stylesheet').sheet; |
比如如果我们想获得这个博客的 Crayon 插件的 CSS 样式,可以这样:
1 |
document.getElementById('crayon-css').sheet; |
然后我们就获得了一个 CSSStyleSheet 对象。
这个对象包含了一个叫 cssRules
的属性,这个属性是一个 CSSRuleList,每个元素是一个 CSSStyleRule 对象,代表了样式表中的每条 CSS 规则,比如:
1 2 |
document.getElementById('crayon-css').sheet.cssRules[0]; // CSSStyleRule {selectorText: "#colorbox.crayon-colorbox...", style: CSSStyleDeclaration {}, type: 1, cssText: "#colorbox.crayon-colorbox, ... overflow: hidden; }", parentRule: null, parentStyleSheet: CSSStyleSheet {}} |
这个对象将这条 CSS 规则分解成了非常方便的形式,于是聪(zhi)明(zhang)的我想到了一个方法:在 CSS 里添加服务器想返回的数据,然后借助这种方式获得它们。毕竟《DOM 启蒙》里提出了这一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<!DOCTYPE html> <html> <head> <link id="linkElement" href="http://yui.yahooapis.com/3.3.0/build/cssreset/reset-min.css" rel="stylesheet" type="text/css"> <style id="styleElement"> body{background-color:#fff;} </style> </head> <body> <script> //取得 <link> 的 CSSStylesheet 对象 console.log(document.querySelector('#linkElement').sheet); //等同于 document.styleSheets[0] //取得 <style> 的 CSSStylesheet 对象 console.log(document.querySelector('#styleElement').sheet); //等同于 document.styleSheets[1] </script> </body> </html> |
于是我在一个跨域的站点上添加了一个 css 文件,css 里有一个 content
属性,属性的值就是我们想要传递的数据,大概是这样:
1 2 3 |
#test::before { content: '{"data": [{"foo": "foo", "bar": "bar"}, {"foo": "bar", "bar": "foo"}], "status": 1, "message": "success"}'; } |
嗯,接下来我们试试在 localhost 下插入这个样式,然后看看能不能获取吧:
1 2 3 4 5 6 7 8 |
var link = document.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); link.setAttribute('href', 'http://ccloli.com/test/cross-origin-test.css'); document.head.appendChild(link); console.log(link.sheet); //CSSStyleSheet {ownerRule: null, cssRules: null, rules: null, type: "text/css", href: …} |
诶?居然是 null
,看来浏览器的防跨域访问机制在这种层面上还是有所防范的。
不过真的没办法么?就在我在群里吐槽的时候,一个菊苣抛来了一个 GitHub 链接,点进去一看:哇!魔法*!
zswang / csst
CSS Text Transformation
简单看了下 Readme,原理大概是这样:首先脚本使用了 animationstart
事件来监听 CSS3 动画事件(这个事件监听很奇妙,一种比 DOMSubtreeModified
事件更好的监听子元素节点变化的事件+),当需要从服务器获取一个请求时,会向 CSST 的 API 发送一个包含 id
属性的请求。这个 API 会返回一个 CSS 文件,文件内不仅仅包含了像上面那样的 CSS 文本,同时还包含了 @keyframes
并给对应的 CSS 选择器添加了 animation
属性。当 CSS 被加载后,animationstart
事件被触发,接下来可以用 getComputedStyle()
方法来获取到返回的数据了!
稍稍改进一下,这里我们不使用 Animation 事件,而改用元素的 onload
事件来处理:当加载成功后创建一个指定的 <div>
元素,然后获取它的伪元素的 content
属性,再将它移除。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
var link = document.createElement('link'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); link.setAttribute('href', 'http://ccloli.com/test/cross-origin-test.css?_=6'); link.addEventListener('load', function(event){ var div = document.createElement('div'); div.setAttribute('id', 'test'); document.body.appendChild(div); console.log(getComputedStyle(div, '::before').content); document.body.removeChild(div); }); document.head.appendChild(link); |
这回我们成功了,在控制台里我们看到了一个被转义的字符串。
1 |
'"{\"data\": [{\"foo\": \"foo\", \"bar\": \"bar\"}, {\"foo\": \"bar\", \"bar\": \"foo\"}], \"status\": 1, \"message\": \"success\"}"' |
所以到此我们已经成功一大半了,至少证明了使用 CSS 跨域获取数据是完全可行的了。
那么怎么处理这个奇怪的字符串呢?CSST 提出的在服务器端与客户端进行 Base64 加密解密是一种好方法,不过刚刚发现也可以这样:
1 2 3 4 |
// var c = '"{\"data\": [{\"foo\": \"foo\", \"bar\": \"bar\"}, {\"foo\": \"bar\", \"bar\": \"foo\"}], \"status\": 1, \"message\": \"success\"}"'; var c = getComputedStyle(div, '::before').content; JSON.parse(c.substr(1, c.length - 2)); // Object {data: Array[2], status: 1, message: "success"} |
借助 substr()
方法,我们可以很方便地将字符串首尾部分的引号去掉。而 JavaScript 会自动处理不需要转义的反斜杠,所以在 JavaScript 眼中这就是一个正常的 JSON 文本啦。虽然实际上反斜杠并没有从字符串中去掉——你可以用 length
属性检验——但是聪明的 JavaScript 已经知道我们想做什么啦 =w=
+ 使用 Animation 事件对第三方脚本(User Script)来说非常有用,这一姿势是以前从鲁夫(OpenGG 作者)的博客里学到的。由于我们有时不能知道目标 DOM 何时被插入,所以我们之前都是使用定时器或者直接 hook 相关函数来处理。虽然有提出 DOMSubtreeModified
事件来监听 DOM 插入,但是由于每次 DOM 树发生变化都会触发事件,所以性能不是很好。Animation 事件配合 CSS3 Animation 特性使用,它的强大之处在于由 CSS 来监测元素的插入,只要有指定的元素插入都会被渲染指定的样式。因此只要有 Animation 事件响应就有元素被插入,由于一个页面目前不会有太多的动画,这种方法相比较与 DOMSubtreeModified
事件有一定的性能优势。
Updated on 2016/05/13:
刚刚发现如果直接对返回的字符串 substr()
是没有效果的,反斜杠并没有被 JavaScript 自动处理(自动处理只出现在主动赋值的时候?)。所以要实现还是得要靠替换,将最后获得 JSON 的代码进行修改,这样应该能解决这个问题了:
1 2 3 4 5 6 7 8 9 10 11 |
var div = document.createElement('div'); div.setAttribute('id', 'test'); document.body.appendChild(div); var res = getComputedStyle(div, '::before').content; var finalRes = res.substr(1, res.length - 2).replace(/\\"/g, '"'); var data = JSON.parse(finalRes); console.log(res, finalRes, data); document.body.removeChild(div); document.head.removeChild(link); |
Updated on 2016/05/13:
最后自己也造了个轮子,欢迎围观(
ccloli / JSON-CSSP
Get JSON callback with CSS Padding, like JSONP
在线示例(Codepen.io 跨域至 ccloli.com):
See the Pen JSON-CSSP Example by ccloli (@ccloli) on CodePen.
暖帖=3=
学到了新姿势
没看懂…