从零开始打造 RandBox:一个强大的 JavaScript 随机数据生成库(5/5)- 文档建设与生态发展
前言
在前四篇文章中,我们完整地探讨了 RandBox 从项目规划、技术架构、功能实现到测试体系的全过程。今天,作为系列的最后一篇,我将分享如何为 RandBox 构建现代化的文档网站,建立开源社区,以及规划可持续发展的生态系统。一个优秀的开源项目不仅需要强大的功能,更需要完善的文档和活跃的社区支持。
文档网站架构设计
技术选型:Nextra + Next.js
经过对比多种文档框架后,我选择了 Nextra 作为 RandBox 文档网站的基础:
{
"dependencies": {
"next": "^14.0.0",
"nextra": "^2.13.0",
"nextra-theme-docs": "^2.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
选择 Nextra 的原因:
- 基于 Next.js:享受现代 React 生态的所有优势
- MDX 支持:可以在 Markdown 中嵌入 React 组件
- 内置功能丰富:搜索、国际化、主题切换等开箱即用
- 性能优秀:静态生成,SEO 友好
- 自定义能力强:可以深度定制样式和功能
项目结构设计
docs/
├── next.config.js # Next.js 配置
├── theme.config.tsx # Nextra 主题配置
├── tailwind.config.js # Tailwind CSS 配置
├── src/
│ ├── app/ # App Router
│ │ ├── layout.tsx # 根布局
│ │ ├── [lang]/ # 国际化路由
│ │ └── sitemap.ts # 网站地图
│ ├── components/ # 自定义组件
│ │ ├── ApiDemo/ # API 演示组件
│ │ ├── CodePlayground/ # 代码playground
│ │ └── FeatureGrid/ # 功能网格
│ ├── content/ # 文档内容
│ │ ├── en/ # 英文文档
│ │ └── zh/ # 中文文档
│ ├── i18n/ # 国际化配置
│ └── styles/ # 样式文件
核心配置实现
1. Nextra 主题配置
// theme.config.tsx
import { DocsThemeConfig } from 'nextra-theme-docs'
import { useRouter } from 'next/router'
const config: DocsThemeConfig = {
logo: (
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 flex items-center justify-center">
<span className="text-white font-bold text-sm">R</span>
</div>
<span className="font-bold text-lg">RandBox</span>
</div>
),
project: {
link: 'https://github.com/027xiguapi/randbox',
},
docsRepositoryBase: 'https://github.com/027xiguapi/randbox/tree/main/docs',
footer: {
text: (
<div className="flex w-full flex-col items-center sm:items-start">
<div className="mb-2">
MIT 2024 © RandBox - 免费开源的随机数据生成库
</div>
<div className="text-xs text-gray-500">
Built with ❤️ using Nextra & Next.js
</div>
</div>
),
},
search: {
placeholder: () => {
const { locale } = useRouter()
return locale === 'zh' ? '搜索文档...' : 'Search documentation...'
},
},
editLink: {
text: () => {
const { locale } = useRouter()
return locale === 'zh' ? '在 GitHub 上编辑此页' : 'Edit this page on GitHub'
},
},
feedback: {
content: () => {
const { locale } = useRouter()
return locale === 'zh' ? '有问题?给我们反馈 →' : 'Question? Give us feedback →'
},
labels: 'feedback',
},
toc: {
title: () => {
const { locale } = useRouter()
return locale === 'zh' ? '页面导览' : 'On This Page'
},
},
gitTimestamp: ({ timestamp }) => {
const { locale } = useRouter()
return (
<div className="text-xs text-gray-500">
{locale === 'zh' ? '最后更新于' : 'Last updated on'}{' '}
{timestamp.toLocaleDateString(locale === 'zh' ? 'zh-CN' : 'en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</div>
)
},
}
export default config
2. 国际化路由配置
// next.config.js
const withNextra = require('nextra')({
theme: 'nextra-theme-docs',
themeConfig: './theme.config.tsx',
})
module.exports = withNextra({
i18n: {
locales: ['en', 'zh'],
defaultLocale: 'en',
localeDetection: false,
},
redirects: async () => {
return [
{
source: '/docs',
destination: '/docs/getting-started',
permanent: true,
},
{
source: '/zh/docs',
destination: '/zh/docs/getting-started',
permanent: true,
},
]
},
// 优化构建
experimental: {
optimizeCss: true,
},
// SEO 优化
async generateStaticParams() {
return [
{ lang: 'en' },
{ lang: 'zh' },
]
},
})
3. 动态 sitemap 生成
// src/app/sitemap.ts
import { MetadataRoute } from 'next'
import { getPageMap } from 'nextra/page-map'
interface FileType {
kind: 'Folder' | 'MdxPage' | 'Meta'
name: string
route?: string
children?: FileType[]
}
function extractRoutes(pageMap: FileType[], parentRoute = ''): string[] {
const routes: string[] = []
for (const item of pageMap) {
if (item.kind === 'MdxPage' && item.route) {
routes.push(item.route)
} else if (item.kind === 'Folder' && item.children) {
routes.push(...extractRoutes(item.children, item.route || parentRoute))
}
}
return routes
}
async function getPagePaths(locale: string): Promise<string[]> {
try {
const pageMap = await getPageMap(`/${locale}`)
return extractRoutes(pageMap as FileType[])
} catch (error) {
console.warn(`Could not get page map for locale: ${locale}`, error)
return []
}
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://randbox.top'
const lastModified = new Date()
// 获取所有语言的页面路径
const [enPaths, zhPaths] = await Promise.all([
getPagePaths('en'),
getPagePaths('zh')
])
const staticPages = [
'',
'/playground',
'/examples',
'/api-reference',
]
const sitemapEntries: MetadataRoute.Sitemap = []
// 静态页面
for (const locale of ['', '/zh']) {
for (const page of staticPages) {
const url = `${baseUrl}${locale}${page}`
const priority = page === '' ? 1.0 : 0.8
sitemapEntries.push({
url,
lastModified,
changeFrequency: 'weekly',
priority,
})
}
}
// 文档页面
for (const path of enPaths) {
sitemapEntries.push({
url: `${baseUrl}${path}`,
lastModified,
changeFrequency: 'weekly',
priority: path.includes('/docs/') && path.split('/').length === 4 ? 0.9 : 0.7,
})
}
for (const path of zhPaths) {
sitemapEntries.push({
url: `${baseUrl}${path}`,
lastModified,
changeFrequency: 'weekly',
priority: path.includes('/docs/') && path.split('/').length === 4 ? 0.9 : 0.7,
})
}
return sitemapEntries
}
交互式组件开发
1. API 演示组件
为了让用户更好地理解 RandBox 的功能,我开发了交互式的 API 演示组件:
// src/components/ApiDemo/index.tsx
import { useState, useEffect } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
import { Button } from '../ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
interface ApiDemoProps {
category: string
methods: Array<{
name: string
description: string
example: string
options?: Record<string, any>
}>
}
export function ApiDemo({ category, methods }: ApiDemoProps) {
const [results, setResults] = useState<Record<string, any>>({})
const [randBox, setRandBox] = useState<any>(null)
useEffect(() => {
// 动态加载 RandBox
import('randbox').then((RandBoxModule) => {
const RandBox = RandBoxModule.default
setRandBox(new RandBox())
})
}, [])
const executeMethod = (methodName: string, options?: any) => {
if (!randBox) return
try {
const result = options
? randBox[methodName](options)
: randBox[methodName]()
setResults(prev => ({
...prev,
[methodName]: result
}))
} catch (error) {
console.error(`Error executing ${methodName}:`, error)
setResults(prev => ({
...prev,
[methodName]: `Error: ${error.message}`
}))
}
}
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500"></span>
{category} API 演示
</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue={methods[0]?.name} className="w-full">
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-3">
{methods.map((method) => (
<TabsTrigger key={method.name} value={method.name}>
{method.name}()
</TabsTrigger>
))}
</TabsList>
{methods.map((method) => (
<TabsContent key={method.name} value={method.name} className="space-y-4">
<div>
<h4 className="font-medium mb-2">{method.description}</h4>
<div className="bg-gray-100 dark:bg-gray-800 p-3 rounded-md">
<code className="text-sm">{method.example}</code>
</div>
</div>
<div className="flex gap-2">
<Button
onClick={() => executeMethod(method.name)}
variant="default"
>
执行
</Button>
{method.options && (
<Button
onClick={() => executeMethod(method.name, method.options)}
variant="outline"
>
带选项执行
</Button>
)}
</div>
{results[method.name] && (
<div className="mt-4">
<h5 className="font-medium mb-2">执行结果:</h5>
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-md border border-blue-200 dark:border-blue-800">
<code className="text-blue-800 dark:text-blue-200">
{JSON.stringify(results[method.name], null, 2)}
</code>
</div>
</div>
)}
</TabsContent>
))}
</Tabs>
</CardContent>
</Card>
)
}
2. 代码 Playground
// src/components/CodePlayground/index.tsx
import { useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
import { Button } from '../ui/button'
import { Textarea } from '../ui/textarea'
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
const DEFAULT_CODE = `// 欢迎使用 RandBox Playground
import RandBox from 'randbox'
const randBox = new RandBox()
// 生成基础数据
console.log('随机整数:', randBox.integer(1, 100))
console.log('随机字符串:', randBox.string({ length: 10 }))
console.log('随机布尔值:', randBox.bool())
// 生成个人信息
console.log('随机姓名:', randBox.name())
console.log('随机邮箱:', randBox.email())
console.log('随机电话:', randBox.phone())
// 生成地理信息
console.log('随机地址:', randBox.address())
console.log('随机坐标:', randBox.coordinates())
// 生成时间数据
console.log('随机日期:', randBox.date())
console.log('随机时间:', randBox.time())`
export function CodePlayground() {
const [code, setCode] = useState(DEFAULT_CODE)
const [output, setOutput] = useState('')
const [isRunning, setIsRunning] = useState(false)
const runCode = async () => {
setIsRunning(true)
setOutput('')
try {
// 创建一个新的控制台实现来捕获输出
const logs: string[] = []
const originalLog = console.log
console.log = (...args) => {
logs.push(args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
).join(' '))
}
// 动态导入 RandBox 并执行代码
const RandBoxModule = await import('randbox')
const RandBox = RandBoxModule.default
// 创建一个安全的执行环境
const context = {
RandBox,
console: { log: console.log },
Math,
Date,
JSON
}
// 使用 Function constructor 执行代码
const wrappedCode = `
const { RandBox, console } = arguments[0]
${code.replace(/import.*from.*['"]randbox['"];?\n?/g, '')}
`
const executeCode = new Function(wrappedCode)
executeCode(context)
// 恢复原始 console.log
console.log = originalLog
setOutput(logs.join('\n') || '代码执行完成,无输出')
} catch (error) {
setOutput(`执行错误: ${error.message}`)
} finally {
setIsRunning(false)
}
}
const resetCode = () => {
setCode(DEFAULT_CODE)
setOutput('')
}
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>代码 Playground</span>
<div className="flex gap-2">
<Button onClick={resetCode} variant="outline" size="sm">
重置
</Button>
<Button
onClick={runCode}
disabled={isRunning}
size="sm"
>
{isRunning ? '运行中...' : '运行代码'}
</Button>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="editor" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="editor">代码编辑器</TabsTrigger>
<TabsTrigger value="output">执行结果</TabsTrigger>
</TabsList>
<TabsContent value="editor" className="space-y-4">
<Textarea
value={code}
onChange={(e) => setCode(e.target.value)}
className="min-h-[400px] font-mono text-sm"
placeholder="在这里输入你的 RandBox 代码..."
/>
</TabsContent>
<TabsContent value="output" className="space-y-4">
<div className="bg-gray-900 text-green-400 p-4 rounded-md min-h-[400px] font-mono text-sm overflow-auto">
{output || '点击"运行代码"查看输出结果...'}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}
SEO 与性能优化
1. 元数据优化
// src/app/[lang]/layout.tsx
import { Metadata } from 'next'
import { getDictionary, I18nLangKeys } from '@/i18n'
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: I18nLangKeys }>
}): Promise<Metadata> {
const { lang } = await params
const dictionary = await getDictionary(lang)
const { metadata } = dictionary
return {
title: {
template: metadata.titleTemplate,
default: metadata.title,
},
description: metadata.description,
keywords: metadata.keywords,
authors: [{ name: metadata.author }],
creator: metadata.author,
publisher: metadata.author,
generator: metadata.generator,
applicationName: metadata.appTitle,
// Open Graph
openGraph: {
type: 'website',
locale: lang === 'zh' ? 'zh_CN' : 'en_US',
url: 'https://randbox.top',
siteName: metadata.siteName,
title: metadata.ogTitle,
description: metadata.ogDescription,
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: metadata.ogImageAlt,
},
],
},
// Twitter
twitter: {
card: 'summary_large_image',
title: metadata.twitterTitle,
description: metadata.twitterDescription,
images: ['/og-image.png'],
},
// 额外的 SEO 标签
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
verification: {
google: 'your-google-verification-code',
// 其他搜索引擎验证码
},
// 结构化数据
other: {
'application/ld+json': JSON.stringify({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'RandBox',
description: metadata.description,
url: 'https://randbox.top',
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Cross-platform',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
}),
},
}
}
2. 性能优化配置
// next.config.js
module.exports = withNextra({
// 图片优化
images: {
domains: ['images.unsplash.com', 'github.com'],
formats: ['image/avif', 'image/webp'],
},
// 压缩配置
compress: true,
// 实验性功能
experimental: {
optimizeCss: true,
optimizePackageImports: ['randbox'],
},
// 头部配置
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
},
{
source: '/api/(.*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=3600, stale-while-revalidate=86400',
},
],
},
]
},
})
社区建设与生态发展
1. 贡献指南建设
# 贡献指南
## 开发环境搭建
### 前置要求
- Node.js >= 16.0.0
- pnpm >= 7.0.0
- Git
### 安装依赖
```bash
# 克隆项目
git clone https://github.com/027xiguapi/randbox.git
cd randbox
# 安装依赖
pnpm install
# 构建项目
pnpm build
# 运行测试
pnpm test
代码贡献流程
1. 分支管理
main
: 主分支,用于发布develop
: 开发分支,用于集成功能feature/*
: 功能分支bugfix/*
: 修复分支
2. 提交信息规范
我们使用 Conventional Commits 规范:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
类型包括:
feat
: 新功能fix
: 错误修复docs
: 文档变更style
: 代码格式变更refactor
: 重构test
: 测试相关chore
: 构建/工具变更
3. Pull Request 流程
- Fork 项目到个人账户
- 创建功能分支:
git checkout -b feature/amazing-feature
- 提交变更:
git commit -m 'feat: add amazing feature'
- 推送分支:
git push origin feature/amazing-feature
- 创建 Pull Request
功能开发指南
添加新的数据生成方法
- 创建方法文件
// src/newCategory.ts
import { RandBox } from './core'
export function newMethod(this: RandBox, options?: NewMethodOptions): string {
const opts = this.initOptions(options, {
// 默认选项
})
// 实现逻辑
return result
}
- 添加类型定义
// src/types.ts
export interface NewMethodOptions {
// 选项定义
}
- 编写测试
// test/newCategory.test.ts
import test from 'ava'
import RandBox from '../src/index'
test('newMethod should work correctly', t => {
const randBox = new RandBox('test-seed')
const result = randBox.newMethod()
t.true(typeof result === 'string')
// 更多断言
})
- 更新文档 在相应的文档文件中添加使用说明和示例。
### 2. **插件系统设计**
```typescript
// src/plugins/index.ts
export interface RandBoxPlugin {
name: string
version: string
install: (randBox: RandBox) => void
uninstall?: (randBox: RandBox) => void
}
export class PluginManager {
private plugins = new Map<string, RandBoxPlugin>()
register(plugin: RandBoxPlugin): void {
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin ${plugin.name} is already registered`)
}
this.plugins.set(plugin.name, plugin)
}
install(randBox: RandBox, pluginName: string): void {
const plugin = this.plugins.get(pluginName)
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`)
}
plugin.install(randBox)
}
uninstall(randBox: RandBox, pluginName: string): void {
const plugin = this.plugins.get(pluginName)
if (plugin && plugin.uninstall) {
plugin.uninstall(randBox)
}
}
}
// 示例插件
export const chineseIdiomPlugin: RandBoxPlugin = {
name: 'chinese-idiom',
version: '1.0.0',
install(randBox: RandBox) {
randBox.idiom = function(this: RandBox): string {
const idioms = [
'一帆风顺', '二龙腾飞', '三羊开泰', '四季平安',
'五福临门', '六六大顺', '七星高照', '八方来财'
]
return this.pickone(idioms)
}
}
}
3. CLI 工具开发
// bin/randbox.ts
#!/usr/bin/env node
import { Command } from 'commander'
import RandBox from '../src/index'
const program = new Command()
program
.name('randbox')
.description('RandBox CLI - 命令行随机数据生成工具')
.version('1.0.0')
program
.command('generate')
.alias('g')
.description('生成随机数据')
.option('-t, --type <type>', '数据类型', 'string')
.option('-c, --count <count>', '生成数量', '1')
.option('-s, --seed <seed>', '随机种子')
.option('-o, --output <file>', '输出文件')
.action((options) => {
const randBox = new RandBox(options.seed)
const count = parseInt(options.count)
const results = []
for (let i = 0; i < count; i++) {
switch (options.type) {
case 'name':
results.push(randBox.name())
break
case 'email':
results.push(randBox.email())
break
case 'phone':
results.push(randBox.phone())
break
default:
results.push(randBox.string())
}
}
if (options.output) {
require('fs').writeFileSync(options.output, results.join('\n'))
console.log(`数据已保存到 ${options.output}`)
} else {
results.forEach(result => console.log(result))
}
})
program
.command('interactive')
.alias('i')
.description('交互式模式')
.action(() => {
console.log('启动交互式模式...')
// 实现交互式 REPL
})
program.parse()
持续集成与发布流程
1. GitHub Actions 工作流
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run test
- run: npm run build
release:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
# 发布到 npm
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# 创建 GitHub Release
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
docs:
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Build docs
run: |
cd docs
npm ci
npm run build
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
working-directory: docs
2. 语义化版本发布
// release.config.js
module.exports = {
branches: ['main'],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
[
'@semantic-release/changelog',
{
changelogFile: 'CHANGELOG.md',
},
],
'@semantic-release/npm',
[
'@semantic-release/git',
{
assets: ['CHANGELOG.md', 'package.json'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
},
],
'@semantic-release/github',
],
}
总结
经过五篇文章的详细分享,我们完整地探讨了从零开始打造 RandBox 的全过程:
- 项目起源与规划:明确目标,制定技术方案
- 技术架构与核心实现:构建稳固的技术基础
- 功能模块深度实现:实现丰富的数据生成功能
- 测试体系与质量保证:确保代码质量和可靠性
- 文档建设与生态发展:完善项目的外围生态
关键经验总结
- 用户体验优先:简单易用的 API 设计是成功的关键
- 模块化架构:良好的架构设计为后续扩展奠定基础
- 完善的测试:全面的测试体系保证项目的稳定性
- 优秀的文档:详细的文档和示例降低用户学习成本
- 社区驱动:开放的贡献机制促进项目健康发展
未来展望
RandBox 的发展不会止步于此,未来的规划包括:
- 性能优化:持续优化算法,提升生成效率
- 功能扩展:根据社区反馈增加新的数据类型
- 生态建设:开发更多插件和工具
- 国际化:支持更多语言和地区的数据
开源项目的成功需要社区的共同努力,我期待更多开发者能够参与到 RandBox 的建设中来,一起打造更好的开发工具。
关于 RandBox
🏠 官网: https://randbox.top 📦 GitHub: https://github.com/027xiguapi/randbox 📚 文档: https://randbox.top/zh/docs
如果这个项目对你有帮助,欢迎给我们一个 ⭐ Star!
系列文章回顾:
最后更新于: