20 Dec 2017

angular1.X 框架搭建

简介
angularJS 是由谷歌开源的产品,是一个mvc框架,不操作dom元素,由数据驱动页面变化,适用于SPA应用。
angularJS2.0 在升级过程过程中相当于重构了angularJS1.0,api等改变较大。
本文是基于angular1.X 的搭建过程。
本文使用\{\{ \}\} 代替双大括号(即删掉斜线)
特性:
MVC、Module(模块化)、依赖注入、指令系统、双向数据绑定
每个模块都有自己的:scope,model,controller
模块内通过注入可以使用:service,directive,filter

1. 初始化:

# 初始化项目 
mkdir angularDemo&&cd angularDemo&&npm init -y
# 安装依赖 
npm install --save requirejs jquery angular angular-ui-router requirejs-domready ngstorage ng-sanitize

添加:favicon.ico

2. 基本页面:

touch index.html

<!DOCTYPE html>
<html xmlns:ng="//angularjs.org" ng-app="myApp">
<head>
<meta charset="utf-8">
<script src="./node_modules/angular/angular.min.js"></script>
<script src="./js/app.js"></script>
</head>
<body>
<div  ng-controller="ctrl">\{\{str\}\}</div>
</body>
</html>

mkdir js&&touch js/app.js

var app = angular.module('myApp', []);
app.controller('ctrl', function($scope) {
    $scope.str = 'hello world';
});

3. requrieJS+ui-router集成:

mkdir js\controller js\directive views
index.html

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
</head>
<body>
<div ui-view></div>
<script type='text/javascript' src='./node_modules/requirejs/require.js' data-main='./js/main.js'></script>
</body>
</html>

touch js/main.js

(function() {
    var libPath = "";
    var rtPath = "../node_modules/";
    var requireConfig = {
        basePath: './',
        paths: {
            'jquery': rtPath + 'jquery/dist/jquery.min',
            'angular': rtPath + 'angular/angular.min',
            'angular-route': rtPath + 'angular-ui-router/release/angular-ui-router.min',
            'domReady': rtPath + 'requirejs-domready/domReady',
            'bootstrap': "bootstrap",
            'app': "app",
            'router': "router"
        },
        shim: {
            'angular': {
                exports: 'angular'
            },
            'angular-route': {
                deps: ['angular'],
                exports: 'angular-route'
            }

        },
        deps: ['bootstrap'],
        urlArgs: "bust=" + (new Date()).getTime() //防止读取缓存,调试用
    };
    var ok = true;
    var appInit = function() {
        if (ok) {
            require.config(requireConfig);
            module.exports=requireConfig;
        } else {
            setTimeout(appInit, 50);
        }
    }
    appInit();
})();

touch js/app.js

define(["angular", 'angular-route', './controller/mainController', './directive/mainDirective'], function(angular) {
    return angular.module("webapp", ['ui.router', 'webapp.controllers', 'webapp.directive']);
})

touch js/bootstrap.js

define(['require', 'angular', 'angular-route', 'jquery', 'app', 'router'], function(require, angular) {
    require(['domReady!'], function(document) {
        angular.bootstrap(document, ['webapp']);
    });
});

touch js/router.js

define(["app"], function(app) {
    return app.run(['$rootScope', '$state', '$stateParams',
            function(root, state, stateParams) {
                root.$state = state;
                root.$stateParams = stateParams
            }
        ])
        .config(['$stateProvider', '$urlRouterProvider', '$locationProvider', '$uiViewScrollProvider',
            function(state, urlRouter, location, uiViewScroll) {
                //用于改变state时跳至顶部
                uiViewScroll.useAnchorScroll();
                // 默认进入先重定向
                urlRouter.otherwise('/home');
                state
                    .state('home', {
                        //abstract: true,
                        url: '/home',
                        views: {
                            '': {
                                templateUrl: '../views/home.html'
                            }
                        }
                    })
            }
        ])
})

touch views/home.html

<div  ng-controller="controller1">
这是home页\{\{str\}\}
<div dir-demo str-demo="'指令'"/>
</div>

touch js/controller/mainController.js

define(['./controller1'], function() {});

touch js/controller/controller1.js

define(['./module', 'jquery'], function(controllers, $) {
    controllers.controller('controller1', ['$scope', function(scope) {
        //控制器的具体js代码
        scope.str = 'controller1';
    }])
})

touch js/controller/module.js

define(['angular'], function(angular) {
    return angular.module('webapp.controllers', []);
});

touch js/directive/mainDirective.js

define(['./directive'], function() {});

touch js/directive/directive.js

