Skip to content

mofishless/hybrid_shell

Repository files navigation

HybridShell

Flutter 混合开发基座,将 H5 页面嵌入 WebView,并通过 JSBridge 暴露原生设备能力给 H5 调用。支持 Android 和 iOS 平台。

架构概览

┌─────────────────────────────────────────────┐
│  H5 页面 (WebView)                          │
│  window.$bridge.module.method(params)       │
├─────────────────────────────────────────────┤
│  bridge_sdk.js (自动注入)                    │
│  Promise 风格 API + 模块化封装              │
├─────────────────────────────────────────────┤
│  flutter_inappwebview (通信通道)             │
├─────────────────────────────────────────────┤
│  BridgeRegistry → BridgeModule (16个模块)    │
├─────────────────────────────────────────────┤
│  Flutter 原生插件 (image_picker, geolocator…)│
└─────────────────────────────────────────────┘

核心流程: H5 调用 → JS SDK 序列化 → WebView 通道 → BridgeRegistry 分发 → BridgeModule 执行 → 返回 BridgeResult

快速开始

环境要求

  • Flutter SDK (Dart ^3.11.5)
  • Android Studio / Xcode
  • Android: Java 17

国内镜像配置

Windows:

setx PUB_HOSTED_URL https://pub.flutter-io.cn
setx FLUTTER_STORAGE_BASE_URL https://storage.flutter-io.cn

macOS/Linux (添加到 ~/.bashrc 或 ~/.zshrc):

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

Gradle 镜像已在 android/build.gradle.kts 中配置阿里云 Maven 源。

应用配置

核心配置集中在 lib/config/app_config.dart,通过 kReleaseMode 自动区分 Debug / Release 环境:

class AppConfig {
  // API 基础地址(登录、授权码、用户信息等接口)
  static const String apiBaseUrl = kReleaseMode
      ? 'https://api.example.com'                                    // Release
      : 'https://m1.apifoxmock.com/m1/8319886-8083682-default';     // Debug (Mock)

  // 底部 5 个 Tab 对应的 H5 页面地址,顺序为:首页、任务、一张图、通讯录、我的
  static const List<String> tabUrls = kReleaseMode
      ? [
          'https://www.example.com/home',
          'https://www.example.com/task',
          'https://www.example.com/map',
          'https://www.example.com/contacts',
          'https://www.example.com/mine',
        ]
      : [
          'https://oss.liliudong.me/test.html',
          'https://oss.liliudong.me/test.html',
          'https://oss.liliudong.me/test.html',
          'https://oss.liliudong.me/test.html',
          'https://oss.liliudong.me/test.html',
        ];
}
配置项 说明
apiBaseUrl 后端 API 基础地址,用于 /auth/login/auth/code/auth/me/auth/logout
tabUrls 5 个 Tab 页面的 H5 地址,顺序对应:首页、任务、一张图、通讯录、我的

Debug 模式下可通过设置页面(首页右上角齿轮图标)临时修改 Tab 地址,修改值存储在本地 SharedPreferences 中,优先级高于 AppConfig.tabUrls 默认值,支持一键恢复默认。

安装与运行

# 安装依赖
flutter pub get

# 查看可用设备
flutter devices

# 运行
flutter run

# 构建 Android APK
flutter build apk

# 构建 iOS
flutter build ios

测试

# 运行全部测试
flutter test

# 运行单个测试文件
flutter test test/bridge/bridge_registry_test.dart

# 带覆盖率
flutter test --coverage

# 静态分析
flutter analyze

# 代码格式化
dart format .

H5 集成教程

SDK 自动注入到 WebView 中,H5 页面无需手动引入任何脚本。

检查 SDK 可用性

if (window.$bridge) {
  console.log('JSBridge SDK 已就绪');
}

两种调用方式

方式一:模块化方法(推荐)

// 直接通过模块名+方法名调用,参数更直观
const result = await window.$bridge.image.pick();
const result = await window.$bridge.storage.get('user_token');
const result = await window.$bridge.map.open(39.9, 116.4, '天安门');

方式二:invoke 通用方法

const result = await window.$bridge.invoke('image.pick');
const result = await window.$bridge.invoke('storage.get', { key: 'user_token' });

两种方式完全等价,推荐使用方式一,代码更简洁且 IDE 自动补全更友好。

API 模块总览

