BrewDaily 全栈开发实录 — Vanilla JS + SSM + PWA

BrewDaily 全栈开发实录

Vanilla JS + SSM (Spring + Spring MVC + MyBatis) 全栈 Web 应用开发记录

基于实际项目 /Users/ann/Desktop/期末项目/brewdaily · 2026年6月

目录

一、项目概述

二、技术栈选型

三、后端架构设计(SSM)

四、前端模块化设计

五、API 设计与交互

六、PWA 与离线支持

七、彩蛋系统设计

八、构建与部署

九、踩坑记录与经验总结

一、项目概述

BrewDaily 是一款专为手冲咖啡爱好者设计的 Web 应用,涵盖冲煮计时、水粉比计算、冲煮记录管理、方案管理等功能。项目采用前后端分离架构,前端为 Vanilla JS SPA(无框架),后端为 SSM(Spring + Spring MVC + MyBatis)。

以下从实际代码出发,记录从零搭建该项目的技术决策、实现细节和踩坑经验。

项目基本信息:

// pom.xml(实际)
<groupId>com.brewdaily</groupId>
<artifactId>brewdaily-backend</artifactId>
<version>1.1.0</version>
<packaging>war</packaging>

// 技术栈版本
Spring Framework 6.1.6
MyBatis 3.5.16
Druid 1.2.23
MySQL 8.0+
Jackson 2.17.1
Java 17 (Dragonwell)

二、技术栈选型

项目技术选型基于两个原则:一是课程要求(SSM 框架),二是实际体验优先(前端无框架)。

后端核心:

  • Spring Framework 6.1.6 — IoC 容器、声明式事务管理
  • Spring MVC — RESTful API,@RestController + @RequestBody/@PathVariable
  • MyBatis 3.5.16 — 数据持久化,XML Mapper 映射
  • Druid 1.2.23 — 数据库连接池,监控功能
  • PageHelper 6.1.0 — 物理分页,避免内存分页
  • BCrypt — 密码加密存储
  • Jackson 2.17.1 — JSON 序列化/反序列化

前端:

  • Vanilla JavaScript (ES6+) — 无框架依赖,零构建步骤
  • HTML5 + CSS3 — 响应式设计,移动端优先
  • PWA — Service Worker + manifest.json,可安装到主屏幕
  • localStorage — 离线数据回退

部署:

  • Tomcat 10 — Servlet 6.0 容器,部署 WAR 包
  • Nginx — 反向代理 /api/ 到后端
  • MySQL 8.0 — 数据持久化存储

选择 Vanilla JS 而非 Vue/React 的理由:手冲计时器对实时性要求较高(毫秒级更新),无框架方案避免虚拟 DOM 开销。同时,作为一个工具型应用,页面交互以计时器计时和表单操作为主,状态管理简单,不需要框架级的响应式系统。

三、后端架构设计(SSM)

SSM 后端采用经典的分层架构:Controller → Service → DAO → MyBatis Mapper。

项目结构:

com.brewdaily/
├── aspect/LogAspect.java          # AOP 日志切面
├── config/                        # Spring 配置
│   ├── AppConfig.java             # 根容器配置
│   ├── DruidConfig.java           # Druid 连接池
│   ├── MyBatisConfig.java         # MyBatis + PageHelper
│   ├── WebAppInitializer.java     # Servlet 3.0+ 初始化
│   └── WebMvcConfig.java          # MVC 配置 + 拦截器
├── controller/                    # REST API 控制器
│   ├── AuthController.java        # 登录/注册/登出
│   ├── BrewController.java        # 冲煮记录 CRUD + 统计
│   ├── RecipeController.java      # 冲煮方案管理
│   └── SystemController.java      # 系统状态/DDL
├── dao/                           # MyBatis Mapper
│   ├── BrewMapper.java
│   ├── RecipeMapper.java
│   └── UserMapper.java
├── dto/                           # 数据传输对象
│   ├── LoginDTO.java
│   ├── PageResult.java
│   ├── RegisterDTO.java
│   └── Result.java                # 统一响应包装
├── entity/                        # 实体类
│   ├── BrewRecord.java
│   ├── Recipe.java
│   ├── RecipeStage.java
│   └── User.java
├── exception/                     # 异常体系
│   ├── BusinessException.java
│   ├── GlobalExceptionHandler.java
│   └── UnauthorizedException.java
├── interceptor/AuthInterceptor.java  # 登录拦截
├── service/impl/                  # 业务实现
│   ├── BrewServiceImpl.java
│   ├── RecipeServiceImpl.java
│   └── UserServiceImpl.java
└── util/PasswordUtil.java         # BCrypt 密码工具

