此博客已弃,请转至 此处
本文仅为培训期间应试作文,不具任何教学价值,具体问题请参考对应文章。
前情提要
Party Bid 是一款基于 AngularJS 的安卓网页应用。所谓安卓网页应用,指的是应用完全使用网页开发模式构造(HTML + CSS + JavaScript),之后使用 Apache Cordova 工具将其生成为安卓本地应用项目。
对于应用内容的介绍,考虑到本文的面向读者,此处不再详细说明,主要内容在于 开发过程中所用到的技术 和 个人学习的一些心得体会 。
在第二张卡片中,增加了活动报名的一些功能,主要涉及内容可以参考下列文章:
- 关于 UnderScore 工具的使用介绍,请参见 UnderscoreJS 之 消灭for循环。
- 关于 Jade 模板语言的语法介绍,请参见 Jade —— 简洁的HTML模版引擎。
- 关于 Grunt 对于 Jade 生成的配置,请参见 Yeoman 之 Jade自动化生成的Grunt实现。
- 关于 Filter 的使用介绍,请参见 AngularJS 之 在数据绑定中的格式转化(待写)。
数据结构设计
在卡片一中,为了保证良好的可读性和可拓展性,采用了 对象数组 的形式来在 locolStorage 中存储活动。为了能够实现对活动报名相关内容的存储,我们需要对数据结构进行相应的拓展。
在活动报名中,主要增加了三个方面的信息: 活动的报名状态 , 当前正在进行的活动 和 人员的报名信息。
在数据库设计中,应当遵循一点,一个数据表中的每个数据项中不应当存在数组类型字段。对于有嵌套关系的不定项内容,应该增加子表来拓展。但对于 Json 字符串来说,由于其主要用于数据传递时,为了提高效率,可以具有不定项的嵌套存在,读者应该对数据结构的使用有一定基本了解。
1.活动的报名状态
对于活动的报名状态,我们在 Activity 类中增加一个 register 字段,主要代码如下:
function Activity(name, createdAt, register) {
this.name = name;
this.createdAt = createdAt || Date.parse(new Date());
this.register = register || 'prepare';
}
注意:读者现在应当理解 Activity 类 , activity.js 和特定语境下的 model 指的是同一个东西。
register 字段具有 3 个枚举值:prepare
, run
和 over
。分别表示活动的 未开始 , 进行中 和 已结束 。
使用逻辑或可以方便地设定默认值,此处设为 prepare
状态。
2.当前正在进行的活动
由于短信的接收和处理在后台进行,因此并不在一个特定的页面,无法使用 url 中的路由参数或 $scope 中的变量来确定当前的活动。为此,我们需要一个能在全局确定当前活动的方法(还真的是方法 0.0)。
在 Activity 类中添加如下代码:
Activity.now = function () {
var activity_now = localStorage.getItem("now") || "";
return Activity.find_by_name(activity_now);
};
其中, localStorage 的 now
字段用于存储 当前或最近进行活动的名称 , 接着通过调用 Activity 类的 find_by_name
方法来转换为对象实例。
当然,我们需要定义 Activity 类的 find_by_name
方法:
Activity.find_by_name = function (activity_name) {
var found = _(Activity.all()).findWhere({name: activity_name}) || {};
return new Activity(found.name, found.createdAt, found.register);
};
等一下,上面的代码中好像出现了奇怪的符号和奇怪的方法?!
其实这个符号也并不奇怪,下划线 _
是 JavaScript 中合法的标识符,在这里,它是我们所使用的 UnderScoreJS 的类名,而 findWhere
是其一个函数,功能为返回指定集合中 满足给定键值对 的第一个元素。(也可以用于对象,用法略有不同)
关于 UnderScoreJS 的详细介绍可以参考 UnderscoreJS 之 消灭for循环。
3.人员的报名信息
人员的报名信息具有自己独立内容且与活动相关,为此增加一个新的 model —— Register.js 。
之后,继续添加 构造函数 , 读取函数 和 存储函数 。
构造函数如下:
function Register(name, phone, activity, createdAt) {
this.name = name;
this.phone = phone;
this.activity = activity || Activity.now().name;
this.createdAt = createdAt || Date.parse(new Date());
}
在一条报名信息中,具有 人员姓名 , 人员电话号码 , 所属活动 和 创建时间 4 个字段。
之后,我们也需要像 Activity 类那样添加类的 all
方法和实例的 save
方法,具体步骤基本相同,只是把 localStorage 的 activities
字段换成 registers
即可,此处不给出具体代码。
获取当前页面的活动
严格的来说,这依然是卡片一的需求,但是卡片一中并没有直接使用。
在卡片一中,我们已经设置了好了路由,并将活动名称作为路由参数传递。
注意:在实际应用中,名称并不是作为路由参数一个很好的选择,考虑到需求中已经说明了名称不能重复,故其具有唯一性,可以作为数据表的主项。否则,应当增加一个不重复的 id 字段来作为唯一标识。
在上面的代码中,我们已经定义了 Acitvity.find_by_name
方法,现在,我们需要通过它把当前页面的活动读取到 controller 当中:
将 ActivityRegisterCtrl 中的初始化代码改为:
...
$scope.this_activity = Activity.find_by_name($routeParams.activityName);
...
这样, $scope.this_activity
就不再是一个活动名称,而是一个 Activity 实例的引用。
接着就能够获取其 报名状态 :
...
$scope.status = this_activity.register;
...
至此, 报名状态 就成为了 $scope
的一个属性,之所以单独提出是为了方便数据绑定,也可以直接以 this_activity.register
来使用。
报名页面的神奇按钮
在卡片二中,存在如下需求:
点击“活动报名”页面的“开始”按钮,活动报名开始,页面中的“开始”按钮替换为“结束”按钮,报名开始。
【结束】按钮变为【开始】按钮,如果点击【开始】按钮,则可以继续之前的报名。
对于按钮的 开始 / 结束 切换,这里介绍 3 种方法。
1.直接内容绑定
最简单的一种方法,也是最容易实现的,但是代码略多。
在 view 中,将 button 的内容设为 AngularJS 的 ng-bind :
<header>
...
<button class='sth' ng-click='sth'>{ { button_content } }</button>
...
</header>
考虑到现在使用的事 jade 模板,其代码应为:
header
...
button.sth(ng-click='sth') { { button_content } }
...
在 jade 语言中,没有尖括号,不需要封闭,使用缩进表示嵌套关系,使用 .
表示 class
,属性放在括号中。
关于 jade 的详细语法介绍,请参见:Jade —— 简洁的HTML模版引擎。
对于 ng-click 的运作,将在下一小节中实现。
同时,在 controller 中对其进行赋值:
...
$scope.button_content = status == 'run'? '结束': '开始';
...
读者要特别注意逻辑关系,如果正在进行中,应该是显示 结束 ;反之,如果没在进行,才是显示 开始 。
2.使用 ng-switch 切换
ng-switch 用于根据条件决定显示项,功能和 ng-if 相似,相当于 ng-if + ng-else(实际不存在)。
这里的基本过程如下:
- 对 header 控件添加
ng-switch on
属性。 - 创建两个按钮控件,添加相反的
ng-switch-when
属性。 - 当
ng-switch on
的值改变时,显示的变为隐藏,隐藏的变为显示。
因此,在这里 不推荐 使用 ng-switch ,理由如下:
- ng-switch 创建了 2 个按钮,增加了不必要的 view 层代码。
- 这里根本不符合 ng-switch 的使用理念,其目的是为了根据条件显示不同的控件组,比如在一个问卷(HTML 表单)中,根据前面的性别选项决定后面需要出现的问题组。而在此处,本身就确实是一个按钮,只是因为一条属性的改变被拆分成 2 个按钮,这样在算法设计上就已经是一种倒退。
- 可能存在静态冒险(学多了数电,就这样叫吧 >o<),即可能在一个瞬间 2 个按钮同时显示或同时不显示。
综上所述,并不推荐使用这种方法。
以下给出的实例代码仅供参考,让读者了解 ng-switch 的用法:
view 中(使用 jade 实现,下同):
header(ng-switch on="status")
...
button.sth(ng-click='sth' ng-switch-when='status=="run"') 结束
button.sth(ng-click='sth' ng-switch-when='status!="run"') 开始
...
在 ng-switch 中,还可以使用 ng-switch-default
属性指定在没有 ng-switch-when
满足时显示某(些)控件。
3.传说中的美颜滤镜
AngularJS 中, filter 是一个重要功能,确切的说,是一套重要体系。
在很多文章中, filter 被翻译成过滤器,这样在很大程度上容易造成不必要的误会,觉得 filter 是用来进行数据筛选的。(当然也确实可以)
英文的语言风格和中文具有很大的不同,当出现一个新兴事物时,中文往往会增加一个词汇;而英文由于其仅仅只有区区 26 个字母,排列组合有限,并且每个单词必须满足特定字母顺序和结构(全是辅音看你怎么读 >.<),所以英文往往只是在一个已有单词中增加一个 义项 (不过理由是我瞎扯的 T.T),比如 mouse 在日常生活中指的不一定是老鼠也可以是鼠标,这样的例子还有很多(所以好好学英语吧 ^.^)。
filter 还有一个在生活中(Both 摄影 and 后期)更为常用的义项 —— 滤镜。
在摄影中,有很多种滤镜,比如 偏振镜、UV 镜、渐变镜、移轴镜... What!买不起单反?Photoshop 中的滤镜总该用过吧... Nany?不会修图?谁敢说自己真的不会修图?没自拍过么?没用过美图秀秀么?一键美化也是用了滤镜的啊!
好了,回归主题,总之在这里译成 滤镜 是更为合适的,其用途在于对原始数据的 模式转换 。考虑到大部分程序员都不是英语专业也不是摄影专业的,对 filter 的第一感觉可能都是过滤器(其实我也是这么感觉的,虽然知道,就和看到 mouse 就自动译成老鼠一样),但希望读者了解,这里的 filter ,和诸如 fomatter , parser , encoder , decoder , convertor 是一个意思。什么?一个都不认识?那还是先学英语吧...
举个最简单的例子,在大部分国家或者地区,性别都是可以用一个 boolean 变量(raw)来存储的,但是需要显示的时候,就需要转换成对应的字符串,或者是丘比特的弓箭和维纳斯的镜子(pretty)。这里就是一个增加冗余数据的过程,一般来说也都是美化过程,故应将其译作滤镜而非过滤器。
为了使用自定义的滤镜,我们需要在 module 中配置该滤镜,这里将其命名为 switch(开关)。
_在 app/scripts
文件夹下,新增一个 filter
文件夹,并在其中添加一个 switch.js
的文件。_你是这么想的么?我一开始也是这么想的。但是因为有 Yo 这个神器的存在,这种体力活完全不需要我们自己来做。
打开终端,进入到 party_bid 文件夹。
注意:实际上,在大多数情况下,都可以直接在特定文件夹打开终端。比如在 Mac 中,直接将 Finder 顶部的文件夹图标拖到 Dock 上的终端里;或者在 Sublime Text 3 中安装一个 Terminal 插件,就能直接在目录树里打开当前路径的终端。
终端命令如下:
$ yo angular:filter switch
这样,就已经自动创建好了文件和基本代码了,何乐而不为呢?
考虑到我们要将活动的报名状态($scope.status
)转换为按钮的显示内容,修改代码如下:
view 中:
header
...
button.sth(ng-click='sth') { { status | switch } }
...
filter 中:
angular.module('partyBidApp')
.filter('switch', function () {
return function (input) {
return input == 'run'? '结束': '开始';
};
});
这样,就能够自动将报名状态转换为按钮的显示文字了。
虽然 filter 能实现的功能都可以直接通过 controller 实现,但其能够充分实现 代码复用 的理念,对于大型项目来说是不可缺少的。
活动的开始与结束
在卡片二中,存在如下需求:
点击“活动报名”页面的“开始”按钮,活动报名开始,页面中的“开始”按钮替换为“结束”按钮,报名开始。
报名开始后,组织者误点击“结束”按钮。弹出一个“报名结束确认”提示,二次询问是否要结束报名。
【结束】按钮变为【开始】按钮,如果点击【开始】按钮,则可以继续之前的报名。
只要有活动在报名,其他活动“活动报名”页面上的“开始”按钮就为不可点击的灰色状态。
在上一小节中,我们已经实现了按钮的显示,但还没有对其添加任何功能。
为此,我们需要为 button 添加 ng-click
属性,上一小节中仅仅是为了说明 jade 语法而放置了一个内容占位符。
1.开始或结束活动
在 view , controller 和 model 中,添加代码如下:
view 中:
header
...
button.sth(ng-click='turn_status()') { { status | switch } }
...
这里假定上一小节中读者使用了 filter 实现。
controller 中:
...
$scope.turn_status = function () {
if($scope.status != 'run' || window.confirm('确认要结束本次报名吗?!'')) {
$scope.this_activity.turn_register();
$scope.initiate_data();
}
};
...
这里用到了一点逻辑思维,"如果活动报名没在进行或者弹窗得到肯定答案,那么执行本页活动的改变状态函数?!"。
看起来有点绕,拆开来就是:
- 如果是要开始活动,跳过弹窗直接执行;
- 如果是要结束活动,弹窗提示,得到确认才执行,否则不执行;
- 执行完毕后重新初始化当前数据。
最后一条也可以通过直接改变 status
实现,但存在冒险行为,即万一 model 中的函数执行发生了某种意外,就可能导致页面显示的状态和后台存储的状态不同。
当然,在真实的 Web App 中,异步更新数据是一个更好的选择。因为数据存储在服务器端,数据操作需要很大的延迟,这样做更利于满足用户体验,即直接改变当前页面的状态,同时后台更新服务器数据,假如得到服务器数据返回的失败提示后,再在本地做出相应的处理。
此处考虑到是安卓的本地应用,不存在数据操作的可测延迟,故可以同步更新数据。希望读者在实际案例中自行比较各种方式的利弊然后做出决定,而不是一味地套用之前的代码。
model 中:
Activity.prototype.turn_register = function () {
var next_status = (this.register == 'run'? 'over': 'run');
Activity.alter_status(this.name, next_status);
};
这里的整个过程中,活动的开始和结束 共用同一个函数 ,为此需要判断活动状态。
接着,调用 Activity 的 alter_status
方法:
Activity.alter_status = function (the_name, status) {
var list = Activity.all();
var found = _(activity_list).findWhere({name: the_name});
found.register = status;
localStorage.setItem('activities', JSON.stringify(list));
Activity.update_now(found);
};
这里再次调用了 UnderScoreJS 的 findWhere
方法。之后又调用了 Activity 的 update_now
方法:
Activity.update_now = function (activity) {
localStorage.setItem('now', activity.name);
};
该函数用于改变当前活动的标记。
在上面的代码中,3 个函数加起来都不到 15 行。但是为了保证可读性和可维护性,应当使得每个函数 只实现单一功能。
注意:这里要求任何数据操作必须使用实例函数,否则直接使用类函数的话确实可以更加简单一点的。
2.有活动进行中开始按钮不可用
为了实现这个操作,我们还是需要在 view , controller 和 model 中添加相应代码:
view 中:
header
...
button.sth(ng-click='turn_status()',ng-disabled='no_start') { { status | switch } }
...
上面在 button 控件中添加了一个 ng-disabled
属性,注意 jade 中属性之间可以使用 逗号 隔开(也可以空格)。
controller 中:
...
$scope.no_start = $scope.status != 'run' && Activity.on_going();
...
上面代码中,先判断当前是否活动未在进行(即按钮显示为"开始"),接着在调用 Activity.on_going
方法判断是否有活动在进行。
接着定义 Activity.on_going
方法的实现。
model 中:
Activity.on_going = function () {
return _(Activity.all()).some(function (activity) {
return activity.register == "run";});
};
上面使用到了 UnderScoreJS 的 some
方法,其用途为在给定集合中是否存在满足条件(即返回值为 Truthy )的元素。
至此,就能够根据情况实现"开始"的不可用,和创建活动时"返回"按钮的不可见的方法类似。
进行中的列表项变黄
在卡片二中,存在如下需求:
点击【返回】按钮,返回“活动列表”页面,活动列表中正在报名中的活动底色为黄色。
要实现的内容就是在 ng-repeat 中对控件的样式进行动态绑定。
这里依然给出 3 中方法。
1.直接使用 ng-bind 绑定 class 属性
...
ul.sth
li(ng-repeat='activity in activitys | orderBy: ...')
a(class='sth { {someColor(activity.register)} }',ng-click='..') ..
...
即把颜色属性写成 css 然后以活动报名状态为参数通过调用 $scope.someColor
方法返回相应的 css 字段。
controller 中的代码过于简单,此处从略。
2.使用 ng-class 指定属性
...
ul.sth
li(ng-repeat='activity in activitys | orderBy: ...')
a(ng-class='{yelloCss: status=="run", whiteCss: status!="run"}',ng-click='..') ..
...
其中,yelloClass
和 whiteClass
为对应的 css 属性名。
这里是 ng-class 的一种用法,ng-class 总共具有 3 种用法:
- 直接给定 字符串(可含空格)变量,和上面 class 用法相似,只是不需要花括号。
- 给定 数组 变量,其中每个元素均为可含空格的字符串,全部作为 class 属性。
- 给定 对象 ,对于每个键值对,以所有值为真的键名作为 class 属性。
本处使用的是第 3 中方法,也是最复杂但是最实用的方法。
3.继续使用滤镜
将 view 中代码改为:
...
a(ng-class='activity.register | yello',ng-click='..') ..
...
创建一个很黄很暴力的滤镜:
$ yo angular:filter yello
其核心代码如下:
...
return input == 'run'? 'yelloCss': 'whiteCss';
...
虽然说在第 2 种方法的 ng-class 中学到了新东西,但站在工程角度来说,对于这类具有明确的 一一映射 属性的变量还是推荐使用滤镜来实现。
在具有更为复杂的动态 css 系统时,ng-class 将会是一种很好的选择。
短信的后台处理
在卡片二中,存在如下需求:
报名开始后,报名者发送短信:BM+姓名 到18601126251进行报名后,报名者接收到一条由系统返回的报名确认信息,“恭喜!报名成功”。
BM的大小写不限,BM后可以有空格。
报名者重名,如果来自不同的手机号码,保留重名者。
如果报名者在活动创建完,但是第一次点击活动按钮前,开始前发送报名短信,系统返回其一条错误信息,“活动尚未开始,请稍后”。
报名者在活动报名结束后发送信息报名,系统返回一条错误信息,“Sorry,活动报名已结束”。
在没有部署到安卓设备上之前,我们使用 浏览器控制台 来模拟短信收发。
再次新建一个 model —— message.js 。
在 sms.js 中添加如下代码:
...
process_received_message: function (json_message) {
Message.receive(json_message);
...
即将收到短信后的 所有操作 都在 Message 类中进行,sms.js 仅作为库文件。
1.校验短信类型
对于一个手机号码,其可能收到任何短信,包括朋友聊天、垃圾广告、新闻推送、验证码接收等等。而我们目前仅仅需要接收报名短信。
Message.received_new_item = function (message_json) {
var text = message_json.messages[0].message;
var phone = message_json.messages[0].phone;
var header = text.substring(0,2).toUpperCase();
if(header == "BM") {
Message.cope_new_register(Message.get_name(text), phone);
}
};
上面对短信内容进行判断,确认其是否为 BM , Bm , bM 或 bm 开头。为此,直接将其 转为大写 与 BM 比较。
Message.get_name
用来去除可能的空格:
Message.get_name = function (text) {
return text.substring(2).replace(/\s/g, '');
};
replace 中,使用了正则表达式进行匹配。在 javascript 中,/ /
之间的内容会被识别为正则表达式,其中 \s
表示匹配任何空字符,最后的 g
表示匹配全部(否则只会匹配第一个出现的满足条件部分)。
2.报名有效性验证
即便是报名短信,也要在活动进行中才有效,并且需要防止同一人多次报名,处理代码如下:
Message.cope_new_register = function (name, phone) {
var bad_status = (Activity.now().register != "run");
if (!bad_status && (bad_status = Register.check_if_repeat(phone))) {
status = "repeat";
}
Message.sendback_info(phone, status);
if (!bad_status) {
var new_register = new Register(name, phone);
new_register.save();
Message.refresh_ui_list();
};
};
上面的代码中,先判断活动报名 是否正在进行 ,设立了一个 bad_status
标记,并记录 status
。如果确实正在进行,再判断 手机号码是否重复 ,如果重复,bad_status
也设为 true ,更新 status
;根据 status
回复相应内容;如果 bad_status
为 false ,创建该报名实例并存储,同时 刷新页面 。
通过设立 标记 ,可以减少 if-else
判断的使用量, 防止多级缩进 。
3.回复报名结果
在上文中,我们已经对不同情况设立了不同的 status
,接下来需要根据具体的 status
回复相应信息:
Message.sendback_info = function (phone, status) {
var back_info = {
'run': '恭喜!报名成功!^o^',
'prepare': '活动尚未开始,请稍后~ >.<',
'over': 'Sorry,活动报名已结束.. =.=',
'repeat': '您已经报过名了,请勿浪费短信费.. -_-||'
};
var text = back_info[status];
native_accessor.send_sms(phone, text);
};
这里,通过建立 哈希表 ,直接通过 索引值 得到对应的文本数据,避免产生大量的 if-else
语句或者 switch-case
语句。
哈希表是一种非常常用并且功能强大的技术,除了作为字典外,还可以实现数据存储、查重等多项任务。
报名页面的实时刷新
在卡片二中,存在如下需求:
“活动报名”页面用以列表形式显示接收到的报名人的姓名和联系方式信息并统计报名人数(每一名参与者报名成功后自动更新)。
为了获取页面元素,我们需要在页面上添加相应标记,为此,对活动报名页面的 view 层添加 id
标签。
...
#register...
...
上面的代码可能看起来比较隐晦,#
在 jade 中表示 HTML 的 id
属性,并且 div
标签可以 省略不写 。
之后,我们就能通过该 id
来获取该控件。
Message.refresh_ui_list = function () {
var ui_scope = angular.element('#register').scope() || { $apply: angular.noop };
ui_scope.$apply(function (scope) {
scope.update_data();
});
};
这里用到了 angular.element
函数,是不是感觉和 jQuery 的 $('#register') 调用方式很像?
其实,angular.element
就是调用了 jQuery 的方法来工作的,如果没有引用 jQuery ,就会调用 AngularJS 中内置的 jQuery Lite 来实现,返回的也都是 jQuery 对象。
找到元素后,通过 .scope
就能获取报名页面 controller 的 $scope,从而可以发现,可以在页面的 任何一个控件 上添加该 id 来实现获取页面的 $scope
。
angular.noop 是一个空函数,什么都不做。如果当前应用没有在报名页面,则获取到的 $scope
为 undefined ,因此直接调用其方法会在控制台报错(虽然对功能没有任何影响),作为一款优秀的应用,需要避免错误出现。
之后,我们需要调用 $scope.update_data
方法。由于是在 AngularJS 应用外进行调用,我们需要使用 $scope.$apply
方法,否则 AngularJS 中内置的检查机制不会得到响应, controller 中的变化也就无法传递到 view 。controller 中代码如下:
$scope.update_data = function () {
$scope.member_list = Register.read_members_of_activity($scope.this_activity);
$scope.count_of_members = $scope.member_list.length;
};
该函数的作用就是重新加载列表和计算人数,Register.read_members_of_activity
方法能够通过简单的 UnderScoreJS 函数实现。
Register.read_members_of_activity = function (the_activity) {
return _.where(Register.all(), {activity: the_activity.name}) || [];
};
在 view 中,使用 滤镜 处理 count_of_members
,在无人报名时为 空字符串 ,反之为 (x) 。
至此,卡片二中短信处理的核心功能已全部实现。
第二张卡片中主要用的技术和心得体会主要就是这些,如果有任何疑问欢迎在下方回复 ^.^
本站地址: http://trotyl.github.io/