没错,米饭的个人主页又又又更新了,这一次依旧是使用 Flutter for Web 构建。比起之前的版本,米饭这一次的个人主页加载更快并且动画更流畅啦~

由于 Flutter 编译的网页是纯静态文件,所以可以使用 EdgeOne Pages 或者 Github Pages 等类似服务实现无服务器部署,这一次米饭使用的是腾讯的 EdgeOne Pages
动画效果预览
这一次所有页面都有适当的动画~切换页面的时候也会有颜色的改变

- 动图由于压缩的原因,似乎无法正常播放
大家可以直接访问米饭的个人主页查看效果:米饭的个人主页
使用 GitHub Action 实现自动部署 EdgeOne Pages
使用 Github Action 可以让我们很方便的自动编译 web 文件,并且部署到 EdgeOne Pages 中,下面是米饭使用的 action 配置文件,大家可以参考~
name: Flutter Web Build & EdgeOne Deploy
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: '3.32.6'
- name: Install Dependencies
run: flutter pub get
- name: Set Permission
run: chmod +x build.sh
- name: Build Web Release
run: ./build.sh --no-web-resources-cdn
env:
NODE_OPTIONS: '--max_old_space_size=8192'
- name: Deploy to EdgeOne Pages
run: npx edgeone pages deploy build/web -n 这里替换为你的 pages 名字 -t ${{ secrets.EDGEONE_API_TOKEN }}
env:
EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }}
YAML注意其中 Build Web Release 步骤,因为米饭的个人主页会在编译后自动注入加载页面,所以写了独立的构建脚本,如果不需要注入加载页面,改为 flutter 本身的构建命令就可以啦 flutter build web --no-web-resources-cdn
最后一步 Deploy to EdgeOne Pages 的时候,需要提前在 action 的 secrets 中配置好 token 内容,关于 API 的获取可以查看这里:API Token 文档。并且修改这里替换为你的 pages 名字为对应名称,如果名称不存在则会自动创建。
不过需要注意的是,自动创建的 pages 默认加速大陆地区,如果你的域名没有备案就不可用。如果不需要加速大陆地区,需要手动在控制台创建新的 pages,并填写对应的名称。
关于 GitHub Actions 的详细使用可以查看官网文档:GitHub Actions 部署文档
自动化添加加载页面 减少第一屏时间
众所周知,Flutter for web 在打开网页时,会加载文件大小极大的 canvaskit.wasm 与 main.dart.js,在这两个文件加载完成前,浏览器不会显示任何内容,导致第一屏时间非常长。所以我们可以添加一个加载动画来提升用户体验。