define(['./module', 'jquery'], function(directives, $) {
    directives.directive('dirDemo', function() {
        return {
            restrict: 'EA',
            replace: true,
            transclude: true,
            require: '^?accordion',
            scope: {
                str: '=strDemo'
            },
            template: '<div> \{\{str\}\}</div>',
            link: function(scope, element, attrs, controller) {

            }
        }
    });
})

touch js/directive/module.js

define(['angular'], function(angular) {
    return angular.module('webapp.directive', []);
});

4. ui-router 路由嵌套:

index.html

<a ui-sref="test">test</a>   
<div ui-view></div>

touch views/layout.html

<div ui-view="nav"></div>
<div ui-view></div>

touch views/nav.html

<ol>
    <li><a ui-sref=".page({id:1})">test.page</a></li>
    <li><a ui-sref="test.home">test.home</a></li>
</ol>

router.js

//用于改变state时跳至顶部
 uiViewScroll.useAnchorScroll();
 // // 默认进入先重定向
 // urlRouter.otherwise('/pageTab');   
 // // urlRouter.when("", "/PageTab");
 state.state("test", {
         url: "/test",
         //abstract: true,
         views: {
             "": {  templateUrl: "../views/layout.html"},
             "nav@test": {templateUrl: "../views/nav.html"}
         }
     })
     .state("test.page", {
         url: "/page?:id",
         // templateUrl:"'page3.html'",
         template: "test page 页",
         controller: ["$stateParams", function($stateParams) {
             console.log($stateParams.id); // 1  这里实现传参
         }],
         params: { id: null }
     })
     .state("test.home", {
         url: "/home",
         templateUrl:   "../views/home.html"
     })

5. 动态加载:

准备:去掉mainControllermainDirective

1、使用angular-async-loader模块

安装依赖: npm install --save angular-async-loader
main.js

'asyncLoader': rtPath + 'angular-async-loader/angular-async-loader',
// ...
 'asyncLoader': {
         exports: 'asyncLoader'
     }

app.js

define(["angular", 'asyncLoader','angular-route', './controller/module', './directive/module'], function(angular,asyncLoader) {
    var app= angular.module("webapp", ['ui.router', 'webapp.controllers', 'webapp.directive']);
 asyncLoader.configure(app);
return app;
})

router.js

.state("test.home", {url: "/home",
        templateUrl:   "../views/home.html",
        controllerUrl: './controller/controller1',
        // controller: 'controller1',
       // dependencies: ['services/usersService']  
        })

controller1

define(['app', 'jquery'], function(app, $) {
    app.controller('controller1', ['$scope', function(scope) {      
        scope.str = 'controller1';
    }])
})

2、手动注册

router.js

app.config(['$controllerProvider','$compileProvider','$filterProvider','$provide',function(controllerProvider,compileProvider,filterProvider,provide){
    app.register = {
       controller : controllerProvider.register,
       directive: compileProvider.directive,
       filter: compileProvider.register,
       service: provide.service
    };
}])
//...
.state("test.home", {
          url: "/home",
          templateUrl:   "../views/home.html",
          resolve: {
           loadCtrl: ["$q", function($q) {
            var deferred = $q.defer();
            require(['./controller/controller1'], function() { 
                deferred.resolve(); });
                return deferred.promise;
                   }] 
                 } 
            }) 

controller1.js

define(['app', 'jquery'], function(app, $) {
    app.register.controller('controller1', ['$scope', function(scope) {
        //控制器的具体js代码
        scope.str = 'controller1';
    }])
})

6. post,get请求:

touch js/myhttp.js

define(['app'],function(app){
    app.service("myhttp",function($http,http,errorMessage,$log){
        this.get = function(url, params, success, error){  
        //$http.post(url ,params).success(function(resp){  }).error(function(resp){  })  
        $http.get(http + url ,{data:params})
            .success(function (resp) {
                success && success(resp);   })
            .error(function (resp) {
               $log.debug(resp);    });
        };
    })
})

7. 静态资源缓存页面:

注入:$templateCache,$http

if ($templateCache.get(url) && !$routeParams.nocache ) {
    $scope.templateUrl = url;
} else
    $http({ 
    url: url,
    method: "POST",
    data: DATA,
    withCredentials: true,
    crossDomain: true
    }).success(function(data) {
        $templateCache.put(url, data);
        $scope.templateUrl = url; 
    }).error(function(data) {
        $scope.templateUrl = 'views/404.html';
    });

8. grunt:自动化前端构建工具

8.1 安装依赖

npm install -g  grunt-cli grunt-init
npm install --save-dev grunt grunt-contrib-jshint@1.1.0 grunt-contrib-watch grunt-contrib-qunit grunt-contrib-concat grunt-contrib-uglify 

