npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ayden-fc2/riffle-bridge-web

v1.0.12

Published

Riffle Bridge Web SDK - WebView 与 Native 通信桥接库

Readme

Riffle Bridge API

1. tweaks和RiffleBridge用法总览

本文档为当前Web应用提供Bridge使用指导,分为必选的tweaks和可选的RiffleBridge两个API,从而与app应用通信。

安装依赖:

npm install @ayden-fc2/riffle-bridge-web

tweaks用于联通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 样式属性(backgroundColoropacity 等),不支持 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>;