最简单的方法就是编辑编译后的 index.html 文件,在里面加入加载动画的 HTML,但是这样的做法完全不自动化,不能实现 EdgeOne Pages 的自动部署,所以我们使用注入的方法~
在项目根目录创建文件夹 tools, 并创建文件 inject_loading_screen.dart
import 'dart:io';
import 'dart:convert';
void main(List<String> args) {
// 检查是否使用了本地资源
final bool localAssets = args.contains('--local-assets');
print('Injecting loading screen (local assets: $localAssets)...');
// 1. 获取构建目录
final projectDir = Directory.current.path;
final buildDir = '$projectDir/build/web';
// 2. 处理 index.html
final indexFile = File('$buildDir/index.html');
if (!indexFile.existsSync()) {
print('Error: index.html not found in build directory!');
exit(1);
}
// 备份原始文件
indexFile.copySync('$buildDir/index.html.bak');
var indexContent = indexFile.readAsStringSync();
// 添加加载页面结构
const loadingScreenHTML = '''
<!-- Flutter Loading Screen (Auto-injected) -->
<div id="loading-screen" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: white; z-index: 9999; transition: opacity 0.5s;">
<div class="spinner" style="border: 4px solid rgba(0, 0, 0, 0.1); width: 36px; height: 36px; border-radius: 50%; border-left-color: #09f; animation: spin 1s linear infinite;"></div>
<p style="margin-top: 20px; font-family: sans-serif;">Loading application...</p>
</div>
<div id="flutter-host"></div>
''';
// 修复正则表达式问题
indexContent = indexContent.replaceFirstMapped(
RegExp(r'<body(\s[^>]*)?>', caseSensitive: false),
(match) => '<body${match.group(1) ?? ''}>$loadingScreenHTML'
);
// 添加 CSS 样式
const loadingScreenCSS = '''
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
''';
indexContent = indexContent.replaceFirstMapped(
RegExp(r'<head>', caseSensitive: false),
(match) => '<head>$loadingScreenCSS'
);
// 3. 处理 flutter_bootstrap.js
final bootstrapFile = File('$buildDir/flutter_bootstrap.js');
if (!bootstrapFile.existsSync()) {
print('Error: flutter_bootstrap.js not found!');
exit(1);
}
// 备份原始文件
bootstrapFile.copySync('$buildDir/flutter_bootstrap.js.bak');
var bootstrapContent = bootstrapFile.readAsStringSync();
// 添加加载逻辑
String loaderLogic = '''
// ===== Loading Screen Logic (Auto-injected) =====
console.log('[LOADER] Starting Flutter loader...');
const loadingScreen = document.getElementById('loading-screen');
// 确保我们找到了加载页面
if (!loadingScreen) {
console.error('[LOADER] Loading screen element not found!');
}
// 更新加载状态
function updateLoadingText(text) {
console.log('[LOADER] ' + text);
if (loadingScreen) {
const textElement = loadingScreen.querySelector('p');
if (textElement) textElement.textContent = text;
}
}
// 隐藏加载页面的函数
function hideLoadingScreen() {
if (!loadingScreen) {
console.error('[LOADER] Cannot hide loading screen: element not found');
return;
}
console.log('[LOADER] Hiding loading screen');
// 添加淡出动画
loadingScreen.style.opacity = "0";
// 动画完成后移除元素
setTimeout(() => {
console.log('[LOADER] Removing loading screen');
loadingScreen.style.display = 'none';
// 可选:完全移除元素
// loadingScreen.remove();
}, 300);
}
// 初始化加载逻辑
updateLoadingText(' 正在加载,请稍后');
// 配置对象 - 根据是否使用本地资源调整
const loaderConfig = {
hostElement: document.getElementById('flutter-host')
''';
loaderLogic += '''
};
try {
_flutter.loader.load({
config: loaderConfig,
onEntrypointLoaded: async function(engineInitializer) {
try {
console.log('[LOADER] Flutter entrypoint loaded');
updateLoadingText(' 初始化 Flutter 引擎...');
// 初始化引擎
const appRunner = await engineInitializer.initializeEngine();
console.log('[LOADER] Engine initialized');
updateLoadingText(' 即将呈现...');
// 运行应用
await appRunner.runApp();
console.log('[LOADER] App running');
hideLoadingScreen();
} catch (error) {
console.error('[LOADER] Error during initialization:', error);
//updateLoadingText('Initialization error: ' + error.message);
updateLoadingText(' 很快就好啦');
hideLoadingScreen();
}
}
});
} catch (e) {
console.error('[LOADER] Error in loader setup:', e);
updateLoadingText('Fatal loader error: ' + e.message);
hideLoadingScreen();
}
// 监听 Flutter 的首帧渲染事件
window.addEventListener('flutter-first-frame', function() {
console.log('[LOADER] Flutter first frame rendered - hiding loading screen');
hideLoadingScreen();
});
''';
// 确保只添加一次加载逻辑
if (!bootstrapContent.contains('// ===== Loading Screen Logic')) {
bootstrapContent += loaderLogic;
}
// 4. 保存修改后的文件
indexFile.writeAsStringSync(indexContent);
bootstrapFile.writeAsStringSync(bootstrapContent);
// 5. 如果是本地资源模式,添加资源预加载
if (localAssets) {
_addResourcePreloading(buildDir, indexContent);
}
print('Successfully injected loading screen!');
}
void _addResourcePreloading(String buildDir, String indexContent) {
// 查找所有需要预加载的资源
final assetDir = Directory('$buildDir/assets');
final preloadTags = StringBuffer();
if (assetDir.existsSync()) {
assetDir.listSync(recursive: true).whereType<File>().forEach((file) {
final path = file.path.replaceFirst('$buildDir/', '');
final extension = path.split('.').last;
// 只预加载关键资源
if (extension == 'wasm' || extension == 'js' || path.contains('canvaskit')) {
final asType = extension == 'wasm' ? 'fetch' : 'script';
preloadTags.writeln('<link rel="preload" href="$path" as="$asType" crossorigin="anonymous">');
}
});
}
if (preloadTags.isNotEmpty) {
final updatedContent = indexContent.replaceFirstMapped(
RegExp(r'</head>', caseSensitive: false),
(match) => '$preloadTags</head>'
);
File('$buildDir/index.html').writeAsStringSync(updatedContent);
print('Added ${preloadTags.toString().split('\n').length} resource preload tags');
}
}
Dart之后新建一个 build.ps1 文件用于执行脚本就可以啦,当然 GitHub actions 是 Linux 环境,所以使用.sh 脚本即可。
param(
[switch]$NoCdn,
[switch]$DebugMode
)
$flutterArgs = "build", "web"
$injectArgs = @()
$flutterArgs += "--no-web-resources-cdn"
if ($NoCdn) {
$flutterArgs += "--no-web-resources-cdn"
$injectArgs += "--local-assets"
}
if ($DebugMode) {
$flutterArgs += "--debug"
}
Write-Host "Running Flutter build..."
Write-Host $flutterArgs
flutter $flutterArgs
if ($LASTEXITCODE -ne 0) {
Write-Host "Flutter build failed" -ForegroundColor Red
exit 1
}
Write-Host "Injecting loading screen..."
dart run tools\inject_loading_screen.dart $injectArgs
if ($LASTEXITCODE -ne 0) {
Write-Host "Injection script failed" -ForegroundColor Red
exit 1
}
Write-Host "Build and injection completed successfully!" -ForegroundColor Green
PowerShell这样就可以实现有加载进度条,提高用户体验。
感谢你看到这里啦~ 另外个人主页也开源啦,大家可以下载修改自由使用~
https://github.com/cc2562/ricehome3
关于 Flutter for web 的字体问题和 CDN 问题,大家可以看米饭之前的文章~
Comments NOTHING