phantomjs(下载)安装失败:
下载后放到:C:\Users\Administrator\AppData\Local\Temp\phantomjs\phantomjs-1.9.8-windows.zip

8.2 创建配置文件Gruntfile.js:

module.exports = function(grunt) {
    grunt.initConfig({
        meta: {
            version: '0.1.0'
        },
        // 读取package.json文件
        pkg: grunt.file.readJSON('package.json'),
        banner: '/*! <%= pkg.title || pkg.name %> - v<%= meta.version %> - ' +
            '<%= grunt.template.today("yyyy-mm-dd") %>*/\n',
        // 任务配置
    });
    // 加载grunt插件
    grunt.loadNpmTasks('grunt-contrib-concat');  
    // 配置default任务(顺序)
    grunt.registerTask('default', []);
};

8.3 文件合并插件:concat

npm install grunt-contrib-concat --save-dev 在Gruntfile.js配置

module.exports = function(grunt) {
    grunt.initConfig({
        meta: {
            version: '0.1.0'
        },
        // 读取package.json文件
        pkg: grunt.file.readJSON('package.json'),
        banner: '/*! <%= pkg.title || pkg.name %> - v<%= meta.version %> - ' +
            '<%= grunt.template.today("yyyy-mm-dd") %>*/\n',
        // 任务配置
        //concat用来合并js文件  
        concat: {
            options: {
                // 定义一个用于插入合并输出文件之间的字符  
                separator: ';',
                banner: '<%= banner %>',
                stripBanners: true
            },
            dist: {
                // 将要被合并的文件  
                src: ['js/*.js', 'js/**/*.js'],
                // 合并后的JS文件的存放位置  
                dest: 'dist/<%= pkg.name %>.js'
            }
        }
    });
    // 加载grunt插件
    grunt.loadNpmTasks('grunt-contrib-concat');
    // 配置default任务(顺序)
    grunt.registerTask('default', ['concat']);
};

8.3 文件压缩插件:cuglify

npm install grunt-contrib-uglify --save-dev 在Gruntfile.js配置

 //uglify用来压缩js文件  
  uglify: {
      options: {
          banner: '<%= banner %>'
      },
      dist: {
          files: {
              //uglify会自动压缩concat任务中生成的文件  
              'dist/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
          }
          // src: '<%= concat.dist.dest %>',
          // dest: 'dist/<%= pkg.name %>.min.js' 
      }
  }
  // ...
 // 加载grunt插件
 grunt.loadNpmTasks('grunt-contrib-uglify');
 // 配置default任务(顺序)
 grunt.registerTask('default', ['concat', 'uglify']);

8.3 js代码规范检查:jshint

npm install grunt-contrib-jshint --save-dev 在Gruntfile.js配置

 //jshint用来检查js代码规范  
 jshint: {
     // files: ['Gruntfile.js', 'js/*.js', 'js/**/*.js'],  
     gruntfile: {
         src: 'Gruntfile.js'
         // files: {'build/js/base.min.js' : ['assets/js/base.js'] }
     },
     lib_test: {
         src: ['js/*.js', 'js/**/*.js']
     },
     options: {
         curly: true,
         eqeqeq: true,
         immed: true,
         latedef: true,
         newcap: true,
         noarg: true,
         sub: true,
         undef: true,
         unused: false,
         expr: true,
         asi: true, //允许省略分号
         boss: true,
         eqnull: true,
         browser: true,
         force: true,
         globals: {
             jQuery: false,
             $: false,
             define: false,
             require: false,
             console: false,
             module: false,
             document: true
         }
     },
 }
 // ....
 // 加载grunt插件
 grunt.loadNpmTasks('grunt-contrib-jshint');
 // 配置default任务(顺序)
 grunt.registerTask('default', ['jshint', 'concat', 'uglify']);

8.4 html代码规范检查:htmlhint

npm install grunt-htmlhint --save-dev 在Gruntfile.js配置

 //htmlhint用来检查html代码规范  
 htmlhint: {
     build: {
         // htmlhintrc: '.htmlhintrc'
         options: {
             'tag-pair': true,
             'tagname-lowercase': true,
             'attr-lowercase': true,
             'attr-value-double-quotes': true,
             'doctype-first': false,
             'spec-char-escape': true,
             'id-unique': true,
             'head-script-disabled': true,
             'style-disabled': true
         },
         src: ['views/**/*.html']
     }
 }
 // ...
 // 加载grunt插件
 grunt.loadNpmTasks('grunt-htmlhint');
  // 配置default任务(顺序)
 grunt.registerTask('default', ['jshint','htmlhint', 'concat', 'uglify']);

