关于AngularJs

AngularJs确实是一个很方便的Web开发框架,基本上以前最头痛的MVC分层,AJAX回调,数据绑定,显示管理,等等等等,angular都帮你做好了。而且最逆天的一点是你只需要最基本的js知识就可以进行开发了,angular将需要的技能堆栈都埋在里面了,开发者只需要使用最angular暴露出来的接口就行了。当然,这篇文章不是用来歌颂angularjs多么好用的。在开发过程中,你还是会发现angular有很多不支持的功能点,这个时候就不得不借助开源社区的能量了,你总是能找到很多好用的插件。

我们遇到的问题

这里,我们需要解决的问题是:angular官方并不支持在route跳转的时候,不刷新页面进行跳转。这么说可能有点抽象,举例来说。试想一下,我们现在有个站点,有一个货物列表页面,其中货物分为大类、小类、物品本身这样的层次。而页面的布局则是:

  • 最上面的nav(在任何页面跳转发生时,你都不希望它们被刷新)
  • 左边第一栏,是货物大类列表,在页面刷新出来的时候从服务端取得该列表(在后续的分类及货物id变动跳转的时候,这部分不应该被刷新)
  • 左边第二栏,是货物小类列表,即物品id列表,在选中某个大类的时候,从服务端取得该列表(在货物id变动跳转的时候,这部分不应该被刷新)
  • 右边栏,具体的物品信息内容,在选中货物小类列表中的物品id的时候,从服务端获得并显示该页内容

针对这样的需求,angular官方只能完全刷新整个页面,而不能做到我们上述的需求。这个时候,我们需要第三方的插件辅助。

插件选择

综合下来,可选的插件主要有两款:

从行文标题就可以看出,我选择的是下者,angular-route-segment插件,理由我觉得segment插件作者网站上说的很清楚了:

While it seems that this library has very similar goal to what UI-Router provides, there are some important differences between their implementations, though.

UI-Router implements its own URL routing mechanics with its own "state" concept on top of it.angular-route-segment doesn't try to replace something in AngularJS. It is based on built-in $route engine, so that it tries to extend it rather than to replace. $routeSegmentProvider.when method is just a shorthand to $routeProvider.when with the simplified syntax. Inner segment-handling logic is built on top of events propagated by $route service, with internal usage of some route params from it.

Such approach makes it possible to accomplish the desired nested routing task in more simpler manner, which produces less code, less complexity and potential bugs, provides better cohesion with Angular core engine and is easier to understand, use and debug.

我做选择的时候有两点标准:

  • 易学易懂:这点很重要,segment明显比较容易上手,基本上没有新的概念,只需要熟悉几个segment函数就可以上手了
  • 功能足够:不求万能全能,只要能满足需求就足够了

当然,segment也有缺点,在我写这篇文章的时候,segment只有225个start,且已经2个月没更新了,最新的版本只支持angular的1.2.x版本,angular最新的发展方向1.3还未有支持的动静。反观UI-Router,2501个star,13个小时前刚更新过。从支持上来说,无疑是UI-Router更好。AngularUI团队下的东西都不错,虽然这个UI-Router我不是很满意。

segment还有一个很致命的问题就是,作者在写tutorial的时候,并没有很系统地介绍使用方法和设计中的几个关键的点,导致使用的时候发现各种不可理解的状况的发生。我是下载了github上的源码,玩了example一阵之后才完全理解的。

segment的学习

类库引入和依赖注入

[codesyntax lang="html4strict"]

<script src="../build/angular-route-segment.min.js"></script>

[/codesyntax]

或者使用requirejs,都可以。

在定义angular的app的时候,记得要进行依赖申明,这里必须记住一点,因为设计上segment是在angular route上的扩展,所以它是依赖于angular route插件的 。

[codesyntax lang="javascript"]

var app = angular.module('app', ['ngRoute', 'route-segment', 'view-segment']);

[/codesyntax]

简单使用

segment插件的使用分为两部分,一部分是和angular route差不多的app.config定义,另一部分则是页面上的directive使用。源代码也很清晰地分为了route-segment.js和view-segment.js两部分。

app.config举个例子来看:

[codesyntax lang="javascript"]

app.config([
    "$routeProvider", "$routeSegmentProvider",
function($routeProvider, $routeSegmentProvider) {
    $routeSegmentProvider.options.autoLoadTemplates = true;
    $routeSegmentProvider.options.strictMode = true;
    $routeSegmentProvider.
        when("/item",                                     "item").
        when("/item/category/:categoryId",                "item.items").
        when("/item/category/:categoryId/item/:itemId",   "item.items.detail").
        when("/others",                                   "data").
        when("/others/data/:dataId",                      "data.detail").
        // ITEM
        segment("item", {
            "templateUrl": VIEW_URL + "item/home.html",
            "controller": "AppItemCtrl"
        }).
        within().
            segment("items", {
                "templateUrl":VIEW_URL + "item/items.html",
                "controller": "AppItemListModuleCtrl",
                "dependencies": ["categoryId"]
            }).
            within().
                segment("detail", {
                    "templateUrl": VIEW_URL + "item/detail.html",
                    "controller": "AppItemDetailCtrl",
                    "dependencies": ["itemId"]
                }).
        up().up().
        // DATA
        segment("data", { /* ... */ });
    $routeProvider.otherwise({redirectTo: "/item"});
}]);

