搞个 PostCSS 插件,自动处理文字在安卓手机上不加粗的问题

问题:设计稿复现中的字体难题

开发 H5 项目时,设计同学给出的设计稿中全部使用 PingFang 字体。但当我们按照设计稿完成开发并进行验收时,遇到了一个问题:在 iOS 上,一切按照期望进行,但在 Android 手机上,需要加粗的文本却不如期望那样显示了。

原因分析:PingFang 字体在 Android 手机上的兼容问题

不加粗的文本,其对应的 CSS 如下:

css 复制代码
.foo {
    font-family: 'PingFangSC-Medium';
}

经验丰富的开发很快就能看出原因,PingFang 字体在 Android 手机上并不受支持。

解决思路:利用 font-weight 替代 font-family

首次尝试的解决方案是使用 font-weight 来替代 font-family。一个小提示:PingFang 字体支持6种 font-weight,如下表所示:

font-weight font-family
100 PingFangSC-Ultralight
200 PingFangSC-Thin
300 PingFangSC-Light
400 PingFangSC-Regular
500 PingFangSC-Medium
600 PingFangSC-Semibold

同时设置全局的 font-family,如下:

css 复制代码
body {
    font-family: system-ui, -apple-system, "PingFang SC", sans-serif;
}

现在,元素的 CSS 更改为:

css 复制代码
.foo {
    font-weight: 500;
}

这个解决方案看似完美,实际上,在大多数 Android 手机上仍然存在问题------文本并没有被成功地加粗。原因在于,大多数 Android 手机的自带字体并不支持 500 的字重,浏览器将其视作 400 来进行呈现,导致文本显示为普通粗细。

安卓默认字体对中英文的支持情况如下:

  • 中文、日文、韩文字体 Noto Sans,只支持两种字重 Regular 400 和 Bold 700。
  • 英文字体 Roboto,支持完整字重 100-900。

浏览器的字体匹配算法如下:

当明确指定了 font-weight 数值,即所需的字重,并且字体中确实存在该对应字重时,浏览器将直接选择对应的字重进行渲染。若无法找到对应的字重,浏览器将依照以下规则选择最合适的字重进行展示:

  • 若所需的字重小于400,则首先降序检查小于所需字重的各个字重,如仍然没有,则升序检查大于所需字重的各字重,直到找到匹配的字重。
  • 若所需的字重大于500,则首先升序检查大于所需字重的各字重,之后降序检查小于所需字重的各字重,直到找到匹配的字重。
  • 若所需的字重是400,那么会优先匹配500对应的字重,如仍没有,那么执行第一条所需字重小于400的规则。
  • 若所需的字重精确为500,首选字重为400的选项;如无法匹配,将同样启用对应字重小于400的匹配规则。

引入外部字体?增大 font-weight?

当然,我们可以引入外部字体来解决 Android 不支持 500 字重的问题。然而,中文字体往往体积较大,会严重影响页面的 LCP 指标。同时,大部分字体的商业使用需要相应的费用,因此这不是一个适合我们项目的解决方案。

中文字体体积大主要因素如下:

  1. 英文字体仅包含26个字母以及一些符号,而中文字体包含的字形则海量无比。
  2. 中文字形的线条复杂度远胜于英文字形,因为控制中文字形线条的数据点比英文字形更多,导致数据量更大。

鉴于目前的情况,我们可以尝试将 font-weight 提高到 700 来修复问题。然而,这会使 iOS 端的字体变得过于粗重,与设计稿有出入,这让设计团队难以接受。

那么,是否存在这样一种方法,让我们可以在 iOS 端将 font-weight 设置为 500,而在 Android 端将 font-weight 设置为 700,这样就可以完美地调和两者了。

根据设备来设置 font-weight

我们当然可以使用 JavaScript 通过 navigator.userAgent 属性来判断设备是 Android 还是 iOS,从而为元素在不同设备上应用不同的 CSS。

但有一种更加简单的方式,可以使用 @supports CSS 指令来分辨设备类型。现在,元素的 CSS 更改为:

css 复制代码
.foo {
    font-weight: 700;
}
 @supports (-webkit-touch-callout: none) {
    .foo {
        font-family: PingFangSC-Medium;
    }
}

现如今,Android 设备上的样式为 font-weight: 700,而 iOS 设备上元素的样式为 font-family: PingFangSC-Medium,从而优雅地解决了问题。

自动化处理:使用 PostCSS 插件

然而,每次要手动替换元素的样式需要大量的工作量。为了简化流程,我编写了一个 PostCSS 插件,可以使这个过程完全自动化。

PostCSS 是什么

