領先一步
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 對映呈現 Owner 的示例。
@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 返回 owner。然後可以將您的模板移動到 AngularJS 中,AngularJS 會將 owner 物件繫結到 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 Context 配置的片段。如果不需要自定義日期格式(或任何其他序列化要求),您可以使用預設格式,這意味著您甚至不需要包含此部分,因為 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 服務的 Javascript 程式碼示例如下:
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 架構的幽默指代,“Whatever”)。由於它基於 MVx 架構,AngularJS 為 Javascript 開發提供了一個結構,從而提升了 Javascript 的地位,與傳統的 Spring + JSP 應用程式相比,後者僅使用 Javascript 在使用者介面上提供一點互動性。
藉助 AngularJS,您的基於 Javascript 的檢視層還繼承了依賴注入、HTML 詞彙擴充套件(透過使用自定義指令)、單元測試和功能測試整合以及類似 JQuery 的 DOM 選擇器(使用 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 控制元件中的值將即時反映在物件中。
繫結在處理 AJAX 表格等複雜 UI 元件時是一個有用的概念。
例如:我們需要在 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,可以以有組織且優雅的方式編寫相對複雜的 User Interfaces,始終將所需的邏輯封裝在您的元件中,並且絕不會冒 errant global Javascript 變數汙染您的作用域的風險。它也非常易於測試,並且內建了在單元和功能級別執行測試的機制,確保您的 User Interface 程式碼庫經過與您的 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 DOCTYPEs 的模板,它應該仍然可以正常工作(儘管 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-custom company="wds">
替換為包含有關名為 wds
的模型資訊的標記。該模型元素是在包裹指令的控制器作用域中宣告的。您可以檢視 app.js
、index.html
檔案和指令模板 wds-custom-directive.html
,以瞭解在可用的 plunkr 程式碼片段中這是如何工作的,請點選此處檢視。
由於本文不打算教您如何編寫指令,您可以參考官方文件,請點選此處檢視。