Penny Lens 开发指南

2025年1月28日
10 分钟阅读
作者:Penny Lens Team

Penny Lens 开发指南

🎯 概述

本指南为 Penny Lens 项目的开发者提供完整的开发环境搭建、开发规范、工具配置和最佳实践指导,帮助开发者快速上手并高效开发。

🏗️ 项目架构

整体架构

Penny Lens 项目
├── 小程序端 (UniApp + Vue 3)
│ ├── 微信小程序
│ ├── 支付宝小程序
│ ├── H5 应用
│ └── 移动 App
├── PC端 (Nuxt 4 + Vue 3)
│ ├── Web 应用
│ └── 桌面应用
└── 服务端 (支付宝小程序云)
├── Serverless 函数
├── MongoDB 数据库
└── OSS 文件存储

技术栈

组件技术栈版本说明
小程序端UniApp + Vue 3 + TypeScriptVue 3.3+跨平台移动端开发
PC端Nuxt 4 + Vue 3 + TypeScriptNuxt 4.0+现代化Web应用
服务端Node.js + TypeScript + MongoDBNode 18+Serverless后端服务
UI框架Ant Design Vue + UnoCSS4.x组件库和样式框架
状态管理Pinia2.x状态管理
构建工具Vite + TypeScript5.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 --version

Git 配置

# 配置用户信息
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-secret

3. PC端开发环境

安装依赖

# 进入PC端目录
cd penny-lens-pc
 
# 安装依赖
pnpm install

开发服务器

# 启动开发服务器
pnpm dev
 
# 构建生产版本
pnpm build
 
# 预览生产版本
pnpm preview

4. 服务端开发环境

安装依赖

# 进入服务端目录
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: 修复bug
  • docs: 文档更新
  • 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-pc

3. 服务端部署

支付宝小程序云部署

# 部署到支付宝小程序云
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 开发指南 - 让开发更高效、更规范 🚀