Flutter最佳实践:Web项目中文字体首次加载乱码问题解决

一、问题本质(关键)

用flutter 开发 web项目时,中文字体先乱码再出现,根本原因是渲染流程导致的问题。

流程是这样的:

复制代码
页面渲染
  ↓
系统 fallback 字体(Roboto / sans-serif)
  ↓
NotoSansSC
  ↓
全局字体替换
  ↓
UI 重排 + "闪一下"

知道了问题来源,就知道了解决办法:预加载字体。

二、解决方案

Roboto 保持英英文字母和数字效果各平台统一。

Noto Sans SC 字体用于支持中文。

js 复制代码
  fonts:
    - family: Roboto
      fonts:
        - asset: assets/fonts/Roboto-Regular.ttf
        - asset: assets/fonts/Roboto-Medium.ttf
          weight: 500
    - family: Noto Sans SC
      fonts:
        - asset: assets/fonts/NotoSansSC-Regular.ttf
        - asset: assets/fonts/NotoSansSC-Medium.ttf
          weight: 500

方案 1: 提前 preload 字体

web/index.html

js 复制代码
 <link rel="preload" href="../assets/fonts/NotoSansSC-Regular.ttf" as="font" type="font/ttf" crossorigin>
  <link rel="preload" href="../assets/fonts/NotoSansSC-Medium.ttf" as="font" type="font/ttf" crossorigin>
  <link rel="preload" href="../assets/fonts/Roboto-Regular.ttf" as="font" type="font/ttf" crossorigin>
  <link rel="preload" href="../assets/fonts/Roboto-Medium.ttf" as="font" type="font/ttf" crossorigin>

作用:

  • 提前进入下载队列
  • 减少首屏等待时间
  • 明显降低闪烁概率

方案 2: 提前 preload 字体

dart 复制代码
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await AppFontLoader.preload();//字体预加载
  runApp(const MyApp());
}

app_font_loader.dart

dart 复制代码
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

/// Web 端字体预加载
class AppFontLoader {
  AppFontLoader._();

  /// Web 端在首帧绘制前注册字体;配合 index.html 的 preload 命中浏览器缓存
  static Future<void> preload() async {
    if (!kIsWeb) {
      return;
    }
    await Future.wait([
      _loadFont(
        family: 'Noto Sans SC',
        assets: [
          'assets/fonts/NotoSansSC-Regular.ttf',
          'assets/fonts/NotoSansSC-Medium.ttf',
        ],
      ),
      _loadFont(
        family: 'Roboto',
        assets: [
          'assets/fonts/Roboto-Regular.ttf',
          'assets/fonts/Roboto-Medium.ttf',
        ],
      ),
    ]);
  }

  static Future<void> _loadFont({
    required String family,
    required List<String> assets,
  }) async {
    final FontLoader loader = FontLoader(family);
    for (final String asset in assets) {
      loader.addFont(rootBundle.load(asset));
    }
    await loader.load();
  }
}

三、使用

dart 复制代码
class AppThemeService extends ChangeNotifier {
  static final AppThemeService _instance = AppThemeService._();

  AppThemeService._();

  factory AppThemeService() => _instance;

  /// 全平台使用本地 Noto Sans SC,避免 Web CDN 与系统字体名不一致
  static const String appFontFamily = 'Noto Sans SC';

  /// Web 端西文回退到本地 Roboto
  static List<String>? get appFontFamilyFallback => AppPlatform.isWeb ? const ['Roboto'] : null;

......

  ThemeData get lightTheme => ThemeData(
        useMaterial3: false,
        brightness: Brightness.light,
        fontFamily: appFontFamily,
        fontFamilyFallback: appFontFamilyFallback,

......

  ThemeData get darkTheme => ThemeData(
        useMaterial3: false,
        brightness: Brightness.dark,
        fontFamily: appFontFamily,
        fontFamilyFallback: appFontFamilyFallback,

最后、总结

特殊情况相下比如:

SnackBar / Dialog / Overlay 中的字体可能因为Overlay + Web 字体异步加载 + TextTheme 未完全接管导致的链路断裂,无法使用 fontFamily 和 fontFamilyFallback。这时候需要单独处理一下,TextStyle后边调用 .copyWithFontFamily()即可。

dart 复制代码
extension TextStyleFontFamilyExtension on TextStyle {
  /// 设置字体家族
  TextStyle copyWithFontFamily() {
    return copyWith(
      fontFamily: AppThemeService.appFontFamily,
      fontFamilyFallback: AppThemeService.appFontFamilyFallback,
    );
  }
}
相关推荐
朱 欢 庆2 小时前
云服务器附件备份到本机内网服务器
运维·服务器·前端·经验分享
支支დ7 小时前
VO by Vercel 前端特定优势:为什么它是构建 AI 应用的新范式
前端·人工智能
香芋芋圆8 小时前
AI 冲击内卷之下,普通前端如何破局?WebGIS—— 低门槛突围赛道
前端·javascript·人工智能·学习·职场发展
INS_KF8 小时前
【编程笔记】成员函数中两个 const 的区别(const Data &getData() const;)
前端·javascript·笔记
宿6749 小时前
vue3-async
前端·javascript·vue.js
YXWik69 小时前
记录前端请求接口在浏览器请求响应的Preview和Response展示的一样的问题
前端
2501_928996229 小时前
GPT-4o换DeepSeek迁移成本多少?中科热备解析API聚合平台技术账本
前端·数据库·人工智能
东风破_9 小时前
从跨域到 WebSocket:前端跨域方案、SSE 与双向实时通信详解
前端·后端
原则猫9 小时前
TS 类型工具
前端
excel10 小时前
Nuxt + Twin CSS 中宽度超过屏幕时底部出现空白的原因与解决方案
前端·javascript