本文非AngularJS 1.X的入门级教程,很多细节可能会被无意识的忽略。AngularJS相比于Backbone要复杂不少,两者的设计思路也完全不同,更可以说是大相径庭。面对一个SPA项目的前端技术选型时,需要根据实际需要进行选择。本文更多的是给出一些注意点,强烈建议好好阅读官方API。AngularJS不依赖于jquery,连最基本的编码方式都完全不同,所以jquery的经验并不一定能帮助你快速将其掌握。
另外,需要注意以下几点:
- Ionic这个移动端开发框架当前也是基于AngularJS 1.x的,本文内容同样对其适用
- AngularJS2.0已经发布了beta版本,预计2016年第一季度会见到rc版本甚至是release版本,AngularJS 2.0与本文的1.x是完全不兼容的
- 由于AngularJS本身的特性,其开发中大量依赖外部组件(尤其是UI类),而AngularJS2.0的组件短时间内还不会足以支持应用级开发需求,1.x的生命力应该还有1年左右
阅读本文之前最好对以下知识点已经熟悉:
简要介绍
AngularJS 1.x是一个非常强大的前端MVC框架,其双向数据绑定功能很强大,用其开发很多应用甚至不再需要考虑Dom操作。
参考资料
Angular权威教程 【书】
Angular Github
AngularUI Github
简要示意图
关于前端MVC基本示意图可以参考javascript MVC框架之 Backbone 实用指南,本文给出的是基于AngularJS特性的示意图。
AngularJS 1.x顺序图
下面的示意图不是很严谨,读者有mvc的概念和angular基础知识的话应该很好理解
AngularJS MVC应用基本原则
- 基本的MVC原则与javascript MVC框架之 Backbone 实用指南相同
- 在Angular的启动程序中完成所有controller、directive、service等等的注册配置工作
- view只与controller/scope产生交互
- service为controller/scope提供服务,由service完成与后台的交互
- rootScope尽可能少绑定内容,甚至最好为空
- scope的继承性质要特别注意,杜绝在子scope直接使用父scope的数据、功能,容易导致代码结构难以理解和维护
页面布局设计
在Bootstrap官网中有一个布局概念的范例,本文基于其结构进行基于AngularJS的SPA化,如下图:
【待补充】
切换到report的效果图:
【待补充】
项目基本目标
- 基于Bootstrap
- 支持SPA路由
- 支持多国语言
项目构建配置
配置项目基本与javascript 前端 基于 npm、bower、grunt的标准项目构建中的完全一致,下面只给出差异项。
bower.json
{
"name": "myApp",
"version": "0.1.0",
"license": "MIT",
"private": true,
"dependencies": {
"angular": "1.4.x",
"angular-loader": "1.4.x",
"angular-messages": "~1.4.x",
"html5-boilerplate": "~4.3.0",
"requirejs-text": "2.0.x",
"requirejs": "2.1.x",
"bootstrap": "~3.3.5",
"angular-translate": "~2.7.2",
"angular-sanitize": "~1.4.3",
"angular-translate-loader-static-files": "~2.7.2",
"angular-ui-router": "~0.2.15"
},
"appPath": "app",
"moduleName": "myAppApp"
}
重要库说明:
名稱 | 介绍 |
---|---|
angular-translate | 多国语言的基本支持库 |
angular-translate-loader-static-files | 支持从json文件加载多国语言资源的库 |
angular-sanitize | angular-translate的依赖项 |
angular-ui-router | 实现SPA应用需要的路由支持库 |
以上主要部件会在下面具体的使用到的文件中进行讲述,这里只要明白其作用即可。
目录结构
注意:这里给出的目录结构与javascript 前端 基于 npm、bower、grunt的标准项目构建的有些许差异,当作历史遗留问题的一个反面例子。
- app
- i18n
- cn.json
- en.json
- de.json
- controllers
- translate.js
- directives
- filter
- services
- translate.js
- views
- app.js
- app-router.js 123123123
- index.html 123123123
- main.js
- i18n
说明:
名称 | 类型 | 用途 |
---|---|---|
i18n | dir | 多国语言文件存放目录 |
controllers | dir | 控制器脚本存放目录 |
directives | dir | 指令脚本存放目录【本文将其忽略】 |
filters | dir | 过滤器脚本存放目录【本文将其忽略】 |
services | dir | 服务脚本存放目录 |
views | dir | 视图文件存放目录 |
app.js | js file | Anguarl启动程序 |
app-router.js | js file | 路由配置程序 |
index.html | html file | browser加载主文件 |
main.js | js file | RequireJS启动程序 |
各文件主要内容
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="description" content="">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--(if target dist)>
<link rel="stylesheet" href="lib/bootstrap/bootstrap.min.css" />
<!(endif)-->
<!--(if target dummy)><!-->
<link rel="stylesheet" href="../bower_components/bootstrap/dist/css/bootstrap.css" />
<!--<!(endif)-->
<title> AngularJS+Bootstrap3范例</title>
<link href="styles/non-responsive.css" rel="stylesheet">
<link href="styles/dashboard.css" rel="stylesheet">
<style>
/* 重点:防止加载时闪烁*/
[ng-cloak] {
display: none;
}
</style>
</head>
<body ng-cloak>
<!-- 重点:顶部导航栏,使用ng-include加载外部文件 -->
<div ng-include="'views/partials/nav.html'"></div>
<!-- 主要显示区域 -->
<div class="container-fluid">
<div class="row">
<!-- 重点:左侧导航栏,使用ng-include加载外部文件 -->
<div ng-include="'views/partials/sidebar.html'"></div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!-- 重点:主要区域内容 -->
<div ui-view></div>
</div>
</div>
</div>
<!--(if target dist)>
<script src="lib/jquery/jquery.js"></script>
<script src="lib/bootstrap/bootstrap.js"></script>
<script data-main="main" src="lib/requirejs/require.js"></script>
<!(endif)-->
<!--(if target dummy)><!-->
<script src="../bower_components/jquery/dist/jquery.js"></script>
<script src="../bower_components/bootstrap/dist/js/bootstrap.js"></script>
<script data-main="main" src="../bower_components/requirejs/require.js"></script>
<!--<!(endif)-->
</body>
</html>
上述内容中很多注释标记都是给项目构建使用的,比如if target dummy,参考javascript 前端 基于 npm、bower、grunt的标准项目构建即可很快理解。
说明:
index.html文件主要完成了以下工作:
- 初始化主页面,将页面分成多个view组成部分,使达到view模块化的目的(类似javascript MVC框架之 Backbone 实用指南中提到的AppView)
- 利用RequireJS加载main.js启动App
本AppView的view组成:
名称 | 说明 |
---|---|
nav | 顶部导航栏 |
sidebar | 左侧导航栏 |
main | 右侧内容区域,使用angular-ui的ui-view指令标记 |
熟悉bootstrap的同学对该布局不会陌生,这里不再赘述,可以参考javascript MVC框架之 Backbone 实用指南中的讲解。
主要使用的Angular命令:
名称 | 介绍 |
---|---|
ng-include | 将外部html文件引入到当前文件中 |
ui-view | angular-ui引入的指令,标记内容主要区域 |
views/partials/nav.html
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<!-- The mobile navbar-toggle button can be safely removed since you do not need it in a non-responsive implementation -->
<!-- 使用图标 -->
<a class="navbar-brand" href="#">
<span class="glyphicon glyphicon-home"></span><span class="glyphicon glyphicon-signal"></span>项目名称
</a>
</div>
<!-- Note that the .navbar-collapse and .collapse classes have been removed from the #navbar -->
<div id="navbar">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li class="dropdown-header">Nav header</li>
<li><a href="#">Separated link</a></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<form class="navbar-form navbar-left" role="search">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right">
<!--重点:语言切换功能 -->
<div ng-controller="TranslateController">
语言
<select class="language-switching" ng-model="cur_lang" ng-change="changeLanguage(cur_lang)">
<option value="en">English</option>
<option value="de">German</option>
<option value="cn">中文</option>
</select>
</div>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
nav.html中绝大部分内容都是Bootstrap范例部分,但是标注出的使用了Angular的controller和service来实现多国语言,其中使用了第三方的angular-translate组件,本文将其进行了简单封装,具体可看controller/translate.js和service/translate.js文件的内容。本部分可以被一般项目标准化,作为团队基础库进行维护。
views/partials/sidebar.html
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<!-- 重点:利用angular-ui-router进行SPA导航,并且设定元素状态 -->
<!-- 重点:利用多国语言功能对界面内容进行设定 -->
<li ui-sref-active-eq="active"><a ui-sref="start" translate>{{"OVERVIEW"}}</a></li>
<li ui-sref-active="active" ui-sref="report"><a href="" translate>{{"REPORT"}}</a></li>
<li ui-sref-active="active"><a ui-sref="inputbox.detail">收件箱</a></li>
<li><a href="#">Export</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item</a></li>
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
<li><a href="">More navigation</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
</ul>
</div>
主要使用的Angular命令:
名称 | 介绍 |
---|---|
ui-sref-active | angular-ui引入的指令,标记激活状态使用的css类 |
ui-sref-active-eq | 与ui-sref-active作用相同,但限制更加严格 |
ui-sref | 在angular-ui的router中使用的路径,鼠标点击会触发路由变化,在后面的app-router.js会有对应的内容 |
translate | 这是angular-translate提供的指令,用于支持多国语言 |
ui-sref和ui-sref-active指令进行组合之后,可以设定左侧sidebar的路由及实现鼠标点击激活路由功能(对比javascript MVC框架之 Backbone 实用指南实现该功能的方式,就可以发现AngularJS和Backbone完全不同的架构设计)
特别注意:ui-sref、ui-sref-active、ui-sref-active-eq形式的路由控制是基于AngularJS的SPA应用中非常重要的内容,后续涉及路由切换等等都会使用到。
main.js
'use strict';
require.config({
//require配置部分忽略
});
require([
'angular',
'app'//重点:app.js被注入
], function(angular, app) {
angular.element().ready(function() {
angular.bootstrap(document, [app]);//启动app.js
});
}
);
main.js完成的工作非常简单:启动app.js!
app.js
'use strict';
define(['angular'
,'angularUiRouter'
,'angularTranslate'
,'angularSanitize'
,'angularTranslateLoaderStaticFiles'
,'angularMessages'
//重点:自定义路由
,'app-router'
//重点:以下为自定义指令、服务加载区,根据项目需要进行配置
,'scripts/controllers/translate'
,'scripts/services/translate'
], function(angular, angularUiRouter,angularTranslate,angularSanitize
,angularTranslateLoaderStaticFiles
,angularMessages
,appRouter//重点:自定义路由
//重点:以下为自定义指令、服务加载区,根据项目需要进行配置
,translateCtrl
,translateSrv
) {
var appName='myApp';
var appModule=angular.module(appName, [
'ui.router'
,'ngMessages'
,'ngSanitize'
,'pascalprecht.translate'
//重点:以下为自定义指令、服务加载区,根据项目需要进行配置
,translateSrv
]);
appModule.controller('TranslateController',translateCtrl)
//设置多国语言i18n参数
.config(['$translateProvider',function($translateProvider){
//$translateProvider.useSanitizeValueStrategy('escapeParameters');//只在调试中用代码调用$translate.instant获取中文编码时使用,官方标注3.0及以后版本将 deprecated
$translateProvider.useSanitizeValueStrategy('sanitize');
//获取上次使用的语言,使用localStorage存储
var langKey = window.localStorage.langKey||'en';
//使用json文件定义语言资源,静态加载
$translateProvider.useStaticFilesLoader({
prefix: 'i18n/', /* 当前目录下的i18n目录存放了所有多国语言资源文件 */
suffix: '.json'/*多国语言文件以json结尾*/
});
//将语言与json文件名做映射,这一步建议屏蔽!!!测试用用即可,用语言代码可以做到通用化
$translateProvider.registerAvailableLanguageKeys(['en','cn','de'],{
"en_*":"en",
"de_*":"de",
"zh_*":"cn"//这里将zh_CN、zh_TW都转为cn
});
$translateProvider.determinePreferredLanguage();
$translateProvider.fallbackLanguage(langKey);
}])
//设置定位参数
.config(['$locationProvider',function($locationProvider){
//没有使用Html5的history模式
$locationProvider.html5Mode({
enabled: false,
requireBase: false
}).hashPrefix('!');
}])
//重点:设置自定义路由,app-router.js
.config(appRouter);
return appName;
}
);
app.js主要完成的工作依次有:
- 加载所有的指令、服务(包括第三方、自定义)
- 配置多国语言
- 配置路由
- 配置history模式
- 配置路由(加载自定义的app-router.js)
上述app.js几乎可以作为一个标准模板使用,只需要根据项目需要配置下指令、服务即可
app-router.js
define([
// 'angularUiRouter'
],function(){
return ['$stateProvider','$urlRouterProvider'
,function($stateProvider,$urlRouterProvider) {
//如果没有路由引擎能匹配当前的导航状态,那它就会默认将路径路由至 /start,
// 这个页面就是状态名称被声明的地方. 只要理解了这个,那它就像switch case语句中的default选项.
$urlRouterProvider
.otherwise("/start");
$stateProvider
.state('start', {
url: "/start",//mainview区域显示overview界面
templateUrl: 'views/partials/dashboard.html'
})
.state('report', {
url: '/report',//mainview区域显示report界面
templateUrl: 'views/partials/report.html'
})
.state('report.current', {//mainview区域显示report.current界面
url: '/current',
templateUrl: 'views/report/current.html'
})
.state('report.last', {//mainview区域显示report.last界面
url: '/last',
templateUrl: 'views/report/last.html'
})
.state('report.resolvetest', {//mainview区域显示report.resolvetest界面
url: '/resolvetest',
templateUrl: 'views/report/resolvetest.html',
//resolve中的内容可以被注入到controller中
resolve: {
person: function() {
return {
name: "Ari",
email: "ari@fullstack.io"
}
}
}
,controller: ['$scope','person',function($scope,person/*,currentDetails, facebookId*/) {
$scope.person = person;
}]
});
}
];
});
app-router.js只完成唯一的一项工作(也是其最重要的工作):
- 配置路由
- 控制路由,所有的路由变换会触发route事件,ui-view指令指向的区域会加载路由指向的templateUrl内容
上述文件中描述的路由有:
- overview(显示dashboard界面)
- report(显示报表界面)
- current(显示本月报表)
- last(显示上月报表)
- resolvetest (展示了router的特殊功能,其可以直接定义controller和scope,展示但不推荐如此使用)
特别注意:angular-ui-router有嵌套路由的概念,真的非常有创意。每个ui-view都是完全独立的,其上下级关系由router字符串描述。比如report.current就描述了两层路由。一定要熟练掌握angular-ui-router的用法,这是非常灵活的部分。
router与template
start
对应template:views/partials/dashboard.html
<h1 class="page-header">Dashboard</h1>
<!-- 都是bootstrap范例中的内容,此处忽略 -->
report
对应template:views/partials/report.html
<h3 class="page-header">报表数据<small>(这里展示了路由嵌套)</small></h3>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group">
<a class="btn btn-default" ui-sref-active="active" ui-sref="report.current">当月报表</a>
<a class="btn btn-default" ui-sref-active-eq="active" ui-sref="report.last">上月报表</a>
<a class="btn btn-default" ui-sref-active="active" ui-sref="report.resolvetest">resolve_test</a>
</div>
<div class="btn-group hidden">
<a class="btn btn-default" ui-sref-active="active" ui-sref="report.current">当月报表</a>
<a class="btn btn-default" ui-sref-active-eq="active" ui-sref="report.last">上月报表</a>
</div>
</div>
<div ui-view></div>
report.html充分展示了angular-ui-router的嵌套路由,请仔细体会app-router.js章节中的特别注意部分,掌握了该部分概念,设计angular路由就会变成非常简单的工作。
report.current
对应template:views/report/current.html
<table class="table table-hover">
<caption>本月报表数据</caption>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>User Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>aehyok</td>
<td>leo</td>
<td>@aehyok</td>
</tr>
<tr>
<td>lynn</td>
<td>thl</td>
<td>@lynn</td>
</tr>
</tbody>
</table>
report.last
对应template:views/report/last.html
<h5>上月报表数据</h5>
report.resolvetest
对应template:views/report/resolvetest.html
<div>
<p>姓名:{{person.name}}</p>
<p>email:{{person.email}}</p>
</div>
resolvetest.html中使用了controller与scope,其定义在app-router.js中,本文中并没有怎么描述controller与scope,读者最好将angular的基本概念与best practice部分综合思考。
controllers与services
多国语言功能
多国语言功在很多项目中是可以固化的功能,下面内容可以直接在生产环境中使用,读者如需要了解细节,需要阅读angular-translate的官网资料。
controller/translate.js
对应功能:多国语言功能的controller
define([
'angular'
],function(angular){
return ['$scope','$translate','T','$log','$q',
function($scope,$translate,T,$log,$q){
$scope.cur_lang = $translate.use();
$scope.changeLanguage=function(langKey){
$translate.use(langKey).then(function(){
//方法1:直接或间接调用$translate.instant
//$log.info("测试T.T服务:"+T.T('HINT_TEXT'));//这个T服务使用了$translate.instant
//方法2:使用promise方式
$translate('HINT_TEXT').then(function (HINT_TEXT) {
var str = eval("'" + HINT_TEXT + "'"); // "我是unicode编码"
$log.info("测试translate服务:"+str);
});
$log.warn("注意中文编码问题与 $translateProvider.useSanitizeValueStrategy 这个设置有关");
});
$scope.cur_lang=langKey;
window.localStorage.langKey = langKey;
};
}
];
});
上述代码中顺便展示了手动获取多国语言的两种办法
services/translate.js
对应功能:多国语言功能的service,由上述controller/translate.js调用
define([
'angular'
],function(angular){
var moduleName="TranslateService";
angular
.module(moduleName, [])
.factory('T', ['$translate',function ($translate) {
return {
T:function(key){
if (key){
return $translate.instant(key);
}
return key;
}
};
}]);
return moduleName;
});
i18n多国语言文件
cn.json
说明:中文语言资源包
{
"REPORT":"报表",
"OVERVIEW":"概要"
}
en.json
说明:英文语言资源包
{
"REPORT":"Report",
"OVERVIEW":"Overview"
}
Best Practice
- 与其他SPA框架一样,要好好规划路由app-router
- 一个Angular SPA应用中主要的开发工作是view、controller、scope、service
- controller是与view进行绑定
- scope通过controller与view进行绑定
- service为controller提供服务与数据(model)
- view、controller、scope是实例型工作方式
- service是单例型工作方式
- directive与filter应该作为项目团队的基础模块进行开发和维护,与具体项目分开维护
- 不使用directive去实现大量ui型组件,ui型directive开发越少越好
- 尽可能使用第三方UI成套组件
- 尽量不要与jquery、d3等框架组合使用,不然容易产生directive的开发需求
总结
本文目标是通过分析Angular SPA的几大组成部分特性,如router,view,controller,service,directive,filter等等,给出一个适合大部分Angular SPA应用的标准应用架构。
AngularJS 1.x相对于Backbone等是一个比较重型的框架,其通过directive、filter、service等功能屏蔽了非常多的实现细节、包括交互等等,同时有非常多的外部组件可以被使用,一般应用中可能都不需要处理Dom事件即可完成整个应用的开发,开发效率非常高。这些都是AngularJS的优点,但也是它的缺点!那些现成的组件(尤其是UI类)如果不满足功能需求,那就比较悲催了,需要详细了解Angular 的directive设计理念并自己去制造这些轮子。
本文几乎没有怎么提到directive也是因为其设计思路复杂及篇幅原因,需要单独写一片文章才能讲述清楚。而且我个人并不推崇在项目中利用directive去做很多ui组件,这真的是个苦活累活,要是还要与jquery等等配合才能完成,那更是两种截然不同风格的碰撞。并且,Angular的digest循环和$watch也没有在文中体现,面面俱到真的是要写多篇才行。
当面对什么时候使用Angular的问题时,我总会给个自己认为比较简单的答案:
- 如果交互类控制需求非常多,就用Backbone之类的框架,因为控制力强,再配合jquery、D3等框架协同工作即可;
- 如果数据展示类需求较多,且基本不需要对DOM进行控制,刚巧AngularUI组件又够用,那必须Angular,非常舒服;
最后,AngularJS 2.x是非常重大的升级,万分期待。