8.5 监听文件:watch

npm install grunt-contrib-watch --save-dev 在Gruntfile.js配置

 watch: {
     gruntfile: {
         files: '<%= jshint.gruntfile.src %>',
         tasks: ['jshint:gruntfile']
     },
     lib_test: {
         files: '<%= jshint.lib_test.src %>',
         tasks: ['jshint:lib_test']
     }
 },
// ...
 // 加载grunt插件
 grunt.loadNpmTasks('grunt-contrib-watch');
 // 配置default任务(顺序)
 grunt.registerTask('default', ['jshint', 'htmlhint', 'concat', 'uglify', 'watch']);

8.6 小知识

 // 复制
 copy: {
     build: {
         cwd: 'js',      //指向的目录是相对的,全称Change Working Directory更改工作目录
         src: ['**'],    //指向源文件,**是一个通配符,用来匹配Grunt任何文件
         dest: 'images', //用来输出结果任务
         expand: true    //expand参数为true来启用动态扩展,涉及到多个文件处理需要开启
     },
     // 注:如果src: [ '**', '!**/*.styl' ],表示除去.styl文件,!在文件路径的开始处可以防止Grunt的匹配模式
 }, 
 // 清除
 clean: {
     build: {
         src: ['css/**/*.*']
     },
 }, 
 less: {
     dynamic_mappings: {
         files: [{
             expand: true,
             cwd: 'build/less',
             src: ['**/*.less', '!**/header.less'], 
             dest: 'css',
             ext: '.css',
         }],
     },
 }, 
 // CSS压缩
 cssmin: {
       build: {
           expand: true,
         cwd: 'css/',
         src: ['*.css', '!*.min.css'],
         dest: 'css/',
         ext: '.css'
       }
 },
// 在Grunt中加载npm任务
require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks);

//JSHint其他配置
jshint:{
    files:['Gruntfile.js','app/**/*.js','!app/release/**','modules/**/*.js','specs/**/*Spec.js'],
    options:{   
        curly:true,//使用if和while等结构语句时加上{}        
        eqeqeq:true,//是否都用了===或者是!==,而不是使用==和!。
        immed:true,////匿名函数:(function(){//}());而不是(function(){}(//bla bla));
        latedef:true,
        newcap:true,//J构造函数名都要大写字母开头。
        noarg:true,//如果为真,JSHint会禁止arguments.caller和arguments.callee的使用
        sub:true,//如果为真,JSHint会允许各种形式的下标来访问对象。
        undef:true,//如果为真,JSHint会要求所有的非全局变量,在使用前都被声明。
        boss:true,//如果为真,那么JSHint会允许在if,for,while里面编写赋值语句。
        eqnull:true,//JSHint会允许使用"== null"作比较
        browser:true,
        globals:{
            //AMD
            module: true,
            require:true,
            requirejs: true,
            define: true,
            //Environments
            console: true,
            //General Purpose Libraries
            $: true,
            jQuery:true,
            //Testing
            sinon: true,
            describe:true,
            it: true,
            expect:true,
            beforeEach: true,
            afterEach: true
        }
    }
}

9. 自动化测试

Jasmine做单元测试,Karma自动化完成单元测试,Grunt启动Karma统一项目管理

9.1 初始化

# 安装Karma及其依赖
npm install --save-dev angular-mocks karma karma-jasmine jasmine-core karma-chrome-launcher karma-cli karma-mocha-reporter karma-ng-scenario karma-requirejs
npm install grunt-karma --save-dev
# 初始化配置Karma
karma init karma.require.config.js

9.2 修改配置文件:karma.require.config.js

