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,User,Post和Photo。一個user可以有很多個post(和很多粉絲),一個post可以展示很多photo(像collection或者gallery那樣)。每個post有一個標題和一個可選描述。
- from django.db import models
- from django.contrib.auth.models import AbstractUser
- class User(AbstractUser):
- followers = models.ManyToManyField('self', related_name='followees', symmetrical=False)
- class Post(models.Model):
- author = models.ForeignKey(User, related_name='posts')
- title = models.CharField(max_length=255)
- body = models.TextField(blank=True, null=True)
- class Photo(models.Model):
- post = models.ForeignKey(Post, related_name='photos')
- 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來說,我們可以只使用用其中的部分字段,而不包括password和email之類包含用戶隱私的屬性;或者拿photo來說,我們可以返回一個ImageField作為圖片的URL地址,而不是其相對於目錄的位置。
比如我們想要直接從Post中獲取user的信息(而不是一般情況下提供一個超鏈接),我們就可以如下定義一個PostSerializer。這使得user的信息在客戶端很容易獲取,而不需要在獲取每個post時都調用額外的API再來獲取user。使用超鏈接的方法用注釋寫在下面作為對比。序列化的好處在你可以通過超鏈接來擴展來衍生的不同序列,而無需使用嵌套(比如我們要呈現用戶訂閱的源里的post列表)。
PostSerializer中的author信息,是由author的API視圖提供的,因此我們要在serializer中將其設為可選(required=False),並將其添加到驗證排除(validation exclusion)里。
- from rest_framework import serializers
- from .models import User, Post, Photo
- class UserSerializer(serializers.ModelSerializer):
- posts = serializers.HyperlinkedIdentityField('posts', view_name='userpost-list', lookup_field='username')
- class Meta:
- model = User
- fields = ('id', 'username', 'first_name', 'last_name', 'posts', )
- class PostSerializer(serializers.ModelSerializer):
- author = UserSerializer(required=False)
- photos = serializers.HyperlinkedIdentityField('photos', view_name='postphoto-list')
- # author = serializers.HyperlinkedRelatedField(view_name='user-detail', lookup_field='username')
- def get_validation_exclusions(self):
- # Need to exclude `author` since we'll add that later based off the request
- exclusions = super(PostSerializer, self).get_validation_exclusions()
- return exclusions + ['author']
- class Meta:
- model = Post
- class PhotoSerializer(serializers.ModelSerializer):
- image = serializers.Field('image.url')
- class Meta:
- model = Photo
好嘞,現在我們的示例fixture已經載入了,我們來試試這些serializers吧。你可能會看到一個DeprecationWarning,這是因為我們使用了HyperlinkedIdentityField,但是卻沒有提供一個請求對象來構建URL地址。實際上我們已經提供了,所以我們可以無視之。
- >>> user = User.objects.get(username='bob')
- >>> from example.api.serializers import *
- >>> serializer = UserSerializer(user)
- >>> serializer.data
- {'id': 2, 'username': u'bob', 'first_name': u'Bob', 'last_name': u'', 'posts': '/api/users/bob/posts'}
- >>> post = user.posts.all()[0]
- >>> PostSerializer(post).data
- {'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'}
- >>> serializer = PostSerializer(user.posts.all(), many=True)
- >>> serializer.data
- [{'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,因為這也是一個獨特的標識符(我們會在后面的視圖中看到這個)
- from django.conf.urls import patterns, url, include
- from .api import UserList, UserDetail
- from .api import PostList, PostDetail, UserPostList
- from .api import PhotoList, PhotoDetail, PostPhotoList
- user_urls = patterns('',
- url(r'^/(?P<username>[0-9a-zA-Z_-]+)/posts$', UserPostList.as_view(), name='userpost-list'),
- url(r'^/(?P<username>[0-9a-zA-Z_-]+)$', UserDetail.as_view(), name='user-detail'),
- url(r'^$', UserList.as_view(), name='user-list')
- )
- post_urls = patterns('',
- url(r'^/(?P<pk>\d+)/photos$', PostPhotoList.as_view(), name='postphoto-list'),
- url(r'^/(?P<pk>\d+)$', PostDetail.as_view(), name='post-detail'),
- url(r'^$', PostList.as_view(), name='post-list')
- )
- photo_urls = patterns('',
- url(r'^/(?P<pk>\d+)$', PhotoDetail.as_view(), name='photo-detail'),
- url(r'^$', PhotoList.as_view(), name='photo-list')
- )
- urlpatterns = patterns('',
- url(r'^users', include(user_urls)),
- url(r'^posts', include(post_urls)),
- url(r'^photos', include(photo_urls)),
- )
API視圖
Django Rest Framework的強大之處很大程度上在於其通用視圖,有了它,可以在完全不用(或一點點)修改的情況下完成常規的增查改刪(CRUD)。在最簡單視圖中,你只需要提供一個model和一個serializer_class以及一個擴展過的內建通用視圖(比如ListAPIView或者是RetrieveAPIView)。
在我們的例子中,我們有兩個自定義的部分。首先,對於user來說,我們希望使用username作為查找字段而不是pk。所以我們在視圖中設置了lookup_field(一般來說,這是url_kwarg,也是模型中的字段名)
我們也希望能為我們的視圖創建一個嵌套版本,可用於查看某個給定user的post或者是某個給定post內的photo,我們只要簡單重寫視圖中的get_queryset,就可以自定義參數濾過來queryset的結果(相對username和pk)
- from rest_framework import generics, permissions
- from .serializers import UserSerializer, PostSerializer, PhotoSerializer
- from .models import User, Post, Photo
- class UserList(generics.ListCreateAPIView):
- model = User
- serializer_class = UserSerializer
- permission_classes = [
- permissions.AllowAny
- ]
- class UserDetail(generics.RetrieveAPIView):
- model = User
- serializer_class = UserSerializer
- lookup_field = 'username'
- class PostList(generics.ListCreateAPIView):
- model = Post
- serializer_class = PostSerializer
- permission_classes = [
- permissions.AllowAny
- ]
- class PostDetail(generics.RetrieveUpdateDestroyAPIView):
- model = Post
- serializer_class = PostSerializer
- permission_classes = [
- permissions.AllowAny
- ]
- class UserPostList(generics.ListAPIView):
- model = Post
- serializer_class = PostSerializer
- def get_queryset(self):
- queryset = super(UserPostList, self).get_queryset()
- return queryset.filter(author__username=self.kwargs.get('username'))
- class PhotoList(generics.ListCreateAPIView):
- model = Photo
- serializer_class = PhotoSerializer
- permission_classes = [
- permissions.AllowAny
- ]
- class PhotoDetail(generics.RetrieveUpdateDestroyAPIView):
- model = Photo
- serializer_class = PhotoSerializer
- permission_classes = [
- permissions.AllowAny
- ]
- class PostPhotoList(generics.ListAPIView):
- model = Photo
- serializer_class = PhotoSerializer
- def get_queryset(self):
- queryset = super(PostPhotoList, self).get_queryset()
- 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和已認證用戶等。
- from rest_framework import permissions
- class SafeMethodsOnlyPermission(permissions.BasePermission):
- """Only can access non-destructive methods (like GET and HEAD)"""
- def has_permission(self, request, view):
- return self.has_object_permission(request, view)
- def has_object_permission(self, request, view, obj=None):
- return request.method in permissions.SAFE_METHODS
- class PostAuthorCanEditPermission(SafeMethodsOnlyPermission):
- """Allow everyone to list or view, but only the other can modify existing instances"""
- def has_object_permission(self, request, view, obj=None):
- if obj is None:
- # Either a list or a create, so no author
- can_edit = True
- else:
- can_edit = request.user == obj.author
- return can_edit or super(PostAuthorCanEditPermission, self).has_object_permission(request, view, obj)
除了簡單的授權判斷,我們還想在儲存前添加用戶信息。當有人創建了一個新的Post,我們希望把當前用戶賦值為author。因為PostList和PostDetail都要賦值author字段,所以我們可以寫一個mixin類來處理這些通用配置(你也可以用ViewSets來搞定這個)
- class PostMixin(object):
- model = Post
- serializer_class = PostSerializer
- permission_classes = [
- PostAuthorCanEditPermission
- ]
- def pre_save(self, obj):
- """Force author to the current user on save"""
- obj.author = self.request.user
- return super(PostMixin, self).pre_save(obj)
- class PostList(PostMixin, generics.ListCreateAPIView):
- pass
- class PostDetail(PostMixin, generics.RetrieveUpdateDestroyAPIView):
- 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 %} 標簽中,這用我就能在不同頁面中擴展這個模板
- {% load staticfiles %}
- <html>
- <head>
- <link rel="stylesheet" type="text/css" href="{% static "bootstrap/dist/css/bootstrap.css" %}">
- </head>
- <body ng-app="{% block ng_app %}example.app.static{% endblock %}">
- <div class="content" ng-controller="{% block ng_controller %}AppController{% endblock %}">{% block content %}
- {% verbatim %}
- <div class="panel" ng-repeat="post in posts">
- <div class="panel-heading clearfix">
- <h3 class="panel-title">{{ post.title }}</h3>
- <author class="pull-right">{{ post.author.username }}</author>
- </div>
- <p class="well">{{ post.body }}</p>
- </div>
- {% endverbatim %}
- {% endblock %}</div>
- <script src="{% static "underscore/underscore.js" %}"></script>
- <script src="{% static "angular/angular.js" %}"></script>
- <script src="{% static "angular-resource/angular-resource.js" %}"></script>
- <script src="{% static "js/script.js" %}"></script>
- </body>
- </html>
現在,我們用一個簡單的controller為這個模板提供post的列表。我們暫時用硬編碼來寫post,在下一步我們會用Ajax來獲取這些post。
- app = angular.module 'example.app.static', []
- app.controller 'AppController', ['$scope', '$http', ($scope, $http) ->
- $scope.posts = [
- author:
- username: 'Joe'
- title: 'Sample Post #1'
- body: 'This is the first sample post'
- ,
- author:
- username: 'Karen'
- title: 'Sample Post #2'
- body: 'This is another sample post'
- ]
- ]

用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張照片
- app = angular.module 'example.app.basic', []
- app.controller 'AppController', ['$scope', '$http', ($scope, $http) ->
- $scope.posts = []
- $http.get('/api/posts').then (result) ->
- angular.forEach result.data, (item) ->
- $scope.posts.push item
- ]
$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參數。Post和Photo實例則直接使用作為主鍵的id字段。
- app = angular.module 'example.api', ['ngResource']
- app.factory 'User', ['$resource', ($resource) ->
- $resource '/api/users/:username', username: '@username'
- ]
- app.factory 'Post', ['$resource', ($resource) ->
- $resource '/api/posts/:id', id: '@id'
- ]
- app.factory 'Photo', ['$resource', ($resource) ->
- $resource '/api/photos/:id', id: '@id'
- ]
現在我們定義的我們的API模塊可以作為一個獨立的部分在控制器模塊中使用。我們可以將這些API resource做為service注入到我們的控制器中來訪問API。我們將example.api作為獨立的模塊添加進來,並且將所需的API resource作為控制器的依賴定義。resource默認提供了很多的基本的CRUD方法,包括query()(用來獲取集合),get()(用來獲取單獨的元素),save()和delete()等。
- app = angular.module 'example.app.resource', ['example.api']
- app.controller 'AppController', ['$scope', 'Post', ($scope, Post) ->
- $scope.posts = Post.query()
- ]
結果見例子,和之前的Post列表相同。

為Post添加Photo
我們現在可以用ngResource的API調用來顯示我們的post了,但是現在只有一個調用來獲取數據。在實際的應用里,你的數據幾乎不會僅僅通過一個資源接口來調用,這就需要API調用的協同來構建你的應用的模型層。我們來改進我們的APP,這樣我們就能在每個post里顯示和瀏覽照片了。
首先,我們再添加了兩個嵌套的API調用:
- app.factory 'UserPost', ['$resource', ($resource) ->
- $resource '/api/users/:username/posts/:id'
- ]
- app.factory 'PostPhoto', ['$resource', ($resource) ->
- $resource '/api/posts/:post_id/photos/:id'
- ]
這會再為我們提供兩個service(UserPost和PostPhoto),這樣我們就可以相對於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里了)可以檢測更新。
- app = angular.module 'example.app.photos', ['example.api']
- app.controller 'AppController', ['$scope', 'Post', 'PostPhoto', ($scope, Post, PostPhoto) ->
- $scope.photos = {}
- $scope.posts = Post.query()
- $scope.posts.$promise.then (results) ->
- # Load the photos
- angular.forEach results, (post) ->
- $scope.photos[post.id] = PostPhoto.query(post_id: post.id)
- ]
在模板中我們也做出修改來迭代這個model,從而為每個post顯示phots。注意AngularJS是如何在API讀取數據后再渲染的。在這里,我們對每個post中的每個photo對象通過id進行遍歷。同時,我們使用了ng-src而不是src,這可以防止瀏覽器在計算angular表達式之前加載圖片(不然你在日志里會看到404,無法找到”/media/{{ photo.image }}“)
- <div class="panel" ng-repeat="post in posts">
- <div class="panel-heading clearfix">
- <h3 class="pull-left panel-title">{{ post.title }}</h3>
- <author class="pull-right">{{ post.author.username }}</author>
- </div>
- <p class="well">{{ post.body }}</p>
- <span class="photo" ng-repeat="photo in photos[post.id]">
- <img class="img-thumbnail" ng-src="{{ photo.image }}">
- </div>
- </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在每個請求中都被生成了
- <script>
- // Add the CSRF Token
- var app = angular.module('example.app'); // Not including a list of dependent modules (2nd parameter to `module`) "re-opens" the module for additional configuration
- app.config(['$httpProvider', function($httpProvider) {
- $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token|escapejs }}';
- }]);
- </script>
使用AngularJS創建和修改API資源
現在在我們的視圖中創建一個編輯器來發布新的post(像Facebook里發布新的狀態那樣)。大多數的Angular教程都只是在添加頁面上控制器的功能,我們想呈現的是如何保持控制器的簡練和模塊化,所以我們會為post的編輯器創建一個單獨的控制器,並且展示控制器如何利用模型域的嵌套來擴展功能。這也給了我們一個機會來擴展現有的example.app.photos模塊,它提供了頁面中基本的AppController。
首先,我們要擴展我們的基本模板,為編輯器添加添加html模板。因為我們要使用非安全的方法來調用API保存新的post,所以我們還要添加CSRF token。
- {% extends 'base.html' %}
- {% block ng_app %}example.app.editor{% endblock %}
- {% block content %}
- {% verbatim %}
- <div ng-controller="EditController">
- <h5>Create a New Post</h5>
- <form class="form-inline">
- <div class="form-group block-level">
- <input type="text" class="form-control" ng-model="newPost.title" placeholder="Title">
- </div>
- <div class="form-group">
- <input type="text" class="form-control" ng-model="newPost.body" placeholder="Body">
- </div>
- <div class="form-group">
- <button class="btn btn-default" ng-click="save()">Add Post</button>
- </div>
- </form>
- </div>
- {% endverbatim %}
- {{ block.super }}
- {% endblock %}
- {% block js %}
- {{ block.super }}
- <script>
- // Add the CSRF Token
- 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
- app.config(['$httpProvider', function($httpProvider) {
- $httpProvider.defaults.headers.common['X-CSRFToken'] = '{{ csrf_token|escapejs }}';
- }]);
- </script>
- {% endblock %}
現在我們有了編輯器,還要寫一個與之綁定的控制器。注意我們現在依賴兩個模塊,頁面的基本模塊和包含所有$resource定義的API模塊。
- app = angular.module 'example.app.editor', ['example.api', 'example.app.photos']
- app.controller 'EditController', ['$scope', 'Post', ($scope, Post) ->
- $scope.newPost = new Post()
- $scope.save = ->
- $scope.newPost.$save().then (result) ->
- $scope.posts.push result
- .then ->
- # Reset our editor to a new blank post
- $scope.newPost = new Post()
- ]
之前,在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中,這樣模板就會被更新,提示也就顯示給了用戶。
- app = angular.module 'example.app.editor', ['example.api', 'example.app.photos']
- app.controller 'EditController', ['$scope', 'Post', ($scope, Post) ->
- $scope.newPost = new Post()
- $scope.save = ->
- $scope.newPost.$save().then (result) ->
- $scope.posts.push result
- .then ->
- # Reset our editor to a new blank post
- $scope.newPost = new Post()
- .then ->
- # Clear any errors
- $scope.errors = null
- , (rejection) ->
- $scope.errors = rejection.data
- ]
然后在模板上也加上一個便利的錯誤提示:
<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從站點視圖的列表中刪除。和之前一樣,我們可以捕獲錯誤結果並為用戶提供反饋,在這里就略過了。
- app = angular.module 'example.app.manage', ['example.api', 'example.app.editor']
- app.controller 'DeleteController', ['$scope', 'AuthUser', ($scope, AuthUser) ->
- $scope.canDelete = (post) ->
- return post.author.username == AuthUser.username
- $scope.delete = (post) ->
- post.$delete()
- .then ->
- # Remove it from the list on success
- idx = $scope.posts.indexOf(post)
- $scope.posts.splice(idx, 1)
- ]
在定義好控制器后,我們來更新我們的Post模板,使其在canDelete為真時顯示一個刪除按鈕。
- {% extends 'editor.html' %}
- {% block ng_app %}example.app.manage{% endblock %}
- {% block post_header %}
- <button type="button" class="close" ng-controller="DeleteController" ng-click="delete(post)" ng-show="canDelete(post)">×</button>
- {{ block.super }}
- {% endblock %}
- {% block js %}
- {{ block.super }}
- <script>
- // Configure the current user
- var app = angular.module('example.app.manage'); // Not including a list of dependent modules (2nd parameter to `module`) "re-opens" the module for
- app.factory('AuthUser', function() {
- return {
- username: "{{ user.username|default:''|escapejs }}"
- }
- });
- </script>
- {% 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)接口(切換 關注/不關注)
- 更復雜的許可
- 豐富的驗證
- 測試
