本系列教程将带领大家完成一个 B/S 架构的”在线账本”, 希望通过这个实例教程能帮助大家打通 “任督二脉”, 把所学知识融汇贯通.

此为系列教程的第一部分: 纯前端代码实现的记账本.

只要你有一点 HMTL, CSS, Javascript 的基础就可以跟上我们的脚步. 当然, 了解一点点 Bootstrap 和 jQuery 将会更轻松.

本教程假设你已熟悉 HTML, CSS, 对 Javascript, Java 有一定基础, 对数据库管理系统 (MySQL) 有所了解.

倘若以上假设不成立… 也不要紧, 跟着来, 遇到不明白的问下度娘, 或给我写信.

1. 先看效果

在线记账本最终实现效果

怎么样, 还行吧? 有没有兴趣继续跟着学?

本系列教程分 (一) 至 (三) 节, 运行效果相似, 均如上面动图所示, 只是使用到的技术逐渐加深. 放一个传送门在这里:

( 一 ) 纯前端代码实现的记账本

( 二 ) 客户端 (前端) + 服务器端 (后端) 模式的在线记账本

( 三 ) 真正的在线记账本: 客户端 + 服务器 + 数据库

2. 构建前端静态页面

本节内容虽然听上去有些 low, 只是一个纯前端实现, 但它是第 (二)、(三) 节那些更 “高大上” 的东西的基础.

前端页面的构建主要涉及的技术有: HTML, CSS, Javascript.

当然, 为了视觉效果上漂亮些, 我们加入了 bootstrap, 同时为了让我们的 Javascript 代码写的轻松些, 我们还用到了 jQuery, jquery-confirm, moment.js.

相对而言, 大多数同学对 HTML 和 CSS 的掌握还算不错. 请先试着自己完成如下图所示界面的制作:

前端静态页面效果图-1

本教程重点不在 HTML 和 CSS 代码的编写, 下面将直接给出代码, 只作简要解释.

若想偷懒, 直接复制或是下载 便是, 但index.htmlindex.css中的代码请仔细研读并弄懂.

下载到的压缩包里是不是少了 index.js 文件 ?

哈哈~ 我故意删掉的! 防止你直接解压运行一下, 看个开心就了事~

本节的重点是前端业务逻辑的实现 ( index.js ), 千万别想着在这个环节上偷懒, 否则就失去本教程的意义了!

Code-2.1 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>在线记账本</title>

<!-- Bootstrap -->
<link rel="stylesheet" href="css/bootstrap.css">
<!-- jquery-confirm 的样式 -->
<link rel="stylesheet" href="css/jquery-confirm.3.3.min.css">

<!-- 自定义样式 -->
<link rel="stylesheet" href="css/index.css">

<!-- jQuery -->
<script type="text/javascript" src="js/jquery-1.11.1.min.js"></script>

<!-- jQuery Confirm, jQuery插件, 用于弹出消息框 -->
<script type="text/javascript" src="js/jquery-confirm.3.3.min.js"></script>
<script type="text/javascript" src="js/jquery-confirm-bailey.js"></script>

<!-- Bootstrap -->
<script type="text/javascript" src="js/bootstrap.js"></script>

<!-- Moment, 处理时间/日期的第三方库 -->
<script type="text/javascript" src="js/moment.2.24.0.js"></script>
<script type="text/javascript" src="js/moment.locale.2.24.0.js"></script>

<!-- 本页面自定义 javascript 代码 -->
<script type="text/javascript" src="js/index.js"></script>

</head>
<body>
<div id="container">
<div id="records" class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title"><i class="icon glyphicon glyphicon-list"></i>记账本</h3>
<a id="btn-add-bill" title="打开/关闭记账面板"><i class="icon glyphicon glyphicon-menu-right"></i></a>
</div>
<div class="panel-body">
<ul id="bill-items">
<li class="record">
<div class="date-time">
<h2 class="date" data-field="date">05/13</h2>
<p class="time" data-field="time">12:49:36</p>
</div>
<div class="content">
<h4 class="media-heading io in" data-field="amount">+ 800.00</h4>
<p class="memo" data-field="memo">老爸发生活费啦</p>
</div>
</li>
</ul>
</div>
<div class="panel-footer">
<img id="progress" src="img/spinner.gif">
<h4 id="balance" style="color: green;">
结余: <span>+ 800</span>
</h4>
</div>
</div>

