跳转到正文
YGJoon 技术博客

Flutter 与 Web 平台桥接

发布于:
·3.1k 字 · 约 8 分钟·作者:
在网页端双向调用原生脚本:用注解声明外部函数、把 Dart 回调注册给页面用,并聊聊 Web 端渲染的性能取舍
Flutter 与 Web 平台桥接文章封面
目录

概述

Flutter Web 平台桥接技术使 Flutter 应用能够在 Web 环境中运行,并与 JavaScript、DOM 和 Web API 进行交互。本文将深入探讨 Flutter 与 Web 平台的桥接技术,帮助开发者构建高性能的 Web 应用。

JavaScript 互操作

1. 基础 JavaScript 调用

import 'package:js/js.dart';
import 'dart:js_util' as js_util;

// JavaScript互操作基础类
// 注意:package:js / dart:js_util 已被官方废弃(见文末实战节),新项目请用 dart:js_interop + package:web
class JavaScriptBridge {
  // 使用package:js进行JavaScript互操作
  @JS('console.log')
  external static void consoleLog(dynamic message);

  // 调用全局JavaScript函数
  @JS('myGlobalFunction')
  external static String myGlobalFunction(String param);

  // 调用JavaScript对象方法
  // 注意:package:js 里没有 @static 注解;全局 window 用 dart:js_util 动态取
  static dynamic get window => js_util.getProperty(js_util.globalThis, 'window');

  // 调用浏览器API
  static void showAlert(String message) {
    js_util.callMethod(window, 'alert', [message]);
  }

  // 执行任意JavaScript代码
  static dynamic evalJavaScript(String code) {
    return js_util.callMethod(window, 'eval', [code]);
  }

  // 调用带回调的JavaScript函数
  static void fetchWithCallback(String url, Function(String) callback) {
    final successCallback = allowInterop(callback);
    js_util.callMethod(window, 'fetch', [url]).then((response) {
      return js_util.callMethod(response, 'text', []);
    }).then((data) {
      successCallback(data as String);
    });
  }
}

2. 高级 JavaScript 桥接

// 高级JavaScript桥接管理器
class AdvancedJSBridge {
  static final Map<String, Function> _callbacks = {};
  static int _callbackId = 0;

  // 注册Dart回调供JavaScript调用
  static String registerCallback(Function callback) {
    final id = 'callback_${++_callbackId}';
    _callbacks[id] = callback;

    // 将回调函数暴露给JavaScript
    js_util.setProperty(
      js_util.globalThis,
      id,
      allowInterop(([dynamic args]) {
        final callback = _callbacks[id];
        if (callback != null) {
          callback(args);
        }
      }),
    );

    return id;
  }

  // 注销回调
  static void unregisterCallback(String id) {
    _callbacks.remove(id);
    js_util.deleteProperty(js_util.globalThis, id);
  }

  // 调用JavaScript并等待Promise结果
  static Future<T> callJSFunction<T>(String functionName, List<dynamic> args) async {
    try {
      final result = await js_util.promiseToFuture(
        js_util.callMethod(js_util.globalThis, functionName, args),
      );
      return result as T;
    } catch (e) {
      throw Exception('JavaScript call failed: $e');
    }
  }

  // 创建JavaScript对象
  static dynamic createJSObject(Map<String, dynamic> properties) {
    final obj = js_util.newObject();
    properties.forEach((key, value) {
      js_util.setProperty(obj, key, value);
    });
    return obj;
  }

  // 调用JavaScript类构造函数
  static dynamic createJSInstance(String className, List<dynamic> args) {
    final constructor = js_util.getProperty(js_util.globalThis, className);
    return js_util.callConstructor(constructor, args);
  }
}

// JavaScript类型定义
@JS()
@anonymous
class JSObject {
  external String get name;
  external set name(String value);

  external factory JSObject({String name});
}

@JS()
@anonymous
class JSPromise {
  external dynamic then(Function onFulfilled, [Function onRejected]);
  // catch 是 Dart 保留字,不能做方法名,用 @JS('catch') 映射到 JS 的 catch
  @JS('catch')
  external dynamic catchError(Function onRejected);
}

DOM 操作与 Web API 集成

1. DOM 操作封装

