Nyligen fick jag dyka in i AngularJS på en Rails-webbplats som vi byggde för en kund. Förutom sidinnehåll har kunden vissa sidor som visar rubriker som länkar till externa källor men hämtas in i vår Rails-stack via en rake-uppgift som körs periodiskt. Webbplatsen ska cachas tungt via användande av ett CDN, som CloudFront, men för att hålla rubrikerna aktuella använde vi AngularJS (eftersom kunden var bekant med det och därmed lättare underhåll för dem) för att fråga Rails-stacken direkt efter de senaste rubrikerna.
Det krävde att göra saker på ett sätt som korsade domängränser eftersom webbplatsens domän skulle peka på CloudFront. Jag hade inte skrivit ett Rails API som returnerade JSONP eller en AngularJS-tjänst tidigare, så jag behövde utforska och detta fungerade för mig. (Du kan även läsa gisten.)
Obs: Om du inte är bekant med JSONP kommer detta inte att göra mycket mening. Läs detta.
AngularJS Service
AngularJS har konceptet med tjänster, som är singletons som kan användas var som helst du behöver. I vårt fall kan du tänka på vår rubriktjänst som rubrikernas API-klient som omsluter våra JSONP-anrop i en enklare och centraliserad syntax. AngularJS tillhandahåller vissa tjänster som standard, men du kan skriva dina egna. Faktiskt kunde vi ha använt den inbyggda $resource
tjänsten istället för att skriva vår egen, men jag hade problem med att få JSONP att fungera och jag ville förstå de lägre nivåernas AngularJS-komponenter bättre, så jag skrev min egen:
angular.module('app')
.factory('HeadlineService', ['$http',
function($http) {
'use strict';
var BASE_URL = "<%= [App.settings.api_base_url, '/services/headlines'].join %>",
CALLBACK_STRING = "?callback=JSON_CALLBACK";
return {
getHeadlines: function(){
return $http.jsonp(BASE_URL + CALLBACK_STRING)
},
getHeadlineForId: function(id){
return $http.jsonp(BASE_URL + "/" + id + ".json" + CALLBACK_STRING)
}
}
}
]);
Tjänsten ovan använder den inbyggda $http
tjänsten, som enbart tillhandahåller en AJAX-omslag, för att göra begäranden till vår Rails-backend för att få rubriker. Några saker att notera:
- Den returnerar ett objekt med 2 funktioner:
getHeadlines
ochgetHeadlineForId
. Vi kan sedan använda dessa funktioner som genvägar för att prata med Rails API i våra AngularJS-kontroller. - Den använder JSONP. Observera att vi använder
$http.jsonp
och att vi skickar en frågesträng?callback=JSON_CALLBACK
. AngularJS ersätter strängenJSON_CALLBACK
med namnet på en callback-funktion som den skapar för dig. - Den använder en annan domän. Nåja, det är väntat eftersom vi gör JSONP, men vi uppnår det genom att lägga till en
.erb
-filändelse och använda vad vi har ställt in förApp.settings.api_base_url
i Rails-appen som domänen som AngularJS ska prata med. Kom ihåg att detta är för att kringgå CDN-cachningslagret så att våra rubriker alltid är de senaste.
AngularJS Controller
Med vår HeadlinesService
byggd kan vi nu använda de 2 funktioner den returnerar i vår AngularJS-app:
angular.module('app')
.controller('HeadlinesCtrl', ['$scope', 'HeadlineService',
function($scope, HeadlineService) {
'use strict';
HeadlineService.getHeadlines().success(function(data, status, headers, config){
$scope.headlines = data;
HeadlineService.getHeadlineForId(data[0].id).success(function(data){
$scope.single_headline = data;
}).error(function(error, status, headers, config){
alert('något gick fel när en rubrik hämtades');
});
}).error(function(error, status, headers, config){
alert('något gick fel när alla rubriker hämtades');
});
}
]);
För exempelvisa syften hämtar jag enbart det fulla rubrikobjektet för den första rubriken som returneras i getHeadlines
success-callback.
Det $scope
-objektet är exponerat för vyerna i AngularJS, så vi kan visa rubrikerna på sidan genom att ha en vy som:
<section class="headline_section" ng-controller="HeadlinesCtrl">
<h3>Här är några rubriker...</h3>
<div class="headlines">
<ul>
<li ng-repeat="headline in headlines">
<a href="{{ headline.url }}" target="_none">{{ headline.title }}</a>
</li>
<ul>
</div>
<div class="headline">
<a href="{{ single_headline.url }}" target="_none">{{ single_headline.title }}</a>
</div>
</section>
Rails Backend
Nu när vi har fått AngularJS att ringa vår Rails-backend behöver vi skriva Rails-backenden! Följande kontrollant är ganska grundläggande ruby on rails, men för att returnera JSONP i stället för gammaldags JSON använder vi callback
-alternativet i render
-metoden. Det berättar för Rails att vi använder JSONP och att omsluta de JSON-data vi gav den i den callback och ställa in Content-Type
-headern korrekt så att allting är i enlighet med JSONP-standarden och vår AngularJS-klient kan bearbeta data.
class Services::HeadlinesController < ApplicationController
def index
page = (params[:page] || 1).to_i
per = (params[:per] || 5).to_i
offset = (page - 1) * per
@headlines = Headline.where(:disabled => false).offset(offset).limit(per).to_a
render :json => @headlines.to_json, :callback => params[:callback]
end