UmiJS 3.x 字体资源异步加载问题解决方案

UmiJS 3.x 字体资源异步加载问题解决方案

_

问题背景

在使用 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 确保字体应用

最终效果

通过以上配置,成功解决了:

  1. ✅ 字体预加载问题:页面加载时即开始下载字体

  2. ✅ 模态框字体问题:字体在模态框渲染前已加载完成

  3. ✅ iframe 字体问题:通过全局配置自动应用到 iframe

  4. ✅ CKEditor 字体问题:通过 contentsCss 配置解决编辑器 iframe 字体加载

最佳实践建议

  1. 始终使用 crossorigin="anonymous" 进行字体预加载

  2. 根据场景选择合适的 font-display 值

  3. 为富文本编辑器等特殊组件单独配置字体

  4. 使用专门的字体样式文件便于维护

  5. 在开发环境中配置适当的 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 配置)

这个解决方案具有良好的可维护性和扩展性,适用于类似的字体加载问题。

three.js基础 2025-07-16
react-markdown-editor-lite组件使用 2025-12-30

评论区