Css_text_transformation

Posted by Blog of Tech@Yunlaiwu.com on July 14, 2017

CSST(CSS Text Transformation)

什么是 CSST?

一种用 CSS 跨域传输文本的方案。相比 JSONP 更为安全,不需要执行跨站脚本。

原理

1、动态创建link标签,将参数通过href属性传给服务端。 2、服务端处理相应操作,并生成css数据,最后将返回的数据放进content属性中。 3、前端通过监听读取 css 样式中的 content 属性获取传送内容。

调用流程

image

技术手段

  • 怎么监听 <link> 加载完毕?

IE浏览器可以通过onload事件来监听。FF,webkit则可以通过node.sheet.cssRules属性是否存在来判断是否加载完毕,但是需要使用setTimeout轮询。

如果是 CSS3 场景,可以取巧用动画开始 (animationstart) 这个事件来捕获,但是在server端返回的css代码,需要要加上动画样式。

function cssReady (node, fn) {
        // for IE6-9 and Opera
        if (node.attachEvent) {
          node.attachEvent('onload', fn);
        }
        // polling for Firefox, Chrome, Safari
        else {
          setTimeout(function() {
            poll(node, fn);
          }, 0); // for cache
        }

        function poll(node, fn) {
          if (fn.isCalled) {
            return;
          }

          var isLoaded = false;

          if (/webkit/i.test(navigator.userAgent)) {//webkit
            if (node['sheet']) {
              isLoaded = true;
            }
          }
          // for Firefox
          else if (node['sheet']) {
            try {
              if (node['sheet'].cssRules) {
                isLoaded = true;
              }
            } catch (ex) {
              // NS_ERROR_DOM_SECURITY_ERR
              if (ex.code === 1000) {
                isLoaded = true;
              }
            }
          }

          if (isLoaded) {
            // give time to render.
            setTimeout(function() {
              fn();
            }, 1);
          }
          else {
            setTimeout(function() {
              poll(node, fn);
            }, 1);
          }
        }

      }
  • 怎么传送特殊字符(”、’、\、\n、\r、\t)?

Chrome、Safari 对 content 样式属性字符解析并不一致

为避免未知解析规则影响,统一使用 base64 编码

服务器应答的内容

function response() {
    // 前端传来的元素 id,用来将返回的样式绑定到该元素上
    $__callbackId = $_GET['__callbackId'];
    
    $data = [
        "name" => "张三",
        "age" => "33",
        "sex" => "男",
    ];
    $ret = base64_encode(json_encode($data));
    
    header('Content-type: text/css');
    
    $css = "#%s{content='%s'}";
    
    echo sprintf($css, $__callbackId, $ret);
}

与 JSONP比较

  • 缺点:没有 JSONP 适配广,低版本浏览器兼容方面或多或少会存在问题。
  • 优点:不会因为跨站脚本对网站安全造成影响。

最后,粘贴一下实例:


<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>CSS Text Transformation Example</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0">
  <meta name="format-detection" content="telephone=no"/>

</head>
<body>
  <button onclick="fuck()">获取数据</button><br><br>

  id:<input type="text" id="id" value=""><br>
  name:<input type="text" id="name" value=""><br>
  age:<input type="text" id="age" value=""><br>
  sex:<input type="text" id="sex" value=""><br>

