试试AngularJS的新路由器
因为我参加了于2015年3月21日举办的ng-japan活动,听到了有关AngularJS新路由的介绍,所以我顺便做了些笔记。
新路由器是什么
在会议上演讲的布莱恩·福尔德说:
-
- Angular 1.4(もうすぐリリース予定)と併せてリリースされる, Angularの新しいRouting機構
-
- Angular 2.x / 1.x の両方で動作することを目標としている.
- ngRouterだけでなく、ui-routerのように広く利用されているルーティングモジュール開発者の意見を取り入れながら開発した.
无论如何,试着去碰一下。
如果你想快速试一试,建议使用 npm 安装 new Router。
npm install angular-new-router
cd node_modules/angular-new-router
npm install
将 node_modules/angular-new-router 设置为当前工作目录,并启动 Web 服务器。
npm install -g http-server
http-server
如果从浏览器中输入 http://localhost:8080/examples/angular-1/ ,您就可以查看example列表。
路由的基础
在这里,我们将以example/angular-1/animation作为示例,来阅读源代码。
angular.module('example', [
'example.goodbye',
'example.welcome',
'ngAnimate',
'ngNewRouter'
])
.controller('AppController', ['$router', AppController]);
AppController.$routeConfig = [
{ path: '/', redirectTo: '/welcome' },
{ path: '/welcome', component: 'welcome' },
{ path: '/goodbye', component: 'goodbye' }
];
function AppController($router) {
this.greeting = 'Hello';
}
如果使用新的路由器,需要在模块定义时加载依赖关系到 ‘ngNewRouter’ 模块,这是应用的起点组件。
为什么$routeConfig在那里?
特点在于,AppController.$routeConfig=… 的部分。
作为控制器属性,它描述了路由的定义。
考虑到AngularJS的主要路由模块一般是通过将.config和Provider组合来定义路由,所以刚开始看到new Router的定义方式是有些奇怪的。
angular.modlue('app', ['ui.router']).config(function ($stateProvider) {
$stateProvider.state('welcome', {url: '/welcome', ...});
});
考虑到Angular 2.x的情况,这是可以理解的。实际上,在会议演讲中也提到了,之前的AppControler在Angular 2.x中可以按照以下方式进行编写。
@Component({selector: 'my-app'})
@Template({url: 'myApp.html'})
@RouteConfig([
{ path: '/', redirectTo: '/welcome' },
{ path: '/welcome', component: 'welcome' },
{ path: '/goodbye', component: 'goodbye' }
])
class AppComponent {
greeting: string;
constructor() {
this.greeting = 'Hello';
}
}
在Angular 2.x中,通过注解(或运行时装饰器)可以向框架传递各种信息,以供组件使用。附加在类上的注解会被TypeScript(不再需要称之为AtScript了)展开为类的静态属性。因此,实现既能在1.x版本又能在2.x版本上运行的路由机制,需要将路由定义作为控制器的属性进行编码。相反,1.x版本中使用的依赖注入.provider本身在Angular 2.x中不存在(感谢@shuhei的指正)。
组件和命名规则
接下来是每个组件代码中被父组件引用的部分。
angular.module('example.welcome', []).
controller('WelcomeController', WelcomeController);
function WelcomeController() {
this.heading = 'Welcome to The New Angular Router Demo!';
}
<section>
<h2>{{welcome.heading}}</h2>
</section>
由于goodbye.js和goodbye.html几乎相同,所以我将跳过对它们的解释。
新しい路由器通过名为“组件”的单元来控制路由。
“组件”是指
-
- Angular 1.xでは、Controller + Template HTMLのセットと考えればよい。
- Angular 2.xでは@Component(…) が付与されたclassであり、テンプレートとの紐付けも@Template で制御する模様.
至少在Angular 1.x中,注册Component到new Router很重要的是命名规则。
最起码需要遵循以下规则。
当将组件名设为’xxxYyy’时,请
-
- Componentに対応するControllerは、.controllerでの登録時、XxxYyyController という名前にしなくてなはならない。
-
- (xxxCtrl とか xxxだと、new Routerがwarnを上げてくる)
-
- ComponentのViewとなるHTMLについては、components/xxx-yyy/xxx-yyy.htmlに配置する必要がある
- Template HTMLからは、Component名(=xxxYyy)が自動的にcontrollerAs名として利用できるようになっている.
比如说,如果组件名为’myWidget’,那么结果如下所示:
-
- Controller: controller.(‘MyWidgetController’, …)で登録.
-
- Template HTML: ./components/my-widget/my-widget.html に配置.
- Templateで利用可能なcontrollerAs名: myWidget
为什么会存在这样的命名规则,仔细考虑起来是很显而易见的事情,$routeConfig 提供的路由定义相比其他机制来说信息太少了。
AppController.$routeConfig = [
{ path: '/', redirectTo: '/welcome' },
{ path: '/welcome', component: 'welcome' },
{ path: '/goodbye', component: 'goodbye' }
];
无论是ngRouter还是ui-router,在路由定义时都需要以下3个要点,考虑到这一点,定义信息很少。
-
- URLのパスフラグメント
-
- ng-view(またはui-view)に展開されるhtmlのURL
- 管理するController名
为了补充减少的信息,新的路由模块采用了命名规则,一旦确定组件名称,模板HTML的URL和控制器名称就会自动确定。
顺便提一下,虽然我自己没有尝试过,但似乎可以通过 $componentLoaderProvider 来改变这些命名规则。
angular.module('app', ['ngNewRouter']).config(function ($componentLoaderProvider) {
$componentLoaderProvider.setCtrlNameMapping(function (componentName) {
// component名が'xxx'の場合に、Controller xxxCtrlと紐付ける
return componentName + 'Ctrl';
});
});
视口,深链接
现在,我们已经介绍了如何读取.js等文件并通过指令在组件的显示区域进行设置。以下是相应的index.html文档:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<base href="/examples/angular-1/animation/">
<link rel="stylesheet" href="./app.css">
<title>Routing</title>
</head>
<body ng-app="example" ng-strict-di ng-controller="AppController as app">
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a ng-link="welcome">welcome</a></li>
<li><a ng-link="goodbye">goodbye</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="loader" ng-if="router.isNavigating">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</li>
</ul>
</div>
</nav>
<div class="page-host">
<ng-viewport></ng-viewport>
</div>
<script src="/node_modules/angular/angular.js"></script>
<script src="/node_modules/angular-animate/angular-animate.js"></script>
<script src="/dist/router.es5.js"></script>
<script src="./app.js"></script>
<script src="./components/goodbye/goodbye.js"></script>
<script src="./components/welcome/welcome.js"></script>
</body>
</html>
URL参数
与ngRouter和ui-router类似,可以在URL中使用参数。
通过使用变量名来定义路径参数
angular.module('app', ['ngNewRouter'])
.controller('AppController', ['$router', AppController]);
function AppController ($router) {
$router.config([
{ path: '/', component: 'home' },
{ path: '/detail/:id', component: 'detail' }
]);
}
类似于ui-sref,可以在跳转时传递参数作为参数。
<a ng-link="detail({id: 5})">link to detal</a>
我们可以通过Component从$routeParams中提取参数值。
angular.module('app.detail', ['ngNewRouter'])
.controller('DetailController', ['$routeParams', DetailController]);
function DetailController ($routeParams) {
this.id = $routeParams.id;
}
多视图
在先前的例子中,ng-viewport只有一个,但是也可以将多个viewport分配给路径。
$router.config([{
path: '/home',
components: {
main: 'list',
sub: 'result'
}
}]);
<div ng-viewport="main" ></div>
<div ng-viewport="sub" ></div>
如果您已经熟悉ui-router,那么与URL参数类似,这一点也不会感到不自然。
组件的嵌套
对于在ui-router中实现的“view名@所属state名”定义时相应的功能,是否有对应的功能,这一点让我很在意。如果我了解到任何信息,我会在后面补充。
计划在修复后删除:
目前,在组件化的模板HTML中嵌套ng-viewport会导致ngNewRouter内部发生错误。这个问题已在issue:193中提到,并计划在v0.5.1中修复。
在讨论中心问题:117和问题:127时,讨论的是关于嵌套组件的行为。
生命周期钩子
新的路由器提供了在路由前后插入处理的扩展点。
-
- canActivate: このComponentへ遷移する直前. 「このComponentに遷移する権限があるか」のように、認可処理に用いると便利.
-
- activate: このComponentへ遷移した直後. Ajaxによるデータ取得など、ある程度「重たい」処理を初期処理として挟み込むとよい.
-
- canDeactivate: このComponentから遷移する直前. 終了確認等に用いるとよい.
- deactivate: このComponentから遷移した直後.
为了添加对这些扩展点的处理,可以通过在组件中定义上述名称的方法来实现。即,代码将如下所示(注意到方法定义中出现的.prototype之类的东西,可以看出它强烈地意识到了Angular 2.x的Class写法)。
function MyController(user, $http) {
this.user = user;
this.$http = $http;
}
MyController.prototype.canActivate = function() {
return this.user.isAdmin;
};
MyController.prototype.activate = function() {
return this.bigFiles = this.$http.downloadBigFiles();
};
如果从oldComponent迁移到newComponent,这些扩展点将按照以下顺序启动。
-
- (开始迁移)
-
- oldComponent可以停用
-
- (创建newComponent实例)
-
- newComponent可以激活
-
- oldComponent停用
-
- newComponent激活
- (迁移完成)
当这些扩展点返回以下任一选项时,可以取消转换。
-
- false
-
- promiseオブジェクト(その後rejectされた場合)
- promiseオブジェクト(その後resolve(false)された場合)
尽管可以使用一种粗糙的技巧,在Component的构造函数中抛出错误来取消转移,但根据指南,“不要将转移控制的处理放在构造函数中”。
我是一个喜欢ui-router的孩子,所以在导航时,我主要是通过以下方式实现的。
-
- 取消迁移本身:监听$stateChangeStart事件,如果需要,执行$state.go()。
- 获取迁移后所需的数据:通过$resolve将数据注入到目标Controller中。
在新的路由器中,使用第一种模式时可以使用.canDeactivate或.canActivate,并且在第二种情况下最好使用.activate。(严格来说,在组件实例化之前的处理执行是不存在的,因此似乎也没有任何类似于resolve的时机,但考虑到扩展点变成了实例方法,存在一个“在实例化之前运行的点”也是有点奇怪的…)
对于ui-router来说,它也为State提供了onEnter和onExit处理程序(我并没有经常使用),而且可以认为它们分别对应着activate和deactivate方法,这样一来使用ui-router的人将更容易过渡。
可能并非完全相关,但当看到组件作为类存在与生命周期相关的扩展点时,我不禁想到了类似于ReactJS的componentWillMount和componentDidMount方法。或许只有我一个人这样想吧…
你能立即使用吗?
如果问题是放弃ui-router,转而使用新的Router,目前的感觉是有些无法确切地回答。
虽然考虑到生命周期钩子和注解的组件导向并没有问题,但是 UI-Router 的 “State” 概念的消失可能会让人感到难过。
此外,目前尚未找到能够实现父 – 子 – 孙嵌套的方法(官方文档中也没有任何说明),存在一些实际应用上可能出现的问题尚未解决清楚。
从细节上来说,我还关注如何处理抽象的状态之类的问题。
再稍微追逐一下,等时机成熟的时候,嗯。。。
总结
-
- Angular new RouterはAngular 2.xを強く意識したルーティング機構.
-
- Angular 2.xを見据え、Componentの概念が導入されている.
-
- ネーミングルール重要
-
- Angular 1.xユーザでも, 今から触っておくことで、Angular 2.xにアプリを移行するときに楽できる… かも?
-
- 画面遷移の拡張ポイントがいくつかある.
- multi-viewやPath階層とComponentの配置階層のマッピング等、未整理箇所が多数.
参考资料- 只需一种选择
new Routerのサイト ※ ガイド中のコードサンプルはちょいちょいtypoが残っているので要注意.
ng-japanでのBrianスライド資料