// DOM操作封装类
class DOMManager {
  // 获取DOM元素
  static dynamic getElementById(String id) {
    return js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'document'),
      'getElementById',
      [id],
    );
  }

  // 查询DOM元素
  static dynamic querySelector(String selector) {
    return js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'document'),
      'querySelector',
      [selector],
    );
  }

  // 创建DOM元素
  static dynamic createElement(String tagName) {
    return js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'document'),
      'createElement',
      [tagName],
    );
  }

  // 设置元素属性
  static void setAttribute(dynamic element, String name, String value) {
    js_util.callMethod(element, 'setAttribute', [name, value]);
  }

  // 添加事件监听器
  static void addEventListener(
    dynamic element,
    String eventType,
    Function(Event) handler,
  ) {
    final callback = allowInterop(handler);
    js_util.callMethod(element, 'addEventListener', [eventType, callback]);
  }

  // 移除事件监听器
  static void removeEventListener(
    dynamic element,
    String eventType,
    Function(Event) handler,
  ) {
    final callback = allowInterop(handler);
    js_util.callMethod(element, 'removeEventListener', [eventType, callback]);
  }

  // 修改元素样式
  static void setStyle(dynamic element, Map<String, String> styles) {
    final style = js_util.getProperty(element, 'style');
    styles.forEach((property, value) {
      js_util.setProperty(style, property, value);
    });
  }

  // 添加CSS类
  static void addClass(dynamic element, String className) {
    final classList = js_util.getProperty(element, 'classList');
    js_util.callMethod(classList, 'add', [className]);
  }

  // 移除CSS类
  static void removeClass(dynamic element, String className) {
    final classList = js_util.getProperty(element, 'classList');
    js_util.callMethod(classList, 'remove', [className]);
  }
}

// 事件封装
@JS()
@anonymous
class Event {
  external String get type;
  external dynamic get target;
  external void preventDefault();
  external void stopPropagation();
}

2. Web API 集成

// Web API集成管理器
class WebAPIManager {
  // 本地存储操作
  static Future<void> setLocalStorage(String key, String value) async {
    js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'localStorage'),
      'setItem',
      [key, value],
    );
  }

  static Future<String?> getLocalStorage(String key) async {
    return await js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'localStorage'),
      'getItem',
      [key],
    );
  }

  static Future<void> removeLocalStorage(String key) async {
    js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'localStorage'),
      'removeItem',
      [key],
    );
  }

  // 会话存储操作
  static Future<void> setSessionStorage(String key, String value) async {
    js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'sessionStorage'),
      'setItem',
      [key, value],
    );
  }

  static Future<String?> getSessionStorage(String key) async {
    return await js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'sessionStorage'),
      'getItem',
      [key],
    );
  }

  // 网络请求封装
  static Future<Map<String, dynamic>> fetchData(String url, {
    String method = 'GET',
    Map<String, String>? headers,
    dynamic body,
  }) async {
    final options = <String, dynamic>{
      'method': method,
      'headers': headers ?? {},
    };

    if (body != null) {
      options['body'] = body;
    }

    final response = await js_util.promiseToFuture(
      js_util.callMethod(js_util.globalThis, 'fetch', [url, options]),
    );

    final statusCode = js_util.getProperty(response, 'status') as int;
    final responseData = await js_util.promiseToFuture(
      js_util.callMethod(response, 'json', []),
    );

    return {
      'statusCode': statusCode,
      'data': responseData,
    };
  }

  // WebSocket连接
  static dynamic createWebSocket(String url, {
    Function(dynamic)? onOpen,
    Function(dynamic)? onMessage,
    Function(dynamic)? onError,
    Function()? onClose,
  }) {
    final ws = js_util.callConstructor(
      js_util.getProperty(js_util.globalThis, 'WebSocket'),
      [url],
    );

    if (onOpen != null) {
      js_util.setProperty(
        ws,
        'onopen',
        allowInterop(onOpen),
      );
    }

    if (onMessage != null) {
      js_util.setProperty(
        ws,
        'onmessage',
        allowInterop(onMessage),
      );
    }

    if (onError != null) {
      js_util.setProperty(
        ws,
        'onerror',
        allowInterop(onError),
      );
    }

    if (onClose != null) {
      js_util.setProperty(
        ws,
        'onclose',
        allowInterop(onClose),
      );
    }

    return ws;
  }

  // 地理位置API
  static Future<Map<String, double>> getCurrentPosition() async {
    try {
      final position = await js_util.promiseToFuture(
        js_util.callMethod(
          js_util.getProperty(
            js_util.getProperty(js_util.globalThis, 'navigator'),
            'geolocation',
          ),
          'getCurrentPosition',
          [],
        ),
      );

      final coords = js_util.getProperty(position, 'coords');
      return {
        'latitude': js_util.getProperty(coords, 'latitude') as double,
        'longitude': js_util.getProperty(coords, 'longitude') as double,
      };
    } catch (e) {
      throw Exception('Failed to get location: $e');
    }
  }

  // 媒体设备访问
  static Future<dynamic> getUserMedia(Map<String, dynamic> constraints) async {
    try {
      return await js_util.promiseToFuture(
        js_util.callMethod(
          js_util.getProperty(
            js_util.getProperty(js_util.globalThis, 'navigator'),
            'mediaDevices',
          ),
          'getUserMedia',
          [constraints],
        ),
      );
    } catch (e) {
      throw Exception('Failed to get media devices: $e');
    }
  }
}

