通过写全新的个人主页来优化Flutter For Web的用户体验吧

31天前 · 技术 · 18次阅读

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

由于Flutter编译的网页是纯静态文件,所以可以使用EdgeOne Pages或者Github Pages等类似服务实现无服务器部署,这一次米饭使用的是腾讯的EdgeOne Pages

动画效果预览

这一次所有页面都有适当的动画~切换页面的时候也会有颜色的改变

  • 动图由于压缩的原因,似乎无法正常播放

大家可以直接访问米饭的个人主页查看效果:米饭的个人主页

使用GitHub Action实现自动部署EdgeOne Pages

使用Github Action可以让我们很方便的自动编译web文件,并且部署到EdgeOne Pages中,下面是米饭使用的action配置文件,大家可以参考~

YAML

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.wasmmain.dart.js,在这两个文件加载完成前,浏览器不会显示任何内容,导致第一屏时间非常长。所以我们可以添加一个加载动画来提升用户体验。

最简单的方法就是编辑编译后的index.html文件,在里面加入加载动画的HTML,但是这样的做法完全不自动化,不能实现EdgeOne Pages的自动部署,所以我们使用注入的方法~

在项目根目录创建文件夹tools,并创建文件inject_loading_screen.dart

Dart

import'dart:io';
import'dart:convert';

voidmain(List<String> args) {
// 检查是否使用了本地资源
finalboollocalAssets=args.contains('--local-assets');

print('Injecting loading screen (local assets: $localAssets)...');

// 1. 获取构建目录
finalprojectDir=Directory.current.path;
finalbuildDir='$projectDir/build/web';

// 2. 处理 index.html
finalindexFile=File('$buildDir/index.html');
if(!indexFile.existsSync()) {
print('Error: index.html not found in build directory!');
exit(1);
}

// 备份原始文件
indexFile.copySync('$buildDir/index.html.bak');

varindexContent=indexFile.readAsStringSync();

// 添加加载页面结构
constloadingScreenHTML='''
<!-- 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样式
constloadingScreenCSS='''
<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
finalbootstrapFile=File('$buildDir/flutter_bootstrap.js');
if(!bootstrapFile.existsSync()) {
print('Error: flutter_bootstrap.js not found!');
exit(1);
}

// 备份原始文件
bootstrapFile.copySync('$buildDir/flutter_bootstrap.js.bak');

varbootstrapContent=bootstrapFile.readAsStringSync();

// 添加加载逻辑
StringloaderLogic='''
// ===== 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(StringbuildDir,StringindexContent) {
// 查找所有需要预加载的资源
finalassetDir=Directory('$buildDir/assets');
finalpreloadTags=StringBuffer();

if(assetDir.existsSync()) {
assetDir.listSync(recursive:true).whereType<File>().forEach((file) {
finalpath=file.path.replaceFirst('$buildDir/','');
finalextension=path.split('.').last;

// 只预加载关键资源
if(extension=='wasm'||extension=='js'||path.contains('canvaskit')) {
finalasType=extension=='wasm'?'fetch':'script';
preloadTags.writeln('<link rel="preload" href="$path" as="$asType" crossorigin="anonymous">');
}
});
}

if(preloadTags.isNotEmpty) {
finalupdatedContent=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脚本即可。

PowerShell

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-ne0){
Write-Host"Flutter build failed"-ForegroundColor Red
exit1
}

Write-Host"Injecting loading screen..."
dart run tools\inject_loading_screen.dart$injectArgs

if($LASTEXITCODE-ne0){
Write-Host"Injection script failed"-ForegroundColor Red
exit1
}

Write-Host"Build and injection completed successfully!"-ForegroundColor Green

PowerShell

这样就可以实现有加载进度条,提高用户体验。

感谢你看到这里啦~ 另外个人主页也开源啦,大家可以下载修改自由使用~

https://github.com/cc2562/ricehome3

关于Flutter for web的字体问题和CDN问题,大家可以看米饭之前的文章~

https://world.ccrice.com/2024/11/05/551/

👍 0

Flutter 编程 个人主页 Flutter for web dart

最后修改于2天前

评论

贴吧 狗头 原神 小黄脸
收起

贴吧

  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡

狗头

  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头

原神

  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神

小黄脸

  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸

目录

avatar

ccrice

也许是全新的小世界~

93

文章数

353

评论数

4

分类

还未曾燃烧的足迹

2

随机文章

这是一篇小世界位置变更生效提示

344天前

虎年的年度小记

955天前

2022即将到来!

1351天前

想当年我也是有萌备的人!

1126天前