領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多此文由Han Lim和Tony Nguyen特邀撰寫。Han和Tony在新加坡Spring使用者組上就Spring + Angular JS做了一次精彩的演講。此部落格基於他們的演講內容。
在本文中,我們將描述我們從JSP、Struts和Velocity等伺服器端渲染檢視技術轉向使用AngularJS(一個流行的現代瀏覽器Javascript框架)的客戶端渲染檢視技術的經驗。我們將討論在進行此更改時需要注意的一些事項以及可能遇到的潛在陷阱。如果您有Spring Web MVC和JSP開發經驗,並想了解Spring MVC如何與AngularJS等客戶端Javascript協同工作,本文可能正適合您。
還有一個附錄,其中提供了一些關於AngularJS的額外見解,這些見解對於來自JSP世界的人來說可能看起來陌生或不熟悉。
我們建立了Spring Petclinic應用程式的一個分支,並嘗試將其轉換為AngularJS(由Andrew Abogado提供了新的設計)。我們的分支可以在此處找到。
當您開始從JSP或Thymeleaf等伺服器端模板引擎遷移到客戶端基於Javascript的模板引擎時,您將需要採用客戶端-伺服器架構的正規化轉變。您必須停止將檢視視為Web應用程式的一部分,而應將Web應用程式視為兩個獨立的客戶端和伺服器端應用程式。因此,AngularJS應用程式成為一個獨立執行在您的Web瀏覽器上的應用程式,它與Spring MVC提供的後端服務進行通訊。Spring MVC應用程式和AngularJS之間唯一的共同點可能在於它們部署在同一個Java WAR檔案中,並且索引檔案是從JSP提供的。
下圖對此進行了說明,該圖顯示了Spring應用程式如何成為RESTful Web服務的提供者,服務於各種前端應用程式,包括基於瀏覽器的AngularJS應用程式,以及為平板電腦或智慧手機等移動客戶端提供服務的可能性。這些服務可能包括OAuth、身份驗證和其他業務邏輯服務,這些服務應從公共檢視中進行混淆。應該記住,以JSON或javascript檔案形式釋出的任何資料或業務邏輯都暴露給客戶端可見。因此,如果存在任何不應暴露的業務敏感邏輯或工作流程,則應僅在後端執行。
使用AngularJS而不是JSP的另一個值得注意的區別是,我們更傾向於不使用HTML表單和傳統的表單提交將資料傳遞到伺服器端。相反,我們更傾向於將表單提交封裝在JSON物件中,該物件透過AngularJS HTTP Post方法呼叫傳送到後端RESTful服務。事實上,我們更傾向於使用開發RESTful服務中鼓勵的全範圍HTTP動詞。
如果您需要對使用者輸入執行驗證,可以使用AngularJS的內建驗證或您自己的自定義輸入驗證在前端完成。在將資料釋出到伺服器之前,您應該始終驗證您的資料。同時在伺服器端驗證相同的資料也是謹慎的,以確保不檢查其資料的客戶端不會損害伺服器端資料的完整性。

現在我們來討論如何組織您的Spring + AngularJS應用程式。在WDS(我們公司),我們使用Maven作為Java/Spring的依賴和包管理工具,這影響了我們如何決定放置AngularJS javascript應用程式。AngularJS應用程式是在src/main/webapp中建立的,主要檔案是
components/ # the various components are stored here.
js/app.js # where we bootstrap the application
plugins/ # additional external plugins e.g. jquery.
services/ # common services are stored here.
images/
videos/
您可以在下面看到Eclipse中資料夾結構的影像捕獲。

