用Django Rest Framework和AngularJS开始你的项目

Reference: http://blog.csdn.net/seele52/article/details/14105445

 

译序:虽然本文号称是"hello world式的教程"(这么长的hello world?!),内容上也确实是Django Rest Framework和AngularJS能做出来的相对最简单的东西了,但是鉴于Django, Django Rest Framework和AngularJS本身的复杂程度,本文还是比较适合对于这几个构架还是需要有一点基础性了解的小伙伴们来阅读。


正文开始:

ReSTful API已成为现代web应用的标配了,Django Rest Framework是一个基于django的强大ReST端开发框架。AngularJS是一个可用于构建复杂页面应用的现代JavaScript构架,专注于关注点分离(Separation of concerns)(MVC)及依赖注入(dependency injection),鼓励人们使用可维护(且可测试)的模组来组装富客户端。

在这篇博文中,我将一步步构建出一个AngularJS前端调用ReST API的示例项目,让我们来看看如何简单地结合前端和后端来构建一个复杂的应用。我将会大量的使用代码样例来展示整个过程和一些解决方案。这个Github里有用到的代码。

我们来构建一个Django示例项目

首先,我们来创建一个简单的照片共享应用(简陋版的Instagram)作为演示,让用户可以查看在站点上共享的所有照片。

所有这个项目需要的示例代码都在这里,设置项目环境看这里。在这个过程中,bower+grunt会帮你安装AngularJS和其他一些javascript组件。

搞定以后会有一些可供API演示用的fixture,包括几个用户(['Bob', 'Sally', 'Joe', 'Rachel']),两条post(['This is a great post', 'Another thing I wanted to share'])和一些示例照片。包含在示例代码中的Makefile会为你创建这些数据。

关于这个示例:

  • 我会略过配置,构建和运行示例代码的细节部分,这个repository里的这个说明涵盖了这方面的细节,如果有啥问题请在Github上告诉我,我保证会搞定的。
  • 自从我发现Coffee-Script又高效又可读(还有点Pythonic)以后,我就用它来写前端了。Grunt提供了一个把所有coffee script打包到一个script.js脚本中的task。

项目的数据层(Models)

我们的数据层相当简单,简单得跟Django的入门教程里那种差不多。我们总共有三个models,UserPostPhoto。一个user可以有很多个post(和很多粉丝),一个post可以展示很多photo(像collection或者gallery那样)。每个post有一个标题和一个可选描述。

 

[python] view plain copy
 
  1. from django.db import models  
  2.   
  3. from django.contrib.auth.models import AbstractUser  
  4.   
  5.   
  6. class User(AbstractUser):  
  7.     followers = models.ManyToManyField('self', related_name='followees', symmetrical=False)  
  8.   
  9.   
  10. class Post(models.Model):  
  11.     author = models.ForeignKey(User, related_name='posts')  
  12.     title = models.CharField(max_length=255)  
  13.     body = models.TextField(blank=True, null=True)  
  14.   
  15.   
  16. class Photo(models.Model):  
  17.     post = models.ForeignKey(Post, related_name='photos')  
  18.     image = models.ImageField(upload_to="%Y/%m/%d")  

 

基于Django Rest Framework的API

Django Rest Framework (DRF)提供了一个清晰的构架,你既可以用它来构建简单的一站式API构架,也可以做出复杂的ReST结构。其Serializer,可以把model映射到其对应的一般化表示(JSON, XML之类的),其基于类的视图的扩展集也正符合API接口(endpoint)的需要。Serializer和基于类的视图扩展的清晰的分离是其精妙之处。同时,你也可以自定义URL而无需依赖自动生成,这也是DRF和其他构架(比如Tastypie或者是Piston)不同的地方。这些框架model到API接口的转换基本上都是自动的,这也就造成了灵活性的降低,限制了其在不同情况的使用(尤其是在认证和嵌套资源(nested resources)方面)。

Model Serializers

DRF中的Serializer主要负责把Django中model的实例表示为他们的API,在这其中,我们可以转换任意的数据类型,或者是为model提供额外的信息。拿user来说,我们可以只使用用其中的部分字段,而不包括passwordemail之类包含用户隐私的属性;或者拿photo来说,我们可以返回一个ImageField作为图片的URL地址,而不是其相对于目录的位置。

比如我们想要直接从Post中获取user的信息(而不是一般情况下提供一个超链接),我们就可以如下定义一个PostSerializer。这使得user的信息在客户端很容易获取,而不需要在获取每个post时都调用额外的API再来获取user。使用超链接的方法用注释写在下面作为对比。序列化的好处在你可以通过超链接来扩展来衍生的不同序列,而无需使用嵌套(比如我们要呈现用户订阅的源里的post列表)。