性能优化

1. 资源加载优化

// 资源加载优化器
class ResourceOptimizer {
  static final Map<String, bool> _loadedScripts = {};
  static final Map<String, bool> _loadedStyles = {};

  // 动态加载JavaScript文件
  static Future<void> loadScript(String url, {bool async = true}) async {
    if (_loadedScripts.containsKey(url)) return;

    final script = DOMManager.createElement('script');
    js_util.setProperty(script, 'src', url);
    js_util.setProperty(script, 'async', async);

    final completer = Completer<void>();
    js_util.setProperty(
      script,
      'onload',
      allowInterop((_) => completer.complete()),
    );
    js_util.setProperty(
      script,
      'onerror',
      allowInterop((error) => completer.completeError(error)),
    );

    final head = js_util.getProperty(
      js_util.getProperty(js_util.globalThis, 'document'),
      'head',
    );
    js_util.callMethod(head, 'appendChild', [script]);

    await completer.future;
    _loadedScripts[url] = true;
  }

  // 动态加载CSS文件
  static Future<void> loadStyle(String url) async {
    if (_loadedStyles.containsKey(url)) return;

    final link = DOMManager.createElement('link');
    js_util.setProperty(link, 'rel', 'stylesheet');
    js_util.setProperty(link, 'href', url);

    final head = js_util.getProperty(
      js_util.getProperty(js_util.globalThis, 'document'),
      'head',
    );
    js_util.callMethod(head, 'appendChild', [link]);

    _loadedStyles[url] = true;
  }

  // 预加载资源
  static void preloadResource(String url, String type) {
    final link = DOMManager.createElement('link');
    js_util.setProperty(link, 'rel', 'preload');
    js_util.setProperty(link, 'href', url);
    js_util.setProperty(link, 'as', type);

    final head = js_util.getProperty(
      js_util.getProperty(js_util.globalThis, 'document'),
      'head',
    );
    js_util.callMethod(head, 'appendChild', [link]);
  }

  // 懒加载图片
  static void lazyLoadImages(String selector) {
    final images = js_util.callMethod(
      js_util.getProperty(js_util.globalThis, 'document'),
      'querySelectorAll',
      [selector],
    );

    final observer = js_util.callConstructor(
      js_util.getProperty(js_util.globalThis, 'IntersectionObserver'),
      [
        allowInterop((entries, observer) {
          for (int i = 0; i < js_util.getProperty(entries, 'length'); i++) {
            final entry = js_util.getProperty(entries, i);
            if (js_util.getProperty(entry, 'isIntersecting') as bool) {
              final img = js_util.getProperty(entry, 'target');
              final src = js_util.getProperty(img, 'dataset-src');
              if (src != null) {
                js_util.setProperty(img, 'src', src);
                js_util.callMethod(observer, 'unobserve', [img]);
              }
            }
          }
        }),
      ],
    );

    for (int i = 0; i < js_util.getProperty(images, 'length'); i++) {
      final img = js_util.getProperty(images, i);
      js_util.callMethod(observer, 'observe', [img]);
    }
  }
}

2. 内存管理优化

// 内存管理优化器
class MemoryOptimizer {
  static final Map<String, WeakReference<dynamic>> _cache = {};
  static Timer? _cleanupTimer;

  // 初始化内存优化
  static void initialize() {
    _cleanupTimer = Timer.periodic(Duration(minutes: 5), (_) => _cleanup());
  }

  // 缓存对象
  static void cacheObject(String key, dynamic object) {
    _cache[key] = WeakReference(object);
  }

  // 获取缓存对象
  static T? getCachedObject<T>(String key) {
    final ref = _cache[key];
    final object = ref?.target;
    if (object != null) {
      return object as T;
    } else {
      _cache.remove(key);
      return null;
    }
  }

  // 清理无效引用
  static void _cleanup() {
    final invalidKeys = <String>[];
    _cache.forEach((key, ref) {
      if (ref.target == null) {
        invalidKeys.add(key);
      }
    });

    for (final key in invalidKeys) {
      _cache.remove(key);
    }

    // 通知JavaScript进行垃圾回收
    if (js_util.hasProperty(js_util.globalThis, 'gc')) {
      js_util.callMethod(js_util.globalThis, 'gc', []);
    }
  }