模块 方法 说明
permission check(type), request(type) 权限管理
camera takePhoto() 拍照
image pick() 从相册选择图片
location getCurrent() 获取当前位置
device getInfo() 获取设备信息
clipboard copy(text), read() 剪贴板读写
storage get(key), set(key, value), remove(key), clear(), getAll() 持久化存储
vibrate vibrate(type?) 震动/触觉反馈
phone call(number), sendSms(number, body?) 拨打电话、发送短信
share shareText(text, subject?), shareImage(path, text?) 系统分享
network getStatus() 网络状态检测
map open(lat, lng, title?), navigate(lat, lng) 打开地图、导航
file pick(multi?, types?), save(path, name?), open(path) 文件操作
media saveToAlbum(path, type?), preview(paths) 媒体操作
navigation setTitle(title), close(), open(url, title?), reload(), goBack(), goForward() WebView 导航控制
auth getAuthCode(appId) OAuth2 授权码申请

返回值格式

所有 API 返回统一的 Promise:

interface BridgeResult {
  code: number;       // 0 = 成功,非0 = 错误
  data?: any;         // 响应数据
  message: string;    // 状态消息
}

OAuth2 授权码登录

H5 应用集成壳子登录态的标准流程,遵循 OAuth2 授权码模式。

流程说明:

┌──────────┐         ┌──────────┐         ┌──────────┐
│  H5 应用  │         │  壳子应用  │         │  后端服务  │
└────┬─────┘         └────┬─────┘         └────┬─────┘
     │  自检登录状态       │                    │
     │  (未登录)           │                    │
     │  auth.getAuthCode  │                    │
     │  { appId: 'xxx' }  │                    │
     │ ──────────────────>│                    │
     │                    │  GET /auth/code    │
     │                    │  ?appId=xxx        │
     │                    │ ──────────────────>│
     │                    │  返回授权码 code     │
     │                    │ <──────────────────│
     │  返回 { code }     │                    │
     │ <──────────────────│                    │
     │                    │                    │
     │  携带 code 请求自己的后端验证登录          │
     │ ──────────────────────────────────────>│
     │  返回 H5 应用的登录态                    │
     │ <──────────────────────────────────────│

H5 端接入代码:

// 1. H5 应用自检登录状态(如检查 cookie / localStorage)
function isLoggedIn() {
  return !!localStorage.getItem('h5_token');
}

// 2. 未登录时向壳子申请授权码
async function loginWithShell() {
  try {
    const result = await window.$bridge.invoke('auth.getAuthCode', {
      appId: 'your-app-id'  // H5 应用在壳子端注册的应用标识
    });

    if (result.code === 0) {
      const authCode = result.data.code;

      // 3. 将授权码发送到 H5 自己的后端,后端通过授权码换取用户信息
      const loginResult = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code: authCode })
      });

      const data = await loginResult.json();
      if (data.success) {
        localStorage.setItem('h5_token', data.token);
        console.log('登录成功');
      }
    }
  } catch (error) {
    console.error('授权登录失败:', error);
  }
}

// 4. 页面加载时自动检查
if (!isLoggedIn()) {
  loginWithShell();
}

错误码:

错误码 含义
9001 用户未登录(壳子登录态失效)
9002 获取授权码失败

注意事项:

  • appId 为必填参数,需提前在壳子端注册
  • 授权码为一次性使用,用完即失效
  • 如果壳子用户未登录(9001),H5 应引导用户返回壳子重新登录

使用示例

// 拍照
const photo = await window.$bridge.camera.takePhoto();
if (photo.code === 0) {
  console.log('照片路径:', photo.data.path);
}

// 权限检查 + 定位
const perm = await window.$bridge.permission.check('location');
if (perm.data.granted) {
  const loc = await window.$bridge.location.getCurrent();
  console.log(`位置: ${loc.data.lat}, ${loc.data.lng}`);
}

// 存储
await window.$bridge.storage.set('token', 'abc123');
const result = await window.$bridge.storage.get('token');
console.log(result.data.value); // "abc123"

// 复制到剪贴板
await window.$bridge.clipboard.copy('Hello World');

// 拨打电话
await window.$bridge.phone.call('10086');

// 震动反馈
await window.$bridge.vibrate.vibrate('light'); // light/medium/heavy

// 设置页面标题
await window.$bridge.navigation.setTitle('新标题');

// 在新容器中打开页面
await window.$bridge.navigation.open('https://example.com/detail', '详情页');

// 分享
await window.$bridge.share.shareText('分享内容');

// 选择文件
const files = await window.$bridge.file.pick(false, ['image']);

事件监听

接收原生推送的事件:

window.addEventListener('bridge:web.error', (event) => {
  console.error('WebView 错误:', event.detail.message);
});

错误处理

try {
  const result = await window.$bridge.camera.takePhoto();
  if (result.code !== 0) {
    switch (result.code) {
      case 1001: console.log('用户取消'); break;
      case 1002: console.log('权限被拒绝'); break;
      case 1003: console.log('参数无效'); break;
      default:   console.log('错误:', result.message);
    }
    return;
  }
  // 处理成功结果
} catch (error) {
  console.error('调用异常:', error);
}

