老板: 立项
产品经理: 需求分析-> 需求文档:

  • 需求点(文字形式的描述)
  • 原型图(图形的描述)

UI设计师: 原型图 -> 静态产品图
技术经理: 技术分析 -> 设计文档

  • 流程图
  • 数据库设计
  • 接口设计

项目成员: 编码实现
测试人员: 编码测试
运维人员: 部署/上线

一. 需求分析

评估工期, 敏捷开发(最小原型, 快速迭代)
时间/人力/质量

1.需求点

1. 添加待办

  1. 在文本框内输入内容
  2. 按回车添加代办
  3. 如果文本框中没有内容,提示’内容不能为空’
  4. 如果文本框中有内容, 添加到最上方
  5. 添加完之后,清空文本框的内容

    2. 修改待办

  6. 点击待办内容, 能够修改其中的内容

  7. 修改完成, 按Enter(回车键) 或者 失去焦点时, 更新内容, 同时更新时间
  8. 更新的内容不能为空

    3. 删除待办

  9. 点击删除按钮, 删除对应的todo待办事项

    2.原型图前后端项目-TodoList - 图4

    二. 技术分析

    技术评估

  • 人力成本: 项目团队的人员配置(1~2前端, 1后端, 1个UI, 1测试, 1运维) 全栈(全干)
  • 进度: 项目目标分解成里程碑
  • 经济成本: 人月 58K3月 = 120K = 12W

    1. 技术选型

    平台

  • 操作系统: windows 10

  • 开发平台: VSCode V16.2
  • 测试平台: Chrome/FireFox — 是否兼容IE6/7/8
  • 文档平台: 语雀/ApiFox/Swagger
  • 代码平台: github/gitee

框架

  • 前端jQuery
  • 后端Express

技术栈

  • 前端: H5+CSS+jQuery
  • node+Express+MySQL

    2. 数据库设计

    xzd_todos表

字段名 类型 属性 备注
id int 主键, 自增 id
content varchar(255) 非空, 默认值’’ 待办事项的内容
created_time datetime CURRENT_TIMESTAMP 创建时间
updated_time datetime CURRENT_TIMESTAMP, on UPDATE CURRENT_TIMESTAMP 更新时间
deleted_time datetime 默认值为null, 删除时记录删除时间
  1. CREATE TABLE `xzd_todos` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  3. `content` varchar(255) NOT NULL DEFAULT '' COMMENT '待办事项内容',
  4. `created_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  5. `updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  6. `deleted_time` timestamp NULL DEFAULT NULL COMMENT '默认值为null, 表示没有删除; 如果存在时间, 说明在时间被删除',
  7. PRIMARY KEY (`id`)
  8. ) 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 安装依赖

在后端项目的根目录下, api目录, 执行如下命令

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

测试

前后端项目-TodoList - 图5

4 注册cors中间件

修改app.js

// 导入cors中间件的包
const cors = require('cors')

// 注册全局中间件
app.use(cors())

示例

图片.png5 规划路由

1) 创建路由模块

routes目录下, 创建todos.js文件

// 一. 导入express
// 二. 实例化router对象
// 三. 编写路由规则
// 四. 导出router对象

示例

图片.png

// 一. 导入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)

示例

图片.png

测试

(略)

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)

图片.png2) 根据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>

index.html中引用 业务代码index.js

获取所有待办

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()
      }
    },
  })
})

五. 联调

六. 测试

七. 部署

八. 运维