  // 释放资源
  static void dispose() {
    _cleanupTimer?.cancel();
    _cache.clear();
  }
}

跨框架集成

1. React 集成

// React集成桥接
class ReactBridge {
  // 创建React组件
  static dynamic createReactComponent(String componentName, Map<String, dynamic> props) {
    final react = js_util.getProperty(js_util.globalThis, 'React');
    final component = js_util.getProperty(js_util.globalThis, componentName);

    return js_util.callMethod(react, 'createElement', [
      component,
      props,
    ]);
  }

  // 渲染React组件到DOM
  static void renderReactComponent(dynamic component, dynamic container) {
    final reactDOM = js_util.getProperty(js_util.globalThis, 'ReactDOM');
    js_util.callMethod(reactDOM, 'render', [component, container]);
  }

  // 创建React Hook桥接
  static dynamic createReactHook(Function hookFunction) {
    final react = js_util.getProperty(js_util.globalThis, 'React');
    return js_util.callMethod(react, 'useCallback', [
      allowInterop(hookFunction),
      js_util.newObject(),
    ]);
  }
}

2. Vue 集成

// Vue集成桥接
class VueBridge {
  // 创建Vue应用
  static dynamic createVueApp(Map<String, dynamic> options) {
    final vue = js_util.getProperty(js_util.globalThis, 'Vue');
    return js_util.callMethod(vue, 'createApp', [options]);
  }

  // 挂载Vue应用
  static void mountVueApp(dynamic app, String selector) {
    js_util.callMethod(app, 'mount', [selector]);
  }

  // 注册Vue组件
  static void registerVueComponent(dynamic app, String name, dynamic component) {
    js_util.callMethod(app, 'component', [name, component]);
  }

  // 创建Vue响应式数据
  static dynamic createReactive(Map<String, dynamic> data) {
    final vue = js_util.getProperty(js_util.globalThis, 'Vue');
    return js_util.callMethod(vue, 'reactive', [data]);
  }
}

调试与测试

1. 调试工具

// Web调试工具
class WebDebugTools {
  // 控制台日志
  static void log(String message, {String level = 'log'}) {
    final console = js_util.getProperty(js_util.globalThis, 'console');
    js_util.callMethod(console, level, [message]);
  }

  // 性能监控
  static void startPerformanceMark(String name) {
    final performance = js_util.getProperty(js_util.globalThis, 'performance');
    js_util.callMethod(performance, 'mark', ['${name}_start']);
  }

  static void endPerformanceMark(String name) {
    final performance = js_util.getProperty(js_util.globalThis, 'performance');
    js_util.callMethod(performance, 'mark', ['${name}_end']);
    js_util.callMethod(performance, 'measure', [name, '${name}_start', '${name}_end']);
  }

  // 获取性能指标
  static Future<Map<String, dynamic>> getPerformanceMetrics() async {
    final performance = js_util.getProperty(js_util.globalThis, 'performance');
    final navigation = js_util.callMethod(performance, 'getEntriesByType', ['navigation']);

    if (js_util.getProperty(navigation, 'length') > 0) {
      final navEntry = js_util.getProperty(navigation, 0);
      return {
        'domContentLoaded': js_util.getProperty(navEntry, 'domContentLoadedEventEnd'),
        'loadComplete': js_util.getProperty(navEntry, 'loadEventEnd'),
        'firstPaint': js_util.getProperty(navEntry, 'firstPaint'),
        'firstContentfulPaint': js_util.getProperty(navEntry, 'firstContentfulPaint'),
      };
    }

    return {};
  }
}

2. 单元测试

// Web平台单元测试
class WebPlatformTests {
  // 测试JavaScript互操作
  static void testJavaScriptInterop() {
    test('JavaScript function call', () async {
      final result = AdvancedJSBridge.callJSFunction<String>('testFunction', ['test']);
      expect(result, equals('test result'));
    });

    test('JavaScript object creation', () {
      final obj = AdvancedJSBridge.createJSObject({'name': 'test'});
      expect(js_util.getProperty(obj, 'name'), equals('test'));
    });
  }

  // 测试DOM操作
  static void testDOMOperations() {
    test('Element creation and manipulation', () {
      final element = DOMManager.createElement('div');
      DOMManager.setAttribute(element, 'id', 'test-div');
      DOMManager.addClass(element, 'test-class');

      expect(js_util.getProperty(element, 'id'), equals('test-div'));
      expect(js_util.callMethod(
        js_util.getProperty(element, 'classList'),
        'contains',
        ['test-class'],
      ), isTrue);
    });
  }