几个关键设计:

1. 统一响应封装

所有 API 返回统一使用 Result<T> 包装,前端基于 code 字段判断成功/失败:

{
  "code": 200,        // 200=成功 401=未登录 403=无权限 404=未找到
  "msg": "success",
  "result": { ... }   // 实际数据
}

2. 登录拦截器

AuthInterceptor 拦截 /api/brews/ 和 /api/recipes/ 下的请求,检查 Session 中的登录状态。没登录就返回 401,前端收到后弹出登录框。

3. 分页查询

冲煮记录列表使用 PageHelper 实现物理分页,避免一次性加载大量数据。前端传入 page 和 pageSize 参数,后端返回总记录数和当前页数据。

4. AOP 日志

LogAspect 使用 @Around 注解,自动记录所有 controller 方法的调用参数、执行耗时和返回结果。开发时便于调试,生产时可用于排障。

四、前端模块化设计

前端采用模块化设计,共 9 个独立模块,通过 IIFE(立即执行函数)暴露到 window 对象:

js/
├── api.js              # API 请求封装 + Token 管理 + 离线回退
├── app.js              # 主应用:路由、登录对话框、版本管理
├── calculator.js       # 水粉比计算器(g/oz 切换)
├── data.js             # 数据层:预设方案、存储 Key 常量
├── delight.js          # 彩蛋引擎:微交互、庆祝动画、成就系统
├── icons.js            # SVG 图标系统
├── profile.js          # 个人中心:统计、冲煮记录、头像
├── recipe-editor.js    # 方案编辑器
├── recipe-import.js    # 批量导入(CSV 解析 + 预览 + 确认)
└── timer.js            # 冲煮计时器核心

模块间通信策略:

  • 共享数据通过 window 对象(如 window.ApiModule、window.ProfileModule)
  • 状态存储在 localStorage(离线可用),后端 API 返回时同步
  • 模块间直接调用对方暴露的 API 方法(如 timer 完成时调用 delight.js 的庆祝动画)

单例控制器模式:

为避免多个计时器实例或事件绑定冲突,每个模块使用单例 + init() 模式——只在首次调用时初始化 DOM 绑定,后续只更新数据:

// timer.js 的实际 init 模式
var Timer = (function() {
  var initialized = false;
  function init() {
    if (initialized) return;
    initialized = true;
    // 绑定事件、创建 DOM 等一次性操作
  }
  function start() { /* ... */ }
  return { init: init, start: start };
})();

离线回退策略:

api.js 实现了双层数据策略:优先请求后端 API,请求失败时自动回退到 localStorage。这意味着即使后端服务暂时不可用,计时器和计算器等核心功能依然可用。

五、API 设计与交互

后端提供 RESTful API,共 4 个模块:

# 认证
POST   /api/auth/login     # 登录
POST   /api/auth/register  # 注册
POST   /api/auth/logout    # 登出
GET    /api/auth/me        # 获取当前用户

# 冲煮记录
GET    /api/brews          # 分页查询(支持 sort 排序)
POST   /api/brews          # 创建记录
GET    /api/brews/stats    # 统计(总杯数、连续天数等)
DELETE /api/brews/{id}     # 删除记录

# 冲煮方案
GET    /api/recipes        # 查询我的方案
POST   /api/recipes        # 创建方案
PUT    /api/recipes/{id}   # 更新方案
DELETE /api/recipes/{id}   # 删除方案

# 系统
GET    /api/ping           # 健康检查

前端使用 fetch API 发送请求,Cookie 自动携带 Session ID。生产环境通过 Nginx 反向代理 /api/ → Java 后端 8080 端口。

六、PWA 与离线支持

BrewDaily 是一个完整的 PWA,支持离线使用和安装到主屏幕。

Service Worker:

sw.js 缓存了应用的核心资源(HTML、CSS、JS、图标),缓存名使用版本号管理,版本更新时自动清理旧缓存。

