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 | bash
nvm install 18
nvm use 18
# 验证安装
node --version # v18.x.x
npm --version # 9.x.x包管理器
# 推荐使用 pnpm
npm 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.local
VITE_API_BASE_URL=https://your-api-domain.com
VITE_APP_ID=your-app-id
VITE_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支付宝小程序云配置
# 安装支付宝小程序云 CLI
npm 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 配置
// tsconfig.json
{
"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 配置
// .eslintrc.json
{
"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 配置
// .prettierrc
{
"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 = false
const 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 和 Emits
const 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>服务层结构
// services/user.ts
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 support
fix(api): handle token expiration error
docs(readme): update installation guide
refactor(components): extract common form logic🛠️ 工具配置
1. VS Code 配置
推荐扩展
// .vscode/extensions.json
{
"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"
]
}工作区配置
// .vscode/settings.json
{
"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. 调试配置
小程序端调试
// .vscode/launch.json
{
"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端调试
// .vscode/launch.json
{
"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 配置
// jest.config.js
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'
]
}测试示例
// __tests__/components/UserCard.test.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 部署
# Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["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=production
export JWT_SECRET=your-production-jwt-secret
export MONGODB_URI=your-production-mongodb-uri
export OSS_ACCESS_KEY_ID=your-oss-access-key
export 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 开发指南 - 让开发更高效、更规范 🚀
