Penny Lens 开发指南
2026年6月24日 · 10 分钟阅读 · ixNieStudio
详细的开发指南,包含环境搭建、开发规范、工具配置、最佳实践等内容,帮助开发者快速上手 Penny Lens 项目开发。
Penny Lens 开发指南
🎯 概述
本指南为 Penny Lens 项目的开发者提供完整的开发环境搭建、开发规范、工具配置和最佳实践指导,帮助开发者快速上手并高效开发。
🏗️ 项目架构
整体架构
Penny Lens 项目├── 小程序端 (UniApp + Vue 3)│ ├── 微信小程序│ ├── 支付宝小程序│ ├── H5 应用│ └── 移动 App├── PC端 (Nuxt 4 + Vue 3)│ ├── Web 应用│ └── 桌面应用└── 服务端 (支付宝小程序云) ├── Serverless 函数 ├── MongoDB 数据库 └── OSS 文件存储技术栈
| 组件 | 技术栈 | 版本 | 说明 |
|---|---|---|---|
| 小程序端 | UniApp + Vue 3 + TypeScript | Vue 3.3+ | 跨平台移动端开发 |
| PC端 | Nuxt 4 + Vue 3 + TypeScript | Nuxt 4.0+ | 现代化Web应用 |
| 服务端 | Node.js + TypeScript + MongoDB | Node 18+ | Serverless后端服务 |
| UI框架 | Ant Design Vue + UnoCSS | 4.x | 组件库和样式框架 |
| 状态管理 | Pinia | 2.x | 状态管理 |
| 构建工具 | Vite + TypeScript | 5.x | 构建和类型检查 |
🚀 环境搭建
1. 基础环境
Node.js 环境
# 安装 Node.js 18+ (推荐使用 nvm)curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bashnvm install 18nvm use 18
# 验证安装node --version # v18.x.xnpm --version # 9.x.x包管理器
# 推荐使用 pnpmnpm install -g pnpm
# 验证安装pnpm --versionGit 配置
# 配置用户信息git config --global user.name "Your Name"git config --global user.email "your.email@example.com"
# 配置 SSH 密钥ssh-keygen -t rsa -b 4096 -C "your.email@example.com"2. 小程序端开发环境
安装依赖
# 克隆项目git clone <repository-url>cd penny-lens-frontend
# 安装依赖pnpm install
# 安装 HBuilderX (UniApp 开发工具)# 下载地址: https://www.dcloud.io/hbuilderx.html开发工具配置
# 安装微信开发者工具# 下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html
# 安装支付宝开发者工具# 下载地址: https://opendocs.alipay.com/mini/ide/download环境配置
# 复制环境配置文件cp .env.example .env.local
# 编辑环境配置# .env.localVITE_API_BASE_URL=https://your-api-domain.comVITE_APP_ID=your-app-idVITE_APP_SECRET=your-app-secret3. PC端开发环境
安装依赖
# 进入PC端目录cd penny-lens-pc
# 安装依赖pnpm install开发服务器
# 启动开发服务器pnpm dev
# 构建生产版本pnpm build
# 预览生产版本pnpm preview4. 服务端开发环境
安装依赖
# 进入服务端目录cd penny-lens-serverless
# 安装依赖pnpm install支付宝小程序云配置
# 安装支付宝小程序云 CLInpm install -g @alipay/cloud-cli
# 登录支付宝开发者账号cloud login
# 初始化项目cloud init环境配置
# 复制配置文件cp config.example.json config.json
# 编辑配置文件# config.json{ "jwtSecret": "your-jwt-secret", "jwtExpiresIn": "7d", "mongodb": { "uri": "your-mongodb-uri" }, "oss": { "accessKeyId": "your-access-key", "accessKeySecret": "your-access-secret", "bucket": "your-bucket-name", "region": "your-region" }}📝 开发规范
1. 代码规范
TypeScript 配置
{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]}ESLint 配置
{ "extends": [ "@vue/typescript/recommended", "@vue/prettier" ], "rules": { "no-console": "warn", "no-debugger": "warn", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-explicit-any": "warn", "vue/multi-word-component-names": "off" }}Prettier 配置
{ "semi": false, "singleQuote": true, "tabWidth": 2, "trailingComma": "none", "printWidth": 100, "endOfLine": "lf"}2. 命名规范
文件命名
# 组件文件PascalCase.vue # 组件文件kebab-case.vue # 页面文件
# 工具文件camelCase.ts # 工具函数kebab-case.ts # 配置文件
# 目录命名kebab-case/ # 目录名变量命名
// 常量const API_BASE_URL = 'https://api.example.com'const MAX_RETRY_COUNT = 3
// 变量const userName = 'john'const isLoading = falseconst userList = []
// 函数function getUserInfo() {}function handleSubmit() {}function validateForm() {}
// 类型interface UserInfo {}type UserStatus = 'active' | 'inactive'组件命名
<!-- 组件名使用 PascalCase --><template> <UserCard /> <AssetSelector /> <RecordForm /></template>
<script setup lang="ts">// 组件名定义defineOptions({ name: 'UserCard'})</script>3. 代码结构
组件结构
<template> <!-- 模板内容 --></template>
<script setup lang="ts">// 1. 导入依赖import { ref, computed, onMounted } from 'vue'import { useRouter } from 'vue-router'
// 2. 类型定义interface Props { title: string data: any[]}
interface Emits { submit: [data: any] cancel: []}
// 3. Props 和 Emitsconst props = defineProps<Props>()const emit = defineEmits<Emits>()
// 4. 响应式数据const isLoading = ref(false)const formData = ref({})
// 5. 计算属性const filteredData = computed(() => { return props.data.filter(item => item.active)})
// 6. 方法function handleSubmit() { emit('submit', formData.value)}
// 7. 生命周期onMounted(() => { // 初始化逻辑})</script>
<style scoped>/* 样式内容 */</style>服务层结构
import { api } from '@/utils/api'import type { User, CreateUserRequest, UpdateUserRequest } from '@/types/user'
export class UserService { // 获取用户列表 async getUsers(params?: UserQueryParams): Promise<Page<User>> { return api.get('/users', { params }) }
// 获取用户详情 async getUser(id: string): Promise<User> { return api.get(`/users/${id}`) }
// 创建用户 async createUser(data: CreateUserRequest): Promise<User> { return api.post('/users', data) }
// 更新用户 async updateUser(id: string, data: UpdateUserRequest): Promise<User> { return api.put(`/users/${id}`, data) }
// 删除用户 async deleteUser(id: string): Promise<void> { return api.delete(`/users/${id}`) }}
export const userService = new UserService()4. Git 规范
分支命名
feature/user-authentication # 功能分支bugfix/login-error-handling # 修复分支hotfix/security-vulnerability # 热修复分支release/v1.0.0 # 发布分支提交信息规范
<type>(<scope>): <description>
[optional body]
[optional footer(s)]类型说明:
feat: 新功能fix: 修复bugdocs: 文档更新style: 代码格式调整refactor: 代码重构test: 测试相关chore: 构建过程或辅助工具的变动
示例:
feat(auth): add wechat login supportfix(api): handle token expiration errordocs(readme): update installation guiderefactor(components): extract common form logic🛠️ 工具配置
1. VS Code 配置
推荐扩展
{ "recommendations": [ "Vue.volar", "Vue.vscode-typescript-vue-plugin", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-vscode.vscode-typescript-next", "formulahendry.auto-rename-tag", "christian-kohler.path-intellisense" ]}工作区配置
{ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "typescript.preferences.importModuleSpecifier": "relative", "vue.inlayHints.missingProps": true, "vue.inlayHints.inlineHandlerLeading": true, "vue.inlayHints.optionsWrapper": true}2. 调试配置
小程序端调试
{ "version": "0.2.0", "configurations": [ { "name": "Debug UniApp", "type": "node", "request": "launch", "program": "${workspaceFolder}/node_modules/@dcloudio/vite-plugin-uni/bin/cli.js", "args": ["dev:mp-weixin"], "console": "integratedTerminal" } ]}PC端调试
{ "version": "0.2.0", "configurations": [ { "name": "Debug Nuxt", "type": "node", "request": "launch", "program": "${workspaceFolder}/node_modules/nuxt/bin/nuxt.js", "args": ["dev"], "console": "integratedTerminal" } ]}3. 测试配置
Jest 配置
module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', moduleFileExtensions: ['js', 'ts', 'vue'], transform: { '^.+\\.vue$': '@vue/vue3-jest', '^.+\\.ts$': 'ts-jest' }, testMatch: ['**/__tests__/**/*.test.ts'], collectCoverageFrom: [ 'src/**/*.{ts,vue}', '!src/**/*.d.ts' ]}测试示例
import { mount } from '@vue/test-utils'import UserCard from '@/components/UserCard.vue'
describe('UserCard', () => { it('renders user information correctly', () => { const user = { id: '1', name: 'John Doe', email: 'john@example.com' }
const wrapper = mount(UserCard, { props: { user } })
expect(wrapper.text()).toContain('John Doe') expect(wrapper.text()).toContain('john@example.com') })
it('emits edit event when edit button is clicked', async () => { const user = { id: '1', name: 'John Doe' } const wrapper = mount(UserCard, { props: { user } })
await wrapper.find('[data-testid="edit-button"]').trigger('click')
expect(wrapper.emitted('edit')).toBeTruthy() expect(wrapper.emitted('edit')[0]).toEqual([user]) })})🚀 部署指南
1. 小程序端部署
微信小程序
# 构建微信小程序pnpm build:mp-weixin
# 使用微信开发者工具上传代码# 1. 打开微信开发者工具# 2. 导入项目目录 dist/build/mp-weixin# 3. 点击上传按钮# 4. 填写版本号和项目备注# 5. 提交审核支付宝小程序
# 构建支付宝小程序pnpm build:mp-alipay
# 使用支付宝开发者工具上传代码# 1. 打开支付宝开发者工具# 2. 导入项目目录 dist/build/mp-alipay# 3. 点击上传按钮# 4. 填写版本号和项目备注# 5. 提交审核2. PC端部署
静态部署
# 构建生产版本pnpm build
# 部署到静态服务器# 将 dist 目录内容上传到服务器rsync -avz dist/ user@server:/var/www/html/Docker 部署
# DockerfileFROM node:18-alpine as builder
WORKDIR /appCOPY package*.json ./RUN npm ci --only=production
COPY . .RUN npm run build
FROM nginx:alpineCOPY --from=builder /app/dist /usr/share/nginx/htmlCOPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80CMD ["nginx", "-g", "daemon off;"]# 构建和运行docker build -t penny-lens-pc .docker run -p 80:80 penny-lens-pc3. 服务端部署
支付宝小程序云部署
# 部署到支付宝小程序云cloud deploy
# 查看部署状态cloud status
# 查看日志cloud logs环境变量配置
# 生产环境配置export NODE_ENV=productionexport JWT_SECRET=your-production-jwt-secretexport MONGODB_URI=your-production-mongodb-uriexport OSS_ACCESS_KEY_ID=your-oss-access-keyexport OSS_ACCESS_KEY_SECRET=your-oss-secret📚 最佳实践
1. 性能优化
前端优化
// 使用懒加载const LazyComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'))
// 使用虚拟滚动import { VirtualList } from '@tanstack/vue-virtual'
// 图片懒加载<img v-lazy="imageUrl" alt="description" />
// 防抖和节流import { debounce, throttle } from 'lodash-es'
const debouncedSearch = debounce(searchFunction, 300)const throttledScroll = throttle(scrollHandler, 100)后端优化
// 数据库索引优化db.collection('users').createIndex({ email: 1 })db.collection('transactions').createIndex({ userId: 1, date: -1 })
// 缓存策略import Redis from 'ioredis'const redis = new Redis(process.env.REDIS_URL)
async function getCachedData(key: string) { const cached = await redis.get(key) if (cached) return JSON.parse(cached)
const data = await fetchDataFromDB() await redis.setex(key, 3600, JSON.stringify(data)) return data}2. 错误处理
前端错误处理
// 全局错误处理app.config.errorHandler = (err, instance, info) => { console.error('Global error:', err) console.error('Error info:', info)
// 发送错误报告 errorReportingService.report(err, { component: instance?.$options.name, info })}
// API 错误处理class ApiError extends Error { constructor( public code: number, public message: string, public data?: any ) { super(message) this.name = 'ApiError' }}
async function apiRequest(url: string, options?: RequestInit) { try { const response = await fetch(url, options) const data = await response.json()
if (data.code !== 200) { throw new ApiError(data.code, data.message, data.data) }
return data.data } catch (error) { if (error instanceof ApiError) { throw error }
throw new ApiError(500, '网络请求失败', error) }}后端错误处理
// 统一错误处理export class AppError extends Error { constructor( public code: number, public message: string, public statusCode: number = 500 ) { super(message) this.name = 'AppError' }}
export function handleError(error: any): AppResponse { if (error instanceof AppError) { return { code: error.code, message: error.message, data: null } }
// 记录未知错误 console.error('Unknown error:', error)
return { code: 500, message: '服务器内部错误', data: null }}3. 安全考虑
前端安全
// XSS 防护import DOMPurify from 'dompurify'
function sanitizeHtml(html: string): string { return DOMPurify.sanitize(html)}
// CSRF 防护const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
// 敏感信息处理function maskSensitiveData(data: string): string { if (data.length <= 4) return '*'.repeat(data.length) return data.slice(0, 2) + '*'.repeat(data.length - 4) + data.slice(-2)}后端安全
// 输入验证import Joi from 'joi'
const userSchema = Joi.object({ username: Joi.string().alphanum().min(3).max(30).required(), email: Joi.string().email().required(), password: Joi.string().min(8).pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])')).required()})
// 速率限制import rateLimit from 'express-rate-limit'
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 限制每个IP 100次请求 message: '请求过于频繁,请稍后再试'})
// SQL 注入防护// 使用参数化查询const user = await db.collection('users').findOne({ email: email, // 直接使用变量,不使用字符串拼接 password: hashedPassword})4. 测试策略
单元测试
// 工具函数测试describe('utils/format', () => { describe('formatCurrency', () => { it('should format currency correctly', () => { expect(formatCurrency(1234.56)).toBe('¥1,234.56') expect(formatCurrency(0)).toBe('¥0.00') expect(formatCurrency(-100)).toBe('-¥100.00') }) })})集成测试
// API 集成测试describe('API Integration', () => { it('should create user successfully', async () => { const userData = { username: 'testuser', email: 'test@example.com', password: 'password123' }
const response = await request(app) .post('/api/users') .send(userData) .expect(201)
expect(response.body.data.username).toBe(userData.username) })})E2E 测试
// 端到端测试describe('User Registration Flow', () => { it('should complete user registration', async () => { await page.goto('/register')
await page.fill('[data-testid="username"]', 'testuser') await page.fill('[data-testid="email"]', 'test@example.com') await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="submit-button"]')
await expect(page).toHaveURL('/dashboard') await expect(page.locator('[data-testid="welcome-message"]')).toContainText('Welcome, testuser!') })})🔧 调试技巧
1. 前端调试
Vue DevTools
# 安装 Vue DevTools 浏览器扩展# Chrome: https://chrome.google.com/webstore/detail/vuejs-devtools# Firefox: https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/调试技巧
// 使用 debugger 语句function complexFunction() { debugger // 浏览器会在此处暂停 // 可以检查变量值、调用栈等}
// 使用 console 调试console.log('Debug info:', { user, data })console.table(dataArray) // 以表格形式显示数组console.group('API Response') // 分组显示日志console.log(response)console.groupEnd()2. 后端调试
日志记录
import winston from 'winston'
const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), new winston.transports.Console({ format: winston.format.simple() }) ]})
// 使用日志logger.info('User login successful', { userId, timestamp: Date.now() })logger.error('Database connection failed', { error: error.message })性能监控
// 性能监控中间件function performanceMiddleware(req: Request, res: Response, next: NextFunction) { const start = Date.now()
res.on('finish', () => { const duration = Date.now() - start logger.info('Request completed', { method: req.method, url: req.url, duration: `${duration}ms`, status: res.statusCode }) })
next()}📖 学习资源
官方文档
推荐资源
Penny Lens 开发指南 - 让开发更高效、更规范 🚀