module.exports = function(config) {
    config.set({
        basePath: './',
        frameworks: ['jasmine', 'requirejs'],
        files: [
            { pattern: 'node_modules/jquery/dist/jquery.min.js', included: false },
            { pattern: 'node_modules/angular/angular.min.js', included: false },
            { pattern: 'node_modules/requirejs-domready/domReady.js', included: false },
            { pattern: 'node_modules/angular-ui-router/release/angular-ui-router.min.js', included: false },
            { pattern: 'node_modules/angular-async-loader/angular-async-loader.js', included: false },
            { pattern: 'node_modules/angular-mocks/angular-mocks.js', included: false },
            { pattern: 'js/**/*.*', included: false, served: true },
            { pattern: 'js/*.*', included: false, served: true },
            { pattern: 'test/*_spec.js', included: false, served: true },
            'test/test-main.js'
        ],
        exclude: [
            './js/main.js',
            './karma.require.config.js'
        ],
        preprocessors: {},
        // plugins: ['karma-mocha-reporter','karma-chrome-launcher','karma-jasmine','karma-requirejs'],
        reporters: ['progress', 'mocha'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        browsers: ['Chrome'],
        singleRun: false,
        concurrency: Infinity
    })
}

9.3 修改test/test-main.js

var allTestFiles = [];
var TEST_REGEXP = /(spec|test)\.js$/i;
Object.keys(window.__karma__.files).forEach(function(file) {
    if (TEST_REGEXP.test(file)) {
        // var normalizedTestModule = file.replace(/^\/base\/|\.js$/g, '');
        var normalizedTestModule = file;
        allTestFiles.push(normalizedTestModule);
    }
});
var libPath = "";
var rtPath = "../node_modules/";
var requireConfig = {
    baseUrl: '/base/js',
    paths: {
        'jquery': rtPath + 'jquery/dist/jquery.min',
        'angular': rtPath + 'angular/angular.min',
        'angular-route': rtPath + 'angular-ui-router/release/angular-ui-router.min',
        'domReady': rtPath + 'requirejs-domready/domReady',
        'asyncLoader': rtPath + 'angular-async-loader/angular-async-loader',
        'angularMocks': rtPath + 'angular-mocks/angular-mocks',
        'bootstrap': 'bootstrap',
        'app': "app",
        'router': "router"
    },
    shim: {
        'angular': {
            exports: 'angular'
        },
        'angular-route': {
            deps: ['angular'],
            exports: 'angular-route'
        },
        'asyncLoader': {
            exports: 'asyncLoader'
        },
        'angularMocks': {
            deps: ['angular'],
            exports: 'angular.mock'
        }
    },
    deps: allTestFiles,
    callback: window.__karma__.start
};
require.config(requireConfig);

9.4 编写测试文件: test_spec.js

define(['app','angularMocks','jquery','../js/controller/controller1','../js/directive/directive' ], function(app,mocks,jquery) {
    describe('Test Controller', function() {
        describe('controller1', function() {
            var $scope;
            //模拟Application模块并注入依赖
            beforeEach(mocks.module('webapp'));
            //模拟Controller,并且包含 $rootScope 和 $controller
            beforeEach(mocks.inject(function($rootScope, $controller) {
            //创建一个空的 scope
            $scope = $rootScope.$new();
            //声明 Controller并且注入已创建的空的 scope
            $controller('controller1', {$scope: $scope});
            }));
//             beforeEach(inject(function($injector) {    
//                $scope = $injector.get('$rootScope').$new();  
//                $controller = $injector.get('$controller')('controller1', {
//                   '$scope': $scope
//                  });
//              }));
        it('should str is controller1', function() {
            expect($scope.str).toBe('controller1');
            });
        });
    });
     describe('Test Directive', function() {
        describe('dirDemo', function() {
            var $scope,$compile;
            //模拟Application模块并注入依赖
            beforeEach(mocks.module('webapp'));
            beforeEach(mocks.inject(function(_$compile_,_$rootScope_) {
            $scope = _$rootScope_;
            $compile = _$compile_;
            }));
        it('should display the demo text', function () {
      var elt = $compile('<div dir-demo str-demo="\'指令\'"/>'
      )($scope);
      $scope.$digest();
      expect(elt.html()).toContain('指令');
    });
    });
})
})

运行:karma start karma.require.config.js

9.5 过滤器测试: directive/filter.js

 .filter('demo', function() {
     return function(text,lable) {
       return text+lable;
     };
   });
 describe('Test Filter', function() {
         describe('filterDemo', function() {
             var $filter;
             beforeEach(mocks.module('webapp'));

             beforeEach(mocks.inject(function(demoFilter) {
             $filter = demoFilter;
             }));
             it('should filter the demo ', function () {
                 expect($filter("abc"," filter")).toEqual('abc filter');
             });
         });
 })

9.6 Karma和istanbul代码覆盖率

npm install --save-dev karma-coverage
karma.require.config.js

 preprocessors: {
     'test/*_spec.js': 'coverage'
 },
 reporters: ['progress', 'coverage'],
 coverageReporter: {
     type: 'html',
     dir: 'coverage/'
 }

9.7 集成 grunt

npm install grunt-karma --save-dev
在Gruntfile.js配置

 karma: {
     unit: {
         configFile: 'karma.require.config.js',
         // port: 9999,
         // singleRun: true,
         // browsers: ['Chrome'],
         // logLevel: 'ERROR'
     }
 }
 // ...
 // 加载grunt插件
 grunt.loadNpmTasks('grunt-karma');
 // grunt karma
 grunt.registerTask('test', ['karma']);

运行:grunt test


Tags: