守望者--AIR技术交流

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

搜索
热搜: ANE FlasCC 炼金术
查看: 476|回复: 0

[H5周边技术] WebFont 智能压缩工具——字蛛 1.0.0 正式版发布

[复制链接]
  • TA的每日心情
    擦汗
    2018-4-10 15:18
  • 签到天数: 447 天

    [LV.9]以坛为家II

    1742

    主题

    2094

    帖子

    13万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    562
    贡献
    29
    金币
    51756
    钢镚
    1422

    开源英雄守望者

    发表于 2016-8-3 12:00:23 | 显示全部楼层 |阅读模式

    banner-img

    字蛛是一个 WebFont 智能压缩工具,它能自动化分析页面中所使用的 WebFont 并进行按需压缩,通常好几 MB 的中文字体可以被压缩成几 KB 大小。

    字蛛主页:http://font-spider.org

    字蛛从 2014 年 7 月诞生以来,时隔近两年,终于发布了 v1.0.0 正式版本,改进如下:

    1. 支持绝大多数的中英文 Truetype 字体
    2. 支持开源图标字体库 (New: v1.0.0新特性)
    3. 支持 CSS 伪元素解析,支持 content: "string" 与 content: attr(value) 表达式
    4. 支持远程页面解析,并支持资源映射
    5. 支持四种样式规则: <style><link>@import 、style=""
    6. 支持四种调用方式:命令行、Gulp、Grunt、JS Api
    7. 性能、稳定性大幅提高

    新特性:图标字体库压缩

    得益于对 CSS 伪元素的支持,除了常规中英文字体压缩之外,v1.0.0 还带来了万众期待的——图标字体压缩支持,能够支持业界流行的开源图标字库。

    以 Font Awesome 为例,它是一个典型的开源图标字体项目,目前包含有 628 个图标,并且还不断在添加中。虽然它已经做了很多优化,但字库的体积在移动端来说依然偏大,会影响页面载入速度。使用字蛛可以删除掉字体中没有用到的图标,将字体瘦身。例如一个使用 Font Awesome 的示例页面:

    demo-icons

    输入 font-spider 命令,启动字蛛进行字体压缩:

    font-spider

    经过字蛛分析与压缩处理后,Font Awesome 字体中只保留了页面所用到的 20 个图标,ttf 格式字体体积由 142 KB 降为6 KB,如果再配合使用 Webpack 等前端工具将字体 Base64 编码后内嵌到 CSS 中,载入速度可以进一步提升。

    中文字体瘦身

    中文字体通常都有好几 MB 大小,直接嵌入网页中显然不太现实,利用字蛛压缩后可以大幅度的减少体积:

    爬虫实现原理

    为什么字蛛能够找到字体中没有使用的字形数据?这里就涉及到对 HTML 与 CSS 的静态分析。

    虚拟浏览器技术

    字蛛 v1.0.0 版本使用了虚拟浏览器技术来实现 HTML 与 CSS 加载与解析,爬虫模块所依赖的浏览器相关 API 均为它提供。

    • 处理 <base> 标签以及资源定位
    • 加载 <link> 标签或 @import 语句导入的 CSS 文件
    • 处理 CSS Unicode 字符
    • 管理网络请求,处理资源映射配置
    • 支持 CSS3 选择器、样式表树、文本节点读取

    由于虚拟浏览器部分涉及到太多的东西且不是本文重点,所以本文将会略过这部分细节。这部分代码已经分离出来并开源,有兴趣可以去了解: https://github.com/aui/browser-x

    操作样式语法树

    字蛛是通过解析样式表语法树(CSSOM)来获得 WebFont 信息,在浏览器中可以通过 document.styleSheets 来访问 CSS 的语法树,遍历 CSS 规则的函数实现:

    // 遍历 CSS 的规则
    var eachCssRuleList = (function() {
    
        // 遍历 CSSRuleList
        function cssRuleListFor(cssRuleList, callback) {
            var index = -1;
            var length = cssRuleList.length;
            var cssRule, cssStyleSheet;
    
            while (++index < length) {
                cssRule = cssRuleList[index];
    
                // 导入的样式规则
                if (cssRule instanceof CSSImportRule) {
                    cssStyleSheet = cssRule.styleSheet;
                    cssRuleListFor(cssStyleSheet.cssRules || [], callback);
                // CSS 媒体查询规则
                } else if (cssRule instanceof CSSMediaRule) {
                    cssRuleListFor(cssRule.cssRules || [], callback);
                // 普通的规则
                } else {
                    callback(cssRule);
                }
            }
        }
    
        return function(callback) {
            var index = -1;
            var styleSheetList = document.styleSheets;
            var length = styleSheetList.length;
            var cssStyleSheet, cssRuleList;
            // 遍历 StyleSheetList
            while (++index < length) {
                cssStyleSheet = styleSheetList[index];
                cssRuleList = cssStyleSheet.cssRules || [];
                cssRuleListFor(cssRuleList, callback);
            }
        };
    })();

    注:浏览器环境不允许访问跨域后的 CSSOM,但虚拟浏览器没有做此限制

    查找字体

    遍历样式表每一个规则,收集 CSSFontFaceRule 信息:

    // 字体信息
    var webFonts = {};
    // 字体对应的元素列表
    var elements = {};
    
    // 找到 webFont
    eachCssRuleList(function(cssRule) {
        if (cssRule instanceof CSSFontFaceRule) {
            var style = cssRule.style;
            var family = style['font-family'];
            var src = style.src;
    
            // 保存使用此字体的所有元素列表
            elements[family] = [];
            // 保存字体信息
            webFonts[family] = {
                family: family,
                src: src,
                chars: ''
            };
        }
    });

    以如下页面作为示例:

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <title>font-spider</title>
        <style>
            @font-face {
                font-family: 'demo-font';
                src: url('./demo-font.ttf');
            }
            h1.title {
                font-family: 'demo-font';
            }
            h1.title::after {
                content: '——海子';
            }
        </style>
    </head>
    <body>
        <h1 class="title">面朝大海,春暖花开</h1>
    </body>
    </html>

    得到 webFonts

    {
        "demo-font": {
            "family": "demo-font",
            "src": "url(\"file:///Users/aui/Documents/demo-font.ttf\")",
            "chars": ""
        }
    }

    查找字符

    利用 document.querySelectorAll() 来获取使用 WebFont 的字符:

    // 获取当前节点所使用的 webFont
    function matchFontFamily(cssRule) {
        var style = cssRule.style;
        var family = style['font-family'];
        return webFonts[family];
    }
    
    // 将 fontFace 与元素、字符关联起来
    eachCssRuleList(function(cssRule) {
        if (cssRule instanceof CSSStyleRule) {
            var selector = cssRule.selectorText;
            var webfont = matchFontFamily(cssRule);
    
            if (webfont) {
                // 根据选择器来查找元素
                var elems = document.querySelectorAll(selector);
                Array.prototype.forEach.call(elems, function(element) {
                    // 获取元素的文本
                    webfont.chars += element.textContent;
                    // 将元素与字体关联起来
                    elements[webfont.family].push(element);
                });
            }
        }
    });

    此时 webFonts

    {
        "demo-font": {
            "family": "demo-font",
            "src": "url(\"file:///Users/aui/Documents/demo-font.ttf\")",
            "chars": "面朝大海,春暖花开"
        }
    }

    伪元素

    // 处理伪元素,找到继承的 webFont
    eachCssRuleList(function(cssRule) {
        if (cssRule instanceof CSSStyleRule) {
            var selector = cssRule.selectorText;
            var pseudoName = /\:\:?(?:before|after)$/i;
    
            if (!pseudoName.test(selector)) {
                return;
            }
    
            // 查找伪元素所在的节点
            selector = selector.replace(pseudoName, '');
            var elems = document.querySelectorAll(selector);
    
            // 获取伪元素 content 值
            var content = cssRule.style.content.replace(/^["']|["']$/g, '');
    
            for (var i = 0; i < elems.length; i ++) {
                var elem = elems[i];
                for (var family in webFonts) {
                    // 从伪元素自身不断冒泡,直到找到继承的字体
                    while (elem) {
                        if (elements[family].indexOf(elem) !== -1) {
                            webFonts[family].chars += content;
                            break;
                        }
                        elem = elem.parentNode;
                    }
                }
            }
        }
    });

    此时 WebFont:

    {
        "demo-font": {
            "family": "demo-font",
            "src": "url(\"file:///Users/aui/Documents/demo-font.ttf\")",
            "chars": "面朝大海,春暖花开————海子"
        }
    }

    完整代码在线演示:https://jsfiddle.net/9ont96c4/2

    至此,以上例子已经成功演示了字蛛爬虫查找字体、查找文本的工作原理。实际上 HTML 与 CSS 远比上面示例页面复杂,需要处理:

    1. 伪类选择器
    2. font 缩写
    3. 行内样式
    4. 完整的字体匹配算法

    由于篇幅有限,上述细节部分可以参见字蛛爬虫模块源码

    相关链接

    守望者AIR技术交流社区(www.airmyth.com)
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    
    关闭

    站长推荐上一条 /4 下一条

    QQ|手机版|Archiver|网站地图|小黑屋|守望者 ( 京ICP备14061876号

    GMT+8, 2019-10-23 00:40 , Processed in 0.047481 second(s), 37 queries .

    守望者AIR

    守望者AIR技术交流社区

    本站成立于 2014年12月31日

    快速回复 返回顶部 返回列表