這裡的資源是按照feature-grouping方法組織的。也有一些方法可以根據型別對資源進行分組,例如,將所有控制器、服務和檢視分組到其同名資料夾中。這些選項各有優缺點。
還有一些基於 Javascript 的包管理器,如 npm 或 bower,您可能希望考慮使用它們來簡化外部依賴項的管理。如果您使用 bower,將建立一個名為 bower_components 的資料夾,所有依賴項資源都將安裝在該資料夾中。然後,您需要像包含任何 Javascript 庫一樣將它們包含在您的模板中。至於 npm,您可以使用它來管理所有 Javascript 伺服器端系統工具,如 Grunt(一種類似於 Ant 的任務執行器)。
如果您在JSP中使用了Spring的自定義表單標籤來開發表單,您可能想知道AngularJS是否提供了同樣方便的表單輸入到物件的對映。答案是肯定的!事實上,將任何HTML元素繫結到Javascript物件都非常容易。唯一的區別是現在繫結發生在客戶端而不是伺服器端。
<form:form method="POST" commandName="user">
<table>
<tr>
<td>User Name :</td>
<td><form:input path="name" /></td>
</tr>
<tr>
<td>Password :</td>
<td><form:password path="password" /></td>
</tr>
<tr>
<td>Country :</td>
<td>
<form:select path="country">
<form:option value="0" label="Select" />
<form:options items="${countryList}" itemValue="countryId" itemLabel="countryName" />
</form:select>
</td>
</tr>
</table>
</form:form>
以下是AngularJS中相同表單的示例
<form name="UserForm" data-ng-controller="ExampleUserController">
<table>
<tr>
<td>User Name :</td>
<td><input data-ng-model="user.name" /></td>
</tr>
<tr>
<td>Password :</td>
<td><input type="password" data-ng-model="user.password" /></td>
</tr>
<tr>
<td>Country :</td>
<td>
<select data-ng-model="user.country" data-ng-options="country as country.label for country in countries">
<option value="">Select<option />
</select>
</td>
</tr>
</table>
</form>
AngularJS中的表單輸入增強了額外的功能,例如ngRequired指令,該指令根據某些條件使欄位成為必填項。還有內建的驗證功能,用於檢查範圍、日期、模式等。您可以在此處找到AngularJS官方文件,其中提供了所有相關的表單輸入指令。
為了成功地將基於 JSP 的應用程式遷移到使用 AngularJS 的應用程式,需要考慮以下幾個因素。
您需要轉換您的控制器,以便它們不再將響應轉發給模板引擎以向客戶端渲染檢視,而是提供將序列化為JSON資料的服務。以下是一個示例,說明標準Spring MVC控制器RequestMapping如何使用ModelAndView物件根據URL對映中描述的所有者來渲染檢視。
@RequestMapping("/api/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
ModelAndView mav = new ModelAndView("owners/ownerDetails");
mav.addObject(this.clinicService.findOwnerById(ownerId));
return mav;
}
這樣的控制器RequestMapping可以轉換為等效的RESTful服務,該服務根據ownerId返回所有者。您的模板可以移到AngularJS中,然後AngularJS會將所有者物件繫結到AngularJS模板。
@RequestMapping(value = "/api/owners/{id}", method = RequestMethod.GET)
public @ResponseBody Owner find(@PathVariable Integer id) {
return this.clinicService.findOwnerById(id);
}
為了讓Spring MVC將您返回的物件(需要是Serializable的)轉換為JSON物件,您可以使用Jackson2序列化庫,它是Spring MVC依賴項的一部分。在下面的示例中,我們不得不透過Jackson2自定義日期序列化格式,因此我們在Spring Context xml檔案中添加了xml片段來描述JSON ObjectMapper工廠的日期格式,以便它知道Jackson2 ObjectMapper需要這種格式的日期。您可以在下面看到執行此Spring上下文配置的片段。如果不需要自定義日期格式(或任何其他序列化要求),您可以使用預設的格式,這意味著您甚至不需要包含此部分,因為Spring MVC預設會元件掃描ObjectMapper並透過自動裝配將其注入到您的控制器類中。
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean" p:indentOutput="true" p:simpleDateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSZ"></bean>
<mvc:annotation-driven conversion-service="conversionService" >
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" >
<property name="objectMapper" ref="objectMapper" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
一旦您將控制器轉換為RESTful服務,就可以從AngularJS應用程式訪問這些資源。
在AngularJS中訪問RESTful服務的一種很好的方式是使用內建的ngResource指令,它允許您以優雅簡潔的方式訪問RESTful服務。以下程式碼片段可以說明如何使用此指令訪問RESTful服務
var Owner = ['$resource','context', function($resource, context) {
return $resource(context + '/api/owners/:id');
}];
app.factory('Owner', Owner);
var OwnerController = ['$scope','$state','Owner',function($scope,$state,Owner) {
$scope.$on('$viewContentLoaded', function(event){
$('html, body').animate({
scrollTop: $("#owners").offset().top
}, 1000);
});
$scope.owners = Owner.query();
}];
上面的程式碼片段展示瞭如何透過宣告一個 Owner 資源並將其初始化為 Owner 服務來建立一個“資源”。然後控制器可以使用此服務從 RESTful 端點查詢 Owner。透過這種方式,您可以輕鬆建立應用程式所需的資源,並輕鬆地將其對映到您的業務領域模型。此宣告只在 app.js 檔案中進行一次。您可以實際檢視此檔案在此處的實際操作。
在轉向 RestAPI 時,重要的是要記住 RestAPI 是公共介面而不是網站內容。JSON 模型對使用者是**完全可見**的。例如,如果我們需要顯示使用者配置檔案,密碼掩碼應該在 JSON 物件上完成,而不是在模板中。為了做到這一點,有時我們需要為我們的 RestAPI 建立 DTO 物件。
在開發客戶端-伺服器架構時,狀態同步是需要管理的問題。您需要考慮應用程式如何從後端更新其狀態,或者在某些狀態更改時重新整理其檢視。
將客戶端程式碼公開給大眾,這使得認真考慮如何驗證使用者身份並與您的應用程式保持會話變得更加重要。在選擇身份驗證方法時,一個重要的考慮因素是根據您的應用程式架構選擇有狀態會話還是無狀態會話。
您可以檢視 Dave Syer 關於如何將 AngularJS 與 Spring Security 整合的系列部落格此處。
AngularJS 提供了必要的工具,可幫助您在 Javascript 開發的各個層面(從單元測試到功能測試)執行測試。規劃如何測試和執行包含這些測試的構建將決定前端客戶端的質量。我們使用一個名為 frontend-maven-plugin 的 Maven 外掛來協助我們的構建測試。
從JSP遷移到AngularJS可能看起來令人生畏,但從長遠來看,它會帶來豐厚的回報,因為它能提供更易於維護和測試的使用者介面。客戶端渲染檢視的趨勢也鼓勵構建更具響應性的Web應用程式,這些應用程式以前受到伺服器端渲染設計的阻礙。HTML 5和CSS3的出現將我們帶入了檢視渲染技術的新時代,EmberJs、ReactJs、BackboneJs等各種競爭框架層出不窮。然而,就發展勢頭而言,AngularJS一直備受關注,並且使用了一段時間後,我們也能明白原因。我們希望本文能為那些打算嘗試的人提供有用的建議。您可以檢視Spring Petclinic的分支,其中包含一些程式碼示例,以瞭解我們是如何做到的。
AngularJS是由Google建立的一個Javascript框架,自稱為“超級英雄般的Web MVW框架”(其中“MVW”中的“W”是對各種MVx架構的戲謔性引用,意為“隨便”)。由於它基於MVx架構,AngularJS為Javascript開發提供了結構,從而賦予Javascript比傳統Spring + JSP應用程式更高的地位,後者僅使用Javascript來提供使用者介面上的一點互動性。
藉助AngularJS,您的基於Javascript的檢視層還繼承了依賴注入、HTML詞彙擴充套件(透過使用自定義指令)、單元測試和功能測試整合以及DOM選擇器(類似於JQuery,使用jqlite,因為它只提供了JQuery的一個子集,但如果您願意,也可以輕鬆使用JQuery)。AngularJS還將作用域引入您的Javascript程式碼,以便您程式碼中宣告的變數僅繫結到所需的作用域。這可以防止在Javascript程式碼量增長時無意中出現的變數汙染問題。
當您使用JSP開發Spring Web MVC應用程式時,您很可能會使用Spring提供的表單標籤將表單輸入繫結到伺服器端模型。同樣,AngularJS提供了一種將表單輸入繫結到客戶端模型的方法。事實上,它提供了從表單輸入到Javascript應用程式中模型的即時雙向資料繫結。這意味著您不僅可以受益於檢視隨Javascript模型內部的變化而更新,而且您對UI所做的任何更改也會更新Javascript模型(因此也會更新繫結到該模型的任何其他檢視)。看到所有繫結到同一個JS模型的檢視在應用程式中自動更新模型,這幾乎是神奇的。
此外,由於您的模型可以設定為特定的作用域,因此只有屬於同一作用域的檢視才會受到影響,這允許您對僅應侷限於檢視特定部分的程式碼進行沙盒化。(這是透過在HTML模板中設定的名為ng-controller的AngularJS屬性完成的)。您可以在後面的章節中看到JSP標籤和AngularJS指令的比較差異。
在Spring-JSP Web應用程式中,存在從Spring模型到JSP檢視的單向資料繫結。對模型的任何更改都將反映到JSP檢視,但反之則不然。這是Web應用程式的性質。如果我們構建桌面應用程式,則可以使用Swing UI進行反向資料繫結。
然而,對於暴露REST資源的Web應用程式,可能沒有直接的資料繫結。資料作為JSON物件從伺服器傳送到瀏覽器。如果沒有AngularJS等工具,開發人員需要編寫javascript程式碼才能將javascript物件繫結到html控制元件。
由於手動資料繫結是一項繁瑣的任務,一些開發人員嘗試透過建立JavaScript框架來自動化資料繫結。值得記住的是,這種資料繫結發生在客戶端,資料繫結的模型是一個JavaScript物件,而不是伺服器端模型。
Angular 更進一步,建立了雙向繫結。更改 HTML 控制元件中的值將即時反映在物件中。

