Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebFont 智能压缩工具——字蛛 1.0.0 正式版发布-新增图标字体压缩 #79

Open
aui opened this issue May 12, 2016 · 10 comments

Comments

@aui
Copy link
Owner

aui commented May 12, 2016

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>@importstyle=""
  6. 支持四种调用方式:命令行、Gulp、Grunt、JS Api
  7. 性能、稳定性大幅提高

新特性:图标字体库压缩

得益于对 CSS 伪元素的支持进一步完善,除了常规中英文字体压缩之外,v1.0.0 还带来了万众期待的——图标字体压缩支持。

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

demo-icons

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

font-spider-iconfont

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

中文字体瘦身

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

font-spider-webfont

爬虫实现原理

为什么字蛛能够找到字体中没有使用的字形数据?这里就涉及到对 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. 完整的字体匹配算法

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

相关链接

@aui aui closed this as completed May 12, 2016
@aui aui reopened this May 12, 2016
@aui aui changed the title 字蛛 1.0.0 正式版 字蛛 1.0.0 正式版发布 May 13, 2016
@tpai
Copy link

tpai commented May 13, 2016

請問除了靜態頁面 .html 可以這麼做之外 動態產生的內容像是 .jsx 這一類的也行嗎?

@aui
Copy link
Owner Author

aui commented May 13, 2016

@tpai jsx 是不支持的,它无法用浏览器解析。下一版本计划加入模板片段支持

@aui aui changed the title 字蛛 1.0.0 正式版发布 字蛛 1.0.0 正式版发布-新增图标字体压缩 May 13, 2016
@aui aui changed the title 字蛛 1.0.0 正式版发布-新增图标字体压缩 WebFont 智能压缩工具——字蛛 1.0.0 正式版发布-新增图标字体压缩 May 13, 2016
@Kirito-H
Copy link

请问为什么运行命令之后无效? 只是打印了一行"Load ...."就结束了......

@aui
Copy link
Owner Author

aui commented Feb 24, 2017

@Kirito-H 你的问题我很难回答,请按照标准的提 BUG 方式完善你的问题,不要贴到无关问题下。

@Kirito-H
Copy link

@aui 噢,不好意思,发错地方了。
我说那个我后来发现问题所在了.. 原来是路径中不能包含有中文..
感觉这个在官网加句说明用起来会方便些 :)

@hxdyj
Copy link

hxdyj commented Oct 20, 2017

模板片段求支持啊,用vue,angular单页没法用啊

@WangShuXian6
Copy link

请问前辈,如何在vue中使用~~~~

@14glwu
Copy link

14glwu commented Dec 20, 2018

前辈,请问在国际化方案中如何压缩字体啊,我的字体在不同语言环境下的js文件中

@aui
Copy link
Owner Author

aui commented Dec 21, 2018

@14glwu 字蛛仅仅用来处理静态页面,你可以使用 fontmin 的 API 来处理

@ystcode
Copy link

ystcode commented Sep 13, 2024

基于字蛛font spider 我开发了一个网站,免费在线使用:https://www.mainww.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants