本文非AngularJS 1.X的入门级教程,很多细节可能会被无意识的忽略。AngularJS相比于Backbone要复杂不少,两者的设计思路也完全不同,更可以说是大相径庭。面对一个SPA项目的前端技术选型时,需要根据实际需要进行选择。本文更多的是给出一些注意点,强烈建议好好阅读官方API。AngularJS不依赖于jquery,连最基本的编码方式都完全不同,所以jquery的经验并不一定能帮助你快速将其掌握。
另外,需要注意以下几点:
阅读本文之前最好对以下知识点已经熟悉:
AngularJS 1.x是一个非常强大的前端MVC框架,其双向数据绑定功能很强大,用其开发很多应用甚至不再需要考虑Dom操作。
Angular权威教程 【书】
Angular Github
AngularUI Github
关于前端MVC基本示意图可以参考javascript MVC框架之 Backbone 实用指南,本文给出的是基于AngularJS特性的示意图。
下面的示意图不是很严谨,读者有mvc的概念和angular基础知识的话应该很好理解
在Bootstrap官网中有一个布局概念的范例,本文基于其结构进行基于AngularJS的SPA化,如下图:
【待补充】
切换到report的效果图:
【待补充】
配置项目基本与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的标准项目构建的有些许差异,当作历史遗留问题的一个反面例子。
说明:
名称 | 类型 | 用途 |
---|---|---|
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启动程序 |
<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">
<link rel="stylesheet" href="../bower_components/bootstrap/dist/css/bootstrap.css" />
<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>
<div ng-include="'views/partials/nav.html'">div>
<div class="container-fluid">
<div class="row">
<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>
<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>
body>
html>
上述内容中很多注释标记都是给项目构建使用的,比如if target dummy,参考javascript 前端 基于 npm、bower、grunt的标准项目构建即可很快理解。
说明:
index.html文件主要完成了以下工作:
本AppView的view组成:
名称 | 说明 |
---|---|
nav | 顶部导航栏 |
sidebar | 左侧导航栏 |
main | 右侧内容区域,使用angular-ui的ui-view指令标记 |
熟悉bootstrap的同学对该布局不会陌生,这里不再赘述,可以参考javascript MVC框架之 Backbone 实用指南中的讲解。
主要使用的Angular命令:
名称 | 介绍 |
---|---|
ng-include | 将外部html文件引入到当前文件中 |
ui-view | angular-ui引入的指令,标记内容主要区域 |
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">
<span class="glyphicon glyphicon-home">span><span class="glyphicon glyphicon-signal">span>项目名称
a>
div>
<div id="navbar">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Homea>li>
<li><a href="#about">Abouta>li>
<li><a href="#contact">Contacta>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="#">Actiona>li>
<li><a href="#">Another actiona>li>
<li><a href="#">Something else herea>li>
<li role="separator" class="divider">li>
<li class="dropdown-header">Nav headerli>
<li><a href="#">Separated linka>li>
<li><a href="#">One more separated linka>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">Submitbutton>
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">Englishoption>
<option value="de">Germanoption>
<option value="cn">中文option>
select>
div>
ul>
div>
div>
nav>
nav.html中绝大部分内容都是Bootstrap范例部分,但是标注出的使用了Angular的controller和service来实现多国语言,其中使用了第三方的angular-translate组件,本文将其进行了简单封装,具体可看controller/translate.js和service/translate.js文件的内容。本部分可以被一般项目标准化,作为团队基础库进行维护。
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<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="#">Exporta>li>
ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav itema>li>
<li><a href="">Nav item againa>li>
<li><a href="">One more nava>li>
<li><a href="">Another nav itema>li>
<li><a href="">More navigationa>li>
ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item againa>li>
<li><a href="">One more nava>li>
<li><a href="">Another nav itema>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应用中非常重要的内容,后续涉及路由切换等等都会使用到。
'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!
'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主要完成的工作依次有:
上述app.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: "[email protected]"
}
}
}
,controller: ['$scope','person',function($scope,person/*,currentDetails, facebookId*/) {
$scope.person = person;
}]
});
}
];
});
app-router.js只完成唯一的一项工作(也是其最重要的工作):
上述文件中描述的路由有:
特别注意:angular-ui-router有嵌套路由的概念,真的非常有创意。每个ui-view都是完全独立的,其上下级关系由router字符串描述。比如report.current就描述了两层路由。一定要熟练掌握angular-ui-router的用法,这是非常灵活的部分。
对应template:views/partials/dashboard.html
<h1 class="page-header">Dashboardh1>
对应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_testa>
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路由就会变成非常简单的工作。
对应template:views/report/current.html
<table class="table table-hover">
<caption>本月报表数据caption>
<thead>
<tr>
<th>First Nameth>
<th>Last Nameth>
<th>User Nameth>
tr>
thead>
<tbody>
<tr>
<td>aehyoktd>
<td>leotd>
<td>@aehyoktd>
tr>
<tr>
<td>lynntd>
<td>thltd>
<td>@lynntd>
tr>
tbody>
table>
对应template:views/report/last.html
<h5>上月报表数据h5>
对应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部分综合思考。
多国语言功在很多项目中是可以固化的功能,下面内容可以直接在生产环境中使用,读者如需要了解细节,需要阅读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;
});
说明:中文语言资源包
{
"REPORT":"报表",
"OVERVIEW":"概要"
}
说明:英文语言资源包
{
"REPORT":"Report",
"OVERVIEW":"Overview"
}
本文目标是通过分析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的问题时,我总会给个自己认为比较简单的答案:
最后,AngularJS 2.x是非常重大的升级,万分期待。