问题背景
在使用 UmiJS 3.x 框架的 React 项目中,遇到了字体资源加载的问题:
字体只有在使用到样式时才会触发下载
模态框中的字体样式不生效
项目中使用了 Noto Sans JP 字体
问题发现过程
1. 初始问题现象
字体只在样式被使用时才开始下载(懒加载)
模态框渲染时字体可能还未完全加载
导致模态框中的文字无法正确显示日文字体
2. 深入分析发现
通过分析网络请求,发现正常加载的字体请求包含:
curl 'http://localhost:8000/Noto_Sans_JP/NotoSansJP-VariableFont_wght.ttf' \
-H 'Origin: http://localhost:8000' \
-H 'Sec-Fetch-Dest: font' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Site: same-origin'这表明字体请求使用了 CORS 模式,需要正确的跨域配置。
3. iframe 环境复杂化
项目用于嵌入到页面的 iframe 中
iframe 有独立的文档上下文,父页面的字体预加载不会影响 iframe
发现 iframe 内的页面字体样式无法生效
4. CKEditor iframe 的特殊情况
RichTextEditor 组件基于 CKEditor 4.x 封装
CKEditor 会创建独立的 iframe 来渲染编辑内容
该 iframe 需要单独配置字体加载
问题思考过程
1. 字体加载时机问题
问题根因:
延迟加载:字体资源只有在 CSS 规则匹配时才开始下载
模态框渲染时机:模态框可能在字体加载完成前就已经渲染
样式应用顺序:浏览器可能使用回退字体渲染,后续字体加载完成后不会自动重新渲染
2. CORS 和 crossorigin 属性重要性
技术原理:
crossorigin控制跨域资源共享(CORS)的行为字体预加载和 CSS 使用需要相同的 CORS 策略才能重用资源
没有正确配置会导致字体被重复下载
3. iframe 隔离特性
iframe 限制:
独立的文档上下文
独立的资源加载
独立的 CSS 作用域
父页面资源不会自动共享给 iframe
解决方案实现
1. UmiJS 配置优化
// config/config.ts
export default {
webpack5: {}, // 必须开启
// 开发服务器 CORS 配置
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
},
},
// 字体预加载配置
links: [
{
rel: 'preload',
href: '/Noto_Sans_JP/NotoSansJP-VariableFont_wght.ttf',
as: 'font',
type: 'font/ttf',
crossorigin: 'anonymous', // 关键配置:确保字体可以被重用
},
],
// 全局样式配置
styles: [
`
@font-face {
font-family: "Noto_Sans_JP";
src: url("/Noto_Sans_JP/NotoSansJP-VariableFont_wght.ttf");
font-display: block; // 阻塞渲染直到字体加载完成
}
html, body {
font-family: "Noto_Sans_JP", sans-serif;
}
/* 确保模态框等动态内容也使用字体 */
.ant-modal, .ant-drawer, .ant-popover, .ant-tooltip {
font-family: "Noto_Sans_JP", sans-serif;
}
/* 强制所有文本元素使用字体 */
* {
font-family: inherit !important;
}
`,
],
} as IConfig;2. 创建专用字体样式文件
创建 /public/Noto_Sans_JP/font-styles.css:
@font-face {
font-family: "Noto_Sans_JP";
src: url("./NotoSansJP-VariableFont_wght.ttf");
font-display: block;
}
body {
font-family: "Noto_Sans_JP", sans-serif !important;
}
* {
font-family: inherit !important;
}
p, div, span, h1, h2, h3, h4, h5, h6, li, td, th {
font-family: "Noto_Sans_JP", sans-serif !important;
}3. CKEditor 富文本编辑器配置
// FlowOrch/index.jsx
{
name: 'description',
tooltip: 'none',
renderer: ({ record, name }) => {
return (
<RichTextEditor
name={name}
record={record}
id={`description_${record.key}`}
height={200}
config={{
height: '200px',
// 关键配置:为 CKEditor iframe 添加字体样式
contentsCss: [
'/Noto_Sans_JP/font-styles.css'
],
// 字体选择器配置
font_names: 'Noto Sans JP/Noto_Sans_JP, sans-serif;Arial/Arial, Helvetica, sans-serif;',
font_defaultLabel: 'Noto Sans JP',
// 监听编辑器加载完成事件
on: {
instanceReady: function(evt) {
const editor = evt.editor;
// 直接在编辑器中注入样式
editor.addCss(`
@font-face {
font-family: "Noto_Sans_JP";
src: url("/Noto_Sans_JP/NotoSansJP-VariableFont_wght.ttf");
font-display: block;
}
body, p, div, span, h1, h2, h3, h4, h5, h6 {
font-family: "Noto_Sans_JP", sans-serif !important;
}
`);
}
}
}}
/>
);
},
}关键技术点总结
1. crossorigin="anonymous" 的重要性
确保预加载的字体资源可以被 CSS 正确重用
避免重复下载,提升性能
是字体预加载的最佳实践
2. font-display 属性选择
swap:优化字体加载显示,先显示回退字体block:阻塞渲染直到字体加载完成,适用于关键字体在 iframe 环境中,
block更可靠
3. iframe 字体加载策略
同一应用的 iframe:全局配置会自动生效
CKEditor iframe:需要通过
contentsCss特殊配置外部 iframe:需要在目标页面中单独配置
4. 多层级字体应用
全局配置:UmiJS 的 links 和 styles
组件级配置:RichTextEditor 的 config
样式优先级:使用
!important确保字体应用
最终效果
通过以上配置,成功解决了:
✅ 字体预加载问题:页面加载时即开始下载字体
✅ 模态框字体问题:字体在模态框渲染前已加载完成
✅ iframe 字体问题:通过全局配置自动应用到 iframe
✅ CKEditor 字体问题:通过 contentsCss 配置解决编辑器 iframe 字体加载
最佳实践建议
始终使用 crossorigin="anonymous" 进行字体预加载
根据场景选择合适的 font-display 值
为富文本编辑器等特殊组件单独配置字体
使用专门的字体样式文件便于维护
在开发环境中配置适当的 CORS 头部
项目文件结构
project/
├── config/
│ └── config.ts (UmiJS 配置)
├── public/
│ └── Noto_Sans_JP/
│ ├── font-styles.css (专用字体样式)
│ └── NotoSansJP-VariableFont_wght.ttf
└── packages/xhifai/src/pages/FlowOrch/
└── index.jsx (RichTextEditor 配置)这个解决方案具有良好的可维护性和扩展性,适用于类似的字体加载问题。