zkit-tools
v1.0.1
Published
`zkit-tools` 是 `zkit` 的非业务基础工具库,用来支撑组件库和应用层的通用基础设施:尺寸缩放、字体缩放上限、设备品牌归一化、路由重复跳转守卫等。
Readme
zkit-tools
zkit-tools 是 zkit 的非业务基础工具库,用来支撑组件库和应用层的通用基础设施:尺寸缩放、字体缩放上限、设备品牌归一化、路由重复跳转守卫等。
它不是业务工具箱,也不是 UI 组件层。公共 API 的目标是少、稳、直觉统一,并且在 iOS / Android / Web / Node-like 构建场景下尽量可预测。
设计目标
- 稳定:根入口不因缺少 React Native 运行时而崩溃
- 可预测:配置缺失、非法输入和平台差异都有明确兜底或错误
- 轻量:不引入 UI 逻辑,不绑定 Expo 作为包级依赖
- 高性能:尺寸缩放、路由守卫这些热路径避免多余订阅和全局副作用
- API 统一:命名直接表达能力,不继续扩散历史近义词
- 可维护:公开入口集中在 src/index.ts,内部能力按模块分层
与 zkit-ui 的关系
zkit-ui 负责组件、主题、i18n、交互和视觉系统;zkit-tools 只提供 UI 无关的基础工具。
当前 zkit-ui 会使用:
wp(...)/sp(...):计算组件内部尺寸、圆角、间距、字号getMaxFontSizeMultiplier():统一Text/TextInput的字体缩放上限
业务侧自定义 zkit-ui 组件的像素类 props 时,也应使用 wp(...) 或 sp(...):
import { Button } from 'zkit-ui';
import { sp, wp } from 'zkit-tools';
export function DemoButton() {
return (
<Button
layout={{
minHeight: wp(44),
paddingHorizontal: wp(20),
radius: wp(14),
textSize: sp(16),
}}
>
保存
</Button>
);
}工具库不能反向依赖 zkit-ui,也不能沉入主题 token、业务文案或组件服务。
安装与导入
在本仓库 workspace 内直接使用:
import { wp, sp } from 'zkit-tools';包入口只导出稳定公共 API。不要从 src/internal/* 或具体实现文件导入。
推荐应用初始化
应用入口可以集中配置尺寸和字体上限:
import {
configureFontSizeMultiplier,
configureViewportScale,
installGlobalTextScalingLimit,
} from 'zkit-tools';
configureViewportScale({
baseWidth: 375,
minScale: 0.85,
maxScale: 1.2,
minFontScale: 0.85,
maxFontScale: 1.2,
});
configureFontSizeMultiplier({
maxFontSizeMultiplier: 1.3,
});
// 可选。优先让 zkit-ui 的 Text/TextInput 显式读取上限;
// 只有希望限制所有 RN Text/TextInput 时再安装全局 patch。
installGlobalTextScalingLimit();尺寸缩放
基础用法
import { sp, wp } from 'zkit-tools';
const cardPadding = wp(16);
const iconSize = wp(20);
const titleSize = sp(18);wp(size):用于像素尺寸、间距、圆角、宽高、图标尺寸、阴影半径等sp(size):用于字号和文本行高- 非有限数字会返回
0 - 默认设计宽度是
375 - 默认缩放会夹紧在
0.85x到1.2x - 默认
rounding="pixel",优先走 RNPixelRatio.roundToNearestPixel
配置
import { configureViewportScale, getViewportScaleConfig } from 'zkit-tools';
configureViewportScale({
baseWidth: 390,
fallbackWidth: 390,
minScale: 0.82,
maxScale: 1.18,
minFontScale: 0.9,
maxFontScale: 1.2,
rounding: 'pixel',
});
const config = getViewportScaleConfig();常用配置:
baseWidth:设计稿基准宽度fallbackWidth:非 RN / 无 Dimensions 场景下的兜底宽度minScale/maxScale:普通尺寸缩放范围minFontScale/maxFontScale:文字尺寸缩放范围rounding:pixel/none/round/floor/ceil
读取视口
import {
getViewportFontScale,
getViewportMetrics,
getViewportScale,
subscribeViewportMetrics,
} from 'zkit-tools';
const metrics = getViewportMetrics();
const scale = getViewportScale();
const fontScale = getViewportFontScale();
const unsubscribe = subscribeViewportMetrics((nextMetrics) => {
console.log(nextMetrics.width, nextMetrics.height);
});subscribeViewportMetrics 只在至少存在一个订阅时绑定 Dimensions listener,取消最后一个订阅后会移除 listener。
单次覆盖
import { scaleFont, scaleSize } from 'zkit-tools';
const compactGap = scaleSize(8, { maxScale: 1 });
const fixedTitle = scaleFont(18, { minFontScale: 1, maxFontScale: 1 });字体缩放上限
显式使用
import { Text } from 'react-native';
import { getMaxFontSizeMultiplier } from 'zkit-tools';
<Text maxFontSizeMultiplier={getMaxFontSizeMultiplier()}>
可访问性友好的文字
</Text>;zkit-ui 的 Text、TextInputPrimitive 和 WheelColumn 已经读取这个值,业务通常不需要手动处理组件库内部文字。
配置上限
import {
configureFontSizeMultiplier,
getGlobalTextScalingLimitSnapshot,
getMaxFontSizeMultiplier,
} from 'zkit-tools';
configureFontSizeMultiplier({ maxFontSizeMultiplier: 1.25 });
const max = getMaxFontSizeMultiplier();
const snapshot = getGlobalTextScalingLimitSnapshot();全局 patch
import {
installGlobalTextScalingLimit,
uninstallGlobalTextScalingLimit,
} from 'zkit-tools';
const uninstall = installGlobalTextScalingLimit({ maxFontSizeMultiplier: 1.3 });
uninstall();
uninstallGlobalTextScalingLimit();全局 patch 会尝试给 RN Text / TextInput 注入默认 maxFontSizeMultiplier。这是 escape hatch:优先使用显式组件 API,只有需要兜住第三方或历史 RN 文本时再启用。
设备品牌
当前设备
import { getDeviceBrand } from 'zkit-tools';
const brand = getDeviceBrand();返回值:
type DeviceBrand =
| 'apple'
| 'google'
| 'honor'
| 'huawei'
| 'iqoo'
| 'lenovo'
| 'meizu'
| 'motorola'
| 'oneplus'
| 'oppo'
| 'poco'
| 'realme'
| 'redmi'
| 'samsung'
| 'vivo'
| 'xiaomi'
| 'unknown';纯函数归一化
import { resolveDeviceBrand } from 'zkit-tools';
const brand = resolveDeviceBrand({
os: 'android',
manufacturer: 'Xiaomi',
brand: 'Redmi',
model: '23090RA98C',
});说明:
- iOS / macOS 统一归为
apple - Android 会读取 RN
Platform.constants中的Manufacturer / Brand / Model / Product / Device - Web、模拟器、定制 ROM 或拿不到系统常量时可能返回
unknown - 本工具只做设备品牌归一化,不判断安装来源、应用商店来源或业务渠道
路由守卫
createRouterGuard 用来防止快速连点导致重复前进导航。它会 patch router 的前进方法;前进导航后上锁,后退、状态变化、异常或超时会解锁。
expo-router 用法
import { router } from 'expo-router';
import { createRouterGuard } from 'zkit-tools';
const guard = createRouterGuard({
router,
lockMs: 2000,
});
guard.isLocked();
guard.getSnapshot();
guard.unlock();
guard.destroy();默认前进方法:
pushreplacenavigatedismissTo
默认后退方法:
backdismissdismissAll
自定义 router
import { createRouterGuard } from 'zkit-tools';
let currentPath = '/home';
const listeners = new Set<() => void>();
const router = {
push(path: string) {
currentPath = path;
listeners.forEach((listener) => listener());
},
back() {
currentPath = '/home';
listeners.forEach((listener) => listener());
},
};
const guard = createRouterGuard({
router,
lockMs: 800,
getCurrentPath: () => currentPath,
subscribeToStateChange(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
});跳过守卫
router.push('/settings', { skipRouterGuard: true });
router.push({ pathname: '/settings', skipRouterGuard: true });skipRouterGuard 会绕过本次锁定。旧实验字段 unstable_skipRouterGuard 也可识别。
事件
createRouterGuard({
router,
onEvent(event) {
if (event.type === 'block') {
console.log(event.method, event.reason);
}
},
});事件类型:
readyallowbypassblockunlockdestroy
注意事项:
- 同一个 router 重复调用
createRouterGuard会返回同一个 controller,不会叠加 patch destroy()会尽量只恢复自己 patch 的方法,不覆盖之后其它代码替换的方法onEvent不应触发高频同步重渲染
公开 API 清单
layout
wp(size, options?)sp(size, options?)scaleSize(size, options?)scaleFont(size, options?)configureViewportScale(config?)getViewportScaleConfig()getViewportMetrics()getViewportScale(options?)getViewportFontScale(options?)subscribeViewportMetrics(listener)
accessibility
configureFontSizeMultiplier(config?)getMaxFontSizeMultiplier()installGlobalTextScalingLimit(options?)uninstallGlobalTextScalingLimit()getGlobalTextScalingLimitSnapshot()
device
getDeviceBrand()resolveDeviceBrand(input)
navigation
createRouterGuard(options)
破坏性迁移
| 旧 API | 新 API |
| --- | --- |
| configureScreenUtils | configureViewportScale |
| getScreenScale | getViewportScale |
| getScreenUtilsConfig | getViewportScaleConfig |
| getMaxFontScale | getMaxFontSizeMultiplier |
| configureFontScaling | configureFontSizeMultiplier |
| applyGlobalFontScale | installGlobalTextScalingLimit |
| getPhoneBrand | getDeviceBrand |
| resolvePhoneBrand | resolveDeviceBrand |
| initRouterGuard | createRouterGuard |
| fallbackLockMs | lockMs |
三端行为说明
iOS
wp/sp读取 RNDimensions.get('window')rounding="pixel"走PixelRatio.roundToNearestPixelgetDeviceBrand()返回apple- 全局字体 patch 会尝试覆盖 RN
Text/TextInput
Android
wp/sp行为与 iOS 一致getDeviceBrand()依赖Platform.constants,模拟器或定制 ROM 可能返回unknown- 路由守卫推荐保留
lockMs兜底,避免导航状态事件缺失时永久锁住
Web
- 在 React Native Web 环境下优先读取 RN
Dimensions - 在非 RN / Node-like 环境下,尺寸使用
fallbackWidth - 根入口可被 Node require,用于纯函数和构建验证
包边界与维护规则
- 所有公开导出必须集中在 src/index.ts
src/internal/*不对外导出- 不把业务语义、主题 token、UI 组件或服务放进
packages/tools - 运行时能力必须惰性读取平台依赖,不让根入口因为缺少 RN/Expo 崩溃
- 高频能力要避免隐式订阅、重复 patch 和不可回收全局状态
- 新增能力前先判断是否真的属于通用工具,而不是某个业务或组件的内部 helper
开发验证
pnpm --filter zkit-tools verify
pnpm validateverify 会构建 zkit-tools,并在 Node 中直接 require dist,验证根入口无 RN 环境可加载、核心纯函数可运行、路由守卫基础行为正常。
