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

53天前 · 技术 · 22次阅读

没错,米饭的个人主页又又又更新了,这一次依旧是使用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 }}

注意其中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

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');
  }
}

之后新建一个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

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

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

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

是全新的世界~

94

文章数

353

评论数

4

分类