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,
    );
  }
}
相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端