老板: 立项
产品经理: 需求分析-> 需求文档:
- 需求点(文字形式的描述)
- 原型图(图形的描述)
UI设计师: 原型图 -> 静态产品图
技术经理: 技术分析 -> 设计文档
- 流程图
- 数据库设计
- 接口设计
项目成员: 编码实现
测试人员: 编码测试
运维人员: 部署/上线
一. 需求分析
评估工期, 敏捷开发(最小原型, 快速迭代)
时间/人力/质量
1.需求点
1. 添加待办
- 在文本框内输入内容
- 按回车添加代办
- 如果文本框中没有内容,提示’内容不能为空’
- 如果文本框中有内容, 添加到最上方
-
2. 修改待办
点击待办内容, 能够修改其中的内容
- 修改完成, 按
Enter(回车键) 或者 失去焦点时, 更新内容, 同时更新时间 -
3. 删除待办
-
2.原型图

二. 技术分析
技术评估
- 人力成本: 项目团队的人员配置(1~2前端, 1后端, 1个UI, 1测试, 1运维) 全栈(全干)
- 进度: 项目目标分解成里程碑
-
1. 技术选型
平台
操作系统: windows 10
- 开发平台: VSCode V16.2
- 测试平台: Chrome/FireFox — 是否兼容IE6/7/8
- 文档平台: 语雀/ApiFox/Swagger
- 代码平台: github/gitee
框架
- 前端jQuery
- 后端Express
技术栈
| 字段名 | 类型 | 属性 | 备注 |
|---|---|---|---|
| id | int | 主键, 自增 | id |
| content | varchar(255) | 非空, 默认值’’ | 待办事项的内容 |
| created_time | datetime | CURRENT_TIMESTAMP | 创建时间 |
| updated_time | datetime | CURRENT_TIMESTAMP, on UPDATE CURRENT_TIMESTAMP | 更新时间 |
| deleted_time | datetime | 默认值为null, 删除时记录删除时间 |
CREATE TABLE `xzd_todos` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',`content` varchar(255) NOT NULL DEFAULT '' COMMENT '待办事项内容',`created_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',`deleted_time` timestamp NULL DEFAULT NULL COMMENT '默认值为null, 表示没有删除; 如果存在时间, 说明在时间被删除',PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3. 接口设计
1) baseURL
http://localhost:3000
2) 待办模块
获取所有待办
请求
请求方式: GET
请求URL: /todos
GET /todos
成功的响应
{
"code": 0,
"msg": "获取所有待办成功",
"result": [
{"id":1, "content": "待办1", "updated_time": "2021-11-26 11:15:23"}
]
}
错误的响应
{
"code": 100101,
"message": "获取所有待办失败",
"result": {
"code": "ER_BAD_FIELD_ERROR",
"errno": 1054,
"sqlMessage": "Unknown column 'delete_time' in 'where clause'",
"sqlState": "42S22",
"index": 0,
"sql": "select id, content, updated_time from xzd_todos where delete_time is null"
}
}
根据id获取单个待办
请求
请求方式 GET
请求地址 /todos/:id
GET /todos/:id
GET /todos/1
成功的响应
{
"code": 0,
"msg": "获取待办成功",
"result": {"id":1, "content": "待办1", "updated_time": "2021-11-26 11:15:23"}
}
失败的响应
{
"code": 100102,
"message": "查询单个待办失败",
"result": {
"code": "ER_NO_SUCH_TABLE",
"errno": 1146,
"sqlMessage": "Table 'db2201.xzd_todo' doesn't exist",
"sqlState": "42S02",
"index": 0,
"sql": "select id, content, updated_time from xzd_todo where id=1 and deleted_time is null"
}
}
{
"code": 100103,
"message": "id对应的数据不存在",
"result": ""
}
{
"code": 100104,
"message": "id必须为数字",
"result": ""
}
添加待办
请求
请求方式 POST
接口地址 /todos
POST /todos
请求参数
{
"content": "待办2"
}
响应
{
"code": 0,
"msg": "添加成功",
"result": {"id":1, "content": "待办1", "updated_time": "2021-11-26 11:15:23"}
}
修改待办
请求
请求方式 PUT
请求地址 /todos/1
PUT /todos/:id
PUT /todos/1
请求参数
{
"content": "待办2"
}
响应
{
"code": 0,
"msg": "更新成功",
"result": {"id":1, "content": "待办1", "updated_time": "2021-11-26 11:15:23"}
}
删除待办
请求
请求方式 DELETE
请求地址 /todos/:id
DELETE /todos/:id
DELETE /todos/1
响应
{
"code": 0,
"msg": "删除成功",
"result": ''
}
三. 后端实现
1 搭建项目
通过express脚手架创建一个项目, 默认运行在3000端口
express --no-view api
2 安装依赖
1) 开发环境依赖
npm i nodemon -D
2) 安装mysql依赖
npm i mysql
3) 安装cors中间件
npm i cors
4) 安装所有依赖
npm install
3 启动项目
修改package.json, 使用nodemon启动项目
"scripts": {
"start": "nodemon ./bin/www"
},
运行
npm run start
测试
4 注册cors中间件
修改app.js
// 导入cors中间件的包
const cors = require('cors')
// 注册全局中间件
app.use(cors())
示例
5 规划路由
1) 创建路由模块
在routes目录下, 创建todos.js文件
// 一. 导入express
// 二. 实例化router对象
// 三. 编写路由规则
// 四. 导出router对象
示例