  // 测试Web API
  static void testWebAPIs() {
    test('Local storage operations', () async {
      await WebAPIManager.setLocalStorage('test-key', 'test-value');
      final value = await WebAPIManager.getLocalStorage('test-key');
      expect(value, equals('test-value'));

      await WebAPIManager.removeLocalStorage('test-key');
      final removedValue = await WebAPIManager.getLocalStorage('test-key');
      expect(removedValue, isNull);
    });
  }
}

最佳实践

1. 先接受现实:Web 上没有 MethodChannel 和 dart:io

Flutter Web 的”原生侧”就是浏览器 JS 运行时。dart:io 在 Web 上编译直接失败,MethodChannel 会抛 MissingPluginException——正文里凡用通道调原生的能力(文件、蓝牙、串口),在 Web 端都要换成 dart:js_interop 直接调浏览器 API,或者明确声明不支持。做跨平台抽象时给 Web 单独一层实现,别试图用条件导入把 dart:io 塞进 Web 构建。

2. 包体积是 Web 的第一性能指标,首屏 JS 按 MB 算账

Flutter Web 的 main.dart.js 未压缩轻松超过 1MB,首屏白屏时间基本和它成正比。经验做法:路由级 deferred as 懒加载非首屏页面(比如把 React/Vue 嵌入页、管理后台拆出去),图片走 CDN 压缩,字体只打包用到的字重。反例:官网和后台管理系统塞进同一个入口,首屏加载数秒,移动端用户直接流失——Web 用户没有”安装等待”的耐心。

3. 弹窗和自动播放都认”用户手势”,异步包一层就丢了

window.open 在非用户手势的异步回调里调用会被浏览器拦截,视频自动播放必须静音或等待用户交互——这是浏览器安全策略,不是 bug。正文里”JS 互操作调起”类功能(调起第三方页面、自动播放),触发链路上必须保留用户手势:在点击事件里同步调用,别包一层 Future.delayed 再调,包完手势就丢了,弹窗会被拦。

4. 存大量数据用 IndexedDB,localStorage 只放 KB 级配置

localStorage 只有 5MB 左右且是同步 API,存几 MB 的 JSON 会直接卡 UI 线程。本文”跨框架集成”里和 React/Vue 共享状态如果数据量大,走 IndexedDB(异步、不阻塞渲染);localStorage 只放 token、主题这类小配置。反例:把整个列表缓存 JSON 塞进 localStorage,每次读写几十毫秒,低端机滚动掉帧的锅就这么来的。

实战案例

说实话,Flutter Web 我还没做过生产项目,这个坑是看官方文档和社区踩坑帖总结的,正文里用的 package:js 加 dart:js_util 已经是老路了。Dart 官方从 2025 年的稳定版开始正式废弃 dart:js、dart:js_util、dart:html 和 package:js,替代品是 dart:js_interop(类型安全的静态互操作)加 package:web,而且只有新这套能编译到 Wasm。trade-off 在于:老写法动态调用写起来爽、资料多,但新项目继续用等于给自己埋雷,pub.dev 评分都会掉;新写法要写 extension type 和 @JS 绑定,上手陡一点,但编译期就能抓错,还能走 Wasm。所以结论很简单:新项目直接上 dart:js_interop,老项目如果有 Web 打包计划,趁早把 js_util.callMethod 那一层抽出来做条件导入,别等 Wasm 构建报错才返工。

参考:Dart 官方公告(dart-announce)与 Flutter 官方文档 JS interop 章节

总结

Flutter 与 Web 平台桥接技术为构建跨平台 Web 应用提供了强大的支持。通过掌握 JavaScript 互操作、DOM 操作、Web API 集成和性能优化等技术,开发者可以构建出高性能、用户体验优秀的 Web 应用。

关键成功因素:

  1. 深入理解 Web 平台特性
  2. 合理设计桥接架构
  3. 重视性能优化
  4. 确保跨浏览器兼容性
  5. 持续监控和调优

通过本文的学习,开发者应该能够充分利用 Flutter Web 平台桥接技术,构建出功能丰富、性能卓越的 Web 应用。


分享这篇文章



相关文章

上一篇
Flutter 与原生平台深度桥接
下一篇
Flutter 与桌面平台桥接
YGJoon 的头像

YGJoon

记录 Flutter、Web 与原生移动端工程实践。更多背景见关于页,源码与项目见GitHub。