原作者:Ben Nadel 2012-9-12 9:11
翻译:雪狼 2013-7-3
不久以前,我讨论了映射RESTful资源URI到URL参数和服务端事件的问题。在开发一个瘦客户端程序的时候,同样需要在客户端实现很多与服务端类似的URL路由功能。但是对于不断成长的、复杂的富客户端界面(Rich client UI)来说,“路由-子页面”的机制会变得更加困难。稍后,我找到了Google的AngularJS作为定义客户端应用的框架。模仿上次的思路,我想我同样可以把AngularJS路由映射到客户端URL参数和客户端事件。 AngularJS天生具有一个健壮的路由和浏览历史管理机制。并且,它同时支持基于hash的(hash即url中#后面的部分)和基于HTML5 push-state的方案(这里我只使用基于hash的方案)。路由提供者(route provider)是把路由映射到模板路径时的核心概念,并且通过一系列的when()方法调用来实现:
$route.when(
"/friends/:friendID",
{
templateUrl: "views/friend-detail.htm"
}
);
在上面的例子中,指定的路由导致了指定的模板被渲染到页面上的ng-view指令处。这种方式对于比较小的网站和路由层次比较浅的网站是非常合适的,但是当你具有嵌套的导航和复杂的界面时,这种直接路由到子页面(route-to-partical)的编程模式很快就会变成瓶颈。
但是,你不用非得使用模板来实现路由,更好的方案是,你可以通过一些客户端渲染事件(render-event)来实现它。使用和映射RESTful资源URI到服务端事件相同的原理,我们可以把客户端路由解析为一组树形的、用于描述页面状态的值。
这个小把戏的关键是要明白URL的hash段可以包含任何数据(不过不能与一些保留字冲突)所以,与其我们传入一个templateUrl值,还不如传入一个event数据(event data):
$route.when(
"/friends/:friendID",
{
event: "friends.view"
}
);
在这个例子中,我们把路由映射到一个渲染事件:friends.view。其中“event”数据对应的参数(friends.view)将被解析为$route当前状态($route.current)的一部分,而路由中的“:friendID”值,将被解析为$routeParams的一部分。
为了展示这种方式如何渲染页面,我将创建只有一个controller的演示程序,它将根据路由提供的渲染事件(render-event)来渲染具体内容。
<!doctype html>
<html ng-app="Demo" ng-controller="AppController">
<head>
<meta charset="utf-8" />
<title>AngularJS Routing</title>
<style type="text/css">
a {
color: #333333 ;
}
a.on {
color: #CC0000 ;
font-weight: bold ;
text-decoration: none ;
}
</style>
</head>
<body>
<h1>
AngularJS Routing
</h1>
<p>
Current Render Action:
<!--
我们将把strong元素中的内容绑定到一个当前作用域的模型(model)中,即renderAction变量。然后,当Controller得到renderAction变量时,它将被更新到这里。
-->
<strong ng-bind="renderAction">Unknown</strong>
</p>
<!--
为了控制导航内容,我们将根据当前作用域中的状态来把"on"这个css类附加到下列元素上。
-->
<p>
<a href="#/home" ng-class="{ on: isHome }">Home</a> -
<a href="#/friends" ng-class="{ on: isFriends }">Friends</a> -
<a href="#/contact/ben" ng-class="{ on: isContact }">Contact</a>
</p>
<!--
当路由变化的时候,我们要设置renderPath:一个用于定义页面将如何渲染的数组型变量。
我们可以根据这些值来显示/加载各个子页面。
-->
<div ng-switch on="renderPath[ 0 ]">
<!-- Home Content. -->
<div ng-switch-when="home">
<p>
This is the homepage content.
</p>
<p>
Sub-path: <em>{{ renderPath[ 1 ] }}</em>.
</p>
</div>
<!-- Friends Content. -->
<div ng-switch-when="friends">
<p>
Here are my friends!
</p>
<p>
Sub-path: <em>{{ renderPath[ 1 ] }}</em>.
</p>
</div>
<!-- Contact Content. -->
<div ng-switch-when="contact">
<p>
Feel free to contact me.
</p>
<p>
Sub-path: <em>{{ renderPath[ 1 ] }}</em>.
</p>
<p>
Username: <em>{{ username }}</em>
</p>
</div>
</div>
<!-- Load AngularJS from the CDN. -->
<script
type="text/javascript"
src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">
</script>
<script type="text/javascript">
// 创建我们这个演示程序的主module。
var Demo = angular.module( "Demo", [] );
// 配置路由,$routeProvider服务将被自动注入到config函数中。
Demo.config(
function( $routeProvider ){
// 我们通常定义路由时,把路由映射到要用于渲染的模板,不过,这种方式只适合于简单网站。
// 当你构建具有嵌套导航的复杂应用时,你可能需要做一些更复杂的事情。
// 这种情况下,你需要路由到一组"动作(action)"而不是模板。
$routeProvider
.when(
"/home",
{
action: "home.default"
}
)
.when(
"/friends",
{
action: "friends.list"
}
)
.when(
"/contact/:username",
{
action: "contact.form"
}
)
.otherwise(
{
redirectTo: "/dashboard"
}
)
;
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// 定义我们的根控制器
Demo.controller(
"AppController",
function( $scope, $route, $routeParams ){
// 更新页面的渲染方式
render = function(){
// 取得当前所选路由的action值
var renderAction = $route.current.action;
// 接着,让我们修改renderPath变量,来使它更新到页面的相应位置。
var renderPath = renderAction.split( "." );
// 从params中获得username变量
// 注意:对于除了“contact”路由之外的任何路由,这个值都会是undefined。此处为了简化演示,我不对此做任何防御性编程。
var username = ($routeParams.username || "");
// 重置用于控制导航栏中css类的各个逻辑变量
var isHome = (renderPath[ 0 ] == "home");
var isFriends = (renderPath[ 0 ] == "friends");
var isContact = (renderPath[ 0 ] == "contact");
// 把这些变量保存到model(即$scope)中
$scope.renderAction = renderAction;
$scope.renderPath = renderPath;
$scope.username = username;
$scope.isHome = isHome;
$scope.isFriends = isFriends;
$scope.isContact = isContact;
};
// 开始监听“路由变化”事件,我们修改renderAction变量,以便它显示在Strong元素中
$scope.$on(
"$routeChangeSuccess",
function( $currentRoute, $previousRoute ){
// 刷新显示内容
render();
}
);
}
);
</script>
</body>
</html>
在这个范例中,我们的应用程序控制器监听了路由的变化。当路由变化时(注意:在页面首次被加载时同样会触发),AppController取出路由映射表中的action值,并且注入到当前页面的scope中。 这个值(以及由它派生出来的renderPath变量)又被用于通过ng-switch指令来根据条件渲染页面。
现在,这个页面只有一级导航。但是ng-switch指令可以很轻松的嵌套,以便让你的应用程序支持深度链接(deep-linking)。此外,ng-switch-when指令还可以与AngularJS的ng-include指令配合来实现子页面的延迟加载:
<div ng-switch on=" renderPath[ 0 ]">
<div ng-switch-when="home" ng-include=" 'home.htm' "></div>
<div ng-switch-when="friends" ng-include=" 'friends.htm' "></div>
<div ng-switch-when="contact" ng-include=" 'contact.htm' "></div>
</div>
在服务端,事件用于管理资源映射以及存取/转换这些资源。在富客户端应用程序中,事件则扮演了一个截然不同的角色:不再映射到资源,而是映射到页面状态。并且,根据你用户界面的复杂程度,状态还可以被定义为一个复杂的树形分区结构。幸运的是,AngularJS的路由机制使把路由映射到渲染事件成为可能,然后我们就可以通过渲染页面来展示所需的内容了。