PostSerializer中的author信息,是由author的API视图提供的,因此我们要在serializer中将其设为可选(required=False),并将其添加到验证排除(validation exclusion)里。

[python] view plain copy
 
  1. from rest_framework import serializers  
  2.   
  3. from .models import User, Post, Photo  
  4.   
  5.   
  6. class UserSerializer(serializers.ModelSerializer):  
  7.     posts = serializers.HyperlinkedIdentityField('posts', view_name='userpost-list', lookup_field='username')  
  8.   
  9.     class Meta:  
  10.         model = User  
  11.         fields = ('id', 'username', 'first_name', 'last_name', 'posts', )  
  12.   
  13.   
  14. class PostSerializer(serializers.ModelSerializer):  
  15.     author = UserSerializer(required=False)  
  16.     photos = serializers.HyperlinkedIdentityField('photos', view_name='postphoto-list')  
  17.     # author = serializers.HyperlinkedRelatedField(view_name='user-detail', lookup_field='username')  
  18.   
  19.     def get_validation_exclusions(self):  
  20.         # Need to exclude `author` since we'll add that later based off the request  
  21.         exclusions = super(PostSerializer, self).get_validation_exclusions()  
  22.         return exclusions + ['author']  
  23.   
  24.     class Meta:  
  25.         model = Post  
  26.   
  27.   
  28. class PhotoSerializer(serializers.ModelSerializer):  
  29.     image = serializers.Field('image.url')  
  30.   
  31.     class Meta:  
  32.         model = Photo  

 

好嘞,现在我们的示例fixture已经载入了,我们来试试这些serializers吧。你可能会看到一个DeprecationWarning,这是因为我们使用了HyperlinkedIdentityField,但是却没有提供一个请求对象来构建URL地址。实际上我们已经提供了,所以我们可以无视之。

[python] view plain copy
 
  1. >>> user = User.objects.get(username='bob')  
  2. >>> from example.api.serializers import *  
  3. >>> serializer = UserSerializer(user)  
  4. >>> serializer.data  
  5. {'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': '/api/users/bob/posts'}  
  6. >>> post = user.posts.all()[0]  
  7. >>> PostSerializer(post).data  
  8. {'author': {'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': '/api/users/bob/posts'}, 'photos': '/api/posts/2/photos', u'id': 2, 'title': u'Title #2', 'body': u'Another thing I wanted to share'}  
  9. >>> serializer = PostSerializer(user.posts.all(), many=True)  
  10. >>> serializer.data  
  11. [{'author': {'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': '/api/users/bob/posts'}, 'photos': '/api/posts/2/photos', u'id': 2, 'title': u'Title #2', 'body': u'Another thing I wanted to share'}]  

 

 

API的URL结构

对于我们的API结构,我们希望能够维持一个相对扁平的结构来对给定的资源定义一个规范的接口(canonical endpoints),但同时也希望能够为一些常用的过滤(比如某个给定user的post或者某个给定post的photo)提供一些方便的嵌套列表。注意我们使用model的主键作为其标识符,但对于user来说,我们使用他们的username,因为这也是一个独特的标识符(我们会在后面的视图中看到这个)

[python] view plain copy
 
  1. from django.conf.urls import patterns, url, include  
  2.   
  3. from .api import UserList, UserDetail  
  4. from .api import PostList, PostDetail, UserPostList  
  5. from .api import PhotoList, PhotoDetail, PostPhotoList  
  6.   
  7. user_urls = patterns('',  
  8.     url(r'^/(?P<username>[0-9a-zA-Z_-]+)/posts$', UserPostList.as_view(), name='userpost-list'),  
  9.     url(r'^/(?P<username>[0-9a-zA-Z_-]+)$', UserDetail.as_view(), name='user-detail'),  
  10.     url(r'^$', UserList.as_view(), name='user-list')  
  11. )  
  12.   
  13. post_urls = patterns('',  
  14.     url(r'^/(?P<pk>\d+)/photos$', PostPhotoList.as_view(), name='postphoto-list'),  
  15.     url(r'^/(?P<pk>\d+)$', PostDetail.as_view(), name='post-detail'),  
  16.     url(r'^$', PostList.as_view(), name='post-list')  
  17. )  
  18.   
  19. photo_urls = patterns('',  
  20.     url(r'^/(?P<pk>\d+)$', PhotoDetail.as_view(), name='photo-detail'),  
  21.     url(r'^$', PhotoList.as_view(), name='photo-list')  
  22. )  
  23.   
  24. urlpatterns = patterns('',  
  25.     url(r'^users', include(user_urls)),  
  26.     url(r'^posts', include(post_urls)),  
  27.     url(r'^photos', include(photo_urls)),  
  28. )  

 

API视图

Django Rest Framework的强大之处很大程度上在于其通用视图,有了它,可以在完全不用(或一点点)修改的情况下完成常规的增查改删(CRUD)。在最简单视图中,你只需要提供一个model和一个serializer_class以及一个扩展过的内建通用视图(比如ListAPIView或者是RetrieveAPIView)。

在我们的例子中,我们有两个自定义的部分。首先,对于user来说,我们希望使用username作为查找字段而不是pk。所以我们在视图中设置了lookup_field(一般来说,这是url_kwarg,也是模型中的字段名)

我们也希望能为我们的视图创建一个嵌套版本,可用于查看某个给定user的post或者是某个给定post内的photo,我们只要简单重写视图中的get_queryset,就可以自定义参数滤过来queryset的结果(相对usernamepk

[python] view plain copy
 
  1. from rest_framework import generics, permissions  
  2.   
  3.   
  4. from .serializers import UserSerializer, PostSerializer, PhotoSerializer  
  5. from .models import User, Post, Photo  
  6.   
  7.   
  8. class UserList(generics.ListCreateAPIView):  
  9.     model = User  
  10.     serializer_class = UserSerializer  
  11.     permission_classes = [  
  12.         permissions.AllowAny  
  13.     ]  
  14.   
  15.   
  16. class UserDetail(generics.RetrieveAPIView):  
  17.     model = User  
  18.     serializer_class = UserSerializer  
  19.     lookup_field = 'username'  
  20.   
  21.   
  22. class PostList(generics.ListCreateAPIView):  
  23.     model = Post  
  24.     serializer_class = PostSerializer  
  25.     permission_classes = [  
  26.         permissions.AllowAny  
  27.     ]  
  28.   
  29.   
  30. class PostDetail(generics.RetrieveUpdateDestroyAPIView):  
  31.     model = Post  
  32.     serializer_class = PostSerializer  
  33.     permission_classes = [  
  34.         permissions.AllowAny  
  35.     ]  
  36.   
  37.   
  38. class UserPostList(generics.ListAPIView):  
  39.     model = Post  
  40.     serializer_class = PostSerializer  
  41.   
  42.     def get_queryset(self):  
  43.         queryset = super(UserPostList, self).get_queryset()  
  44.         return queryset.filter(author__username=self.kwargs.get('username'))  
  45.   
  46.   
  47. class PhotoList(generics.ListCreateAPIView):  
  48.     model = Photo  
  49.     serializer_class = PhotoSerializer  
  50.     permission_classes = [  
  51.         permissions.AllowAny  
  52.     ]  
  53.   
  54.   
  55. class PhotoDetail(generics.RetrieveUpdateDestroyAPIView):  
  56.     model = Photo  
  57.     serializer_class = PhotoSerializer  
  58.     permission_classes = [  
  59.         permissions.AllowAny  
  60.     ]  
  61.   
  62.   
  63. class PostPhotoList(generics.ListAPIView):  
  64.     model = Photo  
  65.     serializer_class = PhotoSerializer  
  66.   
  67.     def get_queryset(self):  
  68.         queryset = super(PostPhotoList, self).get_queryset()  
  69.         return queryset.filter(post__pk=self.kwargs.get('pk'))  

 

附:内建API浏览器

Django Rest Freamework的其中一个好处是,它自带了内建的API浏览器来测试我们的API,这与Django的admin模块非常像,这在刚开始开发的时候很有帮助。

只需要在浏览器里加载你的API接口,通过内容协商(content-negotiation),DRF就会为你呈现一个好使的客户端界面用来和你的API交互。

为API添加许可和从属关系

之前写的API视图允许任何人在我们的站上添加任何东西。使用Django Rest Framework的好处之一,是我们可以在不影响相关model和serializer的情况下,很容易在视图里添加许可控制。要让得到许可的用户才能修改我们的API资源,我们可以添加一些许可类来提供授权控制。这些许可类应该对请求返回布尔值。这使得我们可访问完整的请求,包括cookies和已认证用户等。

[python] view plain copy
 
  1. from rest_framework import permissions  
  2.   
  3.   
  4. class SafeMethodsOnlyPermission(permissions.BasePermission):  
  5.     """Only can access non-destructive methods (like GET and HEAD)"""  
  6.     def has_permission(self, request, view):  
  7.         return self.has_object_permission(request, view)  
  8.   
  9.     def has_object_permission(self, request, view, obj=None):  
  10.         return request.method in permissions.SAFE_METHODS  
  11.   
  12.   
  13. class PostAuthorCanEditPermission(SafeMethodsOnlyPermission):  
  14.     """Allow everyone to list or view, but only the other can modify existing instances"""  
  15.     def has_object_permission(self, request, view, obj=None):  
  16.         if obj is None:  
  17.             # Either a list or a create, so no author  
  18.             can_edit = True  
  19.         else:  
  20.             can_edit = request.user == obj.author  
  21.         return can_edit or super(PostAuthorCanEditPermission, self).has_object_permission(request, view, obj)  

 

除了简单的授权判断,我们还想在储存前添加用户信息。当有人创建了一个新的Post,我们希望把当前用户赋值为author。因为PostListPostDetail都要赋值author字段,所以我们可以写一个mixin类来处理这些通用配置(你也可以用ViewSets来搞定这个)

[python] view plain copy
 
  1. class PostMixin(object):  
  2.     model = Post  
  3.     serializer_class = PostSerializer  
  4.     permission_classes = [  
  5.         PostAuthorCanEditPermission  
  6.     ]  
  7.   
  8.     def pre_save(self, obj):  
  9.         """Force author to the current user on save"""  
  10.         obj.author = self.request.user  
  11.         return super(PostMixin, self).pre_save(obj)  
  12.   
  13.   
  14. class PostList(PostMixin, generics.ListCreateAPIView):  
  15.     pass  
  16.   
  17.   
  18. class PostDetail(PostMixin, generics.RetrieveUpdateDestroyAPIView):  
  19.     pass  

 

用AngularJS与你的API对接

随着更具可交互性的页面应用的普及,ReSTful API可以被富客户端界面更好的用于呈现你的应用数据模型,并与之交互。AngularJS拥有清晰的控制分离(separation of controls),所以是一个很好的选择。AngularJS的模块化结构需要一点点配置。你的APP的功能部分由定义了service,directive和controller的模块组成,这样可以获得更清晰的分离。

AngularJS的好处之一,是其提供了类javascript表达的响应式编程。我们可以在模板中简单引用一个变量,我们的页面就会随着变量的变化自动刷新。

对于我们的hello world式的教程来说,我们就简单把我们的post在示例应用中列出来。这是一个AngularJS的基本模板。首先,最顶层的是body标签,我们指定在这也运行的angular app(也就是example.app.basic),我们会在根模块中定义它。其次,我们需要指定一个controller来控制我们代码段(AppController)。AngularJS中的controller更像是传统的MVC中的model+controller(注入的$scope就包括了model层),controller定义的scope包含了可嵌套的model实例,嵌套的层次随着DOM结构逐级向下。最后,我们使用了一个Angular的directive(ng-repeat),这是一个用来遍历我们的储存在$state中的post的控制结构。在这个遍历中,我们用Angular的表达语法(类似于django的模板标签)定义了一些标签来输出作者的username以及post的标题和内容。

提示:使用verbatim标签来包裹AngularJS表达式来避免django的渲染。

提示:我把一些代码段放在了Django的 {% block %} 标签中,这用我就能在不同页面中扩展这个模板

[html] view plain copy
 
  1. {% load staticfiles %}  
  2.   
  3. <html>  
  4. <head>  
  5. <link rel="stylesheet" type="text/css" href="{% static "bootstrap/dist/css/bootstrap.css" %}">  
  6. </head>  
  7. <body ng-app="{% block ng_app %}example.app.static{% endblock %}">  
  8. <div class="content" ng-controller="{% block ng_controller %}AppController{% endblock %}">{% block content %}  
  9. {% verbatim %}  
  10.     <div class="panel" ng-repeat="post in posts">  
  11.         <div class="panel-heading clearfix">  
  12.             <h3 class="panel-title">{{ post.title }}</h3>  
  13.             <author class="pull-right">{{ post.author.username }}</author>  
  14.         </div>  
  15.         <class="well">{{ post.body }}</p>  
  16.     </div>  
  17. {% endverbatim %}  
  18. {% endblock %}</div>  
  19. <script src="{% static "underscore/underscore.js" %}"></script>  
  20. <script src="{% static "angular/angular.js" %}"></script>  
  21. <script src="{% static "angular-resource/angular-resource.js" %}"></script>  
  22. <script src="{% static "js/script.js" %}"></script>  
  23. </body>  
  24. </html>  

 

现在,我们用一个简单的controller为这个模板提供post的列表。我们暂时用硬编码来写post,在下一步我们会用Ajax来获取这些post。

[javascript] view plain copy
 
  1. app = angular.module 'example.app.static', []  
  2.   
  3. app.controller 'AppController', ['$scope', '$http', ($scope, $http) ->  
  4.     $scope.posts = [  
  5.         author:  
  6.             username: 'Joe'  
  7.         title: 'Sample Post #1'  
  8.         body: 'This is the first sample post'  
  9.     ,  
  10.         author:  
  11.             username: 'Karen'  
  12.         title: 'Sample Post #2'  
  13.         body: 'This is another sample post'  
  14.     ]  
  15. ]  

 

 

用XHR从API获取Post

现在我们进一步更新我们的controller,让它从我们的API接口获取post的列表。AngularJS中提供的$http服务和jQuery$.ajax或者其他的XHR部件很相似。注意,在AngularJS中,我们只需要用简单地用ajax请求得到的结果更新我们的model($scope.posts),我们的视图也就会同步地被替换,不需要DOM操作。这种响应模式允许我们开发与数据模型分离的复杂UI,同时能使得UI组件能在无需介入连接细节的情况下能够正确地响应,使得我们能保持视图层和模型层的松耦合。

提示:注意$http中承诺链式(promise chaining)调用(.then())的使用,这样可以很容易地传递回调。你也可以用链式调用的这个有点来写出更复杂的API工作流,比如创建一个新的post,同时保存3张照片

[javascript] view plain copy
 
  1. app = angular.module 'example.app.basic', []  
  2.   
  3. app.controller 'AppController', ['$scope', '$http', ($scope, $http) ->  
  4.     $scope.posts = []  
  5.     $http.get('/api/posts').then (result) ->  
  6.         angular.forEach result.data, (item) ->  
  7.             $scope.posts.push item  
  8. ]  

 

 

$http现在从我们的API获取post的列表,这样我们的例子现在就能展示服务器源的post。

利用Angular-Resource获取API

我们使用$http来发起XHR调用来获取API的数据时,硬编码了许多API细节相关的代码,其中包括URL地址,http动作和其他一些本应包裹在更高层结构里的东西。通过Angular-Resource提供的机制,允许我们将API定义为Angular的service。通过Angular service来管理将更低层的http请求,可以使我们简单地使用ReSTful的动词来与API交互。

要使用Angular-Resource(也就是ngResource),我们只需要简单地映射把API接口映射到URL样式的参数上(和Django的urlpatterns)很像。但不幸的是,要在Django和ngResource的定义之间转换并不是那么的容易,所以这里没有那么的DRY。

当你使用$resource定义你的resource时,你可以简单提供一个url样式、一个默认参数的映射表,外加一些可选的http方法。在我们的例子中,我们希望用Userresource实例的username字段获取URL地址中的:username参数。PostPhoto实例则直接使用作为主键的id字段。

[javascript] view plain copy
 
  1. app = angular.module 'example.api', ['ngResource']  
  2.   
  3. app.factory 'User', ['$resource', ($resource) ->  
  4.     $resource '/api/users/:username', username: '@username'  
  5. ]  
  6.   
  7. app.factory 'Post', ['$resource', ($resource) ->  
  8.     $resource '/api/posts/:id', id: '@id'  
  9. ]  
  10.   
  11. app.factory 'Photo', ['$resource', ($resource) ->  
  12.     $resource '/api/photos/:id', id: '@id'  
  13. ]  

 

现在我们定义的我们的API模块可以作为一个独立的部分在控制器模块中使用。我们可以将这些API resource做为service注入到我们的控制器中来访问API。我们将example.api作为独立的模块添加进来,并且将所需的API resource作为控制器的依赖定义。resource默认提供了很多的基本的CRUD方法,包括query()(用来获取集合),get()(用来获取单独的元素),save()delete()等。

[javascript] view plain copy
 
  1. app = angular.module 'example.app.resource', ['example.api']  
  2.   
  3. app.controller 'AppController', ['$scope', 'Post', ($scope, Post) ->  
  4.     $scope.posts = Post.query()  
  5. ]  

 

结果见例子,和之前的Post列表相同。

为Post添加Photo

我们现在可以用ngResource的API调用来显示我们的post了,但是现在只有一个调用来获取数据。在实际的应用里,你的数据几乎不会仅仅通过一个资源接口来调用,这就需要API调用的协同来构建你的应用的模型层。我们来改进我们的APP,这样我们就能在每个post里显示和浏览照片了。

首先,我们再添加了两个嵌套的API调用:

[javascript] view plain copy
 
  1. app.factory 'UserPost', ['$resource', ($resource) ->  
  2.     $resource '/api/users/:username/posts/:id'  
  3. ]  
  4.   
  5. app.factory 'PostPhoto', ['$resource', ($resource) ->  
  6.     $resource '/api/posts/:post_id/photos/:id'  
  7. ]  

 

这会再为我们提供两个service(UserPostPostPhoto),这样我们就可以相对于user和post来获取资源。我们希望能将嵌套的资源整合在AngluarJS中,使得基本资源加载后就加载他们(另一个方法是使用Angular的$watch机制来响应变化,并触发额外的API调用)。在这儿,我们使用$q服务提供的Promise/Deferred部件来进行链式调用。从ngResource-1.1+开始,任何提供了$promise属性的资源都是可以被链的。我们用这种方式继续发起API调用来获取post的photo。

对于如何处理嵌套的资源,你有两个选择。在这里,我们就用post_id作为标识符,为photo创建另一个$scope的容器。Anguilar的表达式和模板语言会忽略不存在的键,所以我们在模板里只要遍历photos[post.id]来获取照片就可以了。注意,我们不需要显式地改变视图/模板,Angular的$digest过程(也已经整合在ngResource$q里了)可以检测更新。

[javascript] view plain copy
 
  1. app = angular.module 'example.app.photos', ['example.api']  
  2.   
  3. app.controller 'AppController', ['$scope', 'Post', 'PostPhoto', ($scope, Post, PostPhoto) ->  
  4.     $scope.photos = {}  
  5.     $scope.posts = Post.query()  
  6.     $scope.posts.$promise.then (results) ->  
  7.         # Load the photos  
  8.         angular.forEach results, (post) ->  
  9.             $scope.photos[post.id] = PostPhoto.query(post_id: post.id)  
  10. ]  

 

在模板中我们也做出修改来迭代这个model,从而为每个post显示phots。注意AngularJS是如何在API读取数据后再渲染的。在这里,我们对每个post中的每个photo对象通过id进行遍历。同时,我们使用了ng-src而不是src,这可以防止浏览器在计算angular表达式之前加载图片(不然你在日志里会看到404,无法找到”/media/{{ photo.image }}“)

[javascript] view plain copy
 
  1. <div class="panel" ng-repeat="post in posts">  
  2.     <div class="panel-heading clearfix">  
  3.         <h3 class="pull-left panel-title">{{ post.title }}</h3>  
  4.         <author class="pull-right">{{ post.author.username }}</author>  
  5.     </div>  
  6.     <p class="well">{{ post.body }}</p>  
  7.     <span class="photo" ng-repeat="photo in photos[post.id]">  
  8.         <img class="img-thumbnail" ng-src="{{ photo.image }}">  
  9.     </div>  
  10. </div>  

 

最后,我们就得到了带photo的示例页面

附:AngularJS + CSRF保护

Django Rest Framework在使用SessionAuthentication时(比如我们的例子中,在页面应用中使用同一个浏览器session),扩展了Django的跨站请求伪造(Cross Site Request Forgery)。这使得脚本在每次调用API时都要返回服务器提供的token。这有助于防止恶意脚本在用户不知情的情况下的访问。AngularJS的模块结构和依赖注入配置可以很容易地将CSRF Token包含在API请求的首部中(如果你愿意的话也可以放在cookie里)。

在我们的django模板中,只要简单地添加一个<script>标签来配置$httpprovider(这是$http的依赖部件在AngularJS中的叫法),用Django的{{ csrf_token }}模板变量为所有的API调用定义CSRF首部。

提示:确保这段脚本在你的模块定义之后加载

提示:你可以在Angular和Django间通过cookies或其他方式来传递CSRF token。在这里的显式调用只是为了确保csrf_token在每个请求中都被生成了

[javascript] view plain copy
 
  1. <script>  
  2. // Add the CSRF Token  
  3. var app = angular.module('example.app'); // Not including a list of dependent modules (2nd parameter to `module`) "re-opens" the module for additional configuration  
  4. app.config(['$httpProvider', function($httpProvider) {  
  5.     $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token|escapejs }}';  
  6. }]);  
  7. </script>  

 

 

使用AngularJS创建和修改API资源

现在在我们的视图中创建一个编辑器来发布新的post(像Facebook里发布新的状态那样)。大多数的Angular教程都只是在添加页面上控制器的功能,我们想呈现的是如何保持控制器的简练和模块化,所以我们会为post的编辑器创建一个单独的控制器,并且展示控制器如何利用模型域的嵌套来扩展功能。这也给了我们一个机会来扩展现有的example.app.photos模块,它提供了页面中基本的AppController

首先,我们要扩展我们的基本模板,为编辑器添加添加html模板。因为我们要使用非安全的方法来调用API保存新的post,所以我们还要添加CSRF token。

[html] view plain copy
 
  1. {% extends 'base.html' %}  
  2.   
  3. {% block ng_app %}example.app.editor{% endblock %}  
  4.   
  5. {% block content %}  
  6. {% verbatim %}  
  7. <div ng-controller="EditController">  
  8.     <h5>Create a New Post</h5>  
  9.     <form class="form-inline">  
  10.         <div class="form-group block-level">  
  11.             <input type="text" class="form-control" ng-model="newPost.title" placeholder="Title">  
  12.         </div>  
  13.         <div class="form-group">  
  14.             <input type="text" class="form-control" ng-model="newPost.body" placeholder="Body">  
  15.         </div>  
  16.         <div class="form-group">  
  17.             <button class="btn btn-default" ng-click="save()">Add Post</button>  
  18.         </div>  
  19.     </form>  
  20. </div>  
  21. {% endverbatim %}  
  22. {{ block.super }}  
  23. {% endblock %}  
  24.   
  25. {% block js %}  
  26. {{ block.super }}  
  27. <script>  
  28. // Add the CSRF Token  
  29. var app = angular.module('example.app.editor'); // Not including a list of dependent modules (2nd parameter to `module`) "re-opens" the module for additional configuration  
  30. app.config(['$httpProvider', function($httpProvider) {  
  31.     $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token|escapejs }}';  
  32. }]);  
  33. </script>  
  34. {% endblock %}  

 

 

现在我们有了编辑器,还要写一个与之绑定的控制器。注意我们现在依赖两个模块,页面的基本模块和包含所有$resource定义的API模块。

[javascript] view plain copy
 
  1. app = angular.module 'example.app.editor', ['example.api', 'example.app.photos']  
  2.   
  3. app.controller 'EditController', ['$scope', 'Post', ($scope, Post) ->  
  4.   
  5.     $scope.newPost = new Post()  
  6.     $scope.save = ->  
  7.         $scope.newPost.$save().then (result) ->  
  8.             $scope.posts.push result  
  9.         .then ->  
  10.             # Reset our editor to a new blank post  
  11.             $scope.newPost = new Post()  
  12. ]  

 

之前,在API视图中,我们添加了一些许可限制以阻止其他用户修改某人的post。但之前,用户无论干什么其实都无所谓的。现在我们想发post(原文是create user,貌似说不通),就需要确保我们得作为一个有效的用户被授权,否则我们创建post的API请求就会被拒绝。在这个示例中,可以用Django的Authentication Backend自动作为管理员为你登陆。当然,不要在生产环境或者不受信任的环境下这么做。这只是用来帮助我们进行登陆和注册无关的沙箱测试的。

错误处理

跟着教程做的时候,你试过发一个没有title的post么?我们在Django的model中把这个作为必须字段了,所以Django Rest Framework在创建资源之前会验证这个字段。如果你试图创建一个没有title的Post(无论是用API浏览器还是我们刚创建的表单),你都会得到一个400 Bad Request响应,并附上请求失败的原因。我们可以用这个来通知用户。

{
    "title": [
        "This field is required."
    ]
}

要通知用户,我们要修改我们的API调用。因为我们使用了Promise,我们可以简单地添加一个错误回调来捕捉响应,并且把它放在$scope中,这样模板就会被更新,提示也就显示给了用户。

[javascript] view plain copy
 
  1. app = angular.module 'example.app.editor', ['example.api', 'example.app.photos']  
  2.   
  3. app.controller 'EditController', ['$scope', 'Post', ($scope, Post) ->  
  4.   
  5.     $scope.newPost = new Post()  
  6.     $scope.save = ->  
  7.         $scope.newPost.$save().then (result) ->  
  8.             $scope.posts.push result  
  9.         .then ->  
  10.             # Reset our editor to a new blank post  
  11.             $scope.newPost = new Post()  
  12.         .then ->  
  13.             # Clear any errors  
  14.             $scope.errors = null  
  15.         , (rejection) ->  
  16.             $scope.errors = rejection.data  
  17. ]  

 

 

然后在模板上也加上一个便利的错误提示:

<p ng-repeat="(name, errs) in errors" class="alert alert-danger"><strong>{{ name }}</strong>: {{ errs.join(', ') }}</p>

只需要简单的把promise的API链在一起,就可以很直观地添加UI元素来提供过程的反馈(加载提示和进度条之类的)。AngularJS有相当完整的Promise规范来确保error(或者rejection)向下传递,使得处理error变得更简单。

就这个例子而言,我们只是将错误枚举出来放到bootstrap的alert box里。因为错误是以字段名作为标识符的,所以你可以很容易地在模板中将错误提示放在响应的表单字段附近。

删除Post

要完善我们的编辑器,我们还需要添加一个删除post的功能,使得当前用户可以删除自己post,我们已经构建的API可以防止用户来修改/删除不属于自己的资源。当你对AngularJS还不熟悉的时候,Angular的模块让人很难理解到底应该如何给controller提供初始数据。在我们的例子中,我们需要知道当前用户是谁,这样我们就能控制到底哪些post是可以被删除的。

这其中,要理解这一切的诀窍是尝试将你的controller分解成很多处理实际逻辑的service/factory(在angular里这俩差不多一个意思)。controller(很像Django里的view)只应该关注于整合不同的部件。在Django中,你应该在模型层中整合尽可能多的业务逻辑(胖模型),在Angular中,你也应该类似地将业务逻辑放到可组合的service中。

要添加删除post的功能,我们先要添加一个模块扩展我们的编辑器,并一个额外的控制器来处理删除事物。我们需要依赖一个叫AuthUser的service,在这个service中,当前用户会由Django的模版中渲染。在本例中,我们就只简单把当前用户的username属性渲染出来(如果没有登陆就留空)。我们在scope中添加两个函数,canDelete来判断给定post是否能被user删除,delete来删除post。这两个函数都以模板中提供的post为参数。