<div id="panel-bill-editor" class="panel panel-info" style="display: none;">
<div class="panel-heading">
<h3 class="panel-title"><i class="icon glyphicon glyphicon-pencil"></i>账单</h3>
</div>
<div class="panel-body">
<div class="input-group">
<label>金额</label>
<input id="nb-amount" type="number" max="100000" class="form-control" placeholder="收入/支出金额">
</div>
<div class="input-group">
<label>备注</label>
<input id="nb-memo" type="text" maxlength="50" class="form-control">
</div>
</div>
<div class="panel-footer">
<button id="btn-confirm-add" class="btn btn-sm btn-info"><i class="icon glyphicon glyphicon-floppy-save"></i>记一笔</button>
</div>
</div>
</div>
</body>
</html>

对以上代码作简要解释:

  • head 部分: 引入各种 CSS 和 javascript
    • 第 8 行 和 第 23 行引入bootstrap, 加入bootstrap 预定义样式和 boostrap 组件代码. 若对 bootstrap 知之甚少, 强烈建议课外自学. ( 现在别忙着去看 )
    • 第 10 行 和 第19 行引入了一个名为jquery-confirm的 jQuery 插件, 用于弹出漂亮的消息框.
    • 第 16 行引入 jQuery 库, 这里我们使用的是 1.11.1 版本. 若你使用别的版本, 请注意兼容性问题.
    • 第 13 行 和 第 30 行 引入的是针对 index.html 页面写的样式表和 javascript 脚本.
  • body 部分: 页面主体部分的 HTML 代码
    • 页面主体部分分作左、右两个部分: 左侧为账本记录列表 ( 35 ~ 60 行, div#records), 展示已登记的账单列表; 右侧为单条账单的编辑界面 ( 62 ~ 79 行, div#panel-bill-editor), 注意此面板初始时为隐藏状态.
    • 注意其中的第 42 ~ 51 行 ( <li class="record"> ... </li>), 这是账单列表中的一条信息.

篇幅所限, index.css 的代码就不直接附在本文中了. 若有需要参考, 可直接下载前端部分代码.

3. 前端逻辑实现

这一部分我们将编写index.js中的代码, 实现前端逻辑. 完成后你将得到一个 “纯前端代码实现的记账本”. ( 即: 没有服务器端, 也没有数据库 )

本部分代码均放置于 index.js

深吸一口气~

我们开始吧…

3.1 MVC 模型

首先, 我们要强调一个重要的软件设计模式: MVC.

MVC 模型是一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码.

可以毫不夸张地说, MVC 模型指引着我们各种软件设计的大方向,

根据 MVC 模式思想, 我们可以把大到一个软件系统, 小到一个功能模块分解为 M (Model)、V (View) 和 C (Controller) 三个部分(层). 其中:

  • Model - 数据模型层: 使用合适的数据结构存储着软件(模块)中的数据.
  • View - 视图层: 粗浅来说, 它是用户看到的界面呈现. 更准确来说, 是软件(模块)中更靠近”用户”的那一层 (不一定是可视化的界面). View 是 Model 的表现.
  • Controller - 控制层: 实现了 Model 到 View 的映射 ( 根据 Model 来呈现 View ), 以及 View 到 Model 的反馈 ( 根据 View 层的变化来更新 Model ). 也就是说, Controller 实现了 Model 和 View 之间的双向映射 (同步).

盗个图来说, 大概就是这样:

MVC模式

3.2 设计账本数据模型

扯了半天 MVC, 你现在应该知道 Model 算是三兄弟中的老大了吧~

所以, 我们可以先从数据模型 (Model) 设计开始, 代码如下:

Code-3.2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 账本数据模型
var bills = [{
id: 3, // 账单ID
time: new Date('2019-05-15 19:27:38'), // 记账时间
amount: 80000, // 金额, 单位:分
memo: '老爸发生活费了' // 备注
}, {
id: 2,
time: new Date('2019-05-15 17:44:31'),
amount: -1470,
memo: '食堂打的饭, 味道还真是一般'
}, {
id: 1,
time: new Date('2019-05-15 17:27:12'),
amount: -1890,
memo: '买本《小王子》, 补个童年'
}, {
id: 0,
time: new Date('2019-05-14 18:19:34'),
amount: -1200,
memo: '今晚吃饺子'
}];
  • 我们使用一个数组来存储账本数据, 其中每一个数组元素为一条账单记录. 上面的代码是测试时初始的模拟数据(账单按时间降序排列)

  • 第 3 ~ 6 行的注释说明了一条账单数据中各属性的含意

  • 注意: 金额的单位为”分”, 即 amount: 80000 代表金额为 800.00 元. 使用 “分” 为单位可令金额数值表达为整型, 避免了计算过程中的精度问题.

    不明白? 试试 alert( 1 - 0.7 );会出现什么…

3.3 展示账单列表

接下来, 我们来实现账单列表的展示, 即最终运行效果图中左侧列表的显示.

在 3.2 节我们已经完成数据模型的设计, 这里将实现 Model → View 的映射.

先考虑一条账单数据的展示.

对于单条账单, 其数据模型大概是如下的样子:

Code-3.3.1

1
2
3
4
5
6
{
id: 0, // 账单ID
time: new Date(), // 记账时间
amount: 80000, // 金额, 单位:分
memo: '老爸发生活费了' // 备注
}

注意前文 index.html ( Code-2.1 ) 代码中第 42 ~ 51 行:

Code-3.3.2

1
2
3
4
5
6
7
8
9
10
<li class="record">
<div class="date-time">
<h2 class="date" data-field="date">05/13</h2>
<p class="time" data-field="time">12:49:36</p>
</div>
<div class="content">
<h4 class="media-heading io in" data-field="amount">+ 800.00</h4>
<p class="memo" data-field="memo">老爸发生活费啦</p>
</div>
</li>

有没有看出什么门道 ? 对了, 上面两块代码 ( Code-3.3.1Code-3.3.2 ) 其实是对应的.

只是 Code-3.3.1 是 javascript 代码, 是所谓的数据模型. 而 Code-3.3.2 是 HTML 代码, 相对而言, 可称其为 View 层代码.

那现在我们的任务应该很明确了, 就是要根据 Code-3.3.1 所示的一坨数据 ( javascript对象 ) 形成 Code-3.3.2 所示的<li>….</li> 的 DOM ( 文档对象模型 ).

注意: 不是生成 Code-3.3.2 所示的 HTML代码, 而是形成这块代码对应的 DOM

DOM 其实是浏览器世界的 Model 层, 也就是说, 后续我们将操纵 DOM, 浏览器发现 DOM 变化后将自动刷新网页界面. ( 我们不是直接操纵网页上的元素, 而是操纵它们的 DOM, 从而间接改变网页的内容 )

Code-3.3.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 一条账单的 HTML 模板
// 对应 Code-3.3.2, 只是把其中类似 "05/13" 这样的信息删除了, 因为这些信息应是变量值, 而不是"硬编码"
// 注意, 变量 billItemTemplete 的值是一个字符串
var billItemTemplete = '<li class="record">' +
'<div class="date-time">' +
'<h2 class="date" data-field="date"></h2>' +
'<p class="time" data-field="time"></p>' +
'</div>' +
'<div class="content">' +
'<h4 class="media-heading io" data-field="amount"></h4>' +
'<p class="memo" data-field="memo"></p>' +
'</div>' +
'</li>';

// 创建一条账单的 DOM
// 参数 bill 是一条账单的数据, 即 Code-3.3.1 所示的一坨数据
function createBillItemDom(bill) {
// 借助 jQuery 直接使用账单模板形成 DOM
// $item 为一条账单的 DOM
var $item = $(billItemTemplete);

// 将账单时间构建成 moment 对象, 以便后续格式化输出
// moment() 来自第三方库 moment.js
var dt = moment(bill.time);

// 遍历所有含有 data-field 属性的元素
// 并使用数据模型 (bill) 中的值替换此元素的内容(innerText)
$item.find('[data-field]').each(function (i, ele) {
// 针对每一个含有"data-field"属性的元素, 此匿名函数将被执行一次
// 其中参数 i 为顺序号, ele 为当前遍历到的元素

// 将 ele 这个普通的 DOM 元素封装成 jQuery 对象, 以便处理.
var $ele = $(ele);

// 读取当前元素的 data-field 属性值
var field = $ele.attr('data-field');

// 根据 data-field 属性值不同, 分别作不同的处理
switch (field) {
case 'date': // 若 data-field = date, 填入使用 moment.js 格式化后的 "月/日"
$ele.text(dt.format('MM/DD'));
break;
case 'time': // 若 data-field = date, 填入使用 moment.js 格式化后的 "时:分:秒"
$ele.text(dt.format('HH:mm:ss'));
break;
case 'amount': // 金额字段
$ele.text(bill.amount);
// 若金额大于 0, 则给当前元素(金额字段)添加样式类 "in" (显示为绿色)
if (bill.amount > 0) {
$ele.addClass('in');
}
break;
default: // 对于其余元素(如: memo), 直接使用 bill 中的相应属性值填充
$ele.text(bill[field]);
}
});

// 返回构建好的 DOM
return $item;
}

妈呀, 看上去好复杂… 虽然代码中已经写满了注释, 但还是进一步解释一下:

  • 第 20 行 var $item = $(billItemTemplete); , 这里借助 jQuery 来构建 DOM 模型.

    若要动态创建一个<p></p> 的 DOM, 使用纯原生方法可以这样写: document.createElement("p");, 但若使用 jQuery, 则可以写成: $("<p>"), 是不是很简单?

    而如果我们要构建如 Code-3.3.2 那样一大块相对复杂的元素, 使用原生方法就太麻烦了… 此时借助 jQuery 显然就简单多了 ( 参见第 4 ~ 13 行和第 20 行代码 )

  • 第 24 行 var dt = moment(bill.time); , 使用账单时间 ( bill.time ) 构建一个 moment 对象, 第 41, 44 行分别用其格式化输出账单的日期时间部分.

    moment.js 是一个很不错的专门处理日期时间型数据的工具包.

    建议抽空自学: moment.js 官方网站 / moment.js 中文网站

    现在, 暂时不要跑去看 moment.js 文档, 这里你知道我们使用它来格式化输出日期时间就行了, 以后再慢慢去看, 现在请继续…

  • 第 28 ~ 56 行或许是上面这段代码中最难理解的部分, 我们慢慢来…

    • 注意到 HTML 模板中, 凡是需要使用变量值填充的元素, 我们都添加了一个data-field属性, 这样便于我们找到这些元素. 并且, 我们将此元素应对应的数据字段名藏在了 data-field 属性值那里, 例如:

      <p class="memo" data-field="memo"></p>

      表示此 <p>元素的内容对应的数据字段名为 memo

    • 这样我们就可以使用 jQuery 的 .find()函数找到所有含有 data-field 属性的元素

      如: $item.find("[data-field]") 即可找到 $item 元素中所有含 data-field 属性的子元素

    • 对于找到的所有含有 data-field 属性的子元素集合, 使用.each()函数即可轻松遍历.

      如: $item.find("[data-field]").each(function(i, element){ ...})

      注意 each()形参表中的匿名函数function(i, ele){...}, 此函数在遍历到每一个元素时将被自动回调一次, 参数 ielement 分别为顺序号(这是遍历到的第几个元素) 和当前遍历到的元素. 第 25 ~ 55 行的代码都是放在这个匿名函数里的.

  • 第 36 行读取当前遍历到的元素的 data-field 属性中藏着的数据字段名信息, 并在第 39 ~ 55 行 分别进行了处理. 这段代码就不详细解释了, 参看一下注释.

终于讲完了…

如果看 Code-3.3.3 的代码让你一脸迷惑, 千万不要丧失信心. 这段代码算是本节中最难理解的部分了, 挺过这一关, 你将轻松面对后面的内容.

放慢脚步, 结合注释和说明多思考, 你一定能理解!

代码里出现大量了jQuery 的东西, 抽空读一下本博客另一篇文章: jQuery入门精要, 或许会有帮助.

接下来, 如果要生成整个账本数据 (bills数组) 的 DOM 元素, 那不就是一个循环吗?

看现在这段代码

Code-3.3.4

1
2
3
4
5
6
7
8
9
10
11
12
13
// 显示账本中所有账单
function showBillItems() {
// 账单列表的容器, Code-2.1 中 41 行的那个 <ul>
var $billItems = $('#bill-items');

// 清空账单列表
$billItems.empty();

// 遍历整个 bills 数组, 构建每一个账单的 DOM 模型, 并将其"装入" 账单列表容器
for (var i = 0; i < bills.length; i++) {
$billItems.append(createBillItemDom(bills[i]));
}
}

Code-3.3.4 中的showBillItems()函数实现了整个账本中所有账单的展示.

接下来的 Code-3.3.5 实现 “结余” 的展示:

Code-3.3.5

1
2
3
4
5
6
7
8
9
10
11
12
13
// 显示结余
function refreshBalance() {
// 计算 "结余"
var balance = 0;
for (var i = 0; i < bills.length; i++) {
balance += bills[i].amount;
}

// 刷新界面
var $balance = $("#balance");
$balance.css('color', (balance > 0) ? 'green' : '#ce4844');
$balance.find('span').text(balance);
}

还记得吗? 我们的金额的单位是 “分”, 这给用户看到了, 会显得很别扭, 于是补充如下一个函数, 用于格式化最终显示时的金额:

Code-3.3.6

1
2
3
4
5
6
7
8
9
10
// 格式化金额显示
// 参数说明
// money - 金额, 单位 "分"
// signed - 是否强制显示正负号, 例如: 账单列表中, 收入800要显示为 "+ 800", 而结余处则正数无须显示"+"
function formatMoney(money, signed) {
// 换算成 "元"
var m = money / 100.0;
// 在数据前添加 "+ / -"
return (m > 0 ? (signed ? '+ ' : '') : ((m === 0) ? '' : '- ')) + Math.abs(m);
}

有了formatMoney()这个函数, 金额值显示出来就人性化多了…

当然, 我们需要把显示金额的地方都调整一下:

  • 修改 Code-3.3.3 中的第 47 行为: $ele.text(formatMoney(bill.amount, true));
  • 修改 Code-3.3.5 中的第 12 行为: $balance.find('span').text(formatMoney(balance, false));

Code-3.3.6 中第 9 行的那个语句看着点有复杂, 可以展开为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var rt;
if (m > 0) {
if (signed) { // signed == true
rt = '+ ';
} else {
rt = '';
}
} else {
if (m == 0) {
rt = '';
} else {
rt = '- ';
}
rt += Math.abs(m); // Math.abs(m) 为求 m 的绝对值
}
return rt;

最后帮你一次, 以后要自己学会分析, 并习惯 Code-3.3.6 中第 9 行 的写法. 好处嘛, (1) 很简洁 (2) 显示自己很牛!

最后我们把上面这些代码(函数)组装起来, 借助 jQuery 在页面加载就绪时自动展示账单列表:

Code-3.3.7

1
2
3
4
5
6
7
$(document).ready(function () {
// 显示账单列表
showBillItems();

// 显示结余
refreshBalance();
});

看不懂 $(document).ready(function(){ …. }); 是什么鬼的看另一篇文章: jQuery入门精要

快在浏览器中打开 index.html 看一下效果吧~

小结:

  1. 本节我们实现了 “根据一条账单的数据, 构建对应的 DOM 元素” ( 数据 → 视图 )

  2. 本文开篇效果演示的动图中有两个细节还未实现:

    (a) 金额始终显示小数点后 2 位, 即: 即使是 800 元, 也会显示为 + 800.00 (账单列表), 或 800.00 (结余)

    (b) 结余数值在有变化时, 会快速显示数值变化过程. 例如: 结余从 600.00 变成 800.00 时, 会快速显示 620.00, 640.00, 660.00…. 直至 800.00 停止

    这些花哨的效果只是为了挑逗你的兴趣, 与业务逻辑关系不大, 因此本文就不详述了. 你若有兴趣, 自己想办法完成吧.

4. 新增账单

本节将实现新增账单, 即 “记一笔”功能.

4.1 显示/隐藏”新账单编辑面板”

最终完成的作品中, 右侧的 “账单编辑面板” 初始时是隐藏的, 当点击了 > 图标才显示出来, 而点击 < 图标则隐藏.

实现这个效果不难, 并不涉及数据模型的变化, 只是纯粹的界面效果.

Code-4.1.1

1
2
3
4
5
6
7
8
9
10
// 打开/关闭账单编辑面板
function toggleBillEditor() {
// 显示/隐藏"账单编辑面板"
$("#panel-bill-editor").toggle('fast');

// 切换展开(>) 和 关闭(<) 图标
var $btnIcon = $(this).find('i');
$btnIcon.toggleClass('glyphicon-menu-left');
$btnIcon.toggleClass('glyphicon-menu-right');
}

此外, 修改 Code-3.3.7 的代码如下(Code-4.1.2), 即添加其中的第 9 行代码.

监听 打开/关闭”账单编辑面板”按钮 (图标) 的点击事件, 触发 Code-4.1.1 中的 toggleBillEditor()函数.

Code-4.1.2

1
2
3
4
5
6
7
8
9
10
$(document).ready(function () {
// 显示账单列表
showBillItems();

// 显示结余
refreshBalance();

// 注册 打开/关闭"账单编辑面板"按钮 的点击事件监听
$('#btn-add-bill').click(toggleBillEditor);
});
  • Code-4.1.1 中出现了两个 jQuery 函数: toggle() 和 toggleClass(), 如果不明白其含意, 参看 jQuery入门精要 中”复合事件”一节
  • 打开/关闭”账单编辑面板”按钮中上图标并非使用图片标签 ( <img> ) 实现, 而是使用 “glyphicon 字体”, 即这个图标本质上是一个字符, 而非图片, 只是这个字符使用 glyphicon 字体显示出来就是*>/<*图标的样子. 使用字体呈现图标有诸多好处, 具体内容请查阅其它资料.

4.2 实现”新增账单”功能

4.2.1 监听”记一笔”按钮点击事件

在 Code-4.1.2 的基础上添加对 “记一笔” 按钮点击事件的监听, 以触发后续的 “新增账单” 逻辑. 即增加如下代码中的第 12 行.

Code-4.2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
$(document).ready(function () {
// 显示账单列表
showBillItems();

// 显示结余
refreshBalance();

// 注册 打开/关闭"账单编辑面板"按钮 的点击事件监听
$('#btn-add-bill').click(toggleBillEditor);

// 注册 "记一笔" 按钮点击事件监听
$('#btn-confirm-add').click(addBillItem);
});

至于上述代码中第 12 行涉及的 addBillItem 函数, 我们将在 4.2.3 节实现. 继续往下看就知道了…

4.2.2 更新账本数据模型

现在假设用户已经填写了新账单的信息(金额和备注), 点击了”记一笔”按钮. 接下来程序应该干什么呢 ?

还记得 MVC 吗? 又来了…

我们应该先考虑用户的这一系列的操作会对数据模型 (Model, 数据层) 产生什么影响, 因此我们先来实现更新数据模型的逻辑:

Code-4.2.2

1
2
3
4
5
6
7
8
9
10
11
12
// 新增一条账单的数据到账本数据模型
// 参数说明:
// bill - 新账单的数据, 一个对象, 形如 Code-3.3.1 中展示的那样一坨
// callback - 数据模型更新成功后的回调函数
function addBillItemData(bill, callback) {
// 将新账单数据插入账本数据模型(账单数组)的开头
// array.push() 是在数组尾部新增元素, array.unshift() 则是在开头插入新元素
bills.unshift(bill);

// 回调
callback(bill);
}

这段代码看似简单, 就定义了一个函数, 函数中就 2 行代码, 但似乎有点不容易看懂. 解释一下…

  • 假设已经有了新账单的数据 (bill, 一个对象), 我们将bill作为输入参数传递给函数 addBillItemData() , 在上面代码的第 8 行将新账单数据 bill 插入到了账单数组bills的开头. (bills 数组为账本数据, 参看 Code-3.2.1)

  • 函数 addBillItemData() 的第 2 个参数callback很特别, 它是一个函数, 由调用方传入.

    在完成了数据模型的更新后, 我们在第 11 行执行这个函数, 并将 bill 作为参数传入, 以触发后续的逻辑 (如: 刷新界面, 参看 Code-4.2.3 第 24~36 行).

    像 callback 这样意义的函数, 我们一般称作回调函数.

4.2.3 实现新增账单

看完 4.2.2 节估计你是似懂非懂, 到底那个 callback 函数是什么鬼 ? 看完下面的代码 (Code-4.2.3) 你就明白了.

在 Code-4.2.1 中的第 12 行我们注册了 “记一笔” 按钮的点击事件监听, 当用户点击 “记一笔” 按钮时, 将执行如下的addBillItem()函数.

  • 第 2 ~ 22 行主要是取得用户输入的账单信息, 并作校验, 将账单信息构建成一个 bill 对象 (第 17 ~ 22 行)

  • 第 25 ~ 37 行就是传说中的那个回调函数 callback() . 可以看到, 这个函数只是在更新数据模型后更新界面:

    (a) 构建新账单的 DOM, 并将其插入到界面上账单列表的开头

    (b) 清空金额和备注输入框

    (c) 刷新”结余”显示

  • 第 40 行, 调用上一节 ( 4.2.2 节 ) 中定义的那个addBillItemData()函数, 并将账单数据 (bill) 和 更新数据模型成功后的回调入口 (callback 函数) 作为参数传入.

Code-4.2.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function addBillItem() {
// 找到金额和备注输入框
var $fAmount = $('#nb-amount');
var $fMemo = $('#nb-memo');

// 取得用户输入的金额值, 并做验证
var amount = parseFloat($fAmount.val());
if (!amount) {
// $.fail() 将弹出一个漂亮的消息框, 作用和 alert('金额不对哦~') 相同, 只是更漂亮
// $.fail() 借助了 jquery-confirm.3.3.min.js 提供的功能
// 并在 jquery-confirm-bailey.js 做了扩展, 可参看附件中的代码
$.fail('金额不对哦~');
return;
}

// 新增账单的数据
var bill = {
id: Math.random(), // 取一个随机数作为账单ID (不太科学, 姑且先用, 第(三)节中此ID将由数据库自动生成)
time: new Date(), // 取客户端当前系统时间
amount: Math.round(amount * 100), // 用户输入的金额乘以100后取整. 由"元"换算成"分"
memo: $fMemo.val()
};

// 数据模型更新成功后的回调函数, 实现界面刷新
var callback = function (bill) {
// 创建新账单项的DOM
var $billItemDom = createBillItemDom(bill);
// 插入到账单列表(#bill-items) 的第 1 项之前
$('#bill-items').prepend($billItemDom);

// 清空账单编辑面板中的"金额"和"备注"输入框
$fAmount.val('');
$fMemo.val('');

// 刷新结余显示
refreshBalance();
};

// 添加账单数据模型, 成功后回调, 刷新界面
addBillItemData(bill, callback);
}

好了, 有点晕, 是吧~

我帮你捋一下理路:

  • 首先, 我们在 4.2.1 节注册了”记一笔”按钮的点击事件监听, 当此按钮被点击时, 触发4.2.3 节的 addBillItem() 函数
  • 然后, 在 4.2.3 节的 addBillItem() 函数中获得用户输入的账单数据, 在进行了必要的输入验证后, 组装出了新账单数据对象 (bill)
  • 接着, 在 Code-4.2.3 中第 40 行调用 4.2.2 节中定义的 addBillItemData() 函数, 完成账本数据的更新(更新数据模型)
  • 最后, 在账本数据模型更新成功后回调刷新界面 ( Code-4.2.2 中第 11 行 回调 Code-4.2.3 中第 25 ~ 37 行定义的函数)

停下来, 想一想…

现在, 在浏览器中打开 index.html, 你应该可以实现新增账单了, 赶快试试吧~

5. 删除账单

本节将实现删除账单功能.

相对而言, 有了第 4 节的基础, 实现删除账单功能就简单得多了.

本例中, 将监听账单列表中的账单项的”双击”事件, 以触发删除账单逻辑. 即: 当用户双击已有的账单项时, 将删除此账单.

5.1 监听账单项双击事件

修改一下 Code-3.3.3 中的代码, 在第 57 行位置插入如下第 5 ~ 8 代码, 以注册账单项双击事件监听.

修改后的代码大概如下:

Code-5.1.1

1
2
3
4
5
6
7
8
9
10
11
12
function createBillItemDom(bill) {

// ............ 前面的代码参见 Code-3.3.3, 此处省略 ...........

// 注册账单项的双击事件监听
$item.dblclick(function () {
removeBillItem($item, bill);
});

// 返回构建好的DOM
return $item;
}

其中, 第 7 行的removeBillItem()函数定义见下节.

5.2 实现”删除账单”功能

“删除账单” 逻辑的实现与”新增账单”类似. 同样采用先更新数据模型, 再刷新界面的逻辑.

有了前面第 4 节 “新增账单” 的基础, 自己研究一下以下代码吧 ~

Code-5.2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 从账本中删除账单数据 (更新账本数据模型), 成功后回调刷新界面
// 参数说明:
// bill - 待删除的账单数据
// callback - 删除成功(账本数据模型更新成功)后的回调函数
function removeBillData(bill, callback) {
// 删除账本数据模型(账单数组)中的账单记录
for (var i = 0; i < bills.length; i++) {
// 若遍历到的账单正是需要删除的账单(ID 相同), 则从账本(bills数组)中删除
if (bills[i].id === bill.id) {
// 从数组 bills 中移除从第 i 个元素开始的 1 个元素 (即当前遍历到的账单)
bills.splice(i, 1);
break;
}
}

// 回调刷新界面
callback();
}

// 删除一条账单
// 此函数所需的两个参数 $item 和 bill 在 Code-5.1.1 第 7 行传入
// $item 为用户双击的账单项的DOM
// bill 双击的那个账单项对应的数据对象
function removeBillItem($billItemDom, bill) {

// $.question() 将弹出一个漂亮的提示框
// $.question() 借助了 jquery-confirm.3.3.min.js 提供的功能
// 并在 jquery-confirm-bailey.js 做了扩展, 可参看附件中的代码
$.question('确定要删除此账单?', function () {
// 删除数据模型中的账单, 成功后回调刷新界面
removeBillData(bill, function () {
// 从 DOM 中移除当前账单项, 即从界面上移除当前账单
$billItemDom.remove();
// 刷新"结余"显示
refreshBalance();
});
});

}

好了, 现在你可以进行双击账单项删除已有账单的操作了, 赶快在浏览器中打开 index.html 试一下吧~

现在, 在浏览器中打开 index.html, 你应该可以实现新增账单了, 赶快试试吧~


意不意外? 开不开心?

你居然已经完成了一个记账本程序, 它可以实现新增账单(记账)和删除账单的操作.

但是…… 还有些问题需要完善……

  • 修改账单数据的功能未实现, 这留给你自己来完成吧!

    给个小提示, 可以这样来玩:

    1. 让用户点击选择一条已有的账单, 然后在”账单详情面板”中显示出这条账单的信息, 即: 把用户选中的账单的金额和备注信息填充在相应的输入框中, 等待用户修改
    2. 用户点击”保存”按钮后, 执行更新账单数据操作. 记得先更新数据模型, 再刷新界面哦~
  • 一些花哨的效果 (如: 插入新账单时的动画, 删除账单时的动画, 结余数值变化时的递增/减动画) 只是吸引眼球, 让你有兴趣跟着我玩, 在上面的代码中未实现, 你可以自己想想怎么做到. 给个小提示:

    1. 新增账单: 在把新账单的 DOM 添到账单容器前先隐藏, 添加后再慢慢显示:

      $billItemDom.hide(); $('#bill-items').prepend($billItemDom); $billItemDom.show(500);

    2. 删除账单: 先用动画逐渐隐藏账单DOM, 动画结束后彻底移除:

      $billItemDom.hide('fast', function(){ $billItemDom.remove(); });

    3. 结余值滚动: 计算前后两次的结余数值之差(delta), 使用 setInterval() 每隔 100 毫秒让结余显示递增 delta/10.0, 直到结余显示值大于或等于最终值时使用 clearInterval() 停止变化过程, 这样结余数值显示将在 1 秒内滚动完成. (结余由大变小时应是小于或等于终值时停止, 注意结余值在由正负变换时切换文字的颜色).

  • 目前的记账本在用户刷新页面, 或者关闭浏览器后数据将丢失! 杯具啦~~

    因为我们只是把数据保存在前端 javascript 的变量里 ( bills 数组 ), 当然会如此…

本系列教程的 第 (二) 节, 我们将在目前记账本的基础上进一步完善, 加入服务器端, 将数据保存在服务器端.

实现一个真正意义上的 “在线记账本”, 敬请期待!