📚前言
跟着trae规划的学习开发安卓手机app的教程,一步一步学习实践,总规划见下面文档
【Trae+AI】和Trae学习搭建App_00:总项目规划_trae 封装apk-CSDN博客
下面开始规划的第三章,开始之前还有点事要说。
🌍战略调整
Trae的AI太强大了,给它个主题,它就哐哐建完了,对想一步一步学习的我们来说,速度有点太快了,其中的原理机制,都搞不明白,所以放下生化武器,回归原始手搓,文本编辑器+CMD+AI教程指导【豆包】
✍️AI提示语:
我要学习安卓手机app的开发,我已经完成第一章规划,第二章:开发环境配置(包括Android Studio、node.js、MongoDB、Git),请为我编写第三章的详细教程,内容包括 第3章:后端基础框架 Express框架搭建 数据库模型设计(用户、任务、分类) 中间件配置(日志、跨域、认证) 可以分多次输出,一定要有详细的操作过程,和对应原理,指导初学的我逐步掌握。
第二部分:后端API开发
第三章:后端基础框架Express
本章将围绕安卓APP后端开发的核心需求,搭建基于Express的后端框架,完成MongoDB数据库模型设计,并配置核心中间件(日志、跨域、认证)。所有操作均面向初学者,每一步都会讲解原理 +实操,确保你理解"做什么"和"为什么这么做"。
3.1 Express框架核心原理(先懂原理,再动手)
3.1.1 什么是Express?
Express是基于Node.js的轻量级Web开发框架,它不是替代Node.js,而是对Node.js的http模块进行了封装和扩展,解决了原生Node.js开发Web服务时"代码冗余、路由管理复杂、中间件扩展麻烦"等问题。
3.1.2 Express核心特点(为什么选它做安卓APP后端?)
- 极简核心:仅提供基础的Web服务能力(路由、中间件),无冗余功能,学习成本低;
- 中间件架构:所有核心功能(日志、认证、跨域)都通过中间件实现,扩展性极强;
- 生态完善:与MongoDB(非关系型数据库)、Mongoose(数据库ODM)完美适配,适合移动端后端开发;
- 易上手:语法贴近原生Node.js,初学者能快速落地。
3.1.3 Express核心概念(提前铺垫)
- 应用实例 :通过
express()创建的核心对象,负责管理路由、中间件、监听端口; - 路由:匹配客户端请求(如GET/POST),并返回对应数据(安卓APP的接口本质就是路由);
- 中间件:请求处理的"管道",每个中间件可修改请求/响应,或传递给下一个中间件(如日志中间件先记录请求,再交给业务逻辑)。
3.2 Express框架搭建(手把手实操)
前置条件
你已完成Node.js安装(第二章配置),先验证环境:
-
打开终端(Windows:CMD/PowerShell,Mac/Linux:终端);
-
输入以下命令,能显示版本号即正常:
node -v # 如v18.17.0 npm -v # 如9.6.7
步骤1:创建项目目录
后端项目需要独立的目录结构,避免文件混乱,操作如下:
-
新建一个文件夹(建议命名:
android-app-backend),作为后端根目录; -
终端进入该目录(以Windows为例,假设文件夹在E:\99mydata\06vsplace\android-app-backend):
cd /d E:\99mydata\06vsplace\android-app-backend
步骤2:初始化NPM项目
NPM(Node包管理器)是管理项目依赖(如Express)的工具,初始化后会生成package.json(项目配置文件):
-
终端输入以下命令(
-y表示跳过手动配置,直接生成默认package.json):npm init -y
实际执行界面参考:
📌命令解析:
npm init是 初始化 Node.js 项目 ,生成package.json文件(Node.js 项目的 "身份证");package.json是项目的核心,作用如下:
- 记录项目的基本信息(名称、版本、作者);
- 管理项目依赖(安装的 Express、Mongoose 等都会列在
dependencies里);- 定义脚本命令(比如我们后来加的
"dev": "nodemon app.js");- 标识项目入口(
main字段,Node.js 运行时会优先找这个文件)。-y是--yes的简写,代表 "所有配置项都使用默认值,无需手动交互确认"。默认规则如下:
- 项目名称(
name):默认是当前文件夹名称,但会自动转为小写 ,且去掉空格 / 特殊字符(比如文件夹名是Android App Backend,会变成android-app-backend);- 入口文件(
main):默认是index.js(我们后续改成了app.js,不影响,只是约定);- 许可证(
license):默认ISC(可后续手动修改为 MIT 等);- 所有空值项(描述、作者、关键词):后续可手动编辑
package.json补充。
步骤3:安装Express
-
终端输入安装命令(
--save表示将Express写入dependencies(生产依赖),项目上线也需要):npm install express --save -
安装完成后,目录会新增:
node_modules:存放所有依赖包(无需手动修改);package-lock.json:锁定依赖包版本,保证不同环境安装的依赖一致。
实际执行界面参考:
📌命令解析:
npm install:NPM(Node 包管理器)的核心安装命令,用于下载第三方包到当前项目;
express:要安装的包名称(即我们需要的 Express 框架);
--save:标识将该包写入package.json的dependencies字段(生产依赖),确保项目在生产环境(上线)时也能安装该依赖。后面会用到了另一个参数:
--save-dev
*
标识 对应字段 适用场景 --save(-S)dependencies项目运行时必需的依赖 --save-dev(-D)devDependencies仅开发阶段需要的工具
步骤4:创建基础Express服务
核心是编写app.js(入口文件),实现最基础的Web服务:
-
在
android-app-backend目录下新建app.js文件; -
复制以下代码到
app.js,每一行都有注释解释原理 :// 1. 引入Express框架(安装后才能require) const express = require('express'); // 2. 创建Express应用实例(核心对象,管理路由/中间件) const app = express(); // 3. 定义端口号(后端服务监听的端口,安卓APP后续会访问这个端口) const PORT = 3000; // 4. 配置内置中间件:解析JSON格式的请求体(安卓APP传参通常用JSON) // 原理:客户端(安卓)POST请求的JSON数据,需通过此中间件解析后才能在路由中获取 app.use(express.json()); // 5. 定义测试路由(安卓APP的接口本质就是路由) // 格式:app.请求方法(路由路径, 处理函数) // GET请求:客户端获取数据(如安卓APP获取用户列表) app.get('/api/hello', (req, res) => { // req:请求对象(包含客户端传递的参数、请求头等) // res:响应对象(用于向客户端返回数据) res.status(200).json({ code: 200, message: 'Express服务启动成功!', data: null }); }); // 6. 启动服务,监听指定端口 app.listen(PORT, () => { console.log(`Express服务已启动,访问地址:http://localhost:${PORT}`); });
📌代码解释
require('express')模块加载,执行具体内容如下
- 先检查项目的
node_modules文件夹是否有express目录- 进入
express目录,读取package.json文件,找到"main": "index.js"(Express 的入口文件)- 加载
express/index.js,执行该文件中的代码,最终返回 Express 模块导出的核心函数- 将这个函数赋值给
const express,完成模块加载
express()创建一个Express 应用实例(App Instance) ,本质是一个HTTP 请求处理函数 + 功能管理容器:
作为请求处理函数 :Node.js 的
http模块创建服务时,需要一个 "请求处理函数",而app对象本身就是这个函数。比如底层等价于:
const http = require('http'); const express = require('express'); const app = express(); // http.createServer的参数就是请求处理函数,app对象可直接传入 http.createServer(app).listen(3000);我们常用的
app.listen(3000),其实是 Express 对http.createServer(app).listen()的简化封装。作为功能管理容器 :
app对象挂载了大量方法(use/get/post/listen等),用于管理后端服务的所有逻辑,比如:
app.use():注册中间件(日志、跨域、解析 JSON);app.get('/api/hello'):定义 GET 路由;app.listen(3000):启动服务并监听端口。
app.use(express.json());是给 Express 应用实例注册 "JSON 请求体解析中间件" ,作用是自动解析客户端(如安卓 APP)发送的JSON格式请求体数据,让你能在路由处理函数中通过req.body直接获取解析后的 JSON 数据,无需手动处理原始请求流。
app.use()是 Express 注册中间件 的核心方法
- 作用:将指定的中间件 "挂载" 到应用实例上,所有请求(无论 GET/POST/PUT,无论哪个路由)都会先经过这个中间件处理;
express.json()是 Express 内置的中间件生成函数 ,调用后会返回一个 "JSON 解析中间件函数",核心功能:
- 检测请求头中的Content-Type是否为application/json(客户端需设置该请求头);
- 如果是,就把请求体的二进制数据解析成 JSON 对象;
- 把解析后的 JSON 对象挂载到req.body属性上;
- 如果不是(比如请求体是表单格式),则不处理,req.body仍为undefined。
app.get('/api/hello', (req, res) => { ... })是给 Express 应用注册一个「GET 请求路由」 ,作用是:当客户端(如安卓 APP)向后端发送GET方式的http://localhost:3000/api/hello请求时,Express 会触发后面的回调函数,处理该请求并返回响应。
- 语法:
app.get(路由路径, 回调函数)
- 请求方法是
GET(客户端通过 GET 方式发起请求,而非 POST/PUT 等);- 请求路径匹配
/api/hello(比如http://localhost:3000/api/hello,路径部分完全匹配)。
(req, res):回调函数的参数
回调函数的两个核心参数是 Express 自动传入的
参数 全称 核心作用 reqRequest 代表客户端的请求信息: ✅ 获取请求参数( req.query/req.params) ✅ 获取请求头(req.headers) ✅ 获取客户端 IP(req.ip)等resResponse 代表服务端的响应能力: ✅ 返回文本 / JSON / 文件( res.send()/res.json()) ✅ 设置响应状态码(res.status(200)) ✅ 设置响应头(res.setHeader())等app.listen(PORT, () => { ... }):启动 Express HTTP 服务,让其绑定到指定端口(PORT)并监听客户端请求,服务启动成功后执行回调函数(如打印提示)。
步骤5:运行并测试Express服务
-
终端输入启动命令(执行
app.js):node app.js -
终端显示以下内容,说明服务启动成功:
Express服务启动成功,访问地址:http://localhost:3000 -
测试接口:
-
打开浏览器,访问
http://localhost:3000/api/hello; -
页面显示以下JSON数据,说明路由正常:
{"code":200,"message":"Express服务启动成功!","data":null}
-
实际运行界面参考:
💡乱码问题提示:
如果运行命令,出现乱码,是cmd字符集和程序文件字符集不一致的原因,可以修改程序文件的字符集:
- 用记事本打开
app.js→ 点击「文件」→「另存为」;- 在「编码」选项中选择「UTF-8」,覆盖保存文件;
- 重新运行
node app.js,配合方法 1 的编码切换,乱码会消失。📌查看修改CMD的字符集
- chcp 命令,返回 "活动代码页" 对应的数字,这个数字代表当前使用的字符集
936→ GBK(中文 Windows 系统默认编码);65001→ UTF-8(通用编码,支持中文 / 英文 / 特殊符号);437→ 美国英语(英文系统默认)。- chcp 65001 命令,设置cmd的字符集
步骤6:优化开发体验(安装nodemon)
问题:每次修改app.js后,都需要手动执行node app.js重启服务,效率低。
解决:安装nodemon(开发依赖),它能监听文件变化,自动重启服务。
-
安装nodemon(
--save-dev表示仅开发环境使用,上线不需要):npm install nodemon --save-dev- 执行界面:

- 执行界面:
-
修改
package.json的scripts字段(添加启动脚本):
原scripts:"scripts": { "test": "echo \"Error: no test specified\" && exit 1" }修改后:
"scripts": { "start": "node app.js", "dev": "nodemon app.js" }- ⚠️提示:
package.json是严格的 JSON 文件 ,不支持 JavaScript 的注释(//或/* */)、多余逗号等语法;scripts字段的作用是定义快捷命令,"dev": "nodemon app.js"表示用npm run dev执行nodemon app.js。
- ⚠️提示:
-
重启服务(用dev脚本):
npm run dev- 执行界面:

- 执行界面:
-
验证:修改
app.js中/api/hello的message内容,保存后终端会自动重启服务,刷新浏览器即可看到修改后的结果。- 修改app.js保存后,程序自动重启:

- 修改app.js保存后,程序自动重启:
3.3 数据库模型设计(MongoDB + Mongoose)
3.3.1 核心原理铺垫
- 为什么用MongoDB?
安卓APP的后端数据(用户、任务、分类)多为"非结构化/半结构化"(如用户的头像、任务的备注),MongoDB(非关系型数据库)比MySQL(关系型)更灵活,且与Node.js生态适配更好。 - 为什么用Mongoose?
MongoDB原生操作语法不够直观,Mongoose是MongoDB的ODM(对象文档映射)工具,能:- 定义数据模型(约束字段类型、必填项);
- 简化CRUD(增删改查)操作;
- 提供数据验证、中间件等功能。
3.3.2 安装Mongoose并连接MongoDB
步骤1:安装Mongoose
终端输入命令(写入生产依赖):
npm install mongoose --save
执行界面参考:
📌命令执行位置
在项目目录
android-app-backend下执行 :mongoose会被安装到项目根目录的node_modules文件夹 中,同时写入当前项目的package.json的dependencies字段。→ 这是正确的操作,依赖属于当前项目,仅在该项目中可用。在 CMD 默认目录(如
C:\Users\你的用户名)下执行 :mongoose会被安装到当前默认目录的node_modules文件夹 中(如果默认目录没有package.json,则会全局安装,具体看 npm 配置),且不会关联到android-app-backend项目。→ 这是错误的操作 ,依赖不属于目标项目,后续在android-app-backend中引入mongoose会报错(找不到模块)。如果已经在默认目录误装了,只需在项目目录重新执行一次命令即可(不影响),如果删除默认目录的包用npm uninstall mongoose。
步骤2:连接MongoDB
-
确保你已安装并启动MongoDB(第二章配置):
-
Windows:MongoDB默认服务已启动;通过任务管理器确认:

-
Mac/Linux:终端输入
mongod启动(或配置成服务)。 -
技术参考文档:
-
-
在
app.js中添加MongoDB连接代码(放在const app = express();之后):// 引入Mongoose const mongoose = require('mongoose'); // 连接MongoDB(本地数据库,数据库名:android_app_db) // 原理:mongoose.connect()建立与MongoDB的连接,返回Promise mongoose.connect('mongodb://localhost:27017/android_app_db') .then(() => console.log('MongoDB连接成功!')) .catch(err => console.error('MongoDB连接失败:', err)); -
重启服务(
npm run dev),终端显示MongoDB连接成功!即正常。
执行界面参考:

📌代码解释:
mongoose.connect():发起数据库连接
这个字符串是 MongoDB 的连接地址(URI),拆解如下:
部分 含义 mongodb://协议头,标识这是 MongoDB 的连接协议(类似 HTTP 协议的 http://)localhost数据库服务器地址, localhost代表本地电脑(也可写本机 IP:127.0.0.1)27017MongoDB 的默认端口号(安装后默认监听该端口,除非手动修改) android_app_db要连接 / 创建的数据库名称:✅ 如果该数据库不存在,MongoDB 会自动创建;✅ 如果已存在,直接连接该数据库。
mongoose.connect()返回一个Promise 对象(异步操作特征)
.then(() => console.log('MongoDB连接成功!')):连接成功的回调
- Promise 的
.then()用于处理 "异步操作成功" 的场景- 当 Mongoose 与 MongoDB 服务器建立连接、且数据库
android_app_db可访问时,执行该回调函数
.catch(err => console.error('MongoDB连接失败:', err)):连接失败的回调
- Promise 的
.catch()用于处理 "异步操作失败" 的场景📜补充:JavaScript 箭头函数的语法规则
javascript// 有参数的情况 (参数1, 参数2) => { 函数体 } // 只有1个参数时,括号可省略 参数 => { 函数体 } // 无参数时,必须用()占位 () => { 函数体 } // 函数体只有一行代码时,{}也可省略 () => 单行代码
3.3.3 设计数据模型(用户、任务、分类)
核心思路
先梳理安卓APP的核心数据关系:
- 一个用户可以有多个任务;
- 一个任务属于一个分类;
- 分类属于某个用户(避免不同用户的分类混淆)。
步骤1:创建模型目录
为了代码结构清晰,新建models文件夹(在android-app-backend根目录),用于存放所有数据模型文件。
步骤2:用户模型(User.js)
在models下新建User.js,定义用户的核心字段(用户名、密码、邮箱、创建时间等):
const mongoose = require('mongoose');
// 1. 定义Schema(模式):约束数据结构,相当于"表结构"
// 原理:Schema规定了每个字段的类型、是否必填、默认值等,保证数据一致性
const UserSchema = new mongoose.Schema({
username: {
type: String, // 字段类型:字符串
required: true, // 必填项
unique: true, // 唯一值(不能重复)
trim: true // 自动去除首尾空格
},
password: {
type: String,
required: true,
minlength: 6 // 最小长度:6位
},
email: {
type: String,
required: true,
unique: true,
match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, '请输入合法的邮箱'] // 邮箱格式验证
},
avatar: {
type: String,
default: '' // 默认值:空字符串(无头像)
}
}, {
timestamps: true // 自动添加createdAt(创建时间)、updatedAt(更新时间)字段
});
// 2. 创建Model(模型):基于Schema创建操作数据库的对象
// 原理:Model对应MongoDB的集合(collection),名称为User(自动转为小写复数users)
const User = mongoose.model('User', UserSchema);
// 3. 导出模型,供其他文件使用
module.exports = User;
📌语法解释:
const UserSchema = new mongoose.Schema(...)是通过 Mongoose 定义「用户数据的结构规则(Schema)」 ,相当于给 MongoDB 的users集合(表)制定 "数据模板",约束存入的用户数据必须符合这个规则
Schema 的核心选项作用:
配置项 作用 示例 type指定字段类型(String/Number/Date 等) type: Stringrequired是否必填(true/false) required: trueunique是否唯一(避免重复) unique: truedefault默认值 avatar: { default: '' }minlength字符串最小长度 password: { minlength: 6 }match正则验证(如邮箱格式) match: [/^\w+@\w+\.\w+/, '邮箱格式错误']
const User = mongoose.model('User', UserSchema);是基于已定义的用户数据规则(Schema),创建可操作 MongoDB 数据库的「模型(Model)」对象 。后续所有对 "用户数据" 的增删改查(CRUD)操作,都要通过这个模型完成。
'User'会映射到 MongoDB 的users集合
- Mongoose 有一个默认规则 :将模型名(
'User')转为小写 + 复数形式,作为对应的 MongoDB 集合名Model 是 Schema 的 "实例化操作对象",封装了所有对 MongoDB 集合的 CRUD 方法
常用 Model 方法 作用 示例 create()新增数据(插入用户) User.create({ username: 'test', password: '123456' })find()查询多条数据(查所有用户) User.find()findOne()查询单条数据(按用户名查) User.findOne({ username: 'test' })updateOne()更新单条数据(改密码) User.updateOne({ username: 'test' }, { password: '654321' })deleteOne()删除单条数据(删用户) User.deleteOne({ username: 'test' })
module.exports = User;是将User模型对象暴露(导出)出去 ,让项目中其他文件可以通过require()引入并使用这个User模型,实现代码复用和模块化管理。
module:当前模块的内置对象,它代表 "当前文件模块本身"
module.exports:模块的 "导出接口",默认值是{}(空对象)
=User:将User模型赋值给导出接口
Node.js 在加载模块时,会为每个
.js文件创建一个专属的module对象,比如
- 加载
models/User.js时,生成module_User对象,module_User.exports只属于这个文件;- 加载
models/Task.js时,生成module_Task对象,module_Task.exports只属于这个文件;同一个js文件不同赋值的情况:
写法 效果 适用场景 module.exports = User覆盖默认值,导出单个值(User 模型) 模块只需要导出一个核心对象(如模型) module.exports.User = User,也可简写为exports.User = User;不覆盖默认值,往空对象里加属性 模块需要导出多个内容(如同时导出 User 和 UserSchema) 在
app.js中引入并使用
// 引入User模型(核心:能拿到User.js中导出的User对象) const User = require('./models/User'); // 直接使用User模型操作数据库(新增用户) User.create({ username: 'testuser', password: '123456', email: 'test@example.com' }).then(user => console.log('用户创建成功:', user));
步骤3:分类模型(Category.js)
在models下新建Category.js,分类关联用户(通过userId):
const mongoose = require('mongoose');
const CategorySchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
userId: {
type: mongoose.Schema.Types.ObjectId, // 类型:MongoDB的ObjectId(关联User的_id)
ref: 'User', // 关联的模型:User(用于联表查询)
required: true // 分类必须属于某个用户
},
color: {
type: String,
default: '#333333' // 分类颜色(默认黑色)
}
}, {
timestamps: true
});
const Category = mongoose.model('Category', CategorySchema);
module.exports = Category;
步骤4:任务模型(Task.js)
在models下新建Task.js,任务关联用户和分类:
const mongoose = require('mongoose');
const TaskSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
content: {
type: String,
default: ''
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
categoryId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Category',
required: true
},
status: {
type: Number,
enum: [0, 1, 2], // 枚举值:0-未完成,1-已完成,2-已取消
default: 0
},
deadline: {
type: Date, // 截止时间(日期类型)
default: null
}
}, {
timestamps: true
});
const Task = mongoose.model('Task', TaskSchema);
module.exports = Task;
步骤5:验证模型(可选)
在app.js中引入模型(测试连接),添加以下代码:
// 引入模型(放在MongoDB连接代码之后)
const User = require('./models/User');
const Category = require('./models/Category');
const Task = require('./models/Task');
// 测试创建一个用户(仅测试,后续可删除)
// 原理:通过Model.create()插入数据
User.create({
username: 'testuser',
password: '123456',
email: 'test@example.com'
}).then(user => {
console.log('测试用户创建成功:', user);
// 基于该用户创建分类
return Category.create({
name: '工作任务',
userId: user._id,
color: '#4285F4'
});
}).then(category => {
console.log('测试分类创建成功:', category);
// 基于用户和分类创建任务
return Task.create({
title: '完成安卓APP后端接口',
content: '编写用户、任务、分类的CRUD接口',
userId: category.userId,
categoryId: category._id,
deadline: new Date('2025-12-01')
});
}).then(task => {
console.log('测试任务创建成功:', task);
}).catch(err => {
console.error('数据创建失败:', err);
});
重启服务后,终端会打印创建的用户、分类、任务信息,说明模型设计和数据库连接正常(测试完成后可删除这段代码)。
命令窗口界面参考:

通过「MongoDB Compass」,查看mongodb中的数据:

3.4 中间件配置(日志、跨域、认证)
3.4.1 中间件核心原理
- 中间件是一个函数 ,格式:
(req, res, next) => {}; req:请求对象(可获取请求参数、请求头);res:响应对象(可设置响应头、返回数据);next:函数,调用后将请求传递给下一个中间件(如果不调用,请求会"卡住");- 中间件通过
app.use()注册,可全局生效(所有路由)或局部生效(指定路由)。
3.4.2 日志中间件(morgan)
原理
morgan 是 Node.js/Express 生态中最常用的 HTTP 请求日志中间件 ,核心作用是自动化记录每一个客户端对后端的 HTTP 请求细节(如请求方法、路径、状态码、响应时间、客户端 IP 等),并可将日志输出到控制台、文件等位置,是后端调试、排查问题、监控请求的核心工具。
实操步骤
-
安装morgan:
npm install morgan --save执行界面参考:

💡提示:
安装插件后,服务器自动重启,执行app.js,其中会再执行各数据记录创建,引起重复key的错误,如下图,将之前验证模型的代码注释掉就可以了

-
在
app.js中配置(放在app.use(express.json());之前):// 引入morgan const morgan = require('morgan'); // 配置日志格式:combined(标准格式,包含请求头、IP、响应时间等) // 原理:app.use(morgan('combined')) 表示所有请求都经过日志中间件 app.use(morgan('combined'));📌代码解释:
morgan('combined')是创建一个基于「combined 格式」的 HTTP 请求日志中间件-
语法:
morgan(日志格式[, 配置项]),调用后返回一个 Express 中间件函数,可通过app.use()注册。格式名 日志内容(示例) 特点 适用场景 combined如上示例(包含 IP、时间、方法、URL、状态码、UA 等) 最详细 生产环境 / 排查问题 common::1 - - [28/Nov/2025:10:00:00 +0800] "GET /api/hello HTTP/1.1" 200 13精简版 combined 生产环境 devGET /api/hello 200 2.321 ms - 13极简(方法 + 路径 + 状态码 + 响应时间) 开发环境 short::1 GET /api/hello HTTP/1.1 200 13 - 2.3ms介于 dev 和 common 之间 开发 / 测试环境 -
配置项示例(生产环境,输出到文件):const express = require('express'); const morgan = require('morgan'); const fs = require('fs'); const path = require('path'); const app = express(); // 创建日志文件写入流 const accessLogStream = fs.createWriteStream( path.join(__dirname, 'access.log'), // 日志文件路径 { flags: 'a' } // 追加模式(避免覆盖旧日志) ); // 注册中间件,将日志写入文件 app.use(morgan('combined', { stream: accessLogStream })); app.get('/api/hello', (req, res) => { res.json({ message: 'Hello' }); }); app.listen(3000);
-
-
测试:重启服务后,访问
http://localhost:3000/api/hello,终端会打印日志信息(示例):::1 - - [26/Nov/2025:10:00:00 +0800] "GET /api/hello HTTP/1.1" 200 57 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"实际运行界面参考:

日志解析:字段段 含义 具体解读 ::1客户端 IP 地址 ::1是 IPv6 的本地回环地址 (等价于 IPv4 的127.0.0.1),说明请求来自本机(你在本地测试)。-远程登录用户(Remote User) '-'表示无该信息(morgan 无法获取客户端的系统登录用户,开发 / 测试环境默认显示-)。-认证用户(Authenticated User) '-'表示该请求无需身份认证(如公开的/api/hello接口),需登录的接口会显示用户名。[28/Nov/2025:02:47:02 +0000]请求时间(UTC 时区) 格式: [日/月/年:时:分:秒 时区],+0000是 UTC 时区(东八区需加 8 小时,实际时间是 2025-11-28 10:47:02)。"GET /api/hello HTTP/1.1"请求行(Request Line) 拆解:✅ GET:请求方法(你定义的app.get('/api/hello'));✅/api/hello:请求路径;✅HTTP/1.1:HTTP 协议版本。304响应状态码(HTTP Status Code) 304=Not Modified(未修改),表示客户端本地有该请求的缓存,服务端无需返回数据,直接告知 "用缓存"(比 200 更高效)。【响应状态码(200 = 成功,404 = 不存在,500 = 服务器错误)】-响应体字节数(Response Size) '-'表示无响应体(304 状态码不会返回数据,所以字节数为空;200 状态码会显示具体数字,如13代表 13 个字节)。"-"来源页面(Referer) '-'表示请求不是从其他页面跳转而来(比如直接在浏览器地址栏输入 URL、安卓 APP 直接调用接口)。"Mozilla/5.0 ... Edg/142.0.0.0"客户端标识(User-Agent,UA) 说明请求来自Windows 10 系统的 Edge 浏览器(142 版本) ;如果是安卓 APP 调用,这里会显示 APP 的 UA(如 Android/14; Mobile; rv:100.0)。
3.4.3 跨域中间件(cors)
原理
CORS(Cross-Origin Resource Sharing,跨域资源共享)是解决浏览器 "同源策略" 限制 的 HTTP 协议规范,而**cors中间件** (Express 生态的cors包)是对该规范的落地实现,核心作用是:
- 让你的 Express 后端服务允许不同域名 / 端口 / 协议的客户端(如前端网页、安卓 APP)跨域请求接口,避免浏览器因同源策略拦截响应。
📌**"同源策略"说明:**
浏览器为了安全,默认限制 "非同源" 的 AJAX 请求(即 "跨域请求"),只有满足「协议、域名、端口全部相同」才是 "同源":
请求场景 是否同源 浏览器是否拦截 前端 http://localhost:8080→ 后端http://localhost:3000否(端口不同) 拦截(跨域) 前端 https://a.com→ 后端https://b.com否(域名不同) 拦截(跨域) 前端 http://localhost:3000→ 后端http://localhost:3000是 不拦截
实操步骤
-
安装cors:
npm install cors --save执行界面参考:

-
在
app.js中配置(放在日志中间件之后):// 引入cors const cors = require('cors'); // 全局配置跨域(允许所有域名访问,开发环境可用;生产环境需指定具体域名) app.use(cors({ origin: '*', // 允许所有域名(生产环境改为安卓APP的域名/IP) methods: ['GET', 'POST', 'PUT', 'DELETE'], // 允许的请求方法 allowedHeaders: ['Content-Type', 'Authorization'] // 允许的请求头 }));📌
cors中间件会根据配置项,自动给后端响应添加对应的 HTTP 头,告诉浏览器 / 安卓 APP "该跨域请求被允许",cors配置项说明:配置项 取值 / 示例 核心含义 实用说明 origin*/['https://app.com', 'http://192.168.1.100']允许跨域请求的「来源(域名 / IP: 端口)」 ✅ *:开发环境用,允许所有域名(最宽松,方便测试); ❌ 生产环境禁用*,需改为安卓 APP 的真实域名 / IP(如http://192.168.1.200:8080),避免任意网站跨域调用你的接口; ✅ 也可传数组,指定多个可信来源(如同时允许前端网页和安卓 APP)。methods['GET', 'POST', 'PUT', 'DELETE']允许跨域请求的「HTTP 方法」 ✅ 明确限定仅开放业务需要的方法(比如只开放 GET/POST,关闭 PUT/DELETE),减少攻击面; ✅ 若不配置,默认允许 GET,HEAD,PUT,PATCH,POST,DELETE(全部常用方法); ✅ 对应你后端的app.get()/app.post()等路由方法,确保前端 / 安卓 APP 的请求方法在列表内。allowedHeaders['Content-Type', 'Authorization']允许跨域请求携带的「请求头」 ✅ Content-Type:必须配置!安卓 APP 传 JSON 数据时会带Content-Type: application/json,没这个配置会跨域失败; ✅Authorization:如果接口需要 token 认证(如Bearer token),必须配置这个请求头,否则安卓 APP 携带 token 时会被拦截; ✅ 若不配置,默认只允许Content-Type, Accept, Accept-Language等基础头,自定义头(如token)会被拦截。其他常用配置项:
配置项 核心解决问题 生产环境建议 credentials能否携带登录态 /token 需认证时设为 true,同时指定originmaxAge"预检请求(OPTIONS)" 的缓存时长(秒),减少重复预检请求,提升性能。 设为 3600(1 小时)或 86400(1 天);默认值 : 0(每次跨域请求都发预检)
3.4.4 认证中间件(JWT)
原理
JWT(JSON Web Token)是一种轻量级的、基于 JSON 的令牌规范,核心作用是「在客户端和服务端之间安全传递身份 / 权限信息」,无需服务端存储会话(Session),是前后端分离 / 跨域场景下(如安卓 APP + 后端)最常用的身份认证方案。
安卓APP的用户登录后,后端需要生成一个"令牌(JWT)"返回给APP,后续APP每次请求都携带该令牌,后端通过认证中间件验证令牌的合法性,确认用户身份。
JWT(JSON Web Token)由三部分组成:
- Header(头部):指定加密算法;
- Payload(载荷):存储用户ID等非敏感信息;
- Signature(签名):用密钥加密Header+Payload,防止篡改。
实操步骤
步骤1:安装JWT相关依赖
npm install jsonwebtoken bcryptjs --save
jsonwebtoken和bcryptjs是两个独立的包名jsonwebtoken:生成和验证JWT令牌;bcryptjs:加密用户密码(避免明文存储)。
执行界面参考:

步骤2:封装密码加密/解密工具(可选,优化代码)
在根目录新建utils文件夹,创建authUtils.js:
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
// 1. 密码加密(注册/修改密码时使用)
// 原理:bcrypt.hash()生成哈希值,saltRounds=10表示加密强度
exports.encryptPassword = async (password) => {
const salt = await bcrypt.genSalt(10);
return await bcrypt.hash(password, salt);
};
// 2. 密码验证(登录时使用)
exports.comparePassword = async (password, hashedPassword) => {
return await bcrypt.compare(password, hashedPassword);
};
// 3. 生成JWT令牌
// 原理:jwt.sign()生成令牌,expiresIn指定过期时间(24小时)
exports.generateToken = (userId) => {
// 密钥(生产环境需放在环境变量中,避免硬编码)
const SECRET_KEY = 'android_app_secret_key_2025';
return jwt.sign({ userId }, SECRET_KEY, { expiresIn: '24h' });
};
// 4. 验证JWT令牌(认证中间件核心逻辑)
exports.verifyToken = (token) => {
const SECRET_KEY = 'android_app_secret_key_2025';
try {
// 验证令牌,返回解码后的载荷(包含userId)
return jwt.verify(token, SECRET_KEY);
} catch (err) {
// 令牌过期/篡改,返回null
return null;
}
};
📌代码解释:
async/await语法
async:声明这是一个异步函数 ,函数内部可使用await等待异步操作,且函数执行后会返回一个Promise对象;
await:等待右侧的异步操作完成调用异步函数:(在
async函数中用await):
const bcrypt = require('bcryptjs'); // 定义加密函数 const encryptPassword = async (password) => { const salt = await bcrypt.genSalt(10); return await bcrypt.hash(password, salt); }; // 调用加密函数(必须在async函数中用await) const run = async () => { const rawPwd = '123456'; // 明文密码 const hashPwd = await encryptPassword(rawPwd); // 等待加密完成 console.log(hashPwd); // 输出加密后的字符串:$2a$10$xxxxxx... }; run();bcrypt代码
*
代码行 核心作用 关键细节 async (password) => {}定义异步箭头函数,参数 password是用户输入的明文密码 (如123456)异步函数调用时必须用 await,或.then()接收返回值(加密后的密码)。const salt = await bcrypt.genSalt(10);生成「盐值(salt)」------bcrypt 加密的核心,相当于 "随机加密因子"。 ✅ 10是rounds(轮数):值越高,加密越安全,但耗时越长(推荐 8-12); ✅ 盐值是随机生成的,即使相同密码,每次生成的盐值不同,最终加密结果也不同(防止彩虹表破解)。return await bcrypt.hash(password, salt);用盐值对明文密码进行哈希加密,返回加密后的字符串。 ✅ 加密过程是不可逆的:无法从加密后的字符串还原出明文密码; ✅ 最终返回的是类似 $2a$10$xxxxxx...的字符串(包含盐值 + 加密后的密码,无需单独存储盐值)。bcrypt.compare 异步密码校验方法 「对比用户输入的明文密码」和「数据库中存储的 bcrypt 加密密码」,返回布尔值( true/false)jwt.sign({ userId }, SECRET_KEY, { expiresIn: '24h' }):生成一个 JWT 令牌 :将用户标识(
userId)嵌入令牌,用服务端私钥(SECRET_KEY)签名,并设置令牌 24 小时后过期,是用户登录成功后颁发身份凭证的关键代码
SECRET_KEY→ 签名私钥✅ 用这个私钥对「Header + Payload」进行加密,生成 JWT 的 Signature(签名);
✅ 客户端携带令牌请求时,服务端需用相同的私钥验证签名,确认令牌未被篡改;
jwt.verify(token, SECRET_KEY):校验客户端携带的 JWT 令牌是否合法有效
token→ 客户端携带的 JWT 令牌- SECRET_KEY→ 服务端的私钥(必须和生成令牌时的
jwt.sign私钥完全一致)
exports是 Node.js 每个模块内置的空对象(属于 CommonJS 规范),作用是「对外暴露模块内的变量 / 函数 / 对象」
步骤3:编写认证中间件
在根目录新建middleware文件夹,创建authMiddleware.js:
const { verifyToken } = require('../utils/authUtils');
// 认证中间件:验证请求头中的JWT令牌
exports.auth = (req, res, next) => {
// 1. 获取请求头中的令牌(安卓APP需在请求头中携带:Authorization: Bearer <token>)
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
code: 401,
message: '未登录,请先登录',
data: null
});
}
// 2. 提取令牌(去掉Bearer前缀)
const token = authHeader.split(' ')[1];
// 3. 验证令牌
const decoded = verifyToken(token);
if (!decoded) {
return res.status(401).json({
code: 401,
message: '令牌过期或无效,请重新登录',
data: null
});
}
// 4. 将用户ID存入req对象,供后续路由使用
req.userId = decoded.userId;
// 5. 传递给下一个中间件/路由
next();
};
📌代码解释:
req.headers.authorization,获取 HTTP 请求头里
Authorization字段,专门用于携带身份认证凭证
HTTP 请求头的实际形态
GET /api/user/info HTTP/1.1 Host: your-server.com Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....
req的属性,对应 HTTP 请求头的所有键值对(如Content-Type、User-Agent、Authorization等)
步骤4:注册认证中间件并测试
-
在
app.js中引入并使用认证中间件:javascript// 引入认证中间件 const { auth } = require('./middleware/authMiddleware'); const { encryptPassword, generateToken } = require('./utils/authUtils'); const User = require('./models/User'); // 示例1:登录接口(无需认证,生成令牌) app.post('/api/login', async (req, res) => { try { const { username, password } = req.body; // 1. 查询用户 const user = await User.findOne({ username }); if (!user) { return res.status(400).json({ code: 400, message: '用户不存在', data: null }); } // 2. 验证密码==》此处逻辑有错误❌=》后面有更正代码 const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { return res.status(400).json({ code: 400, message: '密码错误', data: null }); } // 3. 生成令牌 const token = generateToken(user._id); res.status(200).json({ code: 200, message: '登录成功', data: { token, username: user.username } }); } catch (err) { res.status(500).json({ code: 500, message: '服务器错误', data: null }); } }); // 示例2:需要认证的接口(获取用户的任务列表) // 原理:auth中间件先验证令牌,通过后才执行路由逻辑 app.get('/api/tasks', auth, async (req, res) => { try { // req.userId由auth中间件存入,查询该用户的所有任务 const tasks = await Task.find({ userId: req.userId }); res.status(200).json({ code: 200, message: '获取任务列表成功', data: tasks }); } catch (err) { res.status(500).json({ code: 500, message: '服务器错误', data: null }); } });📌代码解释:
- app.get('/api/tasks', auth, async (req, res)
auth是authMiddleware的简写(通常为了代码简洁重命名),本质是一个函数,请求到达接口后会先执行该中间件- 若
auth中间件验证通过(调用next()),才会进入后面的业务逻辑回调 - 若验证失败(如 JWT 令牌无效),中间件会直接返回
res.status(401)错误,终止请求,不会执行后续业务逻辑。
- 逻辑图如下:
- app.get('/api/tasks', auth, async (req, res)
-
测试流程:
-
先通过Postman/APIPost发送POST请求到
http://localhost:3000/api/login,参数:{ "username": "testuser", "password": "123456" }**🧩可选工具:**curl测试工具,也可以使用hoppscotch,参考文档:
【工具】curl测试工具Hoppscotch 测试工具使用教程(零基础版)_hoppscotch本地部署-CSDN博客
测试参考界面:

代码中有错误,因为之前的用户表存的密码是明文:调整代码,增加注册功能,注册时,将密码加密后,保存进数据库:
javascript// 1. 引入Express框架(安装后才能require) const express = require('express'); // 2. 创建Express应用实例(核心对象,管理路由/中间件) const app = express(); // 引入Mongoose const mongoose = require('mongoose'); // 连接MongoDB(本地数据库,数据库名:android_app_db) // 原理:mongoose.connect()建立与MongoDB的连接,返回Promise mongoose.connect('mongodb://localhost:27017/android_app_db') .then(() => console.log('MongoDB连接成功!')) .catch(err => console.error('MongoDB连接失败:', err)); // 引入模型(放在MongoDB连接代码之后) const User = require('./models/User'); const Category = require('./models/Category'); const Task = require('./models/Task'); /* // 测试创建一个用户(仅测试,后续可删除) // 原理:通过Model.create()插入数据 User.create({ username: 'testuser', password: '123456', email: 'test@example.com' }).then(user => { console.log('测试用户创建成功:', user); // 基于该用户创建分类 return Category.create({ name: '工作任务', userId: user._id, color: '#4285F4' }); }).then(category => { console.log('测试分类创建成功:', category); // 基于用户和分类创建任务 return Task.create({ title: '完成安卓APP后端接口', content: '编写用户、任务、分类的CRUD接口', userId: category.userId, categoryId: category._id, deadline: new Date('2025-12-01') }); }).then(task => { console.log('测试任务创建成功:', task); }).catch(err => { console.error('数据创建失败:', err); }); */ // 3. 定义端口号(后端服务监听的端口,安卓APP后续会访问这个端口) const PORT = 3000; // 引入morgan const morgan = require('morgan'); // 配置日志格式:combined(标准格式,包含请求头、IP、响应时间等) // 原理:app.use(morgan('combined')) 表示所有请求都经过日志中间件 app.use(morgan('combined')); // 引入cors const cors = require('cors'); // 全局配置跨域(允许所有域名访问,开发环境可用;生产环境需指定具体域名) app.use(cors({ origin: '*', // 允许所有域名(生产环境改为安卓APP的域名/IP) methods: ['GET', 'POST', 'PUT', 'DELETE'], // 允许的请求方法 allowedHeaders: ['Content-Type', 'Authorization'] // 允许的请求头 })); // 引入认证中间件 const { auth } = require('./middleware/authMiddleware'); const { encryptPassword,comparePassword, generateToken,verifyToken } = require('./utils/authUtils'); //const User = require('./models/User'); //const bcrypt = require('bcryptjs'); // 4. 配置内置中间件:解析JSON格式的请求体(安卓APP传参通常用JSON) // 原理:客户端(安卓)POST请求的JSON数据,需通过此中间件解析后才能在路由中获取 app.use(express.json()); // 5. 定义测试路由(安卓APP的接口本质就是路由) // 格式:app.请求方法(路由路径, 处理函数) // GET请求:客户端获取数据(如安卓APP获取用户列表) app.get('/api/hello', (req, res) => { // req:请求对象(包含客户端传递的参数、请求头等) // res:响应对象(用于向客户端返回数据) res.status(200).json({ code: 200, message: 'Express服务启动成功!==》该了一下,会重启吗?', data: null }); }); // 1. 注册接口:加密明文密码后存储 app.post('/api/register', async (req, res) => { try { const { username, password ,email} = req.body; // 校验参数 if (!username || !password) { return res.status(400).json({ message: '用户名/密码不能为空' }); } /* // 检查用户是否已存在 const existUser = User.find(u => u.username === username); if (existUser) { return res.status(400).json({ message: '用户已存在' }); } */ // 核心:加密明文密码 const hashedPassword = await encryptPassword(password); User.create({ username: username, password: hashedPassword, email: email }).then(user => { console.log('测试用户创建成功:', user); // 基于该用户创建分类 return Category.create({ name: '工作任务', userId: user._id, color: '#4285F4' }); }).then(category => { console.log('测试分类创建成功:', category); // 基于用户和分类创建任务 return Task.create({ title: '完成安卓APP后端接口', content: '编写用户、任务、分类的CRUD接口', userId: category.userId, categoryId: category._id, deadline: new Date('2025-12-01') }); }).then(task => { console.log('测试任务创建成功:', task); }).catch(err => { console.error('数据创建失败:', err); }); } catch (err) { res.status(500).json({ message: `注册失败:${err.stack}` }); } }); // 示例1:登录接口(无需认证,生成令牌) app.post('/api/login', async (req, res) => { try { const { username, password } = req.body; console.log(`input=> username:${username};password:${password}`); // 1. 查询用户 const user = await User.findOne({ username }); console.log(`user:${user};user.password:${user.password}`); if (!user) { return res.status(400).json({ code: 400, message: '用户不存在', data: null }); } // 2. 验证密码 const isMatch = await comparePassword(password, user.password); if (!isMatch) { return res.status(400).json({ code: 400, message: '密码错误', data: null }); } // 3. 生成令牌 const token = generateToken(user._id); res.status(200).json({ code: 200, message: '登录成功', data: { token, username: user.username } }); } catch (err) { res.status(500).json({ code: 500, message: '服务器错误:' + err.stack, data: null }); } }); // 示例2:需要认证的接口(获取用户的任务列表) // 原理:auth中间件先验证令牌,通过后才执行路由逻辑 app.get('/api/tasks', auth, async (req, res) => { try { // req.userId由auth中间件存入,查询该用户的所有任务 const tasks = await Task.find({ userId: req.userId }); res.status(200).json({ code: 200, message: '获取任务列表成功', data: tasks }); } catch (err) { res.status(500).json({ code: 500, message: '服务器错误', data: null }); } }); // 6. 启动服务,监听指定端口 app.listen(PORT, () => { console.log(`Express服务已启动,访问地址:http://localhost:${PORT}`); });注册界面如下:

服务器输出:

-
后端返回令牌(token);
-
执行界面参考: 然后用user3登录:

-
-
发送GET请求到
http://localhost:3000/api/tasks,请求头添加:Authorization: Bearer <返回的token>执行界面参考:
测试提取用户的任务列表:
用上面返回的认证信息,放在"Bearer "(注意Bearer后面要有空格)后面

并设置查询的userid,如下图

-
能获取任务列表,说明认证中间件生效;
-
若不携带令牌/令牌无效,会返回401错误,符合预期。
- 如果认证信息错误,会有类似下面提示

- 这说明中间件authMiddleware生效:

- 如果认证信息错误,会有类似下面提示
-
3.5 本章总结
本章完成了安卓APP后端的核心基础搭建:
- 基于Express搭建了Web服务,理解了路由、中间件的核心原理;
- 设计了用户、任务、分类的MongoDB数据模型,实现了数据关联;
- 配置了日志(morgan)、跨域(cors)、认证(JWT)三大核心中间件,解决了后端开发的基础问题。
🐳心得:
纯手搓,有点累啊,有点累😮💨,特别是调错🐞,但心里明明白白☀️,光明磊落☀︎,阳光万丈☼,哇哈哈🤣~
✅AI教程,推荐豆包,比DeepSeek更适合初学者。