错误码速查:

错误码 含义 错误码 含义
0 成功 3001 位置服务未开启
1001 用户取消 3002 位置获取超时
1002 权限被拒绝 4001 存储操作失败
1003 参数无效 5001 网络不可用
1004 通信错误 6001 文件不存在
2001 相机不可用 8001 不支持的操作
9001 用户未登录 9002 获取授权码失败

新增 Bridge 模块

添加新的原生能力只需两步:

1. 创建模块文件

lib/bridge/modules/ 下新建文件,实现 BridgeModule 接口:

// lib/bridge/modules/my_module.dart
import '../bridge_module.dart';
import '../bridge_result.dart';

class MyModule implements BridgeModule {
  @override
  String get name => 'myModule'; // JS 端通过 window.$bridge.myModule.xxx() 调用

  @override
  Map<String, BridgeHandler> get handlers => {
    'doSomething': _doSomething,
  };

  Future<BridgeResult> _doSomething(Map<String, dynamic> args) async {
    final param = args['param'] as String?;
    if (param == null) {
      return BridgeResult.error(BridgeErrorCodes.invalidParams, 'param is required');
    }
    // 调用原生能力...
    return BridgeResult.success(data: {'result': 'ok'});
  }
}

2. 注册模块

lib/main.dart 中添加注册:

final registry = BridgeRegistry()
  // ...已有模块
  ..register(MyModule());

3. 在 JS SDK 中添加便捷方法

assets/js/bridge_sdk.js 中添加对应的模块封装:

bridge.myModule = {
  doSomething: function (param) {
    return bridge.invoke('myModule.doSomething', { param: param });
  }
};

完成后 H5 即可通过 window.$bridge.myModule.doSomething('xxx') 调用。

Debug 工具

Debug 工具仅在非 Release 模式下启用(DevConfig.debugToolsEnabled),Release 包中不会包含。

Eruda 调试面板

所有 H5 页面在 Debug 模式下自动注入 Eruda 移动端调试工具,提供类似 Chrome DevTools 的调试体验。

注入原理:

  1. assets/js/eruda.js(约 500KB)随 APK/IPA 打包,通过 rootBundle 加载
  2. 每次 WebView 页面加载完成(onLoadStop)时,先注入 Eruda JS,再调用 eruda.init()
  3. 无需网络,完全离线可用

使用方式:

  1. 以 Debug 模式运行应用(flutter run
  2. 打开任意 H5 页面
  3. 页面右下角出现 Eruda 齿轮图标
  4. 点击齿轮展开调试面板

功能面板:

面板 功能
Console 查看所有 console.log/warn/error 输出
Network 查看 XHR/Fetch 网络请求及响应
Elements 检查 DOM 结构和样式
Resources 查看 Cookie、LocalStorage、SessionStorage
Sources 查看页面源码和 JS 文件
Info 设备信息和页面性能数据

适用场景:

  • H5 页面白屏排查:通过 Console 查看报错
  • 接口调试:通过 Network 查看 XHR 请求和响应
  • 样式调试:通过 Elements 检查 DOM 和 CSS
  • 存储调试:通过 Resources 查看 LocalStorage 数据

Debug Shell

首页顶部导航栏齿轮图标,可输入任意 URL 打开 WebView,并附带 Debug Panel 底部抽屉,支持按类型筛选日志(Console/Network/Bridge/Error)、一键复制导出。

关闭 Debug 工具

Release 构建自动关闭。如需在 Debug 模式下关闭,修改 lib/debug/dev_config.dart

static bool get debugToolsEnabled => false;

项目结构

lib/
├── main.dart              # 入口:创建 Registry + Repository
├── app.dart               # MaterialApp 配置
├── bridge/
│   ├── bridge_module.dart  # BridgeModule 抽象类
│   ├── bridge_result.dart  # BridgeResult + 错误码
│   ├── bridge_registry.dart # 模块注册与分发
│   └── modules/            # 15 个功能模块
├── webview/
│   ├── webview_page.dart   # WebView 容器 + Bridge 接线
│   └── tab_webview.dart    # Tab 内嵌 WebView
├── pages/
│   ├── main_page.dart      # 主页面(底部导航 + 5 个 Tab)
│   └── login_page.dart     # 登录页面
├── models/
│   └── app.dart            # App 数据模型
├── repositories/
│   ├── app_repository.dart # 抽象接口
│   └── hardcoded_app_repository.dart # 硬编码实现(可替换为 API)
├── services/
│   └── permission_service.dart
└── debug/                  # 调试工具(仅 Debug 模式)
assets/
├── js/bridge_sdk.js        # H5 端 JSBridge SDK
└── test.html               # API 测试页面

详细文档

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors