本节我们会通过构建一个简单的笔记存储应用(可以载入并修改一组简单的笔记)来学习如何应用Angular的特性。这个应用用到的特性有:
- 在JSON文件中存储笔记
- 展示、创建、修改和删除笔记
- 在笔记中使用Markdown格式
- 同步编辑和预览Markdown
本应用已经包含了基础的HTML和CSS代码,还有一个用Node写的简单的RESTful服务器,用于管理笔记,这样我们就可以专注于Angular而不是API。我们学习的重点是如何把Angular加入其中并学习它的重要特性。
3.2.1 获取项目文件
首先,我们需要来获取一下该项目的文件,可以通过git来获取,执行如下命令:
$ git clone https://github.com/ionic-in-action/chapter3.git(克隆chapter3仓库)
$ cd chapter3(切换到chapter3目录)
$ git checkout step1(检出step1标签)
(如果你不想使用git,可以直接下载文件:https://github.com/ionic-in-action/chapter3/archive/step1.zip)
3.2.2 启动开发服务器
由于这个项目搭载的是RESTful服务器,需要你掌握一些NodeJS的知识。在项目server.js文件中可以看到一个简单的RESTful服务器,它是基于Express.js框架开发的,这样做的原因是你需要长期管理笔记,通过RESTful API可以让应用阅读、创建、编辑和删除列表中的笔记。服务器还可以通过HTTP请求把文件载入浏览器,ionic serve就是通过这种方式来运行你的Ionic应用的。
需要注意的是:
- 服务器运行在3000端口上;
- 服务器会接受请求,根据URL和HTTP方法来修改列表中的笔记;
- 服务器使用JSON文件来作为数据库(data/notes.json),你可以根据自己的情况使用其他的数据库;
如果服务器不能运行,则说明你缺少一些必备的node包,解决方法,在终端中进入项目目录,运行$ npm install,npm会检查依赖列表并下载依赖。
然后输入命令$ node server来启动服务器,如果需要终止服务器可以按Ctrl+S或者直接关闭命令窗口。
运行后的基础模板如下图所示:
3.2.3 创建Angular应用
Angular开发简单来说就是用JavaScript创建一个Angular应用并在HTML中使用它。Angular和DOM紧密结合,所以你可以把一个Angular应用严格限制在一个DOM元素及其子元素中。在本例中使用的是<html>元素,所有Angular可以访问整个页面。Ionic通常使用的是<body>元素。
首先,你必须要先载入Angular库,然后要创建一个Angular应用,你需要在一个元素上使用ngApp指令并声明应用名称。打开index.html文件,并添加ngApp指令:
<html lang="en" ng-app="App">
现在,你已经把一个名为App的Angular应用附加到了HTML根元素上。这样Angular就可以访问整个DOM,不过你也可以把它附加到<body>标签中。我们建议把它放在<html>或者<body>元素中。
上面我们只是添加了ngApp的指令,还没有在JavaScript中声明这个应用,下面我们来完成这一步。Angular有一套模板系统,用来封装程序代码。声明新模块时,你需要提供名字和一个数组,其中包含所有依赖。Ionic本身也是一个Angular模块。Angular模块的声明方式如下,创建一个新文件js/app.js并写入下面的代码:
angular.module('App',[]);
最后,在index.html文件</body>标签前添加一个<script>标签来载入Angular模块:
<script src="js/app.js"></script>
你现在已经在页面中声明并载入了一个最基本的Angular应用。angular.module()方法会创建模块并把它附加到ngApp所属的DOM元素中。这是最基本的Angular应用,实际上它现在没有任何功能。所有的Angular应用都是用这样的方式定义的。
3.2.4 添加控制器
控制器:控制数据和业务逻辑
我们需要添加一个控制器来控制应用中多个部分的业务逻辑,它不会改变浏览器中应用的样子,因为控制器只负责管理数据,不影响应用的视觉效果,不过我们需要在管理视觉元素之前搞定控制器。添加控制器之后,它就可以访问页面中的某个特定区域。
下面我们来声明一个简单的控制器。首先你需要引用App模块并使用控制器方法来声明一个控制器。需要传入控制器的名字以及一个包含控制器逻辑的函数。创建文件js/editor.js:
//编辑控制器 js/editor.js angular.module('App') //引入App模块并把它引入这个控制器中 .controller('EditorController',function($scope){ //声明EditorController控制器,传入一个包含依赖列表的函数 $scope.state={ //创建模型的值并存储到$scope中 editing:false }; });
这个控制器现在非常简单,只是创建了一个简单的模型state。$scope服务被注入,所以你可以设置它的属性。记住,$scope中的值被称为模型,可以在视图中访问。
现在修改index.html文件,把控制器加入应用中,在</body>前引入editor.js文件:
<script src="js/editor.js"></script>
最后将控制器附加到DOM中。这会给控制器创建一个新的子作用域。我们需要用一条Angular指令来盛勇控制器被附加的位置:
<div class="container" ng-controller="EditorController">
注:$开头的服务
Angular中的服务以$符号开头,Ionic的服务也是如此。以$开头的服务,按惯例是Angular核心服务或者Ionic服务。
3.2.5 加载数据并将数据显示在应用中
加载数据:使用控制器来加载数据并显示在视图中
下面我们来加载数据并把它显示到应用中,在应用的基础模板左侧已经有一个创建好的空的笔记列表。然后我们加入一些简单的笔记,更新控制器从而把数据载入应用。要实现这个功能,需要使用Angular的$http服务,通过$http服务来请求Node服务器的数据。我们来具体操作一下:
先修改控制器,通过HTTP请求访问服务器的笔记服务并把返回的数据赋值给作用域。打开js/editor.js文件,更新代码:
angular.module('App') .controller('EditorController', function ($scope,$http) { //把$http服务注入控制器 $scope.editing = true; $http.get('/notes').success(function(data){ //使用$http.get加载笔记,如果成功,使用法内的数据 $scope.notes = data; //把从http返回的数据赋值给$scope }).error(function(err){ //处理错误,存储错误 $scope.error = 'Could not load notes'; }); });
注意控制器函数中可以给函数声明任意数量的参数,Angular会通过名字来定位服务并注入控制器。上面代码中$http的使用方法叫做依赖注入(DI),是Angular一个非常强大的特性,可以让你的控制器使用各种服务。由于Angular的服务并不是全局的,所以必须先注入再使用。
这时我们启动服务,但是在页面上看不到任何数据,而我们访问http://localhost:3000/notes,发现json数据可以访问没有问题,那是为什么我们看不到数据呢?因为我们需要更新index.html模板文件,用Angular指令把数据从$scope中显示出来,对index.html文件,我们需要进行如下修改:
<div class="col-sm-3"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"><button class="btn btn-primary btn-xs pull-right">New</button> My Notes</h3> </div> <div class="panel-body"> <!-- ngIf会根据是否有笔记来判读是否把这个元素插入DOM --> <p ng-if="!notes.length">No notes</p> <ul class="list-group"> <!-- ngRepeat会循环每个笔记并显示笔记标题 --> <li class="list-group-item" ng-repeat="note in notes">{{note.title}}><br /> <!-- 绑定显示日期,使用过滤器显示较短的日期 --> <small>{{note.date | date:'short'}}</small></li> </ul> </div> </div> </div>
说明:模板中{{note.date | date:'short'}}有个| date:'short',这是一个过滤器,它会在不改动作用域值的前提下修改显示内容。在表达式中可以通过管道符来使用过滤器,过滤器可以串联,也就是说可以添加多个过滤器。
加载数据后的笔记截图:
3.2.6 处理选择笔记的单击事件
如果需要单独查看某条笔记,需要单击左侧列表笔记时,将他们显示在右侧。
使用ngClick可处理用户的单击事件,然后把笔记数据赋值给一个新的模型,用来进行显示,我们来打开模版index.html,修改笔记列表的部分,添加单击事件处理器:
<ul class="list-group"> <!-- ngRepeat会循环每个笔记并显示笔记标题;ngClick会调用view()并传入下标;添加ngClass,如果笔记被选中就添加active类 --> <li class="list-group-item" ng-repeat="note in notes" ng-click="view($index)" ng-class="{active: note.id == content.id}">{{note.title}}><br /> <!-- 绑定显示日期,使用过滤器显示较短的日期 --> <small>{{note.date | date:'short'}}</small></li> </ul>
当单击笔记时,Angular会尝试调用$scope.view()函数,ngClass指令可以根据情况向元素添加css类。$index值时ngRepeat提供的特殊变量,被传入视图函数里,作用就是告诉你当前被使用的数组元素的下标,此处指的是被单击元素的下标。
下面我们来创建视图函数,打开editor.js文件,在控制器函数里添加视图函数:
$scope.view = function(index){ //声明一个名为view的新$scope方法,接受被点击元素的下标 $scope.editing = false; //把editing状态设置为false,因为此时用户要查看元素 $scope.content = $scope.notes[index]; //给content模型设置一个新模型,包含被单击的笔记 };
此时,当我们点击笔记时,click时间会触发控制器中view()方法,它会根据传入的下标值找到被点击的笔记,并将笔记内容赋值给新的content模型,同时,editing模型也会被设置为false。
3.2.7 更新模版显示被选择的笔记
此时点击笔记,右侧面板不会有变化,因为我们还没有修改右侧显示的面板,右侧面板有两种状态,一个是展示笔记,一个是编辑笔记,$scope.editing属性将决定显示哪个面板。再次打开index.html文件作如下修改:
<!-- ngHide会在本条件为真时隐藏头部,在这里editing为true的时候条件为真 --> <div class="panel panel-default" ng-hide="editing"> <div class="panel-heading"> <!-- 把title绑定到头部 --> <h3 class="panel-title">{{content.title}} <button class="btn btn-primary btn-xs pull-right">Edit</button></h3> </div> <!-- 把content绑定到正文 --> <div class="panel-body">{{content.content}}</div> <!-- 绑定笔记日期并把它传递给过滤器 --> <div class="panel-footer">{{content.date | date:'short'}}</div> </div> <!-- ngShow会在条件为假时隐藏底部,在这里editing为false的时候条件为假 --> <form name="editor" class="panel panel-default" ng-show="editing">
再次运行应用,此时单击笔记即可实现查看功能,如下图所示:
3.2.8 创建指令,解析Markdown格式的笔记
要实现把Markdown格式的文本转换为HTML,需要使用Showdown这个js库。
打开app.js文件,指令不是控制器的一部分,所以代码需要被存储到应用主文件中,具体代码如下:
//声明命令并命名为markdown .directive('markdown',function(){ //创建showdown转换器,下面会用到 var converter = new Showdown.converter(); //命令会返回一个对象,用来声明命令的设置 return { //声明自定义作用于,等待值被赋给markdown属性 scope:{ markdown:'@' }, //声明link函数,它会把markdown转换成html link:function(scope,element,attrs){ //使用作用于观察器来同步模型改动 scope.$watch('markdown',function(){ //把markdown转换成html并存入content变量 var content = converter.makeHtml(attrs.markdown); //把转换好的html内容注入到元素内 element.html(content); }); } } });
现在我们打开index.html文件,传入markdown内容:
<div class="panel-body" markdown="{{content.content}}"></div>
改为markdown笔记格式的页面效果如下:
笔记展示功能已完成,下一节来实现编辑功能。