zcw-shared
v2.29.1
Published
一个环境无关的 TypeScript 函数库,提供跨平台的工具函数和类型定义。
Downloads
298
Readme
Shared 函数库
一个环境无关的 TypeScript 函数库,提供跨平台的工具函数和类型定义。
技术架构
核心设计原则
环境无关性(Environment Agnostic)
- 所有函数都通过依赖注入接收环境相关的API
- 不直接使用
window、document、fs等全局对象 - 不传入整个模块对象(包括 Node.js API、浏览器 API 和第三方 SDK)
- 只传入实际使用的函数,使用
Interface['method']精确提取类型 - 支持 Node.js、浏览器、UniApp、微信小程序等多种运行环境
模块化架构
- 每个函数独立打包,支持按需导入
- 无统一入口文件,避免不必要的依赖
- 清晰的类型定义和环境抽象
类型安全优先
- 完整的 TypeScript 类型定义
- 从
references/提取类型,避免重复定义 - 使用
import type导入类型,零运行时开销
目录结构
src/
├── constants/ # 常量定义(颜色模式、软件配置等)
├── functions/ # 核心函数库
│ ├── color/ # 颜色处理函数
│ ├── android/ # Android 构建相关
│ ├── css/ # CSS 处理
│ ├── dom/ # DOM 操作
│ ├── utils/ # 通用工具
│ └── ...
├── schemas/ # Zod Schema 定义(用于运行时验证)
│ ├── auth.schema.ts # 认证相关 Schema
│ └── video.schema.ts # 视频相关 Schema
├── vue-hooks/ # Vue 组合式 API(依赖注入 env)
└── reactive/ # 跨框架响应式辅助(可选)
types/ # 应用层类型定义
├── auth.d.ts # 认证相关类型
├── color.d.ts # 颜色相关类型
├── storage.d.ts # 存储相关类型
├── video.d.ts # 视频相关类型
└── ...
references/ # 环境API类型声明
├── node.d.ts # Node.js API
├── browser.d.ts # 浏览器 API
├── dom.d.ts # DOM API
└── ...
docs/ # VitePress 文档站(含 API 与浏览器端交互演示组件)架构层次关系
应用层 (functions/ + vue-hooks/ + reactive/)
↓ 依赖注入
环境抽象层 (references/)
↓ 类型约束
具体环境 (Node.js/Browser/UniApp)环境无关性实现
1. 依赖注入模式
所有环境相关的操作都通过参数传入,并遵循以下规则:
规则 1:不直接导入第三方库
// ❌ 错误:直接导入 Vue 运行时函数
import { ref, watch } from 'vue'
// ✅ 正确:只能使用 import type 导入 Vue 类型
import type { Ref } from 'vue'
// ❌ 错误:不能从 references 导入 Vue 类型
import type { Ref } from '../../references/vue.d'
// 注意:Vue 类型只能从 'vue' 包直接导入,因为 Vue 的类型系统过于复杂,难以在 references 中完整定义规则 2:只传入实际使用的函数,不传入整个模块
// ❌ 错误:传入整个模块
import type { FileSystem, Path } from '../../references/node.d'
export function myFunction(fs: FileSystem, path: Path) {
fs.existsSync(...)
path.join(...)
}
// ✅ 正确:只传入使用的函数
import type { FileSystem, Path } from '../../references/node.d'
export interface MyFunctionDeps {
/** 检查文件是否存在 */
existsSync: FileSystem['existsSync']
/** 路径拼接 */
join: Path['join']
}
export function myFunction(deps: MyFunctionDeps) {
deps.existsSync(...)
deps.join(...)
}规则 3:使用 deps. 前缀,不要解构
// ❌ 错误:解构可能导致命名冲突
export function myFunction(deps: MyFunctionDeps) {
const { existsSync, join } = deps // 难以追踪来源
existsSync(...)
}
// ✅ 正确:使用 deps. 前缀
export function myFunction(deps: MyFunctionDeps) {
deps.existsSync(...) // 清晰明确
deps.join(...)
}规则 4:类型定义在 references 中,使用 typeof 或索引访问提取(Vue 类型除外)
// ❌ 错误:在函数中定义类型
export interface MyFunctionDeps {
exec: (command: string, callback: (error: any) => void) => any
}
// ✅ 正确:从 references 提取类型
import type { ChildProcess } from '../../references/node.d'
import type { setTimeout } from '../../references/timer.d'
export interface MyFunctionDeps {
exec: ChildProcess['exec']
setTimeout: typeof setTimeout
}Vue 类型特殊规则:
Vue 的类型系统过于复杂,难以在 references/ 中完整定义,因此 Vue 类型只能从 'vue' 包直接导入:
// ✅ 正确:Vue 类型只能从 'vue' 包直接导入
import type { Ref, Component, VNode } from 'vue'
export interface MyFunctionDeps {
ref: <T>(value: T) => Ref<T>
component: Component
node: VNode
}
// ❌ 错误:不能从 references 导入 Vue 类型
import type { Ref } from '../../references/vue.d' // Vue 没有 references 文件规则 5:每个函数维护自己完整的依赖,不要分散
// ❌ 错误:依赖分散到多个参数
export function myFunction(
path: string,
fsDeps: FsDeps,
pathDeps: PathDeps,
systemDeps: SystemDeps
) { }
// ✅ 正确:统一的依赖对象
export interface MyFunctionDeps {
existsSync: FileSystem['existsSync']
readFileSync: FileSystem['readFileSync']
join: Path['join']
xmlParser: XMLParserConstructor
}
export function myFunction(path: string, deps: MyFunctionDeps) {
deps.existsSync(...)
deps.readFileSync(...)
deps.join(...)
}规则 6:第三方 SDK 对象也要拆分
不仅是 Node.js 和浏览器 API,第三方 SDK 对象(Vue、微信、UniApp 等)也要遵循同样的规则。
Vue 特殊说明:
- Vue 类型只能通过
import type {} from 'vue'导入,不能从references/导入 - Vue 运行时函数仍然需要通过依赖注入传入,不能直接导入
// ❌ 错误:传入整个 Vue 运行时
import type { VueRuntime } from 'vue'
export interface DynamicMountOptions {
vue: VueRuntime // 包含很多未使用的方法
}
export function dynamicMount(options: DynamicMountOptions) {
options.vue.createVNode(...)
options.vue.render(...)
}
// ✅ 正确:只传入使用的函数,类型从 'vue' 导入
import type { VueRuntime } from 'vue'
export interface DynamicMountDeps {
createVNode: VueRuntime['createVNode']
render: VueRuntime['render']
}
export function dynamicMount(deps: DynamicMountDeps) {
deps.createVNode(...)
deps.render(...)
}常见第三方 SDK 拆分示例:
// 微信小程序 API
import type { Wx } from '../../references/wechat.d'
export interface WxDownloadFileDeps {
USER_DATA_PATH: Wx['env']['USER_DATA_PATH']
downloadFile: Wx['downloadFile']
}
// UniApp API
import type { UniApp } from '../../references/uniapp.d'
export interface WaitForPagesDeps {
getCurrentPages: UniApp['getCurrentPages']
}
// CryptoJS
import type { CryptoJS } from '../../references/crypto-js.d'
export interface GenerateLicenseDeps {
MD5: CryptoJS['MD5']
}
// 腾讯云 SDK
import type { CloudBaseManager } from '../../references/tencent-cloud.d'
export interface DeployTCBDeps {
init: CloudBaseManager['init']
}规则 7:存储能力统一通过 useStorage
- 不允许直接依赖
localStorage/sessionStorage等同步 API - 必须组合
useStorage与具体适配器(如useLocalStorage、useSessionStorage、useStorageWithIndexedDB)创建storage对象 - Env 类型统一定义为
ReturnType<typeof useStorage<string>>(或自定义 value 类型) - 在实现中仅使用
storage.get/set/remove/clear等 Promise API,避免混用同步方法 - 文档站中的 VitePress 交互示例同样需要使用
useStorage与适配器,确保行为与生产代码一致 - 多表结构化 IndexedDB(多 object store、复合索引、
transaction)可使用useDexieShortcuts:将Dexie构造器与indexedDB注入deps,应用侧安装 peer 依赖dexie(版本范围见package.json的peerDependencies)。详见docs/api/storage/useDexieShortcuts.md。
规则 8:浏览器宿主绑定层统一通过 readBrowserHost
- 库内函数不得直接访问
globalThis/window/document/localStorage - 应用边界通过
readBrowserHostFromGlobalThis()读取宿主,组装为ReadBrowserHostDeps后向下传递 readBrowser*/createBrowser*/useBrowser*系列是唯一允许在内部调用readBrowserHostFromGlobalThis()的便捷封装,供 Web 宿主直引useBrowser*Hook 可在绑定层直接import { ref, watch } from 'vue'(与 Vue SFC 同级,属于应用边界)- 浏览器存储统一经
createBrowserStringStorageKit/createBrowserSessionStorageKit组合useStorage+ 适配器;写入只用storage.set/remove,同步 bootstrap 读可经backingStorage - 新增浏览器绑定逻辑时,优先在
src/functions/browser/readBrowserHost.ts扩展BrowserHostSnapshot,不要在业务模块重复读取全局对象
import {
readBrowserHostDepsFromGlobalThis,
requireBrowserHostLocalStorage,
type ReadBrowserHostDeps,
} from '../browser/readBrowserHost'
export function createBrowserExampleAccess(
hostDeps: ReadBrowserHostDeps = readBrowserHostDepsFromGlobalThis(),
) {
return {
read(): string {
return requireBrowserHostLocalStorage(hostDeps).getItem('key') ?? ''
},
}
}规则 9:编排类函数拆分 {Name}Input 与 {Name}Deps
面向业务流程的编排函数(如 IM 复制、转发、媒体出站)允许将参数拆为两层:
{Name}Input:业务数据 + 由调用方实现的 I/O 回调(如copyImagePixels){Name}Deps:环境能力(document、setTimeout、URL等),第二参数传入;Web 宿主可使用readBrowserHost*默认注入
export type ExecuteExampleInput = {
messageId: string
persist: (id: string) => Promise<void>
}
export interface ExecuteExampleDeps {
document?: Document
}
export async function executeExample(
input: ExecuteExampleInput,
deps: ExecuteExampleDeps = {
document: readBrowserHostDocument(readBrowserHostDepsFromGlobalThis()),
},
) {
// input 承载业务数据与回调,deps 承载环境 API
}纯数据变换函数可只使用 {Name}Deps 或单一 options;命名与约定见 src/functions/im/imOrchestrationTypes.ts。
完整示例
// src/functions/android/buildProject.ts
import type { FileSystem, Path, ChildProcess } from '../../../references/node.d'
import type { setTimeout } from '../../../references/timer.d'
export interface AndroidBuildDependencies {
/** 检查文件/目录是否存在 */
existsSync: FileSystem['existsSync']
/** 读取目录内容 */
readdirSync: FileSystem['readdirSync']
/** 获取文件状态 */
statSync: FileSystem['statSync']
/** 路径拼接 */
join: Path['join']
/** 获取进程平台信息 */
platform: string
/** 执行子进程命令 */
exec: ChildProcess['exec']
/** 超时设置函数 */
setTimeout: typeof setTimeout
}
export async function buildProject(
options: AndroidBuildOptions,
deps: AndroidBuildDependencies
): Promise<AndroidBuildResult> {
// 使用 deps. 前缀访问所有依赖
if (!deps.existsSync(projectPath)) {
return { success: false, error: '路径不存在' }
}
const buildPath = deps.join(projectPath, 'build.gradle')
const files = deps.readdirSync(projectPath)
deps.exec(command, (error, stdout, stderr) => {
// ...
})
deps.setTimeout(() => {
// ...
}, 1000)
}2. 类型抽象
references/ 目录定义环境API的抽象类型(接口、类型别名等),所有基础类型都应在此定义:
注意:Vue 类型例外
- Vue 的类型系统过于复杂,难以在
references/中完整定义 - Vue 类型只能从
'vue'包直接导入,使用import type {} from 'vue' - Vue 运行时函数仍然需要通过依赖注入传入,不能直接导入
// references/node.d.ts
export interface FileSystem {
readFileSync(path: string, encoding?: string): string
writeFileSync(path: string, data: any): void
existsSync(path: string): boolean
readdirSync(path: string): string[]
statSync(path: string): { isDirectory(): boolean; isFile(): boolean }
}
export interface Path {
join(...paths: string[]): string
dirname(path: string): string
basename(path: string): string
}
export interface ChildProcess {
exec(command: string, callback?: (error: any, stdout: string, stderr: string) => void): any
}
// references/browser.d.ts
export interface Window {
innerWidth: number
innerHeight: number
localStorage: Storage
Image: new () => HTMLImageElement
File: new (bits: BlobPart[], name: string, options?: FilePropertyBag) => File
}
// references/dom.d.ts
export interface Document {
createElement<K extends keyof HTMLElementTagNameMap>(tagName: K): HTMLElementTagNameMap[K]
head: HTMLElement
}
// references/timer.d.ts
export declare const setTimeout: (callback: (...args: any[]) => void, ms?: number, ...args: any[]) => any
export declare const clearTimeout: (timeoutId: any) => void
// references/console.d.ts
export interface Console {
log(message?: any, ...optionalParams: any[]): void
error(message?: any, ...optionalParams: any[]): void
warn(message?: any, ...optionalParams: any[]): void
}types/ 目录定义应用层的业务类型:
// types/color.d.ts
export interface RgbColor {
r: number
g: number
b: number
}
// types/android-build.d.ts
export interface AndroidBuildOptions {
projectPath: string
buildVariant?: 'debug' | 'release'
}3. 多环境适配
同一个函数可以在不同环境中使用:
// Node.js 环境
import fs from 'fs'
import path from 'path'
import { buildProject } from 'zcw-shared/functions/android/buildProject'
const result = await buildProject(options, {
existsSync: fs.existsSync,
readdirSync: fs.readdirSync,
statSync: fs.statSync,
join: path.join,
platform: process.platform,
exec: require('child_process').exec,
setTimeout
})
// 浏览器 Vue 环境
import { ref, shallowRef, watch } from 'vue'
import { useAsyncState } from 'zcw-shared/vue-hooks/state/useAsyncState'
// 传入 Vue 运行时函数和浏览器 API(注意:运行时函数通过依赖注入传入,类型通过 import type 导入)
const { state, isLoading } = useAsyncState(
async () => fetch('/api/data').then(r => r.json()),
{
immediate: true,
delay: 1000
},
{
vue: { ref, shallowRef, watch }, // Vue Composition API 运行时函数
host: { setTimeout } // 浏览器定时器
}
)
// 浏览器 DOM 操作
import { getViewportRect } from 'zcw-shared/functions/dom/getViewportRect'
const viewport = getViewportRect({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight
})包管理和导入
PNPM Workspace 架构
项目使用 PNPM Workspace 管理:
// package.json
{
"name": "shared",
"exports": {
"./functions/color/convertColor": "./dist/functions/color/convertColor.js",
"./types/color": "./types/color.d.ts",
"./references/node": "./references/node.d.ts"
}
}按需导入
每个函数都可以单独导入,避免打包冗余:
// 导入单个函数
import { convertColor } from 'zcw-shared/functions/color/convertColor'
import type { RgbColor } from 'zcw-shared/types/color'
// 导入环境类型
import type { FileSystem } from 'zcw-shared/references/node'
// 导入常量
import { COLOR_VALIDATION_PATTERNS } from 'zcw-shared/constants/colorPatterns'
// 导入 Schema(用于运行时验证)
import { LoginResponseSchema, VideoSchema } from 'zcw-shared/schemas/auth.schema'
import { VideoListResponseSchema } from 'zcw-shared/schemas/video.schema'
// 导入存储抽象(具体路径以 package.json exports 为准)
import useStorage from 'zcw-shared/functions/storage/useStorage'本地校验以 pnpm exec tsc --noEmit、pnpm run build 与 pnpm run docs:build(文档站)为准;交互演示统一在 docs/ 内维护,说明见 docs/guide/documentation.md(本站内路径为 /guide/documentation)。
开发工作流程
开发注意事项
重要提醒:
文档同步更新:每次新增或修改函数后,必须同步更新 VitePress 文档。详细规范见下方「文档规范」章节。
构建验证:改动后在本包根目录执行
pnpm exec tsc --noEmit与pnpm run build,确保类型与导出正确。
1. 添加新函数
# 1. 创建函数文件
src/functions/category/newFunction.ts
# 2. 定义类型(如需要)
types/category.d.ts
# 3. 创建函数文档(必须!)
docs/api/category/newFunction.md
# 4. 创建 Playground 组件(如果是浏览器端函数)
docs/.vitepress/components/NewFunctionPlayground.vue
# 5. 更新侧边栏配置
docs/.vitepress/config.ts
# 6. 验证文档编译
npm run docs:dev
# 7. 构建和导出
npm run build2. 函数开发模板
遵循最新的依赖注入规范:
// src/functions/category/newFunction.ts
import type { FileSystem, Path } from '../../references/node.d'
import type { setTimeout } from '../../references/timer.d'
import type { Console } from '../../references/console.d'
/**
* 函数依赖接口
* 注意:
* 1. 只包含实际使用的函数
* 2. 使用 Interface['method'] 或 typeof 提取类型
* 3. 每个依赖都要有清晰的注释
*/
export interface NewFunctionDeps {
/** 检查文件是否存在 */
existsSync: FileSystem['existsSync']
/** 读取文件内容 */
readFileSync: FileSystem['readFileSync']
/** 路径拼接 */
join: Path['join']
/** 超时函数 */
setTimeout: typeof setTimeout
/** 错误日志函数 */
error: Console['error']
/** 日志输出函数 */
log: Console['log']
}
/**
* 函数描述
*
* @param input - 输入参数
* @param deps - 环境依赖(包含所有需要的函数)
* @returns 返回值描述
*
* @example
* ```typescript
* import fs from 'fs'
* import path from 'path'
*
* const result = newFunction(input, {
* existsSync: fs.existsSync,
* readFileSync: fs.readFileSync,
* join: path.join,
* setTimeout,
* error: console.error,
* log: console.log
* })
* ```
*/
export function newFunction(
input: string,
deps: NewFunctionDeps
): string {
// ✅ 使用 deps. 前缀访问所有依赖
if (!deps.existsSync(input)) {
deps.error('文件不存在')
return ''
}
const content = deps.readFileSync(input, 'utf8')
const outputPath = deps.join(input, '../output.txt')
deps.setTimeout(() => {
deps.log('处理完成')
}, 1000)
return content
}Vue Hooks 开发模板:
// src/vue-hooks/useNewHook.ts
// ✅ Vue 类型只能从 'vue' 直接导入
import type { Ref } from 'vue'
import type { setTimeout } from '../../references/timer.d'
import type { VueCompositionAPI } from '../../types/vue.d'
export interface NewHookEnvironment {
/** Vue Composition API 运行时函数(通过依赖注入传入) */
vue: Pick<Required<VueCompositionAPI>, 'ref' | 'watch' | 'onMounted' | 'onUnmounted'>
host: {
setTimeout: typeof setTimeout
clearTimeout: typeof clearTimeout
}
}
export function useNewHook<T>(
source: Ref<T>,
options: NewHookOptions,
env: NewHookEnvironment
): NewHookReturn<T> {
const state = env.vue.ref<T>(initialValue)
env.vue.watch(source, () => {
// 监听逻辑
})
env.vue.onMounted(() => {
// 挂载逻辑
})
env.host.setTimeout(() => {
// 定时器逻辑
}, 1000)
return { state }
}3. 代码检查清单
在提交代码前,请确保:
✅ 依赖注入检查
- [ ] 没有直接
import第三方库的运行时函数(如import { ref } from 'vue') - [ ] Vue 类型只能使用
import type {} from 'vue'导入,不能从references/导入 - [ ] 没有直接使用全局对象(
window、document、global、process) - [ ] 所有环境依赖都通过
deps或env参数传入 - [ ] 使用
deps.前缀访问依赖,没有解构
✅ 类型定义检查
- [ ] 类型从
references/导入,使用import type(Vue 类型除外) - [ ] Vue 类型只能从
'vue'包直接导入,使用import type {} from 'vue' - [ ] 依赖接口命名为
{FunctionName}Deps或{HookName}Environment - [ ] 使用
Interface['method']或typeof提取类型 - [ ] 每个依赖都有清晰的 JSDoc 注释
- [ ] 没有在函数中重复定义已存在的类型
✅ 函数设计检查
- [ ] 只传入实际使用的函数,不传入整个模块对象
- [ ] 第三方 SDK 对象(Vue、Wx、UniApp 等)也要拆分
- [ ] 函数签名清晰,参数顺序合理
- [ ] 错误处理完善,不会意外抛出异常
- [ ] 有完整的 JSDoc 文档注释
✅ 文档检查
- [ ] 创建了 VitePress 文档
- [ ] 浏览器函数创建了 Playground 组件
- [ ] 更新了侧边栏配置
- [ ] 运行
npm run build编译成功 - [ ] 运行
npm run docs:dev文档正常显示
4. 构建和发布
# 类型检查
npx tsc --noEmit
# 验证文档编译
npm run docs:build
# 构建项目
npm run build
# 发布版本
npm run publish:patch # 补丁版本
npm run publish:minor # 次要版本
npm run publish:major # 主要版本最佳实践
函数设计
- 纯函数优先:无副作用,相同输入产生相同输出
- 依赖注入:环境相关API通过
deps参数传入 - 类型安全:完整的TypeScript类型定义
- 错误处理:优雅处理异常情况,返回null而非抛出异常
- 精确依赖:只传入实际使用的函数,不传入整个模块
- 使用前缀:通过
deps.访问依赖,避免命名冲突
类型定义
- 分层设计:业务类型放在
types/,环境类型放在references/ - 类型提取:使用
Interface['method']或typeof从 references 提取类型 - 避免重复定义:不在函数中定义已存在于 references 的类型
- 完整注释:每个依赖函数都要有清晰的注释说明用途
- 统一接口:每个函数有自己完整的
Deps接口,不依赖通用的SystemDependencies
依赖管理
- 不导入第三方库运行时函数:不直接
import { ref } from 'vue',而是通过env对象传入 - Vue 类型特殊规则:Vue 类型只能使用
import type {} from 'vue'导入,不能从references/导入 - 不依赖全局对象:不直接使用
window、document、global;浏览器宿主统一经readBrowserHost(规则 8) - 接口命名规范:
{FunctionName}Deps或编排函数的{FunctionName}Input+{FunctionName}Deps - 使用 deps. 前缀:所有依赖访问都使用
deps.methodName(),不解构
快速检查清单
在编写新函数或修改现有函数时,快速检查以下要点:
✅ 9 大核心规则:
- ❌ 不直接导入第三方库运行时函数 → ✅ 从
references/导入类型(Vue 类型除外) - ❌ Vue 类型不能从
references/导入 → ✅ 只能使用import type {} from 'vue' - ❌ 不传入整个模块对象 → ✅ 只传入实际使用的函数
- ❌ 不解构 deps 对象 → ✅ 使用
deps.前缀访问 - ❌ 不重复定义类型 → ✅ 使用
Interface['method']提取 - ❌ 不分散依赖到多个参数 → ✅ 统一的
Deps接口 - ❌ 第三方 SDK 也不能整个传入 → ✅ Vue、Wx 等也要拆分
- ❌ 业务代码不直接读
globalThis→ ✅ 经readBrowserHost/readBrowser*绑定层注入 - ❌ 编排函数混用环境与业务参数 → ✅
{Name}Input+{Name}Deps分层(见规则 9)
✅ 命名规范:
- 函数依赖接口:
{FunctionName}Deps - 编排函数业务入参:
{FunctionName}Input - Hook 环境接口:
{HookName}Environment - 浏览器绑定便捷函数:
readBrowser*/createBrowser*/useBrowser* - Playground 组件:
{FunctionName}Playground.vue
✅ 文档完整性:
- [ ] Markdown 文档(
docs/api/category/functionName.md) - [ ] Playground 组件(浏览器函数必须)
- [ ] 侧边栏配置已更新
常见错误和解决方案
❌ 错误 1:直接导入第三方库运行时函数
import { ref, watch } from 'vue' // ❌ 错误:导入了运行时函数解决方案:
// ✅ 正确:Vue 类型只能从 'vue' 直接导入
import type { Ref } from 'vue'
import type { VueCompositionAPI } from '../../types/vue.d'
export interface UseMyHookEnvironment {
vue: Pick<Required<VueCompositionAPI>, 'ref' | 'watch'>
}
export function useMyHook(env: UseMyHookEnvironment) {
const state = env.vue.ref(0) // 运行时函数通过依赖注入传入
env.vue.watch(state, () => {})
}
// 使用时:
// import { ref, watch } from 'vue'
// const result = useMyHook({ vue: { ref, watch } })❌ 错误 2:传入整个模块对象
export function myFunction(fs: FileSystem, path: Path) { // ❌ 错误
fs.existsSync(...)
}解决方案:
export interface MyFunctionDeps {
existsSync: FileSystem['existsSync'] // ✅ 正确
}
export function myFunction(deps: MyFunctionDeps) {
deps.existsSync(...)
}❌ 错误 3:解构依赖对象
export function myFunction(deps: MyFunctionDeps) {
const { existsSync, join } = deps // ❌ 错误,可能命名冲突
}解决方案:
export function myFunction(deps: MyFunctionDeps) {
deps.existsSync(...) // ✅ 正确,清晰明确
deps.join(...)
}❌ 错误 4:在函数中定义类型
export interface MyFunctionDeps {
exec: (cmd: string, cb: (err: any) => void) => any // ❌ 错误
}解决方案:
import type { ChildProcess } from '../../references/node.d'
export interface MyFunctionDeps {
exec: ChildProcess['exec'] // ✅ 正确,从 references 提取
}❌ 错误 5:依赖全局对象
export function myFunction(window: Window) { // ❌ 错误
const width = window.innerWidth
window.localStorage.setItem(...)
}解决方案:
import type { Window } from '../../references/browser.d'
export interface MyFunctionDeps {
innerWidth: Window['innerWidth'] // ✅ 正确
setItem: (key: string, value: string) => void
}
export function myFunction(deps: MyFunctionDeps) {
const width = deps.innerWidth
deps.setItem('key', 'value')
}❌ 错误 6:传入整个第三方 SDK 对象
// ❌ 错误:Vue 类型可以从 'vue' 导入,但不能传入整个运行时对象
import type { VueRuntime } from 'vue'
import type { Wx } from '../../references/wechat.d'
export interface MyFunctionDeps {
vue: VueRuntime // ❌ 错误,包含很多未使用的方法
wx: Wx // ❌ 错误,包含很多未使用的方法
}
export function myFunction(deps: MyFunctionDeps) {
deps.vue.createVNode(...)
deps.wx.downloadFile(...)
}解决方案:
// ✅ 正确:Vue 类型从 'vue' 导入,运行时函数通过依赖注入传入
import type { VueRuntime } from 'vue'
import type { Wx } from '../../references/wechat.d'
export interface MyFunctionDeps {
createVNode: VueRuntime['createVNode'] // ✅ 正确:只传入使用的函数
downloadFile: Wx['downloadFile'] // ✅ 正确
USER_DATA_PATH: Wx['env']['USER_DATA_PATH'] // ✅ 嵌套属性也要精确提取
}
export function myFunction(deps: MyFunctionDeps) {
deps.createVNode(...)
deps.downloadFile(...)
const path = `${deps.USER_DATA_PATH}/file.txt`
}常见第三方 SDK 对象拆分:
VueRuntime→ 只传入createVNode,render等实际使用的方法(类型用import type {} from 'vue'导入)Wx(微信) → 只传入downloadFile,request等实际使用的方法(类型从references/wechat.d导入)UniApp→ 只传入getCurrentPages等实际使用的方法(类型从references/uniapp.d导入)CryptoJS→ 只传入MD5,SHA256等实际使用的方法(类型从references/crypto-js.d导入)CloudBaseManager→ 只传入init等实际使用的方法(类型从references/tencent-cloud.d导入)Sharp(图像处理) → 已经是函数,直接作为依赖(类型从references/sharp.d导入)
文档规范
重要:每个函数或 Hook 都必须有完整的文档!
0. 文档网站开发
本项目使用 VitePress 构建文档网站。
开发命令:
# 本地开发文档网站
npm run docs:dev
# 构建生产版本
npm run docs:build
# 预览构建后的网站
npm run docs:preview文档目录结构:
docs/
├── index.md # 首页
├── guide/ # 指南文档
├── api/ # API文档
│ ├── color/ # 按功能分类
│ ├── string/
│ └── utils/
└── .vitepress/ # VitePress配置
├── config.ts # 网站配置和侧边栏
├── theme/ # 主题配置
└── components/ # Playground组件1. 文档文件结构
每个函数必须有独立的 Markdown 文档:
docs/api/
├── color/
│ ├── convertColor.md # 每个函数一个文档
│ ├── colorValidation.md
│ └── ...
├── string/
│ ├── capitalize.md
│ └── ...
└── utils/
├── debounce.md
└── ...2. 文档内容模板
每个函数文档应按照以下结构编写(按优先级排序):
必需部分
# functionName
<!-- 1. 函数描述(一句话) -->
简短描述函数的功能和用途。
<!-- 2. Playground 组件(浏览器端函数必须有) -->
<script setup>
import FunctionNamePlayground from '../../.vitepress/components/FunctionNamePlayground.vue'
</script>
<FunctionNamePlayground />
<!-- 3. 前置依赖(如果函数有 deps 参数) -->
## 前置依赖
### 依赖参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| `deps.xxx` | `Type` | 依赖说明 |
### 环境要求
- **第三方库名**: 用途说明
\`\`\`bash
npm install library-name
\`\`\`
<!-- 4. 函数签名 -->
## 函数签名
\`\`\`typescript
function functionName(
param1: Type1,
param2: Type2,
deps?: DepsType
): ReturnType
interface FunctionNameDeps {
// 依赖接口定义
}
\`\`\`
<!-- 5. 参数表格 -->
## 参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| `param1` | `Type1` | 是 | 参数说明 |
| `param2` | `Type2` | 否 | 可选参数说明 |
<!-- 6. 返回值 -->
## 返回值
| 类型 | 说明 |
|------|------|
| `ReturnType` | 返回值说明 |
<!-- 7. 工作原理 -->
## 工作原理
1. 步骤1说明
2. 步骤2说明
3. 步骤3说明
4. 返回结果
补充说明...可选部分
<!-- 异常(如果函数会抛出错误) -->
## 异常
| 错误类型 | 触发条件 | 错误信息 |
|----------|----------|----------|
| `Error` | 条件 | 错误文本 |文档编写注意事项
- ✅ 必须包含:函数描述、函数签名、参数表格、返回值、工作原理
- ✅ 浏览器端函数必须有 Playground 组件
- ✅ 有 deps 参数的函数必须有前置依赖说明
- ❌ 不要包含:
## 导入章节(已在首页说明) - ❌ 不要包含:冗长的使用示例代码
- ❌ 不要包含:
## 注意事项章节(信息整合到其他部分) - ✅ 使用表格:参数、返回值、异常等都用表格展示
- ✅ 工作原理简明扼要:说明核心逻辑即可,不要过于详细
3. Playground 组件规范
浏览器端函数必须创建交互式 Playground 组件:
命名规范:
- 文件名:
{FunctionName}Playground.vue(首字母大小写,驼峰命名) - 位置:
docs/.vitepress/components/
设计规范:
所有 Playground 组件必须遵循统一的设计语言,确保视觉一致性和用户体验:
视觉风格
容器背景
.playground-container { width: 100%; /* 重要:占满容器宽度 */ padding: 24px; background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%); border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); box-sizing: border-box; /* 重要:确保 padding 不溢出 */ }卡片样式
.card { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); /* 注意:是 12px 和 0.06,不是 8px 和 0.08 */ }标题样式
h4 { margin: 0 0 16px 0; font-size: 15px; font-weight: 600; color: #374151; /* 重要:统一使用 #374151,不是 #2c3e50 */ text-transform: uppercase; /* 重要:标题全部大写 */ letter-spacing: 0.05em; /* 重要:增加字母间距 */ }
配色方案
主色调
- 主要按钮:
#3b82f6(蓝色) - 成功状态:
#10b981(绿色) - 错误状态:
#ef4444(红色) - 警告状态:
#f59e0b(橙色) - 中性按钮:
#6b7280(灰色)
- 主要按钮:
文本颜色
- 主标题:
#374151(15-16px) - 正文:
#6b7280(14px) - 次要文本:
#9ca3af(13px)
- 主标题:
边框和背景
- 边框:
#e5e7eb(2px solid) - 卡片背景:
#ffffff - 输入框背景:
#f9fafb
- 边框:
交互元素
输入框
input, textarea, select { padding: 12px 14px; border: 2px solid #e5e7eb; /* 重要:使用 #e5e7eb,不是 #e0e0e0 */ border-radius: 8px; font-size: 14px; box-sizing: border-box; transition: all 0.2s; } input:focus, textarea:focus, select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); /* 重要:焦点阴影 */ }按钮基础样式
.btn { padding: 12px 18px; /* 注意:是 18px,不是 20px */ border: none; /* 重要:按钮无边框 */ border-radius: 8px; font-size: 13px; /* 注意:是 13px,不是 14px */ font-weight: 600; cursor: pointer; transition: all 0.2s; } .btn:hover:not(:disabled) { transform: translateY(-1px); /* 重要:hover 向上移动 */ } .btn:disabled { opacity: 0.5; cursor: not-allowed; }小按钮样式
.btn-small { padding: 4px 12px; /* 用于工具按钮 */ font-size: 12px; }按钮颜色变体
/* 主要按钮 - 蓝色 */ .btn-primary { background: #3b82f6; color: white; } .btn-primary:hover { background: #2563eb; } /* 次要按钮 - 灰色 */ .btn-secondary { background: #6b7280; color: white; } .btn-secondary:hover { background: #4b5563; } /* 成功按钮 - 绿色 */ .btn-success { background: #10b981; color: white; } .btn-success:hover { background: #059669; } /* 危险按钮 - 红色 */ .btn-danger { background: #ef4444; color: white; } .btn-danger:hover { background: #dc2626; } /* 警告按钮 - 橙色 */ .btn-warning { background: #f59e0b; color: white; } .btn-warning:hover { background: #d97706; } /* 信息按钮 - 青色 */ .btn-info { background: #06b6d4; color: white; } .btn-info:hover { background: #0891b2; }
布局规范
间距系统
- 组件间距:
20px - 内边距:
20-24px - 小间距:
12px - 表单元素间距:
16px
- 组件间距:
圆角统一
- 容器:
16px - 卡片:
12px - 输入框/按钮:
8px - 小元素:
6px
- 容器:
响应式设计
@media (max-width: 768px) { .playground-container { padding: 20px; } .grid { grid-template-columns: 1fr; } }
特殊元素
统计卡片
.stat-item { display: flex; flex-direction: column; gap: 8px; padding: 16px; background: #f9fafb; border-radius: 8px; border: 2px solid #e5e7eb; text-align: center; } .stat-label { font-size: 11px; color: #6b7280; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; } .stat-value { font-size: 20px; /* 统计数值使用 20px */ font-weight: 700; color: #3b82f6; /* 重要:统计值使用主色调 */ font-family: monospace; /* 重要:数字使用等宽字体 */ }日志区域(推荐使用整体框方式)
.log-content { background: #f9fafb; border: 2px solid #e5e7eb; border-radius: 8px; padding: 18px; max-height: 320px; overflow-y: auto; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 13px; line-height: 1.6; } .log-item { padding: 4px 0; color: #374151; word-break: break-word; } .log-empty { color: #9ca3af; text-align: center; padding: 24px; font-size: 13px; }结果展示
.result-card { padding: 18px; background: #f9fafb; border: 2px solid #e5e7eb; border-radius: 8px; font-family: monospace; font-size: 13px; line-height: 1.6; }代码块
pre, code { background: #1e293b; color: #e2e8f0; padding: 18px; border-radius: 8px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 13px; line-height: 1.6; }状态指示
.status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 8px; } .status-success { background: #10b981; } .status-error { background: #ef4444; } .status-warning { background: #f59e0b; } .status-info { background: #3b82f6; }
禁止事项
❌ 不使用 emoji
- 不在 UI 中使用 emoji 图标
- 使用简洁的文字描述代替
- 必要时使用 Unicode 符号(如 ✓ ✗)
❌ 不使用渐变色背景(按钮除外)
- 避免在卡片上使用渐变
- 使用纯色或微妙的线性渐变
❌ 不使用过大的字体
- 标题 15-16px
- 正文 14px
- 代码 13px
- label 13px
❌ 不使用左侧色块装饰
- 避免
border-left彩色边框 - 使用统一的边框样式
- 避免
组件模板
<script setup lang="ts">
import { ref } from 'vue'
import { functionName } from '../../../src/functions/category/functionName'
// 组件逻辑
const input = ref('')
const result = ref('')
function handleAction() {
result.value = functionName(input.value)
}
</script>
<template>
<div class="function-playground">
<!-- 配置区域 -->
<div class="config-section">
<h4>配置选项</h4>
<div class="input-group">
<label>输入</label>
<input v-model="input" type="text" placeholder="请输入..." />
</div>
<button @click="handleAction" class="btn-primary">执行</button>
</div>
<!-- 结果区域 -->
<div class="result-section" v-if="result">
<h4>结果</h4>
<div class="result-card">{{ result }}</div>
</div>
<!-- 说明区域 -->
<div class="description-section">
<h4>功能说明</h4>
<p>函数功能的简要说明...</p>
</div>
</div>
</template>
<style scoped>
.function-playground {
width: 100%;
padding: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
}
.config-section,
.result-section,
.description-section {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
h4 {
margin: 0 0 16px 0;
font-size: 15px;
font-weight: 600;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.input-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: #6b7280;
}
input {
width: 100%;
padding: 12px 14px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
transition: all 0.2s;
}
input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
button {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
}
.result-card {
padding: 18px;
background: #f9fafb;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-family: monospace;
font-size: 13px;
line-height: 1.6;
color: #374151;
}
p {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: #6b7280;
}
@media (max-width: 768px) {
.function-playground {
padding: 20px;
}
}
</style>开发注意事项:
- ✅ 使用
../../../src/导入实际函数,不要 mock - ✅ 文件末尾不要有多余空行
- ✅ 所有输入框添加
box-sizing: border-box和min-width: 0 - ✅ 提供实时交互效果
- ✅ 遵循统一的设计规范
- ✅ 使用语义化的类名
- ✅ 添加响应式设计
- ✅ 保持视觉一致性
4. 侧边栏配置
在 docs/.vitepress/config.ts 中添加菜单项:
{
text: '模块名',
collapsed: true,
items: [
{ text: 'functionName', link: '/api/category/functionName' },
// ... 其他函数
]
}5. 文档检查清单
添加新函数后,必须确认:
- [ ] 函数文档已创建(
docs/api/category/functionName.md) - [ ] 文档包含所有必需部分(函数描述、函数签名、参数表、返回值、工作原理;有 deps 时含前置依赖)
- [ ] Playground 组件已创建(浏览器端函数)
- [ ] 组件正确导入实际函数(不是 mock)
- [ ] 侧边栏配置已更新
- [ ] 运行
npm run docs:dev验证无 404 错误 - [ ] 页面可以正常访问和交互
- [ ] 组件末尾无多余空行
6. 常见问题
Q: 页面显示 404?
- 检查文件名大小写是否正确
- 检查侧边栏配置路径是否正确
- 检查 Playground 组件是否有语法错误
- 清除缓存并重启开发服务器
Q: Playground 不显示?
- 检查组件文件名是否符合命名规范
- 检查组件末尾是否有多余空行
- 检查是否正确导入实际函数
Q: Node.js 端函数需要 Playground 吗?
- 不需要,只需要详细的代码示例即可
通过这种架构设计,shared 包实现了真正的环境无关性,可以在任何 JavaScript/TypeScript 环境中使用,同时保持了良好的类型安全和开发体验。