如果您需要處理複雜的 UI 元件,例如 AJAX 表格,繫結是一個非常有用的概念。
例如:我們需要在AngularJs應用程式中渲染使用者和角色列表,使用以下html模板
<tr ng-repeat="user in users">
<td>{{user.username}}</td>
<td>{{user.role}}</td>
</tr>
...
<a ng-click="addUser()">Add new user</a>
新增使用者的程式碼可以如此簡單
$scope.addUser = function(){
newUser = {}
$scope.users.push(newUser );
}
如果陣列users再增加一個元素,表格將自動增加一行。
使用AngularJS,可以以有組織、優雅的方式編寫相對複雜的S使用者介面,始終將所需的邏輯封裝在元件中,絕不會出現錯誤的全域性Javascript變數汙染作用域的風險。它也具有很高的可測試性,並且內建了在單元和功能級別執行測試的機制,確保您的使用者介面程式碼庫經過與Java/Spring程式碼相同的嚴格測試,從而確保甚至在使用者介面層面也具有質量。
使用 AngularJS 編寫 HTML 模板的另一個優點是,即使模板中包含了各種前端邏輯,它們本質上仍然與 HTML 相似。您可以在模板中整合 AngularJS 邏輯,並且仍然進行客戶端驗證控制。在 JSP 世界中,如果您嘗試使用瀏覽器檢視包含所有模板邏輯的 JSP 檔案,您的瀏覽器很可能放棄渲染該頁面。您可以檢視典型的 AngularJS 模板如下所示:
<div class="row thumbnail-wrapper">
<div data-ng-repeat="pet in currentOwner.pets" class="col-md-3">
<div class="thumbnail">
<img data-ng-src="images/pets/pet{{pet.id % 10 + 1}}.jpg"
class="img-circle" alt="My Pet Image">
<div class="caption">
<h3 class="caption-heading" data-ng-bind="pet.name"></h3>
<p class="caption-meta" data-ng-bind="pet.birthdate"></p>
<p class="caption-meta"><span class="caption-label"
data-ng-bind="pet.type.name"></span></p>
</div>
<div class="action-bar">
<a class="btn btn-default" data-toggle="modal" data-target="#petModal"
data-ng-click="editPet(pet.id)">
<span class="glyphicon glyphicon-edit"></span> Edit Pet
</a>
<a class="btn btn-default">
<span></span> Add Visit
</a>
</div>
</div>
</div>
</div>
你可能會發現模板中一些非HTML的新增。它包括像data-ng-click這樣的屬性,它將按鈕的點選對映到方法名呼叫。還有data-ng-repeat,它遍歷JSON陣列並生成必要的HTML程式碼,為陣列中的每個專案渲染相同的檢視。儘管所有邏輯都已就位,我們仍然能夠從瀏覽器驗證和檢視HTML模板。AngularJS將所有非HTML標籤和屬性稱為“指令”,這些指令的目的是增強HTML的功能。AngularJS還支援HTML 4和5,因此如果您的模板仍然依賴HTML 4 DOCTYPE,它應該仍然能正常工作(儘管HTML 4的驗證器將無法識別data-ng-x屬性)。
使用AngularJS和JSP之間的一個重大區別是**渲染時間**。如果您使用JSP,伺服器會渲染HTML內容。相比之下,如果您使用AngularJS,渲染髮生在瀏覽器中。因此,模板和JSON物件都需要傳送到客戶端。值得注意的是,AngularJS在執行DOM操作生成內容之前可能會短暫地顯示模板。例如,如果AngularJS尚未完全載入,頁面中的出生日期將顯示為空值,然後才顯示真實值。
AngularJS 中一個重要的概念是作用域。過去,每當我需要為我的 Web 應用程式編寫 Javascript 時,我必須管理變數名並構建特殊的名稱空間物件來儲存我的作用域屬性。然而,AngularJS 根據其 MVx 概念為您自動完成此操作。每個指令都將從其控制器繼承一個作用域(或者如果您願意,一個不繼承其他作用域屬性的獨立作用域)。在此作用域中建立的屬性和變數不會汙染其餘作用域或全域性上下文。
作用域是AngularJS應用程式的“粘合劑”。AngularJS中的控制器使用作用域與檢視互動。作用域還用於在指令和控制器之間傳遞模型和屬性。這樣做的好處是,我們現在被迫以一種元件自包含的方式設計應用程式,並且元件之間的關係必須透過使用可以從父作用域原型繼承的模型進行仔細考慮。
一個作用域可以在另一個作用域中以原型方式巢狀,就像Javascript透過原型實現其繼承模型一樣。然而,在子作用域中宣告的與父作用域相同的任何屬性名將在此後隱藏父屬性。以下程式碼可以描述一個示例:
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js@*" data-semver="1.4.0-rc.0" src="https://code.angularjs.org/1.4.0-rc.0/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body data-ng-app="demo">
<h1>Scopes in AngularJS</h1>
<div data-ng-controller="parentController">
<div data-ng-controller="childController">
<span>This is a demonstration of scopes</span>
<div>
Parent model: <span data-ng-bind="$parent.model.name"></span>
</div>
<div>
Current model: <span data-ng-bind="model.name"></span>
</div>
<div>
<button data-ng-click="updateModel()">Click me</button>
</div>
</div>
</div>
</body>
</html>
在作用域層次結構的最頂端是$rootScope,一個全域性可訪問的作用域,可以作為最後的手段在整個應用程式中共享屬性和模型。應儘量減少使用此作用域,因為它引入了一種“全域性”變數,如果過度使用,可能會帶來同樣的問題。
有關作用域的更多資訊,請參閱此處的AngularJS文件。
指令是AngularJS中最重要的概念之一。它們將所有額外的自定義標記引入HTML元素、屬性、類或註釋中。它們賦予標記新的功能。
以下程式碼片段展示了一個名為wdsCustom的自定義指令,它將用包含名為wds的模型資訊的標記替換標記元素<wds-custom company="wds">。該模型元素在包含指令的控制器作用域中宣告。您可以在此處的plunkr片段中檢視app.js、index.html和指令模板wds-custom-directive.html檔案,瞭解其工作原理。
由於本文不旨在教您如何編寫指令,您可以參考官方文件此處。