缓存策略:

  • 核心资源(index.html、app.js 等):Install 时预缓存
  • 图标:Install 时预缓存
  • API 响应:Network First(优先网络,失败时回退缓存)

缓存版本管理:

每次发版更新 sw.js 中的 CACHE_VERSION,activate 事件中删除所有旧版本缓存。

七、彩蛋系统设计

彩蛋系统是项目中比较有趣的一部分,记录了冲煮过程中的各种成就里程碑。

彩蛋触发条件:

  • 累计冲煮达到特定杯数(1杯、5杯、10杯、25杯、50杯、100杯)
  • 连续打卡达到一定天数

每个彩蛋包含:

  • 一段趣味描述(与咖啡相关的冷知识或调侃)
  • 解锁时的庆祝动画
  • 冲煮完成时的微交互(咖啡滴落动画、冒热气效果)

彩蛋进度存储于 localStorage,用户清除缓存后会重置。

八、构建与部署

构建流程:

后端使用 Maven 构建 WAR 包:

mvn clean package  # → target/brewdaily-backend-1.1.0.war

部署方式:

生产环境运行方式(直接 Tomcat 启动):

WAR 包部署到 /usr/local/tomcat10/webapps/
Nginx 反向代理 /api/ → http://127.0.0.1:8080/brewdaily/api/

Nginx 配置(取自 /usr/local/nginx/conf/vhost/coffee.conf):

location /api/ {
    proxy_pass http://127.0.0.1:8080/brewdaily/api/;
    proxy_cookie_path /brewdaily /;
    proxy_http_version 1.1;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / { try_files $uri $uri/ /index.html; }

前端静态文件直接由 Nginx 托管,Service Worker 和 index.html 不走缓存,JS/CSS 开发环境不缓存,生产环境有版本控制。

九、踩坑记录与经验总结

9.1 brewed_at 字段默认值被覆盖

问题:冲煮记录的日期始终为 NULL,统计功能全部失效。原因是 MyBatis 的 INSERT 语句显式写入了 brewed_at = NULL,覆盖了数据库列的 DEFAULT CURRENT_TIMESTAMP。

解决:在 BrewServiceImpl.create() 中加入自动设置逻辑——如果记录中 brewed_at 为 null,则设置为 LocalDateTime.now()。

9.2 ratio 字段类型不匹配

问题:MySQL 中 brew_record.ratio 字段类型为 DECIMAL(3,1),无法存储 “1:16” 格式的字符串。

解决:执行 ALTER TABLE brew_record MODIFY COLUMN ratio VARCHAR(20),将数字类型改为字符串类型。

9.3 生产环境 Cookie 跨域问题

问题:开发时前端与后端同源(localhost:8080),Session Cookie 自动携带。部署后前端(coffee.ann.hi.cn)与后端(同域不同端口)跨域,Cookie 丢失。

解决:通过 Nginx 同源反向代理解决,前端访问 /api/ 时 nginx 转发到 8080 端口,浏览器认为是同源请求,Cookie 可以正常携带。

9.4 PJAX/SPA 路由与后退按钮

问题:SPA 页面切换使用 JavaScript 控制显示/隐藏,浏览器后退按钮不支持页面级导航。

解决:页面切换时更新 URL hash(#timer、#calculator、#profile),监听 hashchange 事件恢复对应页面状态。

9.5 离线回退导致的数据不一致

问题:启用了 localStorage 离线回退,用户离线时写入的数据无法同步到服务器。

解决:仅对读操作启用离线缓存,写操作(创建冲煮记录、保存方案等)必须在线完成。离线缓存的数据仅作为只读回退,不提供离线写入。

9.6 设计上的取舍

作为期末作业,BrewDaily 在设计和实现上做了不少取舍:

  • 无前端框架:减少了构建步骤和依赖管理,但也意味着没有组件化和虚拟 DOM
  • Session 而非 JWT:课程要求使用 Session 认证,Spring Session 管理用户状态更符合教学目的
  • WAR 部署而非 Docker:虽然项目中有 Dockerfile 和 docker-compose.yml,但生产环境最终使用传统 WAR 包部署

每个取舍背后都有具体的理由,这大概就是全栈开发最有趣的地方——技术没有绝对的好坏,只有合不合适的场景。


← 返回首页