javascript MVC框架之 AngularJS 1.x 实用指南

本文非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年左右

阅读本文之前最好对以下知识点已经熟悉:

  • javascript 前端 基于 npm、bower、grunt的标准项目构建
  • javascript MVC框架之 Backbone 实用指南

    • 简要介绍
    • 参考资料
    • 简要示意图
      • AngularJS 1x顺序图
    • AngularJS MVC应用基本原则
    • 页面布局设计
    • 项目基本目标
    • 项目构建配置
    • 目录结构
    • 各文件主要内容
      • indexhtml
      • viewspartialsnavhtml
      • viewspartialssidebarhtml
      • mainjs
      • appjs
      • app-routerjs
      • router与template
        • start
        • report
        • reportcurrent
        • reportlast
        • reportresolvetest
      • controllers与services
        • 多国语言功能
      • i18n多国语言文件
        • cnjson
        • enjson
    • Best Practice
    • 总结

简要介绍

AngularJS 1.x是一个非常强大的前端MVC框架,其双向数据绑定功能很强大,用其开发很多应用甚至不再需要考虑Dom操作。

参考资料

Angular权威教程 【书】
Angular Github
AngularUI Github

简要示意图

关于前端MVC基本示意图可以参考javascript MVC框架之 Backbone 实用指南,本文给出的是基于AngularJS特性的示意图。

AngularJS 1.x顺序图

下面的示意图不是很严谨,读者有mvc的概念和angular基础知识的话应该很好理解

Created with Raphaël 2.1.0 browser browser router router view view controller\scope controller\scope service service 访问地址 用户触发 加载视图 获取视图使用的数据、方法 获取model数据等等 返回视图使用的数据、方法 返回视图 局部刷新页面

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 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


<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文件主要完成了以下工作:

  • 初始化主页面,将页面分成多个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">
            
            
            <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文件的内容。本部分可以被一般项目标准化,作为团队基础库进行维护。

views/partials/sidebar.html

<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应用中非常重要的内容,后续涉及路由切换等等都会使用到。

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: "[email protected]"
                            }
                        }

                    }
                    ,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">Dashboardh1>

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_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路由就会变成非常简单的工作。

report.current

对应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>

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是非常重大的升级,万分期待。


你可能感兴趣的:(frontend,javascript,angular)