PostCSS 是一个用于转换 CSS 代码的工具。它提供处理 CSS 的 API,开发者可以使用这些 API 编写插件实现各种 CSS 处理任务,比如 autoprefixer 就是使用 PostCSS 编写的。这些插件可以完成许多任务,包括:

  • 自动添加供应商前缀以确保 CSS 在不同的浏览器中能够兼容性地运行
  • 使用未来的 CSS 语法
  • 添加 CSS 变量和混合
  • 转换 px 为 em
  • 还有许多其他任务......

如何开发 PostCSS 插件

PostCSS 插件就是默认导出是一个函数的 CommonJS 文件,如下:

js 复制代码
module.exports = (opts = {}) => {
    return {
        postcssPlugin: 'PLUGIN NAME',
        // 插件的监听器
        Once (root) {
            // 每个文件调用一次,每个文件都对应一个 root 对象
        },
        Declaration (decl) {
            // 全部声明的节点
        }
    }
}

module.exports.postcss = true

大多数 PostCSS 插件做2件事:

  1. 在 CSS 中查找元素(例如,will-change 属性)。
  2. 更改找到的元素(例如,在 will-change 之前插入 transform: translateZ(0) 来兼容旧浏览器)。

PostCSS 将 CSS 解析为抽象语法书树(AST)。这棵树包含:

  • Root:树的根节点,表示 CSS 文件。
  • AtRule:@ 开头的语句,如 @charset "UTF-8"@media (screen) {}
  • Rule:包含声明的选择器。如 button {}
  • Declaration:键值对,如 color: black;
  • Comment:独立存在的注释。选择器、规则参数和值中的注释存储在节点的 raws 属性中。

当你找到正确的节点时,将需要更改、插入或删除它们周围的其他节点。PostCSS 节点有一个类似 DOM 的 API 来转换 AST。节点有方法可以四处移动(如 Node#nextNode#parent)、查看子节点(如 Container#some)、删除节点或添加新节点。

js 复制代码
Declaration (node, { Rule }) {
    let newRule = new Rule({ selector: 'a', source: node.source })
    node.root().append(newRule)
    newRule.append(node)
}

我的插件流程比较简单,扫描所有的 CSS,当发现 font-family 包含 PingFang 时,就将其类名和 font-family 值记录下来,然后将 font-family 转换为 font-weight。扫描结束后,将记录下来的 CSS 类名和其 font-family 值放到 @supports (-webkit-touch-callout: none) 规则中。

你可以在以下地址看到详细的代码实现:github.com/SyMind/post...

现在,你不需要再对 CSS 进行任何更改------它可以维持最初的样子:

css 复制代码
.foo {
    font-family: 'PingFangSC-Medium';
}

插件会自动将其转换为:

css 复制代码
.foo {
    font-weight: 700;
}
 @supports (-webkit-touch-callout: none) {
    .foo {
        font-family: PingFangSC-Medium;
    }
}

于开发者而言,这个过程是完全无感知的,你不再需要操心任何关于 font-familyfont-weight 的问题,仅需按需复制粘贴设计稿中的 CSS 代码即可。

通过 npm 包安装并使用

我已经将此工具作为 npm 包发布。你可以通过以下方式进行安装和使用:

安装:

bash 复制代码
npm install postcss-pingfang

使用:

js 复制代码
// 依赖
const fs = require('fs')
const postcss = require('postcss')
const pingfang = require('postcss-pingfang')

// 要处理的 css
const css = fs.readFileSync('input.css', 'utf8')

// 处理 css
const output = postcss()
  .use(pingfang())
  .process(css)
  .css

总结

面对设计同学使用 PingFang SC 作为设计稿默认字体时,会带来以下的问题:

  1. PingFang SC 并非安卓手机的内置字体,可以使用 font-weight 替换原设计稿中的字体,如 PingFangSC-Medium 替换为 500。
  2. 但大部分安卓手机内置的字体仅对中文支持 3 种字重,此时如 font-weight 为 500 的字体实际上并不会被加粗。

我们提出一种相对轻量级的解决方案相比于引入外部字体极其轻量级的方案,通过 @supports CSS 指令,在 iOS 手机上使用 PingFangSC-Medium,在安卓手机上使用 font-weight: 700。并开发 PostCSS 插件,从而使得整个过程完全自动化。

相关推荐
cwj&xyp6 分钟前
Python(二)str、list、tuple、dict、set
前端·python·算法
dlnu20152506228 分钟前
ssr实现方案
前端·javascript·ssr
古木201912 分钟前
前端面试宝典
前端·面试·职场和发展
轻口味2 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀3 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef5 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端