</body>
<script>

  (function() {

    function CSST(opts) {

      this.timer = null;

      this.opts = opts || {};

      this.opts.data = this.opts.data || {};

      this.opts.data.__csstCallbackId = opts.id || this.randomString();

      this.head = document.getElementsByTagName('head')[0];

      this.link = document.createElement('link');

      this.span = document.createElement('span');

      this.span.id = this.opts.data.__csstCallbackId;

      this.link.href = this.createUrl();

      this.link.rel = 'stylesheet';

      this.link.type = 'text/css';

      this.head.appendChild(this.link);

      document.documentElement.appendChild(this.span);

      this.main();

    }

    CSST.prototype = {

      main: function() {

        if (this.opts.timeout) {
          this.timeout();
        }

        this.cssReady(this.link, this.handler(this));
      },

      handler: function(context) {

        return function() {
          var computedStyle = window.getComputedStyle(context.span, false);

          var content = computedStyle.content;

          var match = content.match(/[\w+=\/]+/);
          // base64 解码
          if (match) {
            try {
              content = decodeURIComponent(escape(context.base64decode(match[0])));
            }
            catch (ex) {
              context.opts.error && context.opts.error(ex);
              return;
            }
          }

          try {
            content = JSON.parse(content);
          }
          catch (ex) {
            context.opts.error && context.opts.error(new Error('json parse error!'), content);
            return;
          }

          context.opts.success && context.opts.success(content);
          context.cleanup();
        }

      },

      cssReady: function (node, fn) {
        // for IE6-9 and Opera
        if (node.attachEvent) {
          node.attachEvent('onload', fn);
        }
        // polling for Firefox, Chrome, Safari
        else {
          setTimeout(function() {
            poll(node, fn);
          }, 0); // for cache
        }

        function poll(node, fn) {
          if (fn.isCalled) {
            return;
          }

          var isLoaded = false;

          if (/webkit/i.test(navigator.userAgent)) {//webkit
            if (node['sheet']) {
              isLoaded = true;
            }
          }
          // for Firefox
          else if (node['sheet']) {
            try {
              if (node['sheet'].cssRules) {
                isLoaded = true;
              }
            } catch (ex) {
              // NS_ERROR_DOM_SECURITY_ERR
              if (ex.code === 1000) {
                isLoaded = true;
              }
            }
          }

          if (isLoaded) {
            // give time to render.
            setTimeout(function() {
              fn();
            }, 1);
          }
          else {
            setTimeout(function() {
              poll(node, fn);
            }, 1);
          }
        }

      },

      createUrl: function() {

        var url = this.opts.url;

        var param = this.json2param(this.opts.data);

        url += (url.indexOf('?') >= 0 ? '&' : '?') + param;
        
        return url.replace('?&', '?');
      },

      json2param: function(data) {

        if (!data) {
          return '';
        }

        var index, item, params = [];

        for (index in data) {
          item = data[index];
          params.push(index + "=" + item);
        }

        return params.join('&');
      },

      timeout: function() {
        var self = this;

        self.timer = setTimeout(function () {
          self.cleanup();

          if (self.opts.error) {
            self.opts.error(new Error('Timeout'));
          }

        }, self.opts.timeout);
      },

      cleanup: function () {
        if (this.link && this.link.parentNode) {
          this.link.parentNode.removeChild(this.link);
          this.link = null;
        }

        if (this.span && this.span.parentNode) {
          this.span.parentNode.removeChild(this.span);
          this.span = null;
        }

        if (this.timer) {
          clearTimeout(this.timer);
          this.timer = null;
        }
      },

      randomString: function (len) {
        len = len || 16;
        var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';    
        /** **默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
        var maxPos = $chars.length;
        var pwd = '';
        for (i = 0; i < len; i++) {
          pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
        }
        return pwd;
      },

      base64decode: function (input) {  
        var _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";  
        var output = "";  
        var chr1, chr2, chr3;  
        var enc1, enc2, enc3, enc4;  
        var i = 0;  
        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");  
        while (i < input.length) {  
            enc1 = _keyStr.indexOf(input.charAt(i++));  
            enc2 = _keyStr.indexOf(input.charAt(i++));  
            enc3 = _keyStr.indexOf(input.charAt(i++));  
            enc4 = _keyStr.indexOf(input.charAt(i++));  
            chr1 = (enc1 << 2) | (enc2 >> 4);  
            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);  
            chr3 = ((enc3 & 3) << 6) | enc4;  
            output = output + String.fromCharCode(chr1);  
            if (enc3 != 64) {  
                output = output + String.fromCharCode(chr2);  
            }  
            if (enc4 != 64) {  
                output = output + String.fromCharCode(chr3);  
            }  
        }  
        output = _utf8_decode(output);  

        function _utf8_decode (utftext) {  
          var string = "";  
          var i = 0;  
          var c = c1 = c2 = 0;  
          while ( i < utftext.length ) {  
              c = utftext.charCodeAt(i);  
              if (c < 128) {  
                  string += String.fromCharCode(c);  
                  i++;  
              } else if((c > 191) && (c < 224)) {  
                  c2 = utftext.charCodeAt(i+1);  
                  string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));  
                  i += 2;  
              } else {  
                  c2 = utftext.charCodeAt(i+1);  
                  c3 = utftext.charCodeAt(i+2);  
                  string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));  
                  i += 3;  
              }  
          }  
          return string;  
        } 
        
        return output;  
      }
    };

    window.csst = function(opts) {
      return new CSST(opts);
    }

  })();

</script>

<script>
function fuck () {
  window.csst({
      url: "http://t2.yunlaiwu.com:8099/sem/test",
      data: {
        id: '1001',
        name: '张三',
        age: 23,
        sex: "男"
      },
      success: function(data) {
        if (!data) {
          alert('蛤?');
          return;
        }

        for (var x in data) {
          document.getElementById(x).value = data[x];
        }
      },
      error: function(err) {
        console.error(err)
      }
  });
};
</script>
</html>