flutter使用shelf 和 shelf_static 创建本地web服务器

发布于:2025-06-21 ⋅ 阅读:(20) ⋅ 点赞:(0)

坑:shelf_static 的createStaticHandler 一直报错

throw ArgumentError('A directory corresponding to fileSystemPath '
    '"$fileSystemPath" could not be found');

解决思路

getApplicationDocumentsDirectory 获取当前目录,并创建文件夹

通过读AssetManifest.json 文件获取静态资源,然后写进去,当静态资源目录


final manifestContent = await rootBundle.loadString('AssetManifest.json'); 

下面是完整代码

import 'dart:developer';
import 'dart:io';
import 'package:permission_handler/permission_handler.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'dart:convert';
import 'package:shelf_router/shelf_router.dart' as shelf_router;
import 'package:shelf_static/shelf_static.dart';
import 'package:flutter/services.dart'; // For rootBundle


Future<void> requestPermissions() async {
  if (Platform.isAndroid) {
    if ((await _isAndroidVersion28OrAbove())) {
      PermissionStatus storageStatus = await Permission.storage.request();
      if (storageStatus.isDenied || storageStatus.isPermanentlyDenied) {
        log('Storage permission denied');
        openAppSettings();
      }
    }

    PermissionStatus manageExternalStorageStatus =
    await Permission.manageExternalStorage.request();
    if (manageExternalStorageStatus.isDenied ||
        manageExternalStorageStatus.isPermanentlyDenied) {
      log("Manage ExternalStorage permission denied");
    }

    // Request internet permissions (Note: INTERNET permission is typically granted by default on Android)
    // Here we're demonstrating ACCESS_NETWORK_STATE which can be useful for network operations
    PermissionStatus networkStatus =
    await Permission.accessMediaLocation.request();
    if (networkStatus.isDenied || networkStatus.isPermanentlyDenied) {
      log('Network permission denied');
    }
    PermissionStatus cameraStatus = await Permission.camera.request();
    if (cameraStatus.isDenied || cameraStatus.isPermanentlyDenied) {
      log('Camera permission denied');
    }
    PermissionStatus microphoneStatus = await Permission.microphone.request();
    if (microphoneStatus.isDenied || microphoneStatus.isPermanentlyDenied) {
      log('Microphone permission denied');
    }
  }
}

Future<void> startWebServer(Directory assetsDirectory) async {
  final staticHandler =
  createStaticHandler(assetsDirectory.path, defaultDocument: 'index.html');
  final router = shelf_router.Router()..all('/<ignored|.*>', staticHandler);

  final server = await io.serve(
    const Pipeline().addMiddleware(logRequests()).addHandler(router.call),
    InternetAddress.loopbackIPv4,
    8080,
  );
  log('Serving at http://${server.address.host}:${server.port}');
}

Future<void> copyAssetsToLocal(
    String webRootBundlePrefix, Directory appDir) async {
  if (!await appDir.exists()) {
    await appDir.create(recursive: true);
  }
  // Copy all assets
  await copyAssetDirectory(webRootBundlePrefix, appDir.path);
}

Future<void> copyAssetDirectory(String assetPath, String localPath) async {
  final manifestContent = await rootBundle.loadString('AssetManifest.json');
  final Map<String, dynamic> manifestMap = json.decode(manifestContent);
  final List<String> assetPaths = manifestMap.keys
      .where((String key) => key.startsWith(assetPath))
      .toList();
  await deleteDirectory(localPath);
  for (String asset in assetPaths) {
    final filePath = asset.replaceFirst(assetPath, localPath);
    final file = File(filePath);

    final ByteData data = await rootBundle.load(asset);
    final List<int> bytes =
    data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
    if (!await file.parent.exists()) {
      await file.parent.create(recursive: true);
    }
    await file.writeAsBytes(bytes);
  }
}

Future<void> deleteDirectory(String path) async {
  final directory = Directory(path);

  if (await directory.exists()) {
    try {
      await directory.delete(recursive: true);
    } catch (e) {
      log('Failed to delete directory: $e');
    }
  } else {
    log('Directory does not exist: $path');
  }
}

Future<bool> _isAndroidVersion28OrAbove() async {
  // 检查是否是Android 28或更高版本
  int sdkVersion = 0;
  try {
    sdkVersion = int.parse(await _getAndroidSdkVersion());
  } catch (e) {
    log("Failed to get Android SDK version: $e");
  }
  return sdkVersion >= 28;
}

Future<String> _getAndroidSdkVersion() async {
  return Platform.isAndroid
      ? (await Permission.storage.request().isGranted)
      ? "28"
      : "0"
      : "0";
}

 调用(在webview启动之前)

    // 不申请权限也可以启动
    await requestPermissions();
    final appRoot = await getApplicationDocumentsDirectory();
    // 前端路径默认dist
    final appDir = Directory('${appRoot.path}/$webDistDir');
    await copyAssetsToLocal("packages/face_liveness_web/$webDistDir", appDir);
    startWebServer(appDir);

文件目录

看到开源项目的写法(使用未验证),项目目录有assets/web

final dir = await getTemporaryDirectory();
    final path = p.join(dir.path, 'web');
    // Create the target directory
    final webDir = Directory(path)..createSync(recursive: true);

    // List of files to copy
    final files = ['index.html', 'full.json', 'main.css', 'main.js'];

    for (var filename in files) {
      final ByteData data = await rootBundle.load('assets/web/$filename');
      final file = File(p.join(webDir.path, filename));
      await file.writeAsBytes(data.buffer.asUint8List());
    }

    final handler = createStaticHandler(
      webDir.path,
      defaultDocument: 'index.html',
      serveFilesOutsidePath: true,
    );

    // Start server on localhost:8080
    try {
      final server = await shelf_io.serve(handler, 'localhost', 8080);
      print('Serving at http://${server.address.host}:${server.port}');
    } catch (e) {
      print('Failed to start server: $e');
    }

 


网站公告

今日签到

点亮在社区的每一天
去签到