# 六、网址、路由和部分

Angular.js 中的[$位置服务](http://docs.angularjs.org/guide/dev_guide.services.$location)解析当前浏览器的网址，并使其对您的应用程序可用。浏览器地址栏或`$location`服务中的更改将保持同步。

根据配置的不同，`$location`服务的行为不同，对您的应用程序有不同的要求。我们将首先研究带有 hashbang URLs 的客户端路由，因为这是默认模式。稍后，我们将看看新的基于 HTML 5 的路由。

## 带有哈希邦网址的客户端路由

### 问题

您希望浏览器地址栏一致地反映应用程序的页面流。

### 溶液

使用`$routeProvider`和`$locationProvider`服务定义您的路线，使用`ng-view`指令作为分支的占位符，该占位符应在特定路线定义中显示。

主模板使用`ng-view`指令:

```js
    <body>
    <h1>Routing Example</h1>
    <ng-view></ng-view>
    </body>

```

路线配置在`app.js`中使用`config`方法实现:

```js
    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`:

```js
    <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`的更详细信息:

```js
    <h3>{{person.name}} Details</h3>
    <p>Name: {{person.name}}</p>
    <p>Age: {{person.age}}</p>

    <a href="#!persons">Go back</a>

```

这个例子同样基于 angular-seed 引导程序，如果不启动开发服务器，它将无法工作。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter6/recipe1) 上找到完整的例子。

### 讨论

让我们试试我们的应用，打开`index.html`文件。`otherwise`定义的路线将我们从`index.html`重定向到`index.html#!/persons`。这是在其他`when`条件不适用的情况下的默认行为。

仔细看看`index.html#!/persons`网址，注意一下 hashbang (#！)将`index.html`与动态的客户端部分`/persons`分开。默认情况下，Angular 将使用散列(#)字符，但我们将其配置为使用 hashbang，遵循谷歌的[使 AJAX 应用程序可抓取](https://developers.google.com/webmasters/ajax-crawling/)指南。

`/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 的方法。

## 使用 HTML 5 历史 API 的常规网址

### 问题

你想要好看的网址，想要提供服务器端的支持。

### 溶液

我们将使用相同的示例，但是使用 [Express](http://expressjs.com/) 框架来服务所有内容并处理 URL 重写。

让我们从路由配置开始:

```js
    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`应用程序的初始典型样板加载模块并创建服务器:

```js
    var express = require('express');
    var app = module.exports = express.createServer();

```

我们将跳过这里的配置，直接跳到服务器端路由定义:

```js
    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`页面的渲染:

```js
    app.get('/', function(req, res) {
    res.render('index');
    });

```

然后，重定向到同一页面的 catchall 路由:

```js
    app.get('*', function(req, res) {
    res.redirect('/');
    });

```

让我们再快速检查一下分音。请注意，他们使用 [Jade](http://jade-lang.com/) 模板引擎，该引擎依赖缩进来定义 HTML 文档:

```js
    p This is the index partial
    ul(ng-repeat="person in persons")
    li
    a(href="/persons/{{person.id}}"){{person.name}}

```

索引页面创建人员列表，显示页面显示更多详细信息:

```js
    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。](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter6/recipe2)

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter6/recipe2) 上找到完整的例子。

### 讨论

如果我们没有将所有请求重定向到根，如果我们在`http://localhost:3000/persons`导航到人员列表会发生什么？`Express`框架会显示一个错误，因为没有为`persons;`定义路由，我们只为根网址(`/`)和部分网址`/partials/:name`定义了路由。重定向确保我们实际上在根网址结束，然后在我们的 Angular 应用程序中启动。当客户端路由接管后，我们再重定向回`/persons` URL。

还要注意导航到一个人的详细信息页面将只加载`show.jade`部分，而导航回`persons`列表将不会执行任何服务器请求。我们的应用程序需要的所有东西都从服务器端加载一次并缓存在客户端。

如果您很难理解服务器实现，我建议您阅读这份优秀的[快速指南](http://expressjs.com/guide.html)。此外，这本书将有一章详细介绍如何将 Angular.js 与服务器端框架集成。

## 使用路线位置实现导航菜单

### 问题

您希望实现一个向用户显示所选菜单项的导航菜单。

### 溶液

使用控制器中的`$location`服务将地址栏网址与用户选择的导航菜单项进行比较。

导航菜单是经典的 ul/li 菜单，使用类别属性将其中一个`li`元素标记为`active`:

```js
    <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`功能:

```js
    app.controller("MainCtrl", function($scope, $location) {
    $scope.menuClass = function(page) {
    var current = $location.path().substring(1);
    return page === current ? "active" : "";
    };
    });

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter6/recipe3) 上找到完整的例子。

### 讨论

当用户选择一个菜单项时，客户端导航将按预期启动。使用`ngClass`指令绑定`menuClass`函数，并根据当前路线自动为我们更新 CSS 类。

使用`$location.path()`我们得到当前路线。`substring`操作删除前导斜杠(`/`)并将`/persons`转换为`persons`。

## 监听路由变化以实现登录机制

### 问题

您希望确保用户在导航到受保护的页面之前必须登录。

### 溶液

在`$routeChangeStart`事件上实现一个监听器来跟踪下一个路线导航。如果用户尚未登录，重定向到登录页面。

最有趣的部分是路由更改监听器的实现:

```js
    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");
    }
    }
    });
    });

```

接下来，为了简单起见，我们将定义一个登录表单来输入用户名，跳过密码:

```js
    <form ng-submit="login()">
    <label>Username</label>
    <input type="text" ng-model="username">
    <button>Login</button>
    </form>

```

最后是登录控制器，它设置登录用户并重定向到该人的网址:

```js
    app.controller("LoginCtrl", function($scope, $location, $rootScope) {
    $scope.login = function() {
    $rootScope.loggedInUser = $scope.username;
    $location.path("/persons");
    };
    });

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter6/recipe4) 上找到完整的例子。

### 讨论

这当然不是一个成熟的登录系统，所以请不要在任何生产系统中使用。但是它举例说明了如何处理对 web 应用程序特定区域的访问。当您在浏览器中打开应用程序时，在任何情况下，您都将被重定向到登录应用程序。只有输入用户名后，您才能访问其他区域。

`run`方法是在[模块](http://docs.angularjs.org/api/angular.Module)中定义的，对于这种路线变更监听器来说是一个很好的地方，因为它在注射器加载完所有模块后，初始化时只运行一次。我们检查`$rootScope,`中的`loggedInUser`，如果没有设置，我们将用户重定向到登录页面。请注意，为了在已经导航到登录页面时跳过此行为，我们必须明确检查下一个`templateUrl`。

登录控制器将`$rootScope`设置为用户名，并重定向至`/persons`。一般来说，我尽量避免使用`$rootScope`，因为它基本上是一种全球状态。但是在我们的例子中，它非常适合，因为应该有一个全球可用的当前用户。