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,
    );
  }
}
相关推荐
程序员老刘1 小时前
Flutter 3.47 更新要点:Material 正式分家,老项目先别急着更新
flutter·ai编程·客户端
西安小哥1 小时前
破局与重生:大厂前端如何借力 AI 转型“超级全栈“
前端·人工智能
sunly_2 小时前
TypeScript总结:16、面向对象速查
前端·javascript·typescript
MXN_小南学前端2 小时前
React超长文本域中实现“返回顶部”浮动按钮
前端·javascript·react.js
学习嵌入式的小周2 小时前
Notepad++8.8.7下载安装(附安装包)
前端·notepad++
gs801402 小时前
解构 Cordis:面向“时空可组合性”的 TypeScript 元框架深度剖析
前端·javascript·typescript
岁岁种桃花儿3 小时前
Vue核心语法第一篇:Vue是什么?
前端·javascript·vue.js
观无4 小时前
若依EasyExcel实现单元格合并
开发语言·前端·javascript
愚公搬代码4 小时前
【愚公系列】《Web应用安全》001-VMware的安装
前端·安全