本系列教程将带领大家完成一个 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 的掌握还算不错. 请先试着自己完成如下图所示界面的制作:
本教程重点不在 HTML 和 CSS 代码的编写, 下面将直接给出代码, 只作简要解释.
若想偷懒, 直接复制或是下载 便是, 但index.html
和 index.css
中的代码请仔细研读并弄懂.
下载到的压缩包里是不是少了 index.js 文件 ?
哈哈~ 我故意删掉的! 防止你直接解压运行一下, 看个开心就了事~
本节的重点是前端业务逻辑的实现 ( index.js ), 千万别想着在这个环节上偷懒, 否则就失去本教程的意义了!
Code-2.1 index.html
1 |
|
对以上代码作简要解释:
head
部分: 引入各种 CSS 和 javascript- 第 8 行 和 第 23 行引入
bootstrap
, 加入bootstrap 预定义样式和 boostrap 组件代码. 若对 bootstrap 知之甚少, 强烈建议课外自学. ( 现在别忙着去看 ) - 第 10 行 和 第19 行引入了一个名为
jquery-confirm
的 jQuery 插件, 用于弹出漂亮的消息框. - 第 16 行引入
jQuery
库, 这里我们使用的是 1.11.1 版本. 若你使用别的版本, 请注意兼容性问题. - 第 13 行 和 第 30 行 引入的是针对 index.html 页面写的样式表和 javascript 脚本.
- 第 8 行 和 第 23 行引入
- body 部分: 页面主体部分的 HTML 代码
- 页面主体部分分作左、右两个部分: 左侧为账本记录列表 ( 35 ~ 60 行,
div#records
), 展示已登记的账单列表; 右侧为单条账单的编辑界面 ( 62 ~ 79 行,div#panel-bill-editor
), 注意此面板初始时为隐藏状态. - 注意其中的第 42 ~ 51 行 (
<li class="record"> ... </li>
), 这是账单列表中的一条信息.
- 页面主体部分分作左、右两个部分: 左侧为账本记录列表 ( 35 ~ 60 行,
篇幅所限, 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 之间的双向映射 (同步).
盗个图来说, 大概就是这样:
3.2 设计账本数据模型
扯了半天 MVC, 你现在应该知道 Model 算是三兄弟中的老大了吧~
所以, 我们可以先从数据模型 (Model) 设计开始, 代码如下:
Code-3.2.1
1 | // 账本数据模型 |
我们使用一个数组来存储账本数据, 其中每一个数组元素为一条账单记录. 上面的代码是测试时初始的模拟数据(账单按时间降序排列)
第 3 ~ 6 行的注释说明了一条账单数据中各属性的含意
注意: 金额的单位为”分”, 即
amount: 80000
代表金额为 800.00 元. 使用 “分” 为单位可令金额数值表达为整型, 避免了计算过程中的精度问题.不明白? 试试
alert( 1 - 0.7 );
会出现什么…
3.3 展示账单列表
接下来, 我们来实现账单列表的展示, 即最终运行效果图中左侧列表的显示.
在 3.2 节我们已经完成数据模型的设计, 这里将实现 Model → View 的映射.
先考虑一条账单数据的展示.
对于单条账单, 其数据模型大概是如下的样子:
Code-3.3.1
1 | { |
注意前文 index.html ( Code-2.1 ) 代码中第 42 ~ 51 行:
Code-3.3.2
1 | <li class="record"> |
有没有看出什么门道 ? 对了, 上面两块代码 ( Code-3.3.1
和 Code-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 | // 一条账单的 HTML 模板 |
妈呀, 看上去好复杂… 虽然代码中已经写满了注释, 但还是进一步解释一下:
第 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){...}
, 此函数在遍历到每一个元素时将被自动回调一次, 参数i
和element
分别为顺序号(这是遍历到的第几个元素) 和当前遍历到的元素. 第 25 ~ 55 行的代码都是放在这个匿名函数里的.
- 第 36 行读取当前遍历到的元素的 data-field 属性中藏着的数据字段名信息, 并在第 39 ~ 55 行 分别进行了处理. 这段代码就不详细解释了, 参看一下注释.
终于讲完了…
如果看 Code-3.3.3 的代码让你一脸迷惑, 千万不要丧失信心. 这段代码算是本节中最难理解的部分了, 挺过这一关, 你将轻松面对后面的内容.
放慢脚步, 结合注释和说明多思考, 你一定能理解!
代码里出现大量了jQuery 的东西, 抽空读一下本博客另一篇文章: jQuery入门精要, 或许会有帮助.
接下来, 如果要生成整个账本数据 (bills数组) 的 DOM 元素, 那不就是一个循环吗?
看现在这段代码
Code-3.3.4
1 | // 显示账本中所有账单 |
Code-3.3.4 中的showBillItems()
函数实现了整个账本中所有账单的展示.
接下来的 Code-3.3.5 实现 “结余” 的展示:
Code-3.3.5
1 | // 显示结余 |
还记得吗? 我们的金额的单位是 “分”, 这给用户看到了, 会显得很别扭, 于是补充如下一个函数, 用于格式化最终显示时的金额:
Code-3.3.6
1 | // 格式化金额显示 |
有了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 | var rt; |
最后帮你一次, 以后要自己学会分析, 并习惯 Code-3.3.6 中第 9 行 的写法. 好处嘛, (1) 很简洁 (2) 显示自己很牛!
最后我们把上面这些代码(函数)组装起来, 借助 jQuery 在页面加载就绪时自动展示账单列表:
Code-3.3.7
1 | $(document).ready(function () { |
看不懂 $(document).ready(function(){ …. }); 是什么鬼的看另一篇文章: jQuery入门精要
快在浏览器中打开 index.html 看一下效果吧~
小结:
本节我们实现了 “根据一条账单的数据, 构建对应的 DOM 元素” ( 数据 → 视图 )
本文开篇效果演示的动图中有两个细节还未实现:
(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 | // 打开/关闭账单编辑面板 |
此外, 修改 Code-3.3.7 的代码如下(Code-4.1.2), 即添加其中的第 9 行代码.
监听 打开/关闭”账单编辑面板”按钮 (图标) 的点击事件, 触发 Code-4.1.1 中的 toggleBillEditor()
函数.
Code-4.1.2
1 | $(document).ready(function () { |
- 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 | $(document).ready(function () { |
至于上述代码中第 12 行涉及的 addBillItem
函数, 我们将在 4.2.3 节实现. 继续往下看就知道了…
4.2.2 更新账本数据模型
现在假设用户已经填写了新账单的信息(金额和备注), 点击了”记一笔”按钮. 接下来程序应该干什么呢 ?
还记得 MVC 吗? 又来了…
我们应该先考虑用户的这一系列的操作会对数据模型 (Model, 数据层) 产生什么影响, 因此我们先来实现更新数据模型的逻辑:
Code-4.2.2
1 | // 新增一条账单的数据到账本数据模型 |
这段代码看似简单, 就定义了一个函数, 函数中就 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 | function addBillItem() { |
好了, 有点晕, 是吧~
我帮你捋一下理路:
- 首先, 我们在 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 | function createBillItemDom(bill) { |
其中, 第 7 行的removeBillItem()
函数定义见下节.
5.2 实现”删除账单”功能
“删除账单” 逻辑的实现与”新增账单”类似. 同样采用先更新数据模型, 再刷新界面的逻辑.
有了前面第 4 节 “新增账单” 的基础, 自己研究一下以下代码吧 ~
Code-5.2.1
1 | // 从账本中删除账单数据 (更新账本数据模型), 成功后回调刷新界面 |
好了, 现在你可以进行双击账单项删除已有账单的操作了, 赶快在浏览器中打开 index.html 试一下吧~
现在, 在浏览器中打开 index.html, 你应该可以实现新增账单了, 赶快试试吧~
意不意外? 开不开心?
你居然已经完成了一个记账本程序, 它可以实现新增账单(记账)和删除账单的操作.
但是…… 还有些问题需要完善……
修改账单数据的功能未实现, 这留给你自己来完成吧!
给个小提示, 可以这样来玩:
- 让用户点击选择一条已有的账单, 然后在”账单详情面板”中显示出这条账单的信息, 即: 把用户选中的账单的金额和备注信息填充在相应的输入框中, 等待用户修改
- 用户点击”保存”按钮后, 执行更新账单数据操作. 记得先更新数据模型, 再刷新界面哦~
一些花哨的效果 (如: 插入新账单时的动画, 删除账单时的动画, 结余数值变化时的递增/减动画) 只是吸引眼球, 让你有兴趣跟着我玩, 在上面的代码中未实现, 你可以自己想想怎么做到. 给个小提示:
新增账单: 在把新账单的 DOM 添到账单容器前先隐藏, 添加后再慢慢显示:
$billItemDom.hide(); $('#bill-items').prepend($billItemDom); $billItemDom.show(500);
删除账单: 先用动画逐渐隐藏账单DOM, 动画结束后彻底移除:
$billItemDom.hide('fast', function(){ $billItemDom.remove(); });
结余值滚动: 计算前后两次的结余数值之差(delta), 使用 setInterval() 每隔 100 毫秒让结余显示递增 delta/10.0, 直到结余显示值大于或等于最终值时使用 clearInterval() 停止变化过程, 这样结余数值显示将在 1 秒内滚动完成. (结余由大变小时应是小于或等于终值时停止, 注意结余值在由正负变换时切换文字的颜色).
目前的记账本在用户刷新页面, 或者关闭浏览器后数据将丢失! 杯具啦~~
因为我们只是把数据保存在前端 javascript 的变量里 ( bills 数组 ), 当然会如此…
本系列教程的 第 (二) 部分, 我们将在目前记账本的基础上进一步完善, 加入服务器端, 将数据保存在服务器端.
实现一个真正意义上的 “在线记账本”, 敬请期待!
Revised on 2019/05/18 16:21:31 by Bailey
-
Next Post实例教程 - 在线账本 (二)
-
Previous PostCayenne 起步 (Version 4.0) [译]