此博客已弃,请转至 此处 。
本文仅为培训期间应试作文,不具任何教学价值,具体问题请参考对应文章。
前情提要
Party Bid 是一款基于 AngularJS 的安卓网页应用。所谓安卓网页应用,指的是应用完全使用网页开发模式构造(HTML + CSS + JavaScript),之后使用 Apache Cordova 工具将其生成为安卓本地应用项目。
对于应用内容的介绍,考虑到本文的面向读者,此处不再详细说明,主要内容在于 开发过程中所用到的技术 和 个人学习的一些心得体会 。
- 关于 AngularJS 的简要介绍,请参见 AngularJS —— 新兴的 Web 前端开发框架(待写)。
- 关于 Yeoman 工具的使用介绍,请参见 Yeoman 之 搭建 AngularJS 开发环境(待写)。
- 关于 Cordova 工具的使用介绍,请参见 Cordova —— 让 Web 统治世界(待写)。
建立项目
为了方便的直接建立出 AngularJS 项目,我们需要使用到 Yeoman 工具。
安装 Yeoman 的步骤已在 [OSX 之 Web 开发环境配置]({% post_url 2014-07-17-OSX之Web开发环境配置 %})文章中给出。
核心步骤的命令行代码如下:
$ npm install -g yo
至此已经安装好了 Yeoman ,随后我们通过其来创建一个 AngularJS 项目:
详细说明及内容拓展请参见 Yeoman 之 搭建 AngularJS 开发环境(待写)。
1.安装 Yoeman 的 AngularJS 模板生成器:
$ npm install -g generator-angular
含义顾名思义。Yo (Yeoman 的一个组件,下文中都会具体说明每个操作是 Yo, Grunt 或 Bower 的功能)可以通过安装相应的 generator 来实现功能拓展,除了 AngularJS 的之外,还有很多其他的 generator 可以使用。也就是说,通过拓展,Yo 理论上可以适用于任何类型的项目。
2.新建一个文件夹并进入,文件夹名称即为项目名 party_bid(为了避免各种地方都加引号而抛弃空格),手动或者命令行如下:
$ mkdir party_bid
$ cd party_bid
3.使用 Yeoman 创建一个 AngularJS 项目:
$ yo angular
之后会有提示如下:
_-----_
| | .--------------------------.
|--(o)--| | Welcome to Yeoman, |
`---------´ | ladies and gentlemen! |
( _´U`_ ) '--------------------------'
/___A___
| ~ |
__'.___.'__
´ ` |° ´ Y `
Out of the box I include Bootstrap and some AngularJS recommended modules.
[?] Would you like to use Sass (with Compass)? (Y/n)
[?] Would you like to include Bootstrap? (Y/n)
[?] Would you like to use the Sass version of Bootstrap? (Y/n)
[?] Which modules would you like to include? (Press <space> to select)
❯⬢ angular-animate.js
⬢ angular-cookies.js
⬢ angular-resource.js
⬢ angular-route.js
⬢ angular-sanitize.js
⬢ angular-touch.js
上面列出了是否需要在 AngularJS 直接添加一些拓展组建,在此全选,主要理由如下:
- 由于目前还不是对 Sass 和 Bootstrap 特别了解,以优先保证可用性为前提;
- Party Bid 为开发项目而不是真正的商业 Web App ,不需要对应用大小最简化来保证用户体验;
- 即便现在的需求中用不到 Sass ,但在 Sass 本身也能使用纯 css ,之后也可以需要对应用进行 Sass 重构。
全部选择后结果显示如下:
[?] Would you like to use Sass (with Compass)? Yes
[?] Would you like to include Bootstrap? Yes
[?] Would you like to use the Sass version of Bootstrap? Yes
[?] Which modules would you like to include? angular-animate.js, angular-cookies.js, angular-resource.js, angular-route.js, angular-sanitize.js, angular-touch.js
注:上述代码可能会随着平台差异和程序版本而不同,请以自己的实际情况为准。
接下来可能生成的过程会比较漫长,在这段休息时间中可以看看本博客的其他内容 o
在 AngularJS 项目生成完毕后(要确定终端中是生成完毕停止而不是报错停止的 =_=||),就会发现当前目录中多了很多文件和文件夹:
- app -- 应用的主文件夹,写代码的地方
- test -- 放置测试文件的文件夹
- node_modules -- Nodejs 安装的程序包
- bower.json -- Bower 的依赖配置文件
- Gruntfile.js -- Grunt 的配置文件
- package.json -- NodeJS 的依赖配置文件
4.在终端中开启 Grunt 的内置服务器:
$ grunt serve
注意:grunt serve
和 grunt server
都能开启服务器,但是 grunt server
已经不被推荐使用。目前的情况是使用 grunt server
会被重定向到 grunt serve
,所以功能上没有任何区别。
接着 grunt 会自动在浏览器中打开 app
文件夹中的 index.html
页面,如果能够正常现实内容及样式的话说明我们这个环节已经成功了。
配置项目
虽然已经建立了一个 AngularJS 的模板项目,但是我们的命令行操作还并没有结束。
或者说,Yo 的强大功能还远不仅仅如此。本文仅讲解所用到的功能,拓展内容具体可参考 Yeoman 之 搭建 AngularJS 开发环境(待写)。
AngularJS 中,路由是一个核心功能,用来响应 url 并连接对应的 view 和 controller。关于 MVC 的更多内容可以参考 AngularJS 之 MVC 架构开发介绍(待写)。
在有 Yo 的情况下,我们无需自己添加每一个 view 和 controller 。在第一张卡片中,我们需要用到三个页面:"活动列表" , "活动报名" 和 "创建活动" ,为此,我们在终端中继续输入如下命令:(因为当前正在运行服务器,故可以使用 "Command + N"(Mac)或 "Ctrl + N" 来创建一个新窗口,并再次回到当前文件夹)
$ yo angular:route ActivityList
$ yo angular:route ActivityRegister
$ yo angular:route CreateActivity
这样就直接创建了 app/scripts/controllers/
文件夹下的 activitylist.js
, activityregister.js
和 createactivity.js
三个 controller 文件,以及 app/views/
下的 activitylist.html
, activityregister.html
和 createactivity.html
三个 view 文件,并且已经在 app/scripts/
下的 app.js
中将模板和控制器相互关联起来了,十分简单粗暴。
不论在上面的命令中使用 PascalCase 还是 camelCase ,文件名都会自动使用小写,但是 controller 的名称和路由中配置的路径会按照上面命令中给出的大小写来生成。
为了代码的可读性和可维护性,我们再新建一个 app/scripts/models
文件夹,并在其中添加一个 activity.js
文件用于处理一切与 activity
相关的数据操作。
特别注意,自己添加文件后也要在 index.html
中添加相应的引用。
...
<#script src='scripts/models/activity.js'></#script>
...
博文中禁止 script 标签,请自行去掉#号。
引用添加的位置不要在它默认生成的脚本块中,放在它的注释块外面。另外 activity.js
中的代码模式为:
function Activity(parameter) {
//Do something
}
Activity.prototype.someMethod = function () {
//Do something
};
Activity.anotherMethod = function () {
//Do something
};
上面的代码中,采用面向对象的方式,依次是 类的构造函数,类的实例函数 以及 类函数。
数据结构设计
首先分析数据结构,为了保证良好的可读性和可拓展性,采用 对象数组 的形式来在 locolStorage 中存储活动。如果之后改用数据库存储的话,这样也是最方便对接的。
在 activity.js
的主要代码如下:
1.构造函数
function Activity(name, createdAt) {
this.name = name;
this.createdAt = createdAt;
}
在上面的代码中, Activity
类的对象具有两个字段(可能有些语言一般用属性,效果都是差不多的),其中,createdAt
用于记录活动创建时间,以便于排序。
注意:因为采用对象数组,虽然其本身的物理存储数据已经可以实现对活动的排序,但是物理存储顺序是绝对不足以作为排序依据的,其具有非常明显的不可靠性。即便不存储创建时间,也需要指定一个 index
属性来支持排序功能。除此之外,也能使用哈希表来作为数据结构,以 index
作为索引,这里不作过多介绍。
2.读取函数
此博客已弃,请转至 此处
因为不针对任何实例,所以此处采用类函数(方法)实现,从 localStorage 中取出相应 json 数组并解析为对象。
Activity.all = function () {
return JSON.parse(localStorage.getItem('activities')) || [];
};
上面的代码中,最后的逻辑或操作是为了防止没有任何数据时导致程序崩溃,因此返回一个空数组而不是 undefined 。而当非空时,由于逻辑运算的短路法则,或运算及其后内容会直接被忽略。
3.存储函数
为了保持面向对象的纯洁性,此处采用实例函数而非类函数来进行存储,存储在浏览器本地的 localStorage 中。
Activity.prototype.save = function () {
var list = Activity.all();
list.push(this);
localStorage.setItem('activities', list);
};
无活动时跳转
在卡片一中,存在如下需求:
在打开程序后判断一下,是否已经存在已创建的活动,如果没有,就要显示“创建活动”页面,引导用户去创建一个活动。
所以,我们需要在 controller 中增加相应的判断。
首先,我们来看看现在的路由配置,打开 app.js
,部分代码如下:
angular
.module('partyBidApp', [
...
])
.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl'
})
...
.when('/ActivityList', {
templateUrl: 'views/activitylist.html',
controller: 'ActivitylistCtrl'
})
.when('/CreateActivity', {
templateUrl: 'views/createactivity.html',
controller: 'CreateactivityCtrl'
})
.otherwise({
redirectTo: '/'
});
});
可以看出,目前的默认页面为 views/main.html
,对应的控制器为 MainCtrl
。
为此,我们需要在 activity.js
和 main.js
中添加一些代码:
activity.js:
Activity.exist = function () {
return (Activity.all()).length == 0;
};
在 model 中增加这个函数用于判断活动列表是否为空。
main.js:
angular.module('partyBidApp')
.controller('MainCtrl', function ($scope, $location) {
$scope.initiate = function () {
var path = Activity.exist()? '/ActivityList': '/CreateActivity';
$location.path(path);
};
$scope.initiate();
});
上面的 controller 中的代码,虽然只有 2 行(甚至可以一行解决),但还是定义了一个 初始化函数 并调用。这是一种习惯,至于是不是好习惯现在还不能确定,只是随着代码量的增加我觉得这种方式能够使代码变得更加 neat 。另一个好处,现在还不能确定是否有用,就是在页面需要更新数据的时候可以直接(被)调用。
特别注意要在控制器的定义函数参数中添加 $location
,当然,实际上除了 $location.path()
外,还有别的页面跳转方法,比如最简单的 href
或者 AngularJS 的 ng-href
等,此处不做介绍。
接着运行 grunt serve
,就能看到现在已经跳到了活动报名页面,虽然目前整个页面上就只有一行 "This is the CreateActivity view."
无活动时返回按钮不显示
在卡片一中,存在如下需求:
无已创建活动情况下,进入”创建活动“页面,”返回“按钮不显示。
AngularJS 中有一个便捷的设置 DOM 可见性的方式,ng-show
和 ng-hide
,用法上没有任何区别,只是效果相反。
注意:所有 ng-someThing
属性都是 AngularJS 中的指令(directive),如果继续深入学习的话读者可能之后会创建自己的指令。
首先要在 views/createactivity.html
中添加一个按钮,位置等样式以及 ng-click
事件由读者自行实现:
...
<button class='' ng-show='{ {activity_exist} }' ng-click=''>返回</button>
...
其中, { { } }
(双层花括号,无空格,因格式问题无法直接打出。)是 AngularJS 的数据绑定语法,表示其值绑定到了 $scope.activity_exist
变量上。
为此,我们需要在 CreateactivityCtrl 中对其进行赋值(仅给出核心代码,下同):
...
$scope.activity_exist = !Activity.exist();
...
运行应用,可以看到按钮不显示(这也叫可以看到么 -.-),因为我们还没有添加创建活动的功能,所以目前看不到按钮显示。
事实上在开发中,我们并不需要按照完整的流程进行测试,在 Jasmine 测试当中,可以直接使用 SpyOn()
来伪造类或对象。这里我们可以先手动创建活动:
...
var new_activity = new Activity('活动一', Date.parse(new Date()));
new_activity.save();
...
将其添加到上面的判断代码之前即可手动创建活动,注意多次运行后将会有多个 活动一 。可在创建一次后删除该代码并重新运行,该活动将保留在 localStorage 中。
关于 Jasmine 测试的使用方法请参见:[2014-08-08-JavaScript之TDD开发简介](% post_url 2014-08-08-JavaScript之TDD开发简介 %})。
为了将按钮固定显示在左上角,读者应自行参看模板应用的 css 代码并对此应用进行相应的修改,本文不对 css 样式本身作过多讲解。
活动列表按时间排序
在卡片一中,存在如下需求:
打开程序后直接进入"活动列表"页面,列表显示为已创建活动。
"活动列表"页面按照时间顺序由新到旧排列活动,最后创建的活动显示在列表的最前。
点击活动列表中的“活动”查看活动信息。
这里明显一定要用到动态的数据绑定,当然这也是 AngularJS 最擅长的地方之一。
和之前的 ng-show
以及 ng-hide
相似,可以使用 ng-repeat
来绑定数组数据。
为此,在 activitylist.html
中添加一个列表控件(写 Xaml 的时候习惯把页面的东东都叫控件了,见谅),主要代码如下:
...
<ul class=''>
<li ng-repeat='activity in activities | orderBy:"-createAt"'>
<a ng-click='go_to_detail(activity.name)'>
{ {activity.name} }
</a>
</li>
</ul>
...
上面的代码中,ng-repeat
中的 activity in activities
将 <li>
列表绑定到了 $scope.activities
数组变量中,对于其中的每个元素生成一个 <li>
,并将其名称作为显示内容,每个活动的点击事件调用 $scope.go_to_detail
函数并将活动名称作为参数。 orderBy:"-createAt"
表示以每个活动的 createAt
属性作为排序依据,-
号表示由大到小。
对于 ng-repeat
中的 ng-click
事件,为了确定具体点击的内容,可以将当前元素的某个属性或者当前元素本身作为参数传递,另一种方法是将 $index
作为参数传递,其代表 ng-repeat
中该元素的位置(从0开始)。
与此对应的 ActivitylistCtrl 核心代码如下:
初始化活动列表:
...
$scope.activities = Activity.all();
...
活动名称点击事件:
...
$scope.go_to_detail = function (activity_name) {
$location.path('/ActivityRegister/' + activity_name);
};
...
按照上面的函数代码运行,点击活动,然后,先自己试一试,真的试了么?好吧我相信你。
页面并没有发生任何变化。Why?因为我们在路由中并没有定义形如 /ActivityRegister/活动一
之类的路径,所以路由按照 .otherwise()
中的配置回到了 起始页面 ,进而在判断有无活动后重新进入到 活动列表页面 。
为此,我们需要返回到 app.js
中修改路由配置(记得原来看到有个人的博客里写的是路由器配置,当时就惊呆了,估计是懒得校稿,不过其实我也没校,如果发现低级错误记得和我说哦 o)。
将原有的 /ActivityRegister
的配置修改为:
...
.when('/ActivityRegister/:activityName', {
templateUrl: 'views/activityregister.html',
controller: 'ActivityregisterCtrl'
})
...
其中的冒号 :
表示 activityName
为参数(或者说变量),由于不存在脱离活动的报名,所以无需保留原有的无参数路径。
之后,在 ActivityregisterCtrl 中,我们就能够取出该参数:
angular.module('partyBidApp')
.controller('ActivityregisterCtrl', function ($scope, $location, $routeParams) {
$scope.this_activity = $routeParams.activityName;
});
之所以给出完整代码是希望读者注意到除了 $location
外现在参数中又多了一个 $routeParams
,用于与路由参数相关的操作。
活动创建
在卡片一中,存在如下需求:
在“创建活动”页面,当输入框内信息为空时,“创建”按钮为灰色的不可点击状态;
创建的活动名称不能重复,如果名称重复,点击【创建】按钮,文本框下红字提示:“*活动名称重复”。页面不跳转。
和之前的数据绑定不同,这里要求 创建 按钮的可用性随着输入框的内容实时变化,而非进入页面时一次性载入。
这里介绍三种方法,ng-model
, ng-change
和 $scope.$watch
。
1.ng-model
ng-model
相当于 ng-bind
(也就是双花括号)的逆向使用,即以使用 ng-model
的控件作为数据源,其属性值中的变量作为数据绑定的对象。因此,我们可以直接通过数据绑定实现。
在 view 中,我们定义一个输入框和一个按钮:
...
<input ng-model='activity_name' placeholder="如:活动一"/>
...
<button ng-disabled='!activity_name' ng-click='create_activity'>创建</button>
...
上面的代码已经可以实现无输入时不可用,其中,Button 的 ng-disabled
属性绑定为输入框文本的取反值,由于无输入文本时其值为 undefined
类型,作为逻辑值时为假;反之,若有输入内容,其值为 string
类型,为逻辑真。
2.ng-change
第二种方法中的 ng-change
指令严格的说来并不像数据绑定,而是类似 ng-click
那样的事件绑定,只是事件的触发源不是点击操作,而是在每当该元素的 Value 发生变化的时候触发。
view 中:
...
<input ng-model='activity_name' ng-change='check_input' placeholder="如:活动一"/>
...
<button ng-disabled='no_create' ng-click='create_activity'>创建</button>
...
controller 中:
...
$scope.check_input = function () {
$scope.no_create = !$scope.activity_name);
//将红色警告取消显示
}
...
使用 ng-change
绑定了一个函数,虽然更为复杂但也能实现更多功能,比如在用户重新输入时把之前的提示活动名称重复给去掉。
3.$scope.$watch
$scope.$watch
的用法和 ng-change
非常相似,但作用范围不同。 ng-change
只能作用于 HTML 元素,而 $scope.$watch
可以用来监测 $scope
中的任何变量或函数的变化。(函数变化指其返回值改变)
在 controller 中,我们定义一个函数作为 $watch
的回调函数,具有三个参数:newValue, oldValue, scope。当然,因为有 scope 参数,实际上该函数也可以不在 controller 内定义。
function watch_callback(newValue, oldValue, scope) {
scope.no_create = !scope.name_to_create;
//将红色警告取消显示
};
接着在 controller 中执行 $scope.$watch
内容:
$scope.$watch('name_to_create', watch_callback, true);
其中,第一个参数为待监视变量或函数,可以传名称也可以传引用,即也可写成:
$scope.$watch($scope.name_to_create, watch_callback, true);
第二个参数为回调函数。
第三个函数为是否深度监视,适用于对象或者数组。如果不加或为 false 只监视其引用值是否发生改变,而不会监视其内部的元素或者属性是否发生改变。对于字符串变量而言,没有实质差异。
另外一个需求是名称不能重复,读者可根据前面的内容在 model 中添加一个 Activity.check_repeat(name)
方法来实现,并根据返回值修改警告框的 ng-show
或 ng-hide
属性值即可,此处不做过多介绍。
第一张卡片中主要用的技术和心得体会主要就是这些,如果有任何疑问欢迎在下方回复 .
本站地址: http://trotyl.github.io/