如何在网页字体加载完成后收到通知

102

Google的Web字体API提供了一种定义回调函数的方式,以便在字体加载完成、无法加载等情况下执行。是否有一种类似的方法可以使用CSS3 web字体(@font-face)实现?


请参考https://dev59.com/8mfWa4cB1Zd3GeqPezv3,了解使用Google Fonts API的方法。 - Simon_Weaver
7个回答

170

2015 更新

Chrome 35+ 和 Firefox 41+ 实现了 CSS 字体加载 API (MDN, W3C)。调用 document.fonts 获取一个 FontFaceSet 对象,该对象具有一些有用的 API 以检测字体的加载状态:

  • check(fontSpec) - 返回给定字体列表中所有字体是否已加载并可用。 fontSpec 使用 CSS 字体的简写语法
    示例: document.fonts.check('bold 16px Roboto'); // true 或 false
  • document.fonts.ready - 返回一个 Promise,表示字体加载和布局操作已完成。
    示例: document.fonts.ready.then(function () { /*... 所有字体都加载完成 ...*/ });

这里是一个片段,展示了这些 API,以及 document.fonts.onloadingdone,该 API 提供了关于字体的额外信息。

alert('Roboto loaded? ' + document.fonts.check('1em Roboto'));  // false

document.fonts.ready.then(function () {
  alert('All fonts in use by visible text have loaded.');
   alert('Roboto loaded? ' + document.fonts.check('1em Roboto'));  // true
});

document.fonts.onloadingdone = function (fontFaceSetEvent) {
   alert('onloadingdone we have ' + fontFaceSetEvent.fontfaces.length + ' font faces loaded');
};
<link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'>
<p style="font-family: Roboto">
  We need some text using the font, for the font to be loaded.
  So far one font face was loaded.
  Let's add some <strong>strong</strong> text to trigger loading the second one,
    with weight: 700.
</p>

IE11不支持此API。如果需要支持IE,请查看可用的polyfills或支持库:


2
在Linux的Firefox 44中,似乎check函数总是返回truedocument.fonts.check('1em NoThisReallyDoesntExist')。根据MDN文档,这似乎是一个非常试验性和不完整的功能。但在Chromium中似乎可以正常工作。 - Martin Tournoij
1
我能让它工作的唯一方法是使用 onloadingdone.ready 总是立即触发。 - Jason
我可以使用CSS字体加载吗?(https://caniuse.com/#feat=font-loading) - Alvaro
1
在我看来,仅仅包含font-face并不意味着加载将会开始。你需要使用字体(例如在html/CSS或JS代码中)。只有这样,我才成功地加载了字体。也就是说,在JS中等待字体加载,当你想在JS中使用它时是不够的。 - TomFree
1
.onloadingdone和.ready似乎会在字体加载完成后但在渲染之前被触发。 - W Biggs
显示剩余2条评论

20

在Safari、Chrome、Firefox、Opera、IE7、IE8、IE9中测试通过:

function waitForWebfonts(fonts, callback) {
    var loadedFonts = 0;
    for(var i = 0, l = fonts.length; i < l; ++i) {
        (function(font) {
            var node = document.createElement('span');
            // Characters that vary significantly among different fonts
            node.innerHTML = 'giItT1WQy@!-/#';
            // Visible - so we can measure it - but not on the screen
            node.style.position      = 'absolute';
            node.style.left          = '-10000px';
            node.style.top           = '-10000px';
            // Large font size makes even subtle changes obvious
            node.style.fontSize      = '300px';
            // Reset any font properties
            node.style.fontFamily    = 'sans-serif';
            node.style.fontVariant   = 'normal';
            node.style.fontStyle     = 'normal';
            node.style.fontWeight    = 'normal';
            node.style.letterSpacing = '0';
            document.body.appendChild(node);

            // Remember width with no applied web font
            var width = node.offsetWidth;

            node.style.fontFamily = font;

            var interval;
            function checkFont() {
                // Compare current width with original width
                if(node && node.offsetWidth != width) {
                    ++loadedFonts;
                    node.parentNode.removeChild(node);
                    node = null;
                }

                // If all fonts have been loaded
                if(loadedFonts >= fonts.length) {
                    if(interval) {
                        clearInterval(interval);
                    }
                    if(loadedFonts == fonts.length) {
                        callback();
                        return true;
                    }
                }
            };

            if(!checkFont()) {
                interval = setInterval(checkFont, 50);
            }
        })(fonts[i]);
    }
};

使用方法如下:

waitForWebfonts(['MyFont1', 'MyFont2'], function() {
    // Will be called as soon as ALL specified fonts are available
});

2
你可以将其设置为visible:hidden,它仍然是可测量的。 - izb
2
callback() 函数会在每个加载的字体上被调用,我猜它应该只被调用一次? - Lutsen
需要注意的是,如果字体名称需要,就需要额外加引号:waitForWebfonts(['"Vectora W01 56 Italic"'],...); - Lukx
1
无法与Chrome版本73.0.3683.86兼容 - 功能在字体加载之前被触发。使用网络限速“慢3G”预设进行测试,仅适用于Chrome浏览器,未检查其他浏览器。 - Norman
1
这种方法有一个问题(解释了@Norman的问题),否则它很棒!最初,您将字体设置为sans-serif,但是如果您请求一个serif字体:"Roboto Slab",serif,浏览器会在等待Roboto Slab加载时自然切换到默认的serif字体,从而提前触发测量。我认为修复这个问题的最佳方法是找出所请求的字体的回退系列,并在创建节点时使用该系列,以便只有在加载最终字体后才会发生交换。 - Tony Bogdanov
显示剩余4条评论

12

Google Web Fonts API (以及 Typekit) 使用的 JS 库可以在不使用该服务的情况下使用:WebFont Loader

这个库定义了您所请求的回调函数以及更多其他内容。


1
从 Google 网站上并不明显,但是这个库可以在以下链接下载:https://github.com/typekit/webfontloader - z0r

10

2017年更新

JS库FontFaceObserver绝对是2017年最好的、最轻量级、跨浏览器的解决方案。它还暴露了一个基于Promise的.load()接口。


这个库在使用图标字体方面存在一些问题 https://github.com/bramstein/fontfaceobserver/issues/109#issuecomment-333356795 - maersu
1
@maersu,这个很简单:根据这位用户的说法,只需从字体中选择非方形字符,然后使用var observe_fa = new FontFaceObserver('FontAwesome'); observe_fa.load('<SOME TEST ICON SYMBOLS GO HERE>').then(...) - vintprox
@vintproykt,除非字体只有方块字符;)至少我在Google Material Icons中没有找到任何非方块字符。 - maersu

