实现功能如下:
1. 连接MySQL数据库
① 使用navicat创建todolist数据库,并新建task表
CREATE TABLE `task` ( `id` varchar(255) NOT NULL, `title` varchar(255) NOT NULL, `completed` tinyint(4) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) CREATE TRIGGER `id_trigger` BEFORE INSERT ON `task` FOR EACH ROW BEGIN SET new.id=REPLACE(UUID(),'-',''); END
注意:MySQL中没有Boolean类型。boolean在MySQL里的类型为tinyint(),
MySQL里有四个常量:true,false,TRUE,FALSE,它们分别代表0 1 0 1
创建一个触发器,在插入数据的时候自动生成UUID。
② 安装mysql第三方模块
npm install mysql --save
③ 封装连接数据库的API:connect.js
// 连接数据库 /** * 封装操作数据库的通用API */ const mysql = require('mysql'); exports.base = (sql,data,callback) => { //创建数据库链接 const connection = mysql.createConnection({ host: 'localhost', //数据库所在的服务器的域名或者IP地址 user: 'root', //登录数据库的账号 password: '你的密码', database: 'todolist' // 数据库名称 }); //执行连接操作 connection.connect(); //操作数据库(数据库操作也是异步的,异步不同通过返回值来处理,只能通过回调函数) connection.query(sql,data,(err,results,fields) => { if(err) throw err; callback(results); }); //关闭数据库 connection.end(); }
2. 展示任务列表
① 准备一个放置任务列表的数组
② 向服务器端发送请求,获取已存在的任务
③ 将已存在的任务存储在任务列表数组中
④ 通过模板引擎将任务列表数组中的任务显示在页面中
获取任务列表接口:
- 请求地址
- /todo/task
- 请求方式
- GET
- 返回值
- 数据库中所有的数据
[ { "id": "761fc28ea6f911ea981d1c3947e9a06a", "title": "今天要早起", "completed": 0 }, { "id": "c112fd6ea6f911ea981d1c3947e9a06a", "title": "今天要早睡", "completed": 0 }, { "id": "6114e4b2a7b611ea9b231c3947e9a06a", "title": "吃水果", "completed": 0 } ]
3. 添加任务
在文本框中输入任务名称,敲击回车键,就可以把任务添加到任务列表中
① 为文本框绑定键盘抬起事件,在事件处理函数中判断当前用户敲击的是否是回车键
② 当用户敲击回车键的时候, 判断用户在文本框中是否输入了任务名称
③ 向服务器端发送请求,将用户输入的任务名称添加到数据库中,同时将任务添加到任务数组中
④ 通过模板引擎将任务列表数组中的任务显示在页面中
添加任务接口:
- 请求地址
- /todo/addTask
- 请求方式
- POST
- 参数
参数名 | 说明 |
title | 任务名称 |
- 返回值
- 数据库中所有的数据
4. 删除任务
把鼠标放在某一任务上,会显示删除按钮,点击即可删除
① 为删除按钮添加点击事件
② 在事件处理函数中获取到要删任务的id
③ 向服务器端发送请求,根据ID删除任务,同时将任务数组中的相同任务删除
④ 通过模板引擎将任务列表数组中的任务重新显示在页面中
存在一个问题:在HTML页面中,一开始是没有li标签的,也就没有删除按钮,li标签是通过模板引擎后期追加到页面当中的,所以我们一开始不能选择到删除按钮,也就不能添加点击事件。
如何解决呢?使用事件委托的方式,将事件绑定到它的父元素ul标签上,并且使用on()方法。因为ul标签是存在的,我们可以获取到它,根据事件的冒泡性,就可以触发点击事件了。
删除任务接口:
- 请求地址
- /todo/deleteTask
- 请求方式
- GET
- 参数
参数名 | 说明 |
id | 要删除的任务id字段 |
- 返回值
- 数据库中所有的数据
5. 更改任务状态
鼠标放在某一任务上,双击任务名称,该任务就处于可编辑状态,当文本框离开焦点的时候,文本框中的任务名称就更改成功了
① 为任务复选框添加onchange事件
② 在事件处理函数中获取复选框是否选中
③ 向服务器端发送请求,将当前复选框的是否选中状态提交到服务器端
④ 将任务状态同时也更新到任务列表数组中
⑤ 通过模板引擎将任务列表数组中的任务重新显示在页面中并且根据任务是否完成为li元素添加completed类名
6. 修改任务名称
① 为任务名称外层的label标签添加双击事件,同时为当前任务外层的li标签添加editing类名,开启编辑状态
② 将任务名称显示在文本框中并让文本框获取焦点
③ 当文本框离开焦点时,将用户在文本框中输入值提交到服务器端,并且将最新的任务名称更新到任务列表数组中
④ 使用模板引擎重新渲染页面中的任务列表。
修改任务接口:
- 请求地址
- /todo/modifyTask
- 请求方式
- POST
- 参数
参数名 | 说明 |
id | 要修改任务的id字段 |
title | 任务名称 |
completed | 任务状态:0表示未完成,1表示已完成 |
- 返回值
- 数据库中所有的数据
7. 计算未完成任务数量
① 准备一个用于存储未完成任务数量的变量
② 将未完成任务从任务数组中过滤出来
③ 将过滤结果数组的长度赋值给任务数量变量
④ 将结果更新到页面中
8. 显示未完成任务
① 为active按钮添加点击事件
② 从任务列表数组中将未完成任务过滤出来
③ 使用模板引擎将过滤结果显示在页面中
9. 清除已完成任务
① 为clear completed按钮添加点击事件
② 向服务器端发送请求将数据库中的已完成任务删除掉
③ 将任务列表中的已完成任务删除
④ 使用模板引擎将任务列表中的最后结果显示在页面中
目录结构如下:
CSS样式文件:
/* base.css */ hr { margin: 20px 0; border: 0; border-top: 1px dashed #c5c5c5; border-bottom: 1px dashed #f7f7f7; } .learn a { font-weight: normal; text-decoration: none; color: #b83f45; } .learn a:hover { text-decoration: underline; color: #787e7e; } .learn h3, .learn h4, .learn h5 { margin: 10px 0; font-weight: 500; line-height: 1.2; color: #000; } .learn h3 { font-size: 24px; } .learn h4 { font-size: 18px; } .learn h5 { margin-bottom: 0; font-size: 14px; } .learn ul { padding: 0; margin: 0 0 30px 25px; } .learn li { line-height: 20px; } .learn p { font-size: 15px; font-weight: 300; line-height: 1.3; margin-top: 0; margin-bottom: 0; } #issue-count { display: none; } .quote { border: none; margin: 20px 0 60px 0; } .quote p { font-style: italic; } .quote p:before { content: '“'; font-size: 50px; opacity: .15; position: absolute; top: -20px; left: 3px; } .quote p:after { content: '”'; font-size: 50px; opacity: .15; position: absolute; bottom: -42px; right: 3px; } .quote footer { position: absolute; bottom: -40px; right: 0; } .quote footer img { border-radius: 3px; } .quote footer a { margin-left: 5px; vertical-align: middle; } .speech-bubble { position: relative; padding: 10px; background: rgba(0, 0, 0, .04); border-radius: 5px; } .speech-bubble:after { content: ''; position: absolute; top: 100%; right: 30px; border: 13px solid transparent; border-top-color: rgba(0, 0, 0, .04); } .learn-bar > .learn { position: absolute; width: 272px; top: 8px; left: -300px; padding: 10px; border-radius: 5px; background-color: rgba(255, 255, 255, .6); transition-property: left; transition-duration: 500ms; } @media (min- 899px) { .learn-bar { width: auto; padding-left: 300px; } .learn-bar > .learn { left: 8px; } }
/* index.css */ html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #4d4d4d; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 300; } :focus { outline: 0; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp input::input-placeholder { font-style: italic; font-weight: 300; color: #e6e6e6; } .todoapp h1 { position: absolute; top: -155px; width: 100%; font-size: 100px; font-weight: 100; text-align: center; color: rgba(175, 47, 47, 0.15); -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; border: 0; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .new-todo { padding: 16px 16px 16px 60px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } label[for='toggle-all'] { display: none; } .toggle-all { position: absolute; top: -55px; left: -12px; width: 60px; height: 34px; text-align: center; border: none; /* Mobile Safari */ } .toggle-all:before { content: '❯'; font-size: 22px; color: #e6e6e6; padding: 10px 27px 10px 27px; } .toggle-all:checked:before { color: #737373; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: 506px; padding: 12px 16px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .glyphicon-check:before { color: #d9d9d9; } .glyphicon-uncheck:before { color: #ffffe8; } .todo-list li .toggle { text-align: center; width: 40px; height: auto; position: absolute; top: 0; bottom: 0; margin: 15px 0 0 10px; border: none; -webkit-appearance: none; appearance: none; } .todo-list li label { word-break: break-all; padding: 15px 60px 15px 15px; margin-left: 45px; display: block; line-height: 1.2; transition: color 0.4s; font-weight: 300; } .todo-list li.completed label { color: #d9d9d9; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #cc9a9a; margin-bottom: 11px; transition: color 0.2s ease-out; } .todo-list li .destroy:hover { color: #af5b5e; } .todo-list li .destroy:after { content: '×'; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { color: #777; padding: 10px 15px; height: 38px; text-align: center; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a:hover { border-color: rgba(175, 47, 47, 0.1); } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 20px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #bfbfbf; font-size: 10px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } .toggle-all { -webkit-transform: rotate(90deg); transform: rotate(90deg); -webkit-appearance: none; appearance: none; } } @media (max- 430px) { .footer { height: 50px; } .filters { bottom: 10px; } }
index.html文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Todo List</title> <link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/css/base.css"> <link rel="stylesheet" href="/css/index.css"> <!-- 进度条的下载地址:https://github.com/rstacruz/nprogress --> <link rel="stylesheet" href="/js/nprogress/nprogress.css"> </head> <body> <section class="todoapp"> <header class="header"> <h1>todos</h1> <input type="text" class="new-todo" placeholder="What needs to be done?" autofocus id="task"> </header> <!-- This section should be hidden by default and shown when there are todos --> <section class="main"> <input class="toggle-all" type="checkbox"> <ul class="todo-list" id="todo-list"></ul> </section> <!-- This footer should hidden by default and shown when there are todos --> <footer class="footer"> <!-- This should be `0 items left` by default --> <span class="todo-count"><strong id="count">0</strong> item left</span> <!-- Remove this if you don't implement routing --> <ul class="filters"> <li> <a class="selected" href="javascript:;">All</a> </li> <li> <a href="javascript:;">Active</a> </li> <li> <a href="javascript:;">Completed</a> </li> </ul> <!-- Hidden if no completed items are left ↓ --> <button class="clear-completed">Clear completed</button> </footer> </section> <script src="/js/jquery.min.js"></script> <script src="/js/template-web.js"></script> <script src="/js/nprogress/nprogress.js"></script> <!-- 任务列表模板 --> <script type="text/html" id="taskTpl"> {{each tasks}} <li class="{{$value.completed ? 'completed' : ''}}"> <div class="view"> <!-- 复选框小按钮使用bootstrap中的字体图标 --> <!-- 判断任务状态,如果是0.则是未选中状态,否则是选中状态,更改对应的样式 --> <span class="{{$value.completed == '0' ? 'glyphicon glyphicon-unchecked' : 'glyphicon glyphicon-check'}} toggle"></span> <!-- <input class="toggle" type="checkbox"> --> <label>{{$value.title}}</label> <button class="destroy" data-id="{{$value.id}}"></button> </div> <input class="edit"> </li> {{/each}} </script> <script type="text/javascript"> // 用于存放任务列表的数组 var taskAry = []; // 选择任务列表容器 var taskBox = $('#todo-list'); // 添加任务的文本框 var taskInp = $('#task'); // 用于存储未完成任务数量的strong标签 var strong = $('#count'); // 当页面中有ajax请求发送时触发 $(document).on('ajaxStart', function () { NProgress.start() }); // 当页面中有ajax请求完成时触发 $(document).on('ajaxComplete', function () { NProgress.done() }); // 向服务器端发送请求 获取已经存在的任务 $.ajax({ url: '/todo/task', type: 'get', success: function (response) { // 将已存在的任务存储在taskAry变量中 taskAry = response; // 将数据和html拼接好并显示在页面中 render(); // 计算未完成任务数量 calcCount(); } }) // 获取文本框并且添加键盘抬起事件 taskInp.on('keyup', function (event) { // 如果用户敲击的是回车键 if (event.keyCode == 13) { // 判断用户是否在文本框中输入了任务名称 var taskName = $(this).val(); // 如果用户没有在文本框中输入内容 if (taskName.trim().length == 0) { alert('请输入任务名称') // 阻止代码向下执行 return; } // 向服务器端发送请求 添加任务 $.ajax({ type: 'post', url: '/todo/addTask', contentType: 'application/json', data: JSON.stringify({title: taskName}), success: function (response) { // console.log(response); // 清空任务列表,然后重新渲染 taskAry = response; // 拼接字符串 将拼接好的字符串显示在页面中 render(); // 添加完成之后清空文本框中的内容 taskInp.val(''); // 计算未完成任务数量 calcCount(); } }) } }); // 拼接字符串 将拼接好的字符串显示在页面中 function render() { // 字符串拼接 var html = template('taskTpl', { tasks: taskAry }); // 将拼接好的字符串显示在ul标签中 taskBox.html(html); } // 当用户点击删除按钮时触发ul标签身上的点击事件 taskBox.on('click', '.destroy', function () { // 要删除的任务的id var id = $(this).attr('data-id'); // 向服务器端发送请求删除 任务 $.ajax({ url: '/todo/deleteTask', type: 'get', data: { id: id }, success: function (response) { // 清空任务列表,然后重新渲染 taskAry = response; // 重新将任务数组中的元素显示在页面中 render(); // 计算未完成任务数量 calcCount(); } }) }); // 当用户改变任务名称前面的复选框状态时触发 taskBox.on('click', '.toggle', function () { // 代表复选框是否选中 true 选中 false 未选中的 let status = $(this).prop("className"); if (status.includes("unchecked")) { // 点击了复选按钮之后,未选中状态则要改为选中状态,status属性也要取反,以便传到数据库中 status = 1; $(this).prop("className","glyphicon glyphicon-checked toggle"); } else { status = 0; $(this).prop("className","glyphicon glyphicon-unchecked toggle"); } // 当前点击任务的id const id = $(this).siblings('button').attr('data-id'); // 向服务器端发送请求 更改任务状态 $.ajax({ type: 'post', url: '/todo/modifyTask', data: JSON.stringify({id: id, completed: status}), contentType: 'application/json', success: function (response) { // 清空任务列表,然后重新渲染 taskAry = response; // 将数组中任务的最新状态更新到页面中 render(); // 计算未完成任务数量 calcCount(); } }); }); // 当双击事件名称的时候触发 // 当label标签发生双击事件的时候触发 taskBox.on('dblclick', 'label', function () { // 让任务处于编辑状态(注意此时任务框是空的,把之前的任务名给清空了) $(this).parent().parent().addClass('editing'); // 将任务名称显示在文本框中,$(this).text()是获取到里面的内容 $(this).parent().siblings('input').val($(this).text()); // 让文本框获取焦点 $(this).parent().siblings('input').focus(); }); // 当文本框离开焦点的时候 taskBox.on('blur', '.edit', function () { // 最新的任务名称 var newTaskName = $(this).val(); // 编辑任务的id var id = $(this).siblings().find('button').attr('data-id'); // 向服务器端发送请求 修改任务名称 $.ajax({ url: '/todo/modifyTask', type: 'post', data: JSON.stringify({id: id, title: newTaskName}), contentType: 'application/json', success: function (response) { // 清空任务列表,然后重新渲染 taskAry = response; // 将数组中任务的最新状态更新到页面中 render(); // 计算未完成任务数量 calcCount(); } }); }); // 用于计算未完成任务的数量 // 只要任务列表中的任务有状态变化的时候就需要调用这个函数 function calcCount () { // 存储结果的变量 var count = 0; // 将未完成的任务过滤到一个新的数组中 var newAry = taskAry.filter(item => item.completed == 0); // 将新数组的长度赋值给count count = newAry.length; // 将未完成的任务数量显示在页面中 strong.text(count) } </script> </body> </html>
JS文件:
// app.js文件 // 引入express框架 const express = require('express'); // 路径处理模块 const path = require('path'); const bodyParser = require('body-parser'); // 创建web服务器 const app = express(); // 静态资源访问服务功能 app.use(express.static(path.join(__dirname, 'public'))); // 引入bodyParser后一定要加上这一句,否则post请求接收不到参数 app.use(bodyParser.urlencoded({extended: false})); // 处理post请求参数:application/json app.use(bodyParser.json()); // 导入todo路由案例 const todoRouter = require('./route/todo.js'); // 当客户端的请求路径以/todo开头时 app.use('/todo', todoRouter); // 监听端口 app.listen(3000); //控制台提示输出 console.log('loading...');
// todo.js文件 // 引入express框架 const express = require('express'); // 引入数据库操作模块 const db = require('../model/connect.js'); // 创建页面路由 const todoRouter = express.Router(); // 引入joi模块 const Joi = require('joi'); // 查询数据库的数据(这个方法暂时还不能用,问题:不能正常显示数据) function selectData() { let sql = 'select * from task'; let data = null; db.base(sql,data,(result) => { return result; }); } let result = selectData(); // 获取任务列表 todoRouter.get('/task', (req, res) => { // 只需要用res.send()方法做出响应,将数据返回给客户端就行了 // 不用使用res.render()将数据渲染到页面上 let sql = 'select * from task'; let data = null; db.base(sql,data,(result) => { res.send(result); }); }); // 添加任务 todoRouter.post('/addTask', (req, res) => { // 接收客户端传递过来的任务名称 // 注意,是post请求,所以用的是req.body。 // 如果在测试时req.body的值一直是空的话,那就检查一下是否引入了处理post请求参数的中间件 const { title } = req.body; // 验证规则 const schema = { title: Joi.string().required().min(2).max(30) }; // 验证客户端传递过来的请求参数 const { error } = Joi.validate(req.body, schema); // 验证失败 if (error) { // 将错误信息响应给客户端 return res.status(400).send({message: error.details[0].message}) } // 执行插入操作 let sql_insert = 'insert into task set ?'; let data_insert = { title: title, completed: 0 }; db.base(sql_insert,data_insert,(result_insert) => { if(result_insert.affectedRows) { // 插入成功后重新查询数据,返回给客户端,让客户端重新渲染 let sql_select = 'select * from task'; let data_select = null; db.base(sql_select,data_select,(result_select) => { // 只需要用res.send()方法做出响应,将数据返回给客户端就行了 res.send(result_select); }); } }); }); // 删除任务 todoRouter.get('/deleteTask', async (req, res) => { // 要删除的任务id const { id } = req.query; // 验证规则 const schema = { id: Joi.string().required().regex(/^[0-9a-zA-Z]{32}$/) } // 验证客户端传递过来的请求参数 const { error } = Joi.validate(req.query, schema); // 验证失败 if (error) { // 将错误信息响应给客户端 return res.status(400).send({message: error.details[0].message}) } // 删除任务 // 根据id删除用户 let sql_delete = 'delete from task where id=?' let data_delete = [id]; db.base(sql_delete,data_delete,(result_delete) => { if(result_delete.affectedRows) { // 插入成功后重新查询数据,返回给客户端,让客户端重新渲染 let sql_select = 'select * from task'; let data_select = null; db.base(sql_select,data_select,(result_select) => { // 只需要用res.send()方法做出响应,将数据返回给客户端就行了 res.send(result_select); }); } }); }); // 修改任务 todoRouter.post('/modifyTask', (req, res) => { // console.log(req.body.completed); // console.log(req.body.title); // 执行修改操作 let sql_update; let data_update; if (req.body.completed !== undefined) { sql_update = 'update task set completed=? where id=?'; data_update = [req.body.completed, req.body.id]; } else if (req.body.title !== undefined) { sql_update = 'update task set title=? where id=?'; data_update = [req.body.title, req.body.id]; } db.base(sql_update,data_update,(result_update) => { if(result_update.affectedRows){ // 修改成功后重新查询数据,返回给客户端,让客户端重新渲染 let sql_select = 'select * from task'; let data_select = null; db.base(sql_select,data_select,(result_select) => { // 只需要用res.send()方法做出响应,将数据返回给客户端就行了 res.send(result_select); }); } }); }); // 将todo案例路由作为模块成员进行导出 module.exports = todoRouter;
上面代码还有一些问题需要改进:
问题一:todo.js中重复代码率很高,查询数据库的代码每个方法都需要用,但是把查询数据库的代码提取出来封装为一个函数,调用的时候有点问题,不能正常显示数据。
问题二:操作数据库的代码应该单独写到一个js文件中,不能直接写在todo.js里面
问题三:还有一些功能没实现,下面的All、 Active、Completed和Clear Completed按钮的点击事件对应的功能还没有完成