Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions packages/api-proxy/__tests__/rn/canIUse.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 测试 RN 环境下的 canIUse 函数
import { canIUse } from '../../src/platform/api/base/rnCanIUse'

describe('canIUse for RN', () => {
test('should support basic APIs', () => {
expect(canIUse('navigateTo')).toBe(true)
expect(canIUse('showToast')).toBe(true)
expect(canIUse('request')).toBe(true)
expect(canIUse('getSystemInfo')).toBe(true)
expect(canIUse('vibrateShort')).toBe(true)
expect(canIUse('getStorage')).toBe(true)
})

test('should return false for unsupported APIs', () => {
expect(canIUse('setClipboardData')).toBe(false)
expect(canIUse('getClipboardData')).toBe(false)
expect(canIUse('someUnsupportedApi')).toBe(false)
expect(canIUse('wx.someMethod')).toBe(false)
expect(canIUse('nonExistentApi')).toBe(false)
expect(canIUse('undefinedMethod')).toBe(false)
})

test('should support API with methods', () => {
expect(canIUse('request.success')).toBe(true)
expect(canIUse('showModal.success.confirm')).toBe(true)
})

test('should support object methods', () => {
expect(canIUse('SelectorQuery')).toBe(true)
expect(canIUse('SelectorQuery.select')).toBe(true)
expect(canIUse('Animation.rotate')).toBe(true)

// 测试 Task 类的方法
expect(canIUse('SocketTask')).toBe(true)
expect(canIUse('SocketTask.send')).toBe(true)
expect(canIUse('SocketTask.onMessage')).toBe(true)
expect(canIUse('RequestTask')).toBe(true)
expect(canIUse('RequestTask.abort')).toBe(true)
})

test('should handle invalid input', () => {
expect(canIUse(null)).toBe(false)
expect(canIUse(undefined)).toBe(false)
expect(canIUse(123)).toBe(false)
})

test('should handle ${} syntax', () => {
expect(canIUse('$' + '{navigateTo}')).toBe(false)
expect(canIUse('$' + '{showToast}')).toBe(false)
})

test('should dynamically detect APIs from platform exports', () => {
// 测试一些从 platform 导出的 API
expect(canIUse('base64ToArrayBuffer')).toBe(true)
expect(canIUse('arrayBufferToBase64')).toBe(true)

// 测试 create* 函数
expect(canIUse('createAnimation')).toBe(true)
expect(canIUse('createSelectorQuery')).toBe(true)
expect(canIUse('createIntersectionObserver')).toBe(true)
})

test('canIUse should be lightweight and fast', () => {
const { canIUse } = require('../../src/platform/api/base/rnCanIUse')

const start = Date.now()

// 执行 1000 次 canIUse 调用
for (let i = 0; i < 1000; i++) {
canIUse('getStorage')
canIUse('request')
canIUse('SelectorQuery.select')
}

const duration = Date.now() - start

// 应该非常快(< 100ms)
expect(duration).toBeLessThan(100)

console.log(`1000 次 canIUse 调用耗时: ${duration}ms`)
})
})
1 change: 1 addition & 0 deletions packages/api-proxy/src/platform/api/base/index.ios.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './index.web'
export { canIUse } from './rnCanIUse'
93 changes: 93 additions & 0 deletions packages/api-proxy/src/platform/api/base/rnCanIUse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { SUPPORTED_APIS as API_LIST, SUPPORTED_OBJECTS as OBJECT_CONFIG } from './rnCanIUseConfig'

let SUPPORTED_APIS = null
let SUPPORTED_OBJECTS = null
let OBJECT_METHODS = null

/**
* 初始化支持的 API 列表
*
* 使用静态配置而不是动态导入,避免加载原生模块
* 这样 canIUse 只做判断,不触发任何模块的实际加载
*/
function initSupportedApis () {
if (SUPPORTED_APIS !== null) {
return
}

// 从静态配置中获取 API 列表
SUPPORTED_APIS = new Set(API_LIST)

// 从静态配置中获取对象和方法
SUPPORTED_OBJECTS = new Set(Object.keys(OBJECT_CONFIG))
OBJECT_METHODS = OBJECT_CONFIG
}