我们在这里又一次用到了$resource的Prominse接口,在成功的从服务器确认已删除后,我们才将post从站点视图的列表中删除。和之前一样,我们可以捕获错误结果并为用户提供反馈,在这里就略过了。

[javascript] view plain copy
 
  1. app = angular.module 'example.app.manage', ['example.api', 'example.app.editor']  
  2.   
  3. app.controller 'DeleteController', ['$scope', 'AuthUser', ($scope, AuthUser) ->  
  4.     $scope.canDelete = (post) ->  
  5.         return post.author.username == AuthUser.username  
  6.   
  7.     $scope.delete = (post) ->  
  8.         post.$delete()  
  9.         .then ->  
  10.             # Remove it from the list on success  
  11.             idx = $scope.posts.indexOf(post)  
  12.             $scope.posts.splice(idx, 1)  
  13. ]  

 

在定义好控制器后,我们来更新我们的Post模板,使其在canDelete为真时显示一个删除按钮。

[html] view plain copy
 
  1. {% extends 'editor.html' %}  
  2.   
  3. {% block ng_app %}example.app.manage{% endblock %}  
  4.   
  5. {% block post_header %}  
  6.   
  7. <button type="button" class="close" ng-controller="DeleteController" ng-click="delete(post)" ng-show="canDelete(post)"</button>  
  8. {{ block.super }}  
  9. {% endblock %}  
  10.   
  11. {% block js %}  
  12. {{ block.super }}  
  13. <script>  
  14. // Configure the current user  
  15. var app = angular.module('example.app.manage'); // Not including a list of dependent modules (2nd parameter to `module`) "re-opens" the module for  
  16.   
  17. app.factory('AuthUser', function() {  
  18.     return {  
  19.         username: "{{ user.username|default:''|escapejs }}"  
  20.     }  
  21. });  
  22. </script>  
  23. {% endblock %}  

 

 

现在你加载示例页面,就能看到在root的post上出现了一个`×`,但是bob的帖子上没有。点×就会调用我们的API来删除post。

到此为止,我们就有了一个简单的分享源可以给用户分享消息了。

总结

好了,我们回过头来看看。用少量的代码(~100行的前端,~200行的后端),Django Rest Framework(当然还有Django本身)和AngularJS,我们得以很快速地创建一个简单的应用来发布一些简单的post,通过ReSTful的API层将Django的Django的数据模型基于用例导出,因为DRF而变得非常容易。AngularJS则使得我们非常容易地以更加结构化和模块化的方式与API交互,使得我们在添加新的功能的时候不会写出意大利面一样的代码。

本文所有的代码都可以在这个GitHub上找到。你可以自己checkout一个repository来继续你自己的尝试。如果发现任何错误,请留一个Issue(Pull-request就更好了),我保证我会更正的。如果你有任何问题,请给我留言(或者在Twitter上@kevinastone)。我会继续这一系列的文章来写更多的DRF+Angular的解决方案,包括:

    • 分页
    • 单列(singleton)接口(切换 关注/不关注)
    • 更复杂的许可
    • 丰富的验证
    • 测试

posted on 2016-10-26 11:26  alex.shu  阅读(1755)  评论(0编辑  收藏  举报

导航