在前端开发中,使用漂亮的字体可以极大地提升用户体验。Google Fonts 提供了海量的免费字体,但管理它们却可能成为一件麻烦事:
- 手动引入:每个新项目或新字体,你都得去 Google Fonts 官网生成 URL,再复制到代码中。
- 字重管理:不同字体支持的字重(font-weight)各不相同,手动管理容易出错。
- 代码冗余:项目中可能存在不再使用的 `@import` 规则,占用带宽。
有没有一种方法可以实现“按需自动引入”字体呢?答案是肯定的!本文将带你了解如何利用 PostCSS 打造一个智能插件,彻底告别手动管理 Google Fonts 的烦恼。
我们的目标
我们希望实现一个自动化流程:
- 自动检测:插件能自动扫描我们 CSS 代码中使用了哪些字体。
- 智能配置:我们可以为每个字体配置其在 Google Fonts 上的名称和支持的字重范围。
- 按需导入:插件根据检测到的字体,自动在 CSS 文件顶部生成对应的 `@import` URL。
- 自动清理:移除代码中已经不再使用的旧字体导入。
PostCSS 工具
在我们开始之前,先简单认识一下 PostCSS。
你可以把 PostCSS 理解为一个“CSS 的加工厂”。它本身不做任何事,但它提供了一个强大的插件系统,让我们可以用 JavaScript 来分析、转换和优化 CSS 代码。
我们所有的魔法都将在 `postcss.config.js` 这个配置文件中发生。
最终的配置概览
在我们的项目中,`postcss.config.js` 的核心配置如下
// postcss.config.js
module.exports = {
plugins: [
// 1. 我们的自定义字体插件,最先运行
require('./path/to/autoFontImport')({ /* ...配置... */ }),
// 2. Tailwind CSS 处理器
require('tailwindcss'),
// 3. 自动添加浏览器前缀
require('autoprefixer'),
],
};
这个配置清晰地展示了处理流程:首先运行我们的字体插件,然后处理 Tailwind CSS,最后由 Autoprefixer 完成兼容性工作。
深入核心:`autoFontImport` 插件
现在,让我们揭开 `autoFontImport` 插件的神秘面纱。
1. 插件的配置
我们为插件设计了非常直观的配置:
// 在 postcss.config.js 中调用插件
autoFontImport({
// 字体名称映射:将 CSS 中的 font-family 映射到 Google Fonts 的 URL 名称
fontMappings: {
'Poppins': 'Poppins',
'Noto Serif HK': 'Noto+Serif+HK', // 注意空格要换成 '+'
},
// 字体字重配置:为每个字体指定可用字重
fontWeights: {
'Poppins': '100..900', // 支持 100 到 900 的所有字重
'Lato': '300;700', // 只支持 300 和 700 两个字重
'Righteous': '400', // 只支持 400 字重
},
// 默认字重:如果某个字体没有在上面配置,就使用这个默认值
defaultWeight: '400',
})
2. 插件实现三步走(简化版)
一个 PostCSS 插件本质上是一个返回特定对象的 JavaScript 函数。
第一步:插件的基本结构
// autoFontImport.js
const autoFontImport = (options = {}) => {
return {
// 必须:定义插件名称
postcssPlugin: 'auto-font-import',
// 核心逻辑:这个函数在处理每个 CSS 文件时会执行一次
Once(root) {
// `root` 代表整个 CSS 文件的内容
// 我们将在这里施展魔法
}
};
};
// 必须:告诉 PostCSS 这是一个合法的插件
autoFontImport.postcss = true;
module.exports = autoFontImport;
第二步:扫描 CSS,找到使用的字体
我们在 `Once` 函数中遍历所有 CSS 规则,找到 `font-family` 声明。
// ... 在 Once(root) 函数内部 ...
const usedFonts = new Set(); // 使用 Set 避免重复
// 遍历所有 CSS 声明
root.walkDecls('font-family', (decl) => {
// decl.value 是字体名称,如 "Poppins", sans-serif
// 我们需要解析并提取主字体
const mainFont = decl.value.split(',')[0].trim().replace(/['"]/g, '');
usedFonts.add(mainFont);
});
// 现在 usedFonts 里就有了:{'Poppins', 'Roboto', ...}
第三步:生成并插入 `@import` 语句
有了字体列表,我们就可以根据插件的配置生成 Google Fonts URL,并将其插入到 CSS 文件的顶部。
// ... 在 Once(root) 函数内部,扫描之后 ...
usedFonts.forEach((fontName) => {
// 1. 从配置中查找字体信息
const googleFontName = options.fontMappings[fontName];
const fontWeight = options.fontWeights[fontName] || options.defaultWeight;
if (!googleFontName) return; // 如果没在配置里,就跳过
// 2. 构建 Google Fonts URL
const url = `https://fonts.googleapis.com/css2?family=${googleFontName}:wght@${fontWeight}&display=swap`;
// 3. 创建一个新的 @import 规则
const newImportRule = postcss.atRule({
name: 'import',
params: `url('${url}')`,
});
// 4. 将新规则插入到 CSS 文件顶部
root.prepend(newImportRule);
});
实现效果
现在,当你在 CSS 中这样写:
/* 你的 CSS 代码 */
.title {
font-family: 'Poppins', sans-serif;
font-weight: 700;
}
.content {
font-family: 'Noto Serif HK', serif;
}
经过 PostCSS 处理后,你的 CSS 文件会自动变成:
/* PostCSS 自动生成的内容 */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+HK:wght@200..900&display=swap');
/* 你原本的 CSS 代码 */
.title {
font-family: 'Poppins', sans-serif;
font-weight: 700;
}
.content {
font-family: 'Noto Serif HK', serif;
}
是不是非常智能和方便?
最后附上我配置好的 postcss.config.js 代码:
// 自定义插件:自动字体导入
const autoFontImport = (options = {}) => {
// 默认配置
const config = {
// 字体映射:CSS中的字体名 -> Google Fonts URL中的字体名
fontMappings: {
// 'WDXL Lubrifont SC': 'WDXL+Lubrifont+SC',
// 'WDXL Lubrifont TC': 'WDXL+Lubrifont+TC',
// 可以添加更多字体映射
},
// 默认字体权重
defaultWeight: '400',
// 是否启用调试日志
debug: false,
...options
};
return {
postcssPlugin: 'auto-font-import',
Once(root) {
const usedFonts = new Set();
const existingImports = new Set();
// 收集已存在的 @import 语句
root.walkAtRules('import', (rule) => {
Object.entries(config.fontMappings).forEach(([cssName, urlName]) => {
if (rule.params.includes(urlName)) {
existingImports.add(cssName);
if (config.debug) {
console.log(`✓ 发现已存在的字体导入: ${cssName}`);
}
}
});
});
// 扫描所有 font-family 声明,收集使用的字体
root.walkDecls('font-family', (decl) => {
Object.keys(config.fontMappings).forEach(fontName => {
if (decl.value.includes(fontName)) {
usedFonts.add(fontName);
if (config.debug) {
console.log(`✓ 发现使用的字体: ${fontName} 在 ${decl.parent.selector || 'unknown'}`);
}
}
});
});
// 为使用但未导入的字体添加 @import 语句
usedFonts.forEach(fontName => {
if (!existingImports.has(fontName)) {
const urlName = config.fontMappings[fontName];
if (urlName) {
// 获取字体的字重配置,优先使用fontWeights中的配置,否则使用defaultWeight
const fontWeight = config.fontWeights && config.fontWeights[fontName]
? config.fontWeights[fontName]
: config.defaultWeight;
const importUrl = `url('https://fonts.googleapis.com/css2?family=${urlName}:wght@${fontWeight}&display=swap')`;
const importRule = require('postcss').atRule({
name: 'import',
params: importUrl
});
// 将 @import 添加到文件开头
root.prepend(importRule);
if (config.debug) {
console.log(`✓ 自动添加字体导入: ${fontName} -> ${importUrl} (字重: ${fontWeight})`);
}
}
}
});
// 移除未使用字体的 @import 语句
root.walkAtRules('import', (rule) => {
let shouldRemove = false;
Object.entries(config.fontMappings).forEach(([cssName, urlName]) => {
if (rule.params.includes(urlName) && !usedFonts.has(cssName)) {
shouldRemove = true;
if (config.debug) {
console.log(`✗ 移除未使用的字体导入: ${cssName}`);
}
}
});
if (shouldRemove) {
rule.remove();
}
});
if (config.debug) {
console.log('自动字体导入完成');
console.log('使用的字体:', Array.from(usedFonts));
console.log('已存在的导入:', Array.from(existingImports));
}
}
};
};
autoFontImport.postcss = true;
console.log('postcss.config.js 加载成功');
module.exports = {
plugins: [
// postcss-import 用于处理条件性的 @import 语句
// require("postcss-import")({
// // 可以在这里添加过滤器来控制哪些 @import 被处理
// }),
// 自动字体导入插件
autoFontImport({
debug: true, // 开启调试日志以便验证功能
fontMappings: {
// 基础英文字体
'Poppins': 'Poppins',
'Roboto': 'Roboto',
'Inter': 'Inter',
'Montserrat': 'Montserrat',
'Open Sans': 'Open+Sans',
'Parkinsans': 'Parkinsans',
'Righteous': 'Righteous',
'Work Sans': 'Work+Sans',
'Noto Sans': 'Noto+Sans',
'Noto Sans Mono': 'Noto+Sans+Mono',
'Lato': 'Lato',
'Sniglet': 'Sniglet',
'Poetsen One': 'Poetsen+One',
'Dancing Script': 'Dancing+Script',
'Monsieur La Doulaise': 'Monsieur+La+Doulaise',
'DM Serif Text': 'DM+Serif+Text',
'Merriweather': 'Merriweather',
'Ubuntu': 'Ubuntu',
'Lobster': 'Lobster',
'Oswald': 'Oswald',
'Raleway': 'Raleway',
'Funnel Display': 'Funnel+Display',
'Poiret One': 'Poiret+One',
'Oooh Baby': 'Oooh+Baby',
'Google Sans Code': 'Google+Sans+Code',
'Special Gothic Expanded One': 'Special+Gothic+Expanded+One',
// 中文字体
'Noto Sans HK': 'Noto+Sans+HK',
'Noto Serif HK': 'Noto+Serif+HK',
'Chiron GoRound TC': 'Chiron+GoRound+TC',
'LXGW WenKai TC': 'LXGW+WenKai+TC',
'Cactus Classical Serif': 'Cactus+Classical+Serif',
'WDXL Lubrifont TC': 'WDXL+Lubrifont+TC',
'WDXL Lubrifont SC': 'WDXL+Lubrifont+SC',
'ZCOOL XiaoWei': 'ZCOOL+XiaoWei',
'ZCOOL KuaiLe': 'ZCOOL+KuaiLe',
'Noto Serif SC': 'Noto+Serif+SC',
},
fontWeights: {
// 基础英文字体字重配置
'Poppins': '100..900',
'Roboto': '100..900',
'Inter': '100..900',
'Montserrat': '100..900',
'Open Sans': '100..900',
'Parkinsans': '300..800',
'Righteous': '400',
'Work Sans': '100..900',
'Noto Sans': '100..900',
'Noto Sans Mono': '100..900',
'Lato': '100;300;400;700;900',
'Sniglet': '400;800',
'Poetsen One': '400',
'Dancing Script': '400..900',
'Monsieur La Doulaise': '400',
'DM Serif Text': '400',
'Merriweather': '300..900',
'Ubuntu': '300;400;500;700',
'Lobster': '400',
'Oswald': '200..700',
'Raleway': '100..900',
'Funnel Display': '300..800',
'Poiret One': '400',
'Oooh Baby': '400',
'Google Sans Code': '300..800',
'Special Gothic Expanded One': '400',
// 中文字体字重配置
'Noto Sans HK': '100..900',
'Noto Serif HK': '200..900',
'Chiron GoRound TC': '200..900',
'LXGW WenKai TC': '400',
'Cactus Classical Serif': '400',
'WDXL Lubrifont TC': '400',
'WDXL Lubrifont SC': '400',
'ZCOOL XiaoWei': '400',
'ZCOOL KuaiLe': '400',
'Noto Serif SC': '200..900',
},
defaultWeight: '400' // 未指定字体的默认字重
}),
// Tailwind CSS
require("tailwindcss"),
// Autoprefixer
require("autoprefixer"),
],
};
总结
通过编写一个简单的 PostCSS 插件,我们成功地将繁琐的字体管理工作自动化了。这不仅提升了开发效率,也让我们的项目代码更加干净、性能更佳。
PostCSS 的能力远不止于此,它打开了用现代工程化思维改造 CSS 的大门。希望这篇文章能启发你,去探索更多可能性,让你的开发工作流更加顺畅!
发表评论
全部评论 (0)