Angular.js 中的$位置服务解析当前浏览器的网址,并使其对您的应用程序可用。浏览器地址栏或$location
服务中的更改将保持同步。
根据配置的不同,$location
服务的行为不同,对您的应用程序有不同的要求。我们将首先研究带有 hashbang URLs 的客户端路由,因为这是默认模式。稍后,我们将看看新的基于 HTML 5 的路由。
您希望浏览器地址栏一致地反映应用程序的页面流。
使用$routeProvider
和$locationProvider
服务定义您的路线,使用ng-view
指令作为分支的占位符,该占位符应在特定路线定义中显示。
主模板使用ng-view
指令:
<body>
<h1>Routing Example</h1>
<ng-view></ng-view>
</body>
路线配置在app.js
中使用config
方法实现:
var app = angular.module("MyApp", []).
config(function($routeProvider, $locationProvider) {
$locationProvider.hashPrefix('!');
$routeProvider.
when("/persons", { templateUrl: "partials/person_list.html" }).
when("/persons/:id",
{ templateUrl: "partials/person_details.html",
controller: "ShowCtrl" }).
otherwise( { redirectTo: "/persons" });
});
它被设置为根据网址渲染部分person_list.html
或部分person_details.html
。部分person_list.html
渲染一个列表persons
:
<h3>Person List</h3>
<div ng-controller="IndexCtrl">
<table>
<thead>
<tr>
<td>Name</td>
<td>Actions</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="person in persons">
<td>{{person.name}}</td>
<td><a href="#!persons/{{person.id}}">Details</a></td>
</tr>
</tbody>
</table>
</div>
部分person_details.html
显示了特定person
的更详细信息:
<h3>{{person.name}} Details</h3>
<p>Name: {{person.name}}</p>
<p>Age: {{person.age}}</p>
<a href="#!persons">Go back</a>
这个例子同样基于 angular-seed 引导程序,如果不启动开发服务器,它将无法工作。
你可以在 GitHub 上找到完整的例子。
让我们试试我们的应用,打开index.html
文件。otherwise
定义的路线将我们从index.html
重定向到index.html#!/persons
。这是在其他when
条件不适用的情况下的默认行为。
仔细看看index.html#!/persons
网址,注意一下 hashbang (#!)将index.html
与动态的客户端部分/persons
分开。默认情况下,Angular 将使用散列(#)字符,但我们将其配置为使用 hashbang,遵循谷歌的使 AJAX 应用程序可抓取指南。
/persons
路由通过 HTTP Request 部分加载person_list.html
(这也是没有开发服务器就无法工作的原因)。它显示了人员列表,因此在模板中定义了ng-controller
指令。现在让我们假设控制器实现在某处定义了$scope.persons
。现在,对于每个人,我们也通过#!persons/{{person.id}}
呈现一个显示细节的链接。
人员详细信息的路线定义使用占位符/persons/:id
,该占位符解析为特定人员的详细信息,例如/persons/1
。person_details.html
部分,此外,为该网址定义了一个控制器。控制器的范围将是局部的,这基本上类似于我们的index.html
模板,我们在其中定义了自己的ng-controller
指令来实现相同的效果。
person_details.html
有一个到#!persons
的反向链接,该链接返回到person_list.html
页面。
让我们回到ng-view
指令。它会自动绑定到路由器定义。因此,您目前只能在页面上使用单个ng-view
。例如,您不能使用嵌套的ng-view
来实现具有一级和二级导航的用户交互模式。
最后,对部分的 HTTP 请求只发生一次,然后通过$templateCache
服务进行缓存。
最后,基于 hashbang 的路由仅在客户端使用,不需要服务器端配置。接下来让我们看看基于 HTML 5 的方法。
你想要好看的网址,想要提供服务器端的支持。
我们将使用相同的示例,但是使用 Express 框架来服务所有内容并处理 URL 重写。
让我们从路由配置开始:
app.config(function($routeProvider, $locationProvider) {
$locationProvider.html5Mode(true);
$routeProvider.
when("/persons",
{ templateUrl: "/partials/index.jade",
controller: "PersonIndexCtrl" }).
when("/persons/:id",
{ templateUrl: "/partials/show.jade",
controller: "PersonShowCtrl" }).
otherwise( { redirectTo: "/persons" });
});
除了html5Mode
方法之外,没有任何变化,该方法启用了我们的新路由机制。控制器的实现完全没有改变。
但是我们必须处理部分装载。我们的Express
应用程序必须为我们服务。Express
应用程序的初始典型样板加载模块并创建服务器:
var express = require('express');
var app = module.exports = express.createServer();
我们将跳过这里的配置,直接跳到服务器端路由定义:
app.get('/partials/:name', function (req, res) {
var name = req.params.name;
res.render('partials/' + name);
});
Express
路线定义从partials
目录加载具有给定名称的部分,并呈现部分的内容。
当支持 HTML 5 路由时,我们的服务器必须将所有其他 URL 重定向到我们应用程序的入口点index
页面。首先,我们定义包含ng-view
指令的index
页面的渲染:
app.get('/', function(req, res) {
res.render('index');
});
然后,重定向到同一页面的 catchall 路由:
app.get('*', function(req, res) {
res.redirect('/');
});
让我们再快速检查一下分音。请注意,他们使用 Jade 模板引擎,该引擎依赖缩进来定义 HTML 文档:
p This is the index partial
ul(ng-repeat="person in persons")
li
a(href="/persons/{{person.id}}"){{person.name}}
索引页面创建人员列表,显示页面显示更多详细信息:
h3 Person Details {{person.name}}
p Age: {{person.age}}
a(href="/persons") Back
个人详细信息链接/persons/{{person.id}}
和后台链接/persons
在我看来,与 hashbang URLs 相比,现在都干净多了。
看看 GitHub 上完整的示例,用node app.js
启动Express
app。
你可以在 GitHub 上找到完整的例子。
如果我们没有将所有请求重定向到根,如果我们在http://localhost:3000/persons
导航到人员列表会发生什么?Express
框架会显示一个错误,因为没有为persons;
定义路由,我们只为根网址(/
)和部分网址/partials/:name
定义了路由。重定向确保我们实际上在根网址结束,然后在我们的 Angular 应用程序中启动。当客户端路由接管后,我们再重定向回/persons
URL。
还要注意导航到一个人的详细信息页面将只加载show.jade
部分,而导航回persons
列表将不会执行任何服务器请求。我们的应用程序需要的所有东西都从服务器端加载一次并缓存在客户端。
如果您很难理解服务器实现,我建议您阅读这份优秀的快速指南。此外,这本书将有一章详细介绍如何将 Angular.js 与服务器端框架集成。
您希望实现一个向用户显示所选菜单项的导航菜单。
使用控制器中的$location
服务将地址栏网址与用户选择的导航菜单项进行比较。
导航菜单是经典的 ul/li 菜单,使用类别属性将其中一个li
元素标记为active
:
<body ng-controller="MainCtrl">
<ul class="menu">
<li ng-class="menuClass('persons')"><a href="#!persons">Home</a></li>
<li ng-class="menuClass('help')"><a href="#!help">Help</a></li>
</ul>
...
</body>
控制器实现menuClass
功能:
app.controller("MainCtrl", function($scope, $location) {
$scope.menuClass = function(page) {
var current = $location.path().substring(1);
return page === current ? "active" : "";
};
});
你可以在 GitHub 上找到完整的例子。
当用户选择一个菜单项时,客户端导航将按预期启动。使用ngClass
指令绑定menuClass
函数,并根据当前路线自动为我们更新 CSS 类。
使用$location.path()
我们得到当前路线。substring
操作删除前导斜杠(/
)并将/persons
转换为persons
。
您希望确保用户在导航到受保护的页面之前必须登录。
在$routeChangeStart
事件上实现一个监听器来跟踪下一个路线导航。如果用户尚未登录,重定向到登录页面。
最有趣的部分是路由更改监听器的实现:
var app = angular.module("MyApp", []).
config(function($routeProvider, $locationProvider) {
$routeProvider.
when("/persons",
{ templateUrl: "partials/index.html" }).
when("/login",
{ templateUrl: "partials/login.html", controller: "LoginCtrl" }).
// event more routes here ...
otherwise( { redirectTo: "/persons" });
}).
run(function($rootScope, $location) {
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
if ($rootScope.loggedInUser == null) {
// no logged user, redirect to /login
if ( next.templateUrl === "partials/login.html") {
} else {
$location.path("/login");
}
}
});
});
接下来,为了简单起见,我们将定义一个登录表单来输入用户名,跳过密码:
<form ng-submit="login()">
<label>Username</label>
<input type="text" ng-model="username">
<button>Login</button>
</form>
最后是登录控制器,它设置登录用户并重定向到该人的网址:
app.controller("LoginCtrl", function($scope, $location, $rootScope) {
$scope.login = function() {
$rootScope.loggedInUser = $scope.username;
$location.path("/persons");
};
});
你可以在 GitHub 上找到完整的例子。
这当然不是一个成熟的登录系统,所以请不要在任何生产系统中使用。但是它举例说明了如何处理对 web 应用程序特定区域的访问。当您在浏览器中打开应用程序时,在任何情况下,您都将被重定向到登录应用程序。只有输入用户名后,您才能访问其他区域。
run
方法是在模块中定义的,对于这种路线变更监听器来说是一个很好的地方,因为它在注射器加载完所有模块后,初始化时只运行一次。我们检查$rootScope,
中的loggedInUser
,如果没有设置,我们将用户重定向到登录页面。请注意,为了在已经导航到登录页面时跳过此行为,我们必须明确检查下一个templateUrl
。
登录控制器将$rootScope
设置为用户名,并重定向至/persons
。一般来说,我尽量避免使用$rootScope
,因为它基本上是一种全球状态。但是在我们的例子中,它非常适合,因为应该有一个全球可用的当前用户。