/**
* 判断小程序的API、回调、参数、组件等是否在当前版本可用
* @param {string} schema - 使用 ${API}.${method}.${param}.${option} 或者 ${component}.${attribute}.${option} 方式来调用
* @returns {boolean} 是否支持
*/
function canIUse (schema) {
// 延迟初始化,确保在首次调用时才加载 API 列表
if (SUPPORTED_APIS === null) {
initSupportedApis()
}

if (typeof schema !== 'string') {
return false
}

// 检查是否包含 ${} 语法,这种语法是无效的,应该返回 false
if (schema.includes('${') || schema.includes('}')) {
return false
}

const parts = schema.split('.')
const [first, second] = parts

// 情况1: 只有一个部分,检查 API 或对象
if (parts.length === 1) {
return SUPPORTED_APIS.has(first) || SUPPORTED_OBJECTS.has(first)
}

// 情况2: 两个部分 - API.method 或 Object.property
if (parts.length === 2) {
// 检查是否是支持的 API
if (SUPPORTED_APIS.has(first)) {
return true // 在 RN 环境下,如果 API 支持,默认其方法也支持
}
// 检查对象的方法/属性
if (SUPPORTED_OBJECTS.has(first)) {
return checkObjectMethod(first, second)
}
return false
}

// 情况3: 三个或更多部分 - API.method.param 等
if (parts.length >= 3) {
// 检查基础 API 是否支持
if (SUPPORTED_APIS.has(first)) {
return true // 简化处理,如果 API 支持则认为其参数也支持
}
return false
}

return false
}

/**
* 检查对象的方法是否支持
*/
function checkObjectMethod (objectName, methodName) {
const methods = OBJECT_METHODS[objectName]
if (!methods) {
return false
}

return methods.includes(methodName)
}

export {
canIUse
}
194 changes: 194 additions & 0 deletions packages/api-proxy/src/platform/api/base/rnCanIUseConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* React Native 平台支持的 API 和类配置
*
* 此文件用于 canIUse 功能,只声明支持的 API,不导入任何实现模块
* 避免在判断 API 可用性时触发原生模块的加载
* 对应 platform/index.js 中的所有导出,且在 RN 平台有实际实现
*/
export const SUPPORTED_APIS = [
// action-sheet
'showActionSheet',

// animation
'createAnimation',

// app
'onAppShow',
'onAppHide',
'offAppShow',
'offAppHide',
'onError',
'offError',

// base
'base64ToArrayBuffer',
'arrayBufferToBase64',

// create-intersection-observer
'createIntersectionObserver',

// create-selector-query
'createSelectorQuery',

// device/network
'getNetworkType',
'onNetworkStatusChange',
'offNetworkStatusChange',

// image
'previewImage',
'compressImage',
'getImageInfo',
'chooseMedia',

// keyboard
'onKeyboardHeightChange',
'offKeyboardHeightChange',
'hideKeyboard',

// location
'getLocation',
'openLocation',
'chooseLocation',
'onLocationChange',
'offLocationChange',
'startLocationUpdate',
'stopLocationUpdate',

// make-phone-call
'makePhoneCall',

// modal
'showModal',

// next-tick
'nextTick',

// request
'request',

// route
'redirectTo',
'navigateTo',
'navigateBack',
'reLaunch',
'switchTab',

// set-navigation-bar
'setNavigationBarTitle',
'setNavigationBarColor',

// socket
'connectSocket',

// storage
'setStorage',
'setStorageSync',
'getStorage',
'getStorageSync',
'getStorageInfo',
'getStorageInfoSync',
'removeStorage',
'removeStorageSync',
'clearStorage',
'clearStorageSync',

// system
'getSystemInfo',
'getSystemInfoSync',

// toast
'showToast',
'hideToast',
'showLoading',
'hideLoading',

// vibrate
'vibrateShort',
'vibrateLong',

// window
'onWindowResize',
'offWindowResize'
]

/**
* 支持的类及其方法
* 对应各个类文件中定义的类和方法
*/
export const SUPPORTED_OBJECTS = {
// SelectorQuery 相关
SelectorQuery: [
'in',
'select',
'selectAll',
'selectViewport',
'exec'
],

NodesRef: [
'boundingClientRect',
'scrollOffset',
'fields',
'context',
'node'
],

// IntersectionObserver
IntersectionObserver: [
'relativeTo',
'relativeToViewport',
'observe',
'disconnect'
],

// Animation
Animation: [
'opacity',
'backgroundColor',
'width',
'height',
'top',
'left',
'bottom',
'right',
'rotate',
'rotateX',
'rotateY',
'rotateZ',
'rotate3d',
'scale',
'scaleX',
'scaleY',
'scaleZ',
'scale3d',
'translate',
'translateX',
'translateY',
'translateZ',
'translate3d',
'skew',
'skewX',
'skewY',
'matrix',
'matrix3d',
'step',
'export'
],

// Task 相关
SocketTask: [
'send',
'close',
'onOpen',
'onClose',
'onError',
'onMessage'
],

RequestTask: [
'abort',
'onHeadersReceived',
'offHeadersReceived'
]
}