// 一. 导入express
const express = require('express')
// 二. 实例化router对象
const router = express.Router()
// 三. 编写路由规则
router.get('/', (req, res) => {
res.send('todos')
})
// 四. 导出router对象
module.exports = router
2) 导入路由对象
在app.js中导入路由对象
// 导入todosRouter对象
const todosRouter = require('./routes/todos')
// 注册路由
app.use('/todos', todosRouter)
示例

测试
6 复用数据库模块
把db模块复制到目录下
在todos.js文件中, 加载数据库操作的方法
// 导入数据库操作的方法
const { getAll, getById, exec } = require('../db')
7 实现接口
1) 获取所有待办
在todos.js文件中, 编写对应的路由
/**
* 获取所有待办
* GET /todos
*/
router.get('/', async (req, res) => {
// 操作数据库
let sql = `select id, content, updated_time from xzd_todos where deleted_time is null`
try {
// 需要测试的代码
const data = await getAll(sql)
res.send({
code: 0,
message: '获取所有待办成功',
result: data,
})
} catch (err) {
// 处理异常情况(错误处理)
res.send({
code: 100101, // 10: 第一个版; 01: todos模块; 01: 错误
message: '获取所有待办失败',
result: debug ? err : '',
})
}
})
实现 debug的配置.
创建config/index.js文件, 内容如下
module.exports = {
debug: true,
mysql: {
host: '127.0.0.1',
port: 3306,
user: 'root',
password: '123456',
database: 'db2201',
timezone: 'SYSTEM', // 解决 时间 显示的 格式问题
},
}
在数据库层, db/index.js改造如下
// 导入配置文件(给mysql对象起别名)
const { mysql: dbconfig } = require('../config')
// 2. 创建连接
const con = mysql.createConnection(dbconfig)
2) 根据id获取单个待办
/**
* 根据id获取todo
* GET /todos/:id
*/
router.get('/:id', async (req, res) => {
console.log(req.params)
// 一. 解析请求数据(id)
const { id } = req.params
// 对请求的参数格式进行校验
const reg = /^\d+$/
if (!reg.test(id)) {
res.send({
code: 100104,
message: 'id必须为数字',
result: '',
})
return
}
// 二. 操作数据库
let sql = `select id, content, updated_time from xzd_todos where id=${id} and deleted_time is null`
console.log(sql)
// 三. 返回结果
try {
var data = await getById(sql)
} catch (err) {
res.send({
code: 100102,
message: '查询单个待办失败',
result: debug ? err : '',
})
}
// 判断...
if (data) {
// 成功
res.send({
code: 0,
message: '查询单个待办成功',
result: data,
})
} else {
// 根据id没有查询到结果
res.status(404).send({
code: 100103,
message: 'id对应的数据不存在',
result: '',
})
}
})
3) 添加待办
/**
* 新增todo
* POST /todos {content: 'todo-test'}
*/
router.post('/', async (req, res) => {
// 一. 解析请求数据(body)
const { content } = req.body
// console.log(content)
// 请求参数的校验, 不能为空
if (!content) {
// 出错!! content == undefined
res.send({
code: 100105,
message: 'content格式错误',
result: '',
})
return
}
// 二. 操作数据库
let sql = `insert into xzd_todos (content) values ('${content}')`
try {
var { insertId } = await exec(sql)
} catch (err) {
res.send({
code: 100106,
message: '添加待办失败',
result: debug ? err : '',
})
return
}
sql = `select id, content, updated_time from xzd_todos where id=${insertId}`
try {
const data = await getById(sql)
// 三. 结果返回
res.send({
code: 0,
message: '添加待办成功',
result: data,
})
} catch (err) {
res.send({
code: 100106,
message: '添加待办失败',
result: debug ? err : '',
})
}
})
4) 更新待办
/**
* 修改todo
* PUT /todos/:id {content: 'todo-new'}
*/
router.put('/:id', async (req, res) => {
// 一. 解析请求参数
const { id } = req.params
const { content } = req.body
// todo: 参数的校验
// 二. 操作数据库
let sql = `update xzd_todos set content='${content}' where id=${id}`
await exec(sql)
res.send({
code: 0,
message: '更新待办成功',
result: {
id: id,
content: content,
},
})
})
5) 删除待办
/**
* 删除todo
* DELETE /todos/:id
*/
router.delete('/:id', async (req, res) => {
// 一. 解析请求数据
const { id } = req.params
// todo: 请求参数的校验
// 二. 操作数据库
let sql = `update xzd_todos set deleted_time = NOW() where id=${id}`
await exec(sql)
// 三. 返回结果
res.send({
code: 0,
message: '删除成功',
result: '',
})
})
四. 前端实现
1 结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo List</title>
</head>
<body>
<!-- 一.完成HTML的结构 -->
<div class="todo">
<div class="header">
添加待办:
<input id="add" type="text" placeholder="按回车添加待办" />
</div>
<div class="list">
<ul></ul>
</div>
</div>
</body>
</html>
2 样式
在index.html中引用两个样式
<link rel="stylesheet" href="css/reset.css" />
<link rel="stylesheet" href="css/index.css" />
1) reset样式
* {
margin: 0;
padding: 0;
}
li {
list-style: none;
}
a {
text-decoration: none;
}
2) 业务样式
编写index.css
.todo {
width: 400px;
margin: 50px auto;
}
.todo .header {
height: 40px;
padding: 0 20px;
background-color: skyblue;
color: #fff;
line-height: 40px;
}
.todo .header input {
height: 30px;
padding-left: 10px;
color: #333;
border: none;
outline: none;
}
.todo .list {
min-height: 200px;
margin-top: 20px;
border: 1px solid #ccc;
}
.todo .list li {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px auto;
padding: 5px 20px;
background-color: #eee;
}
.todo .list li span {
margin-right: 5px;
font-size: 12px;
color: #999;
}
.todo .list li p {
flex: 1;
}
3 交互
在index.html中引用jQuery
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
获取所有待办
const BASE_URL = 'http://localhost:3000'
// 获取列表(所有待办)
function getTodos() {
// 发送ajax请求, 调用 GET /todos接口
$.ajax({
type: 'GET',
url: `${BASE_URL}/todos`,
success: function (res) {
console.log(res)
// 解构res对象
const { code, message, result } = res
if (code == 0) {
// 成功, 遍历result(数组)
result.forEach((item) => {
// 在ul的后面添加li元素
$('.list ul').append(`<li>
<p>${item.content}</p>
<span>上次更新:${item.updated_time}</span>
<a href="#">删除</a>
</li>`)
})
} else {
alert(message)
}
},
})
}
getTodos()
添加待办
// 添加待办
// 一. 监听add这个input框的回车事件
// 1.1 判断input框架的内容为空, 提示"不能为空", 返回
// 1.2 判断input框架的内容不为空, 发送ajax请求, [POST /todos]接口
// 1.3 拿到数据后, 判断是否失败, 如果成功, 重新加载数据
$('#add').keyup((e) => {
// console.log(e)
// 如果按下的键是回车键
if (e.keyCode === 13) {
// 获取input框架的值
var inputValue = $('#add').val()
// 去掉首尾的空白字符
if (inputValue.trim() === '') {
alert('待办事项不能为空')
return
}
// 发送ajax请求, 请求[POST /todos]接口
$.ajax({
type: 'POST',
url: `${BASE_URL}/todos`,
data: { content: inputValue },
success: function (res) {
// 清空输入框的数据
$('#add').val('')
const { code, message } = res
if (code === 0) {
// 重新获取列表
getTodos()
} else {
alert(message)
}
},
})
}
})
发现了一个问题, 当添加时会重复出现多条记录
解决方法
在getTodos时, 先清空子节点
// 获取列表(所有待办)
function getTodos() {
// 发送ajax请求, 调用 GET /todos接口
$.ajax({
type: 'GET',
url: `${BASE_URL}/todos`,
success: function (res) {
console.log(res)
// 解构res对象
const { code, message, result } = res
if (code == 0) {
// 清空ul中的数据
$('.list ul').empty()
// 成功, 遍历result(数组)
result.forEach((item) => {
// 在ul的后面添加li元素
$('.list ul').append(`<li>
<p>${item.content}</p>
<span>上次更新:${item.updated_time}</span>
<a href="#">删除</a>
</li>`)
})
} else {
alert(message)
}
},
})
}
修改待办
// 修改待办
// 当点击文字时, 变成一个可以输入的框. 拿到之前的数据
$('.list').on('click', 'li', function () {
// on---监听动态创建的元素的事件
// console.log($(this).find('p').text())
// console.log($(this).find('span').text())
const content = $(this).find('p').text()
const date = $(this).find('span').text()
$(this).html(
`<input type="text" value="${content}"/><span>${date}</span><a href="#">删除</a>`
)
})
// 阻止input的点击事件向上冒泡
$('.list').on('click', 'input', function (e) {
e.stopPropagation()
})
// 监听新创建的input框的回车事件
$('.list').on('keyup', 'input', function (e) {
if (e.keyCode == 13) {
// 判断当前输入框的值为空
if ($(this).val().trim() === '') {
alert('待办内容不能为空')
return
}
const id = $(this).parent().attr('data-index')
console.log(id)
// 发送ajax请求
$.ajax({
type: 'PUT',
url: `${BASE_URL}/todos/${id}`,
data: { content: $(this).val() },
success: function (res) {
// 重新获取数据
const { code, message } = res
if (code === 0) {
getTodos()
} else {
alert(message)
}
},
})
}
})
$('.list').on('blur', 'input', function () {
// 判断当前输入框的值为空
if ($(this).val().trim() === '') {
alert('待办内容不能为空')
return
}
const id = $(this).parent().attr('data-index')
console.log(id)
// 发送ajax请求
$.ajax({
type: 'PUT',
url: `${BASE_URL}/todos/${id}`,
data: { content: $(this).val() },
success: function (res) {
// 重新获取数据
const { code, message } = res
if (code === 0) {
getTodos()
} else {
alert(message)
}
},
})
})
删除待办
// 删除待办
$('.list').on('click', 'a', function (e) {
// 阻止a元素向上冒泡
e.stopPropagation()
const id = $(this).parent().attr('data-index')
$.ajax({
type: 'DELETE',
url: `${BASE_URL}/todos/${id}`,
success: function (res) {
const { code } = res
if (code === 0) {
getTodos()
}
},
})
})