0
在Safari上,document.fonts.ready字段是不可靠的。我发现唯一可靠的跨浏览器方式(现代浏览器)是反复检查document.fonts.status === 'loaded'。以下是一个带有指数退避的示例:
 const waitForFontsLoaded = document.fonts?.ready.then(() => {
    if (document.fonts?.status === 'loaded') {
        return null;
    }
    console.warn('Browser reported fonts ready but something is still loading...');
    return new Promise((resolve) => {
        let waitTimeMs = 5;
        const checkFontsLoaded = () => {
            if (document.fonts?.status === 'loaded') {
                return resolve();
            }
            waitTimeMs *= 2;
            return setTimeout(checkFontsLoaded, waitTimeMs);
        };
        setTimeout(checkFontsLoaded, 5);
    });
});

await waitForFontsLoaded

0
我创建了两种检查特定字体的方法。第一种方法是最好的,因为它直接使用“fonts”接口和“check”方法。第二种方法不如第一种好,但仍然可用,因为它通过比较文本的大小与默认字体和新字体的文本大小直接在DOM中检测差异。虽然字体大小非常接近时可能会发生事件不触发的情况,但我认为这种情况非常罕见。如果发生这种情况,您可以添加另一个span来检查衬线字体之间的差异。
(尽管它是纯JavaScript,但它可以与React一起使用) 方法1
const fontName = "Fira Sans Condensed",
    maxTime = 2500 // 2.5s

// EXAMPLE 1
fontOnload(fontName).then(() => {
    console.log("success")
})

// EXAMPLE 2
fontOnload(fontName, maxTime).then(() => {
    console.log("success")
}).catch(() => {
    console.log("timeout")
})

async function fontOnload(fontName, maxTime = Infinity, timeInterval = 10) {
    const startTime = performance.now()

    return new Promise((resolve, reject) => {
        setInterval(() => {
            const currentTime = performance.now(),
                elapsedTime = currentTime - startTime
            if (document.fonts.check("12px " + fontName)) {
                resolve(true)
            } else if (elapsedTime >= maxTime) {
                reject(false)
            }
        }, timeInterval)
    })
}

方法二

const fontName = "Fira Sans Condensed",
    maxTime = 2500 // 2.5s

// EXAMPLE 1
fontOnloadDOM(fontName).then(() => {
    console.log("success")
})

// EXAMPLE 2
fontOnloadDOM(fontName, maxTime).then(() => {
    console.log("success")
}).catch(() => {
    console.log("timeout")
})

async function fontOnloadDOM(fontName, maxTime = Infinity, timeInterval = 10) {
    return new Promise((resolve, reject) => {
        const startTime = performance.now(),
            abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
            mainStyle = "font-size:24px!important;display:inline!important;font-family:",
            body = document.body,
            container = document.createElement("div"),
            span1 = document.createElement("span"),
            span2 = document.createElement("span")

        container.classList.add("font-on-load")
        container.setAttribute("style", "display:block!important;position:absolute!important;top:-9999px!important;left:-9999px!important;opacity:0!important;")

        span1.setAttribute("style", mainStyle + "sans-serif!important;")
        span2.setAttribute("style", mainStyle + "\"" + fontName + "\",sans-serif!important;")

        span1.innerText = abc.repeat(3)
        span2.innerText = abc.repeat(3)

        container.append(span1, span2)
        body.append(container)

        const interval = setInterval(() => {
            const currentTime = performance.now(),
                elapsedTime = currentTime - startTime,
                width1 = span1.clientWidth || span1.getBoundingClientRect().width,
                width2 = span1.clientWidth || span2.getBoundingClientRect().width,
                diffWidths = Math.abs(width1 - width2)

            if (diffWidths > 9) {
                clearInterval(interval)
                resolve(true)
            } else if (elapsedTime >= maxTime) {
                clearInterval(interval)
                reject(false)
            }
        }, timeInterval)
    })
}

-6

window.load事件将在所有内容加载完成后触发,包括字体。因此,您可以将其用作回调。但是,如果您决定使用Web字体加载器,则不必这样做。

除了Google、Typekit、Ascender和Monotype选项之外,还有一个自定义模块,可以从任何Web字体提供商加载样式表。

WebFontConfig = { custom: { families: ['OneFont', 'AnotherFont'], urls: [ 'http://myotherwebfontprovider.com/stylesheet1.css', 'http://yetanotherwebfontprovider.com/stylesheet2.css' ] } };

无论您指定哪个提供程序,该库都会发送相同的事件。


4
至少在 WebKit 中,似乎不是这种情况:load 事件可能在网页字体加载完成前就已经触发了。 - Tim Down

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接