[/codesyntax]

这里的when语法和angular route的when不同,参数一都是path,但是参数二从route object换成了segment name,这里的segment名字需要小心,必须以“.”来分隔展现层次。比如说我刚才举的例子里的:“item”,“item.items”,“item.items.detail”。

segment函数则起到了之前when函数里route object定义的作用。dependencies很有用,表示的是path里的哪个/些参数变动的时候,该segment负责的展示区域会被刷新。

within是向下行走一个层级,up则是向上行走一个层级。

这里需要注意,作者并没有将otherwise继承到segment里去,所以需要定义otherwise的时候还必须注入原始的$routeProvider,这个真心无力吐槽。

directive很简单,在页面上写一句:

[codesyntax lang="html4strict"]

<div app-view-segment="0"></div>

[/codesyntax]

就OK了。

这里需要理解app-view-segment属性中的0,到底是什么意思。这里的0表示的是0级的segment。在刚才给的例子中,“item”、“data”都属于0级的segment,而“item.items”、“data.detail”则是1级的segment,“item.items.detail”则属于2级的segment。

插件会在页面上的directive处,检查当前的path所对应的segment配置,如果层级相符合,则将template植入该directive处。

这里吐槽下,这么重要的功能点,在官方的文档中居然是没解释的,我拿下example搞了半天才明白过来。

范例分析

回到我们一开始定义的问题场景上来。我们分解开来看这个页面流程。

index页

首先我们需要主页的nav永远不需要被刷新,index本体结构也不需要被刷新。那么这里我们需要index.html和一个固定controller相搭配,这个controller负责的仅仅只是提供segment对象到scope里,判断nav的高亮。

[codesyntax lang="html4strict"]

<!DOCTYPE html>
<html lang="en">
    <head>
        <!-- ... -->
        <script type="application/javascript">
            function AppNavCtrl($scope, $routeSegment) {
                $scope.segment = $routeSegment;
            }
        </script>
    </head>
    <body>
        <div class="navbar navbar-inverse navbar-fixed-top" ng-controller="AppNavCtrl">
            <div class="container">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#/item">仓储系统</a>
                </div>
                <div class="navbar-collapse collapse">
                    <ul class="nav navbar-nav">
                        <li ng-class="{active: segment.startsWith('item')}"><a href="#/item">物品列表</a></li>
                        <li ng-class="{active: segment.startsWith('data')}"><a href="#/data">物品数据</a></li>
                    </ul>
                </div>
            </div>
        </div>

        <div class="container">
            <div class="row" app-view-segment="0"></div>
        </div>
    </body>
</html>

[/codesyntax]

这里为了演示方便,直接将首页的nav controller写在head里了,其实应该是和angular其他的controller放在同一个地方的。需要注意的地方有两点。

第一,segment中配置的path及其对应的controller,因为有route自动触发,所以不需要在HTML中写死,而nav的controller,因为在segment里没有配置(为了不被path更改而刷新),所以这里必须在nav的HTML上写上controller,以便在angular启动的时候给nav的scope传递segment对象。

第二,这里使用了segment.startWith方法,判断了页面的当前path是否应该在nav上高亮。

item主页 / 大分类

根据我们配置的segment:

[codesyntax lang="javascript"]

...
when("/item", "item").
...
segment("item", {
    "templateUrl": VIEW_URL + "item/home.html",
    "controller": "AppItemCtrl"
}).
...

[/codesyntax]

在item首页,我们会在directive app-view-segment="0" 的地方引入 “item/home.html” 这个template,并启动AppItemCtrl。这个controller会调用service,获取到物品的大分类,并展示在左边栏的第一列:

[codesyntax lang="html4strict"]

<div class="row">
    <div class="col-md-2">
        <!-- 这里负责显示物品大分类列表 -->
    </div>
    <div class="col-md-3" app-view-segment="1">
        <!-- 这里负责显示物品小分类 -->
    </div>
    <div class="col-md-7" app-view-segment="2">
        <!-- 这里负责显示物品细节 -->
    </div>
</div>

[/codesyntax]

item小分类 / item列表

这里的controller负责获取当前物品大分类下的物品id列表,并进行展示。因为没有牵涉到结构性的部分,这里的controller代码和HTML代码就不需要展示了。

item细节

同上,这里的controller负责获得当前物品id所对应的物品细节,并进行展示。

这样,我们在一开始提出的angular原生不可解决问题,就已经完全解决了。

Bug

segment插件我用到现在有一个蛮大的bug,就是在配置了dependencies的segment,在url跳转的时候,即便segment的根节点已经完全不是当前segment了,只要path中的dependencies改变了,该segment还是会被触发。

举例来说:当前页面是:“/item/category/:categoryId/item/:itemId”,当离开这个页面的时候,即便我去的是“/data”这个完全不同的segment,item.items.detail这个segemnt还是会被触发。

鉴于segment插件已经2个月没有更新,且github上issues完全没有人理会的情况下,我也就没有去报这个bug了。

现在的解决方法是在有配置dependencies项的segment的controller里,进行

[codesyntax lang="javascript"]

if (typeof $routeSegment.$routeParams.itemId === 'undefined') {
    return; // wrong route page
}

[/codesyntax]

这样的检查。