@ayden-fc2/riffle-bridge-web
v1.0.12
Published
Riffle Bridge Web SDK - WebView 与 Native 通信桥接库
Maintainers
Readme
Riffle Bridge API
1. tweaks和RiffleBridge用法总览
本文档为当前Web应用提供Bridge使用指导,分为必选的tweaks和可选的RiffleBridge两个API,从而与app应用通信。
安装依赖:
npm install @ayden-fc2/riffle-bridge-webtweaks用于联通web应用与app应用的自定义参数,传递支持颜色、数值、文本、布尔、选项、链接类型的JsonSchema格式的tweaksConfig,使用参考文章第二节。
🚨 重要原则:尽可能多地使用 tweaks 配置,避免硬编码!
web 应用中的以下内容都应该通过 tweaks 系统配置,而不是硬编码在代码中:
- 颜色:背景色、文字颜色、边框颜色等 → 使用
color类型- 数值参数:大小、速度、透明度、间距等 → 使用
number类型- 文本内容:标题、提示语、按钮文字等 → 使用
string类型- 开关选项:功能启用/禁用等 → 使用
boolean类型- 枚举选项:模式选择、难度选择等 → 使用
select类型- 图片资源:背景图、图标、素材等 → 使用
link类型 (type: 'image')- 音频资源:背景音乐、音效等 → 使用
link类型 (type: 'audio')这样做的好处是 app 应用可以在设置页面中响应式地控制和更换这些参数,提升用户体验。
RiffleBridge用于调用震动、音频、相机等模块的app应用功能,展开对应文章第三节各个模块,根据所需要实现的web应用功能,按需参考模块取用即可;
2. tweaks 用法
使用 createTweaks 创建配置实例,使用步骤如下:
2.1 定义配置
抽离出web应用中颜色、数值、文本、布尔、选项类型的参数,使用JsonSchema格式组织,示例如下:
const tweaksConfig = {
// 颜色类型 - app应用 端显示颜色选择器
color: { name: '颜色', customType: 'color', value: '#667eea' },
// 数值类型 - app应用 端显示滑块
size: { name: '大小', customType: 'number', value: 150, min: 50, max: 300 },
opacity: { name: '透明度', customType: 'number', value: 1, min: 0, max: 1, step: 0.1 },
// 文本类型 - app应用 端显示文本输入框
title: { name: '标题', customType: 'string', value: 'Hello World' },
// 布尔类型 - app应用 端显示开关
enabled: { name: '启用', customType: 'boolean', value: true },
// 选项类型 - app应用 端显示下拉选择器
mode: { name: '模式', customType: 'select', value: 'normal', options: ['normal', 'fast', 'slow'] },
vibration: { name: '震动', customType: 'select', value: 'medium', options: [
{ label: '轻', value: 'light' },
{ label: '中', value: 'medium' },
{ label: '重', value: 'heavy' },
]},
// 链接类型 - app应用 端显示链接预览(支持图片和音频)
backgroundImage: { name: '背景图', customType: 'link', value: { type: 'image', url: 'https://example.com/bg.png', name: '默认背景' } },
bgmAudio: { name: '背景音乐', customType: 'link', value: { type: 'audio', url: 'https://example.com/bgm.mp3', name: '默认BGM' } },
};2.2 初始化(必须传入React实例)
通过 { React } 参数显式传入初始化,保证web和app应用数据联通,响应式更新,初始化代码如下:
import React, { useMemo } from 'react';
import { createTweaks } from '@ayden-fc2/riffle-bridge-web';
// 传入 React 以启用 useSyncExternalStore 响应式订阅
const tweaks = useMemo(() => createTweaks(tweaksConfig, { React }), []);2.3 在组件中使用
使用 ${初始化的tweaks实例}.${tweaksConfig参数}.useState(),示例代码如下:
function Demo() {
// .useState() 自动响应式订阅,app应用 修改时自动重渲染
const color = tweaks.color.useState();
const size = tweaks.size.useState();
return <div style={{ backgroundColor: color, width: size, height: size }} />;
}3. RiffleBridge用法
通过 RiffleBridge 类调用app应用功能:
import { RiffleBridge } from '@ayden-fc2/riffle-bridge-web';
const bridge = new RiffleBridge();针对RiffleBridge,分为震动模块、传感器模块、音频模块、相机模块、麦克风模块、文件存储模块、设备信息模块,各自的使用方法与注意事项已经分别说明,根据web应用所需模块,按需阅读使用即可。
注意音频模块、相机模块、麦克风模块部分依赖于文件存储模块,可以先阅读文件存储模块的代码,辅助理解这三个模块中的文件存储操作
3.1 震动模块
// 7种震动类型
await bridge.vibrate('light'); // 普通-轻触
await bridge.vibrate('medium'); // 普通-中等
await bridge.vibrate('heavy'); // 普通-重
await bridge.vibrate('selection'); // 选择
await bridge.vibrate('success'); // 成功 ✓
await bridge.vibrate('warning'); // 警告 ⚠
await bridge.vibrate('error'); // 错误 ✗
// 震动序列(依次播放,间隔300ms)
await bridge.haptic.sequence(['light', 'medium', 'heavy'], 300);
// 按强度震动(0-1,自动映射到 light/medium/heavy)
await bridge.haptic.intensity(0.5);3.2 传感器模块
坐标系约定(已跨平台统一):
- x轴:正值指向设备右侧
- y轴:正值指向设备顶部
- z轴:正值指向屏幕外(朝向用户)
iOS 和 Android 原始坐标系不同,Bridge 已在 app应用 层自动处理,WebView 端收到的数据保持一致。
// 启动传感器
await bridge.sensor.start({
types: ['accelerometer', 'gyroscope', 'magnetometer', 'barometer'],
interval: 100 // 更新间隔 ms
});
// 监听数据(回调参数是数组,取最后一个为最新值)
bridge.sensor.onAccelerometer((data) => {
const { x, y, z } = data[data.length - 1];
console.log('加速度:', x, y, z);
});
bridge.sensor.onGyroscope((data) => {
const { x, y, z } = data[data.length - 1];
console.log('陀蝗仪:', x, y, z);
});
bridge.sensor.onMagnetometer((data) => {
const { x, y, z } = data[data.length - 1];
console.log('磁力计:', x, y, z);
});
bridge.sensor.onBarometer((data) => {
const { pressure, relativeAltitude } = data[data.length - 1];
console.log('气压:', pressure, 'hPa');
});
// 停止传感器
await bridge.sensor.stop();工具函数:
// 计算向量模长
const mag = bridge.utils.magnitude(x, y, z);
// 摇晃强度 → 'none' | 'light' | 'medium' | 'strong'
const shake = bridge.utils.getShakeIntensity(x, y, z);
// 倾斜方向 → 'forward' | 'backward' | 'left' | 'right' | 'center'
const tilt = bridge.utils.getTiltDirection(x, y);3.3 音频模块(录音、播放)
特别注意拿到的uri文件资源是app应用文件路径,当前web应用无法直接播放音频,请参考录音模块代码,先调用readAsBase64转码再播放!
播放模块:
支持多音频同时播放,每次调用 playOnline 会创建新的音频实例。主要用于不需要用户交互就直接播放的背景音乐等,点击音效等仍由 web 应用本身实现。如需切换音频,请先调用 stop() 停止当前音频再播放新音频
// 播放,支持传入 local://xxx 或 file://xxx 的app应用本地路径,或 https://xxx 的url网址
await bridge.audio.playOnline({ uri: playUri });
// 播放并循环(适用于背景音乐)
await bridge.audio.playOnline({ uri: playUri, isLooping: true });
// 调用playOnline后调整播放状态,音量,速度等
await bridge.audio.pause();
await bridge.audio.resume();
await bridge.audio.stop();
await bridge.audio.setVolume(0.5); // 0-1
await bridge.audio.setRate(1.5); // 0.5-2.0
await bridge.audio.setLooping(true); // 动态开启/关闭循环录音模块:
// 请求麦克风权限
const perm = await bridge.microphone.requestPermission();
if (!perm.granted) return;
// 开始录音
await bridge.microphone.start({ enableMonitoring: true });
// 监听音量
bridge.microphone.onVolumeData((data) => {
const vol = Array.isArray(data) ? data[0] : data;
console.log('音量:', vol.normalizedVolume); // 0-1
});
// 暂停 / 恢复
await bridge.microphone.pause();
await bridge.microphone.resume();
// 停止录音
const result = await bridge.microphone.stop();
console.log('时长:', result.durationMillis / 1000, '秒');
// 保存录音(以aydens-voice为例,实际可以替换为web应用的独特键,或者result.uri的独特文件名)
if (result?.uri) {
// 保存文件到app应用应用并记录map
await bridge.fileStorage.addCustomFileMapping('local://recording/aydens-voice', result.uri);
}
// 1. 后续让app应用播放
await bridge.audio.playOnline({ uri: 'local://recording/aydens-voice' });
// 2. 后续直接让web应用播放
const audioData = await bridge.fileStorage.readAsBase64(result.uri);
const audio = new Audio(audioData.dataUrl); // dataUrl 已是完整 'data:audio/mp4;base64,...'
audio.play();
// 或使用 React 方式
<audio src={audioData.dataUrl} controls />
3.4 相机模块(实时相机、拍照、相册、闪光灯)
特别注意拿到的uri文件资源是app应用件路径,当前web应用无法直接渲染,请参考下方拍照/相册的代码,先调用readAsBase64转码再渲染!
实时相机
相机采用「透明 WebView + 原生相机层」实现,打开相机时必须设置web应用背景透明。
// 打开相机
await bridge.camera.open({ facing: 'back' }); // 或 'front'
// ⚠️ 设置透明背景
document.body.style.background = 'transparent';
document.documentElement.style.background = 'transparent';
document.getElementById('root')!.style.background = 'transparent';
// 拍照
// ⚠️ 拍照后不要直接渲染,参考下方**拍照/相册**的代码,先调用readAsBase64转码再渲染!
// ⚠️ 拍照后如果需要保存,也参考下方**拍照/相册**的代码调用app应用的文件存储模块保存并记录map
const photo = await bridge.camera.takePhoto();
// 切换摄像头
await bridge.camera.toggleFacing();
// 闪光灯控制
await bridge.camera.setFlash(true); // 开启闪光灯
await bridge.camera.setFlash(false); // 关闭闪光灯
const result = await bridge.camera.toggleFlash(); // 切换闪光灯
console.log('闪光灯状态:', result.flash); // true | false
// 关闭相机(恢复背景)
await bridge.camera.close();
document.body.style.background = '';实时相机滤镜
注意:滤镜是通过覆盖在相机层上的 View 实现,支持 RN 样式属性(backgroundColor、opacity 等),不支持 CSS filter 属性。、
// 预设滤镜
await bridge.camera.setFilter('grayscale'); // 'sepia' | 'invert' | 'none'
// 自定义样式(React Native View 样式对象)
await bridge.camera.setFilter({
backgroundColor: 'rgba(255, 0, 0, 0.3)', // 红色蒙版
opacity: 0.8
});
// 渐变蒙版效果
await bridge.camera.setFilter({
backgroundColor: 'rgba(0, 0, 0, 0.5)',
});
// 关闭滤镜
await bridge.camera.setFilter('none');拍照/相册
不打开实时相机时,拍照/从相册选择图片
// 1. 拍照(以aydens-demo-photo为例,实际web应用使用picked.uri文件名或组特的key)
const photo = await bridge.fileStorage.takePhoto();
if (!photo?.cancelled) {
// 保存文件到app应用并记录map
await bridge.fileStorage.addCustomFileMapping('local://photos/aydens-demo-photo', photo.uri);
}
// 2. 或从相册选择(以aydens-photo为例,实际web应用使用picked.uri文件名或组特的key)
const picked = await bridge.fileStorage.pickFromGallery();
if (!picked?.cancelled) {
// 保存文件到app应用并记录map
await bridge.fileStorage.addCustomFileMapping('local://photos/aydens-demo-photo', picked.uri);
}
// web应用中拿到图片文件并渲染
const base64 = await bridge.fileStorage.readAsBase64('local://photos/aydens-demo-photo');
// 在 Web 中渲染
<img src={`data:image/jpeg;base64,${base64}`} />保存到系统相册
将图片或视频保存到用户的系统相册(会请求系统相册写入权限)
saveToGallery 支持自动解析多种 URI 格式,web 应用无需关心传入的是真实路径还是虚拟映射键:
file://...真实文件路径:直接使用local://...虚拟映射键:自动从 MAP 查询实际路径http(s)://...网络 URL:自动查找缓存文件
// 拍照后保存到系统相册(直接使用 file:// 路径为例)
const photo = await bridge.fileStorage.takePhoto();
if (!photo?.cancelled) {
// 默认存入系统相册Riffle
const result = await bridge.fileStorage.saveToGallery(photo.uri);
console.log('保存成功:', result.filename);
// 指定自定义相册名
const result2 = await bridge.fileStorage.saveToGallery(photo.uri, 'MyApp相册');
}
返回值类型:
interface SaveToGalleryResult {
success: boolean; // 是否成功
assetId?: string; // 系统相册资产ID
uri?: string; // 保存后的URI
filename?: string; // 文件名
mediaType?: string; // 媒体类型 'photo' | 'video'
width?: number; // 图片宽度
height?: number; // 图片高度
duration?: number; // 视频时长(秒)
}闪光灯
闪光灯必须打开实时相机才能调用,参考实时相机的说明。
当应用需要闪光灯而不要实时相机时,可以使用非透明web应用背景来遮挡实时相机
3.5 文件存储模块
特别注意拿到的uri文件资源是app应用文件路径,当前web应用无法直接渲染图片或播放音频,请参考下方代码,先调用readAsBase64转码再渲染或播放!相机&录音等模块也都有联动说明
文件映射系统维护了一个MAP映射表,支持本地文件和网络资源两种存储,web应用要根据需求选择使用哪种
本地文件(手动管理Map映射,以avatar/123为例)
// 本地文件添加
const photo = await bridge.fileStorage.takePhoto();
if (!photo?.cancelled) {
await bridge.fileStorage.addCustomFileMapping('local://avatar/123', photo.uri);
}
// 查询(查看是否存在)
const cached = await bridge.fileStorage.getLocalFileByUrl('local://avatar/123');
// 拿到base64
const base64 = await bridge.fileStorage.readAsBase64('local://avatar/123');
// 在 Web 中渲染
<img src={`data:image/jpeg;base64,${base64}`} />网络资源(利用Map自动管理缓存,避免重复下载)
// 智能下载(命中缓存直接返回)
const result = await bridge.fileStorage.downloadWithCache(url);
console.log(result.cached ? '使用缓存' : '已下载', result.uri);
// 拿到base64
const base64 = await bridge.fileStorage.readAsBase64(url);读取资源并解码base64,从app应用传入web应用
// uri为mapRecord记录值
const base64 = await bridge.fileStorage.readAsBase64(mapRecord);3.6 设备信息模块
拿到json信息,内部内容不保证,因此谨慎处理获取到的信息的格式
const device = await bridge.device.getDeviceInfo(); // 设备型号
const battery = await bridge.device.getBatteryInfo(); // 电池状态
const network = await bridge.device.getNetworkInfo(); // 网络类型
const system = await bridge.device.getSystemInfo(); // 系统版本
const app = await bridge.device.getAppInfo(); // 应用信息
const screen = await bridge.device.getScreenInfo(); // 屏幕尺寸
const all = await bridge.device.getAllInfo(); // 全部信息4. 主要类型定义
// 基础类型
type HapticFeedbackType = 'light' | 'medium' | 'heavy' | 'selection' | 'success' | 'warning' | 'error';
type SensorType = 'accelerometer' | 'gyroscope' | 'magnetometer' | 'barometer';
type CameraFacing = 'front' | 'back';
type CameraFilter = 'none' | 'grayscale' | 'sepia' | 'invert' | Record<string, unknown>;
// 传感器数据
interface SensorData {
x: number;
y: number;
z: number;
timestamp?: number;
}
interface BarometerData {
pressure: number;
relativeAltitude?: number;
timestamp?: number;
}
// 音频相关
interface VolumeData {
metering: number;
normalizedVolume: number;
durationMillis: number;
timestamp?: number;
}
interface AudioStatus {
isLoaded: boolean;
isPlaying: boolean;
isLooping?: boolean;
volume: number;
rate: number;
durationMillis?: number;
positionMillis?: number;
}
interface RecordingResult {
isRecording: boolean;
isDoneRecording?: boolean;
durationMillis?: number;
canRecord: boolean;
isPaused?: boolean;
uri?: string;
}
// 文件存储相关
interface UrlMapping {
url: string;
localUri: string;
size?: number;
sizeFormatted?: string;
createdAt?: number;
}
interface CachedFileInfo {
exists: boolean;
uri?: string;
size?: number;
sizeFormatted?: string;
}
interface DownloadResult {
cached: boolean;
uri: string;
size?: number;
sizeFormatted?: string;
}
interface PhotoResult {
cancelled: boolean;
filename?: string;
uri?: string;
size?: number;
sizeFormatted?: string;
width?: number;
height?: number;
facing?: CameraFacing;
}
// 权限
interface PermissionResult {
granted: boolean;
canAskAgain: boolean;
status: string;
}
// Tweaks 配置
interface TweakConfigBase {
name: string;
customType: 'color' | 'number' | 'boolean' | 'string' | 'select' | 'link';
group?: string;
description?: string;
index?: number;
}
interface ColorTweakConfig extends TweakConfigBase {
customType: 'color';
value: string;
}
interface NumberTweakConfig extends TweakConfigBase {
customType: 'number';
value: number;
min?: number;
max?: number;
step?: number;
}
interface BooleanTweakConfig extends TweakConfigBase {
customType: 'boolean';
value: boolean;
}
interface StringTweakConfig extends TweakConfigBase {
customType: 'string';
value: string;
}
interface SelectTweakConfig extends TweakConfigBase {
customType: 'select';
value: string;
options: (string | { label: string; value: string })[];
}
type LinkResourceType = 'image' | 'audio';
interface LinkValue {
type: LinkResourceType;
url: string;
name: string;
}
interface LinkTweakConfig extends TweakConfigBase {
customType: 'link';
value: LinkValue;
}
type TweakConfig = ColorTweakConfig | NumberTweakConfig | BooleanTweakConfig | StringTweakConfig | SelectTweakConfig | LinkTweakConfig;
type TweaksConfig = Record<string, TweakConfig>;