V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
cuo9958
V2EX  ›  前端开发

自定义字体服务 - 基于 Node 的 Web 字体解决方案

  •  
  •   cuo9958 · 2023-07-26 14:00:18 +08:00 · 469 次点击
    这是一个创建于 492 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    在前端开发中,字体是非常重要的组成部分,因为字体可以为页面提供良好的可读性和设计感。在设计中,经常需要使用一些特殊的字体,以突出设计效果或满足特殊的审美需求。但是,在使用一些奇怪字体的需求下,使用图片来替代字体可能会影响页面性能,特别是图片大小和加载时间的问题。

    一般来说,为了解决这个问题,我们会使用 CSS3 中的 @font-face 提供的自定义字体功能。通过使用 @font-face ,我们可以自定义一个字体文件,将字体文件嵌入到网页中,从而避免使用图片来替代字体。这种方法可以解决一些特殊字体的问题,但是可能会面临一些挑战。英文字体文件通常比中文字体文件小得多。一个常见的英文字体文件大小通常不超过 100KB ,而一个中文字体文件可能会超过 10MB 。这是因为中文字体通常包含大量的字符,包括不同的汉字、符号和字体效果等。这种情况下,为了一两个字或者一行字,就需要加载几 MB 大小的整个字体文件,显然是不划算的。此外,在网络差的环境下,即使用户浏览完页面,字体文件也可能还没有加载完成,这时候已经没有意义了。 换个角度想想,虽然一个中文字体包含了几千个常用字,但一个网页去掉重复字的情况下,往往只包含数十个到数百个字。因此,是否有必要加载一个完整包含数千个字的字体文件,可以考虑只加载网页需要的字体,这样能够减小加载字体文件的大小,同时也能保证字体加载的速度。

    技术选型

    针对上面的两个问题,常见的解决方案有:

    1. font-spider:字蛛是一个智能 WebFont 压缩工具,它能自动分析出页面使用的 WebFont 并进行按需压缩。https://github.com/aui/font-spider

      * 优点:提供大量可商用字体,便捷获取。
      * 缺点:需要付费,版权复杂,可用性有限。
      
    2. font-carrier:font-carrier 是一个功能强大的字体操作库,使用它你可以随心所欲地操作字体。https://github.com/purplebamboo/font-carrier

      * 优点:可以收集和备份已经购买的商用字体。
      * 缺点:需要确保拥有商用字体的正版授权。
      
    3. 有字库:有字库是全球第一中文 web font (网络字体)服务平台,致力于美化网页界面,降低网页设计和维护的难度,同时提升效率。http://www.webfont.com/

      * 优点:提供大量可商用字体,便捷获取。
      * 缺点:需要付费,版权复杂,可用性有限。
      
    4. fontmin: fontmin 是一个纯 JavaScript 字体子集化方案,提供了 APICLI的使用方式。是一个非常成熟实用的字体优化工具。http://ecomfe.github.io/fontmin/#feature

      * 优点:可以进行字体压缩和子集化优化。
      * 缺点:需要安装依赖,优化效果取决于字体本身。
      

    @font-face 介绍

    @font-face 是 CSS 的一个规则,用于定义自定义字体。它允许网页设计者使用自定义字体,而不必依赖用户的计算机上已安装的字体。 使用 @font-face 规则,可以将字体文件(通常是 TrueType 字体文件或 OpenType 字体文件)嵌入到网页中,并通过 CSS 指定字体的名称和样式 。 以下是一个示例,演示如何使用 @font-face 定义并应用自定义字体:

    @font-face {
      font-family: 'MyCustomFont';
      src: url('myfont.ttf');
    }
    
    body {
      font-family: 'MyCustomFont', sans-serif;
    }
    
    

    在上面的示例中,myfont.ttf 是字体文件的路径。通过 @font-face 规则定义了一个名为 'MyCustomFont' 的自定义字体,然后在 body 元素中应用了这个字体。

    实现方案(介绍字体设计、转换、兼容性处理等技术实现细节。)

    综合考虑之后,我们采用的是 fontmin 。 Alt text

    这是官网的一个例子,从一个大小为 4.2M,包含 7500+的字体包中,提取了 7 个字,输出的子集字体大小在 4.5K 左右。

    结合我们的业务场景,为了方便管理,我们结合 Node.js,开发了字体服务管理系统,实现了字体文件的上传和管理,目前支持 TTF 和 OTF 两种字体格式,可以根据字体文件和需要的文字,动态压缩并输出子集字体同时上传到 cdn ,同时提供了一键复制 css 的功能,复制完成后直接放在自己的 css 文件中,就可以直接使用。

    使用步骤

    第一步:在字体库管理列表维护所使用的字体文件,得到字体库名称以及将字体库同步传到 cdn,获得 url 地址; Alt text 第二步:在字体配置管理页面根据需要的字体和使用的文字得到压缩后的使用地址; Alt text 第三步:拿到“复制地址”里的链接,通过 @font-face 设置自定义字体,就能实现我们想要的效果啦;

    // 定义字体名称,src 里就是复制的地址
    @font-face {
      font-family: yezi; //自己起个字体名字
      src: url("https://xxx/yezi_5f27fae9ad916de19343e2b8a1635680.ttf");
    }
    // 使用
    .font-test{
      font-size: 46px;
      font-family: yezi;  //设置成你上面自定义的字体
    }
    
    

    Alt text

    接口实现方式(实现中遇到的问题和解决方案)

    在维护的自己的字体文件时,能拿到字体包对应的 key 值,此 key 值是唯一的,在压缩字体的时候,基于接口设计了通用的解决方案,接口接收使用的字体包对应的 key 和需要压缩的文字,压缩的时候通过调用接口生成子集文件并同步上传到 cdn,返回 cdn 地址。

      https://example.com/api/font?key=yezi&fontlibfile=url&word=word&pageNme=pageNmae
    // key 表示使用的字体包对应的 key
    // fontlibfile 表示使用的字体包对应的 url(我们的字体包是存在 cdn 上的)
    // word 表示需要提取的文字
    // pageNme 为了记录是哪个项目和页面使用(只针对我们的业务场景)
    
    

    接口实现,因为我们的字体包存在 cdn 上,fontmin 不支持远程文件做为源字体包,所以我们需要先将字体包下载下来临时存放在本地,压缩完成之后再删掉。

    export default async (fontlibfile: string, key: string,  pageName: string, word: string) => {
      // fontlibfile 表示使用的字体包对应的 url(我们的字体包是存在 cdn 上的)
      // key 表示使用的字体包对应的 key
      // word 表示需要提取的文字
      // pageNme 为了记录是哪个项目和页面使用(只针对我们的业务场景)
        
      let data: any = {};
      let text = word;
    
      if (fontlibfile) data.fontlibfile = fontlibfile;
      if (text) data.word = text;
      if (pageName) data.pageName = pageName;
      if (key) data.key = key;
    
      data.fontUrl =`https://example.com/${text}.ttf`;
    
      if (!fontlibfile) throw new Error('字体库或 key 不存在');
      try {
        const cacheFile = path.join(__dirname, `./font/`);
        // 用于存放下载下来的源字体包
        if (!fs.existsSync(cacheFile)) {
          fs.mkdirSync(cacheFile, {recursive: true});
        } else {
          log.info("临时存放目录已存在");
        };
        const filePath = await tempDownload(fontlibfile);  // 获取下载之后的源字体文件地址
        const extname = path.extname(filePath);
        const useTtfType = (extname === '.ttf' || extname === '.TTF') ? true : false;  //因为支持 ttf 和 otf 两种格式,需要区分一下
        const fontmin = new Fontmin()
          .src(filePath)
          .use(useTtfType ? Fontmin.glyph({ text }) : Fontmin.otf2ttf({ text }))  //不管是 ttf 还是 otf,最终我们都转成 ttf 格式
    
          const content:any = await new Promise((resolve, reject) => {
            fontmin.run((error, files) => {
              if (error) {
                return reject(error);
              }
              return resolve(files[0].contents);
            });
          });
        await writeFile(`${cacheFile}${text}.ttf`, content);
        const file = fs.readFileSync(`${cacheFile}${text}.ttf`);
        let res;
        res = await IMGCLIENT.client.put(`dm_auto_config_server/${text}.ttf`, file); // 将压缩后得到的子集字体文件上传到 cdn
        fs.unlink(`${__dirname}/font/${text}.ttf`, (err) => {
          if(err) {
            log.info('删除文件失败');
          };
        });
        return res.url; // 拿到压缩后的子集字体文件地址
      } catch(error) {
        throw new Error('字体文件压缩失败');
      }
    }
    
    

    为了解决文字重复和避免重复压缩同样字体造成资源浪费,需要对文字进行去重,并考虑缓存。

        // 对文字去重以及排序
        let text = Array.from(new Set(word))
        .sort()
        .join('');
    
        const textMd5 = `${key}_${md5(text)}`;  // 并将上面的代码中的 text 替换成 textMd5
        const exist = await GetFontManagByBufferName(`${textMd5}`); // 去数据库中差是否有缓存
        if (exist !== null) {
          throw new Error(`字体配置中已经存在了字体库为${fontlibName}key 值为${key}的字体配置`);  // 直接去复制已有的文件就好了
        }
        
    
    

    如果对已经压缩好的文字进行编辑,但是已经有很多页面使用了,那编辑的时候还要避免子集文件地址的变更。不然一旦重新编辑,地址就会发生改变,那已经使用的页面还需要更换对应的地址并重新发布,非常不友好。

    // 编辑时入参增加 id:新建时生成的唯一值,bufferName:新建时生成的由 key 和 textMd5 组合而成
    
    const textMd5 = id ? bufferName : `${key}_${md5(text)}`;
    
    

    解决了地址不能变更的事情,新的问题又来了,那就是 cdn 缓存,因为我们的文件是自动上传到 cdn 的,地址不变,如果不强制清缓存,请求不到新的资源。

    // 我们的资源是放在阿里云上的,查了 api,发现 api 里有上传时强制不缓存的设置
    
    res = await IMGCLIENT.client.put(`dm_auto_config_server/${textMd5}.ttf`, file, { headers: {'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', 'Pragma': 'no-cache', 'Expires': -1} });
    

    到这里,自定义字体服务的核心功能就完成了。来看下效果吧 效果 原来的叶子字体包大小是 4.5M,压缩之后提取了我们需要的文字,大小为 4.1k 。

    总结

    在这个自定义字体服务的例子中,我们实现了对字体的优化和管理。通过合理的设计和优化,我们可以减小字体文件的大小,提高网页的加载速度和用户体验。但是,这只是优化字体的一个方面。为了确保字体的质量和用户体验,我们还需要注意字体的可读性、风格、版权等问题。同时,在开发过程中,我们也需要考虑字体的兼容性和响应式设计等方面,以适应不同设备和不同屏幕尺寸的用户。 fontmin 虽然很厉害,但不是万能的,由于 font 文件表之间极其复杂的相互关联,转换得到的子字体将会破坏一部分关联信息(比如会影响字体竖排时的间距)。在压缩数字的时候,如果不加入一个标点符号,会导致压缩失败,等等。希望对大家有所帮助,如果有疑问可以多多交流与反馈,共同改进这个方案。

    参考

    1. FontFace https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face
    2. fontmin http://ecomfe.github.io/fontmin/#feature
    3. 基于 Node.js 的 WebFont 解决方案

    作者:洞窝-小米 讨论 QQ 群:199938225

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2738 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 15:06 · PVG 23:06 · LAX 07:06 · JFK 10:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.