美文网首页JavaScript
AngularJS豆瓣电影项目实战

AngularJS豆瓣电影项目实战

作者: LiuliuZhang | 来源:发表于2017-05-03 11:37 被阅读0次

    step-01 构建项目结构

    克隆项目骨架

    $ git clone https://github.com/Micua/angular-boilerplate.git moviecat
    $ cd moviecat
    

    ** 脚本**:npm 在 package.json中的script节点中可以定义脚本任务,脚本可以通过npm run script的方式执行。
    start命令可以直接npm start执行,并执行prestart与postinstall。.bowerrc文件定义了bower的安装路径"directory":"app/bower_components"
    文件说明

    .editorconfig -- 统一不同开发者的不同开发工具的不同开发配置
    在Sublime中使用需要安装一个EditorConfig的插件
    

    项目骨架:为NG做一个项目骨架的目的是为了快速开始一个新的项目,如github上web-starter-kit,angular-seed等。

    设计页面$ bower install bootstrap --save安装bootstrap,使用bootstrap页面框架http://v3.bootcss.com/examples/dashboard/
    header中引入<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">,将app.css中改成dashboard.css中的内容。
    左侧导航

            <div class="col-sm-3 col-md-2 sidebar">
              <ul class="nav nav-sidebar">
                <li class="active"><a href="#/in_theaters">正在热映</a></li>
                <li><a href="#/coming_soon">即将上映</a></li>
                <li><a href="#/top250">TOP</a></li>
              </ul>
            </div>
    

    删除中间部分内容代码,用ng-view管理<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main" ng-view></div>
    创建in_theaters/coming_soon/top250三个文件夹,每个文件夹中新建view.html/controller.js文件,view.html中新建初始化代码<h1 class="page-header">正在热映</h1>,Controller中,创建代码

    (function(angular){
    'use strict';
    //创建模块
    var module = angular.module('moviecat.in_theaters', ['ngRoute'])
    //配置模块路由
    module.config(['$routeProvider', function($routeProvider) {
      $routeProvider.when('/in_theaters', {
        templateUrl: 'in_theaters/view.html',
        controller: 'InTheatersController'
      });
    }])
    module.controller('InTheatersController', ['$scope',function($scope) {
    }]);        
    })(angular)
    

    app.js中引入这3个模块,并配置默认模块

    'use strict';
    // Declare app level module which depends on views, and components
    angular.module('moviecat', [
      'ngRoute',
      'moviecat.in_theaters',
      'moviecat.coming_soon',
      'moviecat.top250',
    ]).
    config(['$routeProvider', function($routeProvider) {
      $routeProvider.otherwise({redirectTo: '/in_theaters'});
    }]);
    

    index.html中引入这3个js文件

      <script src="in_theaters/controller.js"></script>
      <script src="coming_soon/controller.js"></script>
      <script src="top250/controller.js"></script>
    

    浏览页面


    API的概念:Application Programming Interface应用程序编程接口
    豆瓣API V2 https://developers.douban.com/wiki/?title=api_v2
    正在上映:api.douban.com/v2/movie/in_theaters 可以加?count=1
    WebAPI 通过WEB方式提供结构叫做 WEBAPI
    所有有输入有输出的事物都可以是API都是函数
    测试WebAPI的工具: POSTMAN

    step-02 假数据绑定

    使用postman获取数据后,选取subjects数组,在in_theaters的controller中,新建var data = [subjects 数组];$scope.subjects = data;绑定model。
    使用bootstrap中的list group linked作为行项目,使用Media作为内容,使用list-group-badges作为评分,修改后html如下,使用ng-repeat遍历数据,图片使用ng-src,导演项使用ng-repeat遍历数组并逗号分隔

    <h1 class="page-header">正在热映</h1>
    <div class="list-group">
        <a ng-repeat="item in subjects" href="#" class="list-group-item">
            <span class="badge">{{item.rating.average}}</span>
            <div class="media">
                <div class="media-left">
                    ![]({{item.images.small}})
                </div>
                <div class="media-body">
                    <h2 class="media-heading">{{item.title}}</h2>
                    <p>导演:<span ng-repeat="d in item.directors">
                        {{d.name}}
                        <span ng-if = '!$last'>,</span>
                    </span></p>
                    <p>类型:<span>{{item.genres.join(',')}}</span></p>
                </div>
            </div>
        </a>
    </div>
    

    在app.css文件中加上

    .list-group .media{
        margin-top: 0;
    }
    

    step-03 请求真实数据

    请求本地数据:
    将postman中得到的raw data复制到json文件
    修改controller,加入$http,首先使得$scope.subjects = [];,否则由于还没初始化,页面上$scope.subjects是个undefind类型

    module.controller('InTheatersController', ['$scope','$http',function($scope,$http) {
        $scope.subjects = [];
        $scope.message = '';
        $http.get('/moviecat/app/datas/in_theaters.json').then(function(res){
            if (res.status == 200){
                $scope.subjects = res.data.subjects;
            }else{
                $scope.message = '错误信息'+ res.statusText;
            }
        },function(err){
            $scope.message = '错误信息'+ err.statusText;
        })
    
    }]);    
    

    JSONP
    处理异步请求如下,调用时,会将JSON_CALLBACK替换成angular.callbacks_0等命名规则的随机函数,但豆瓣API不支持加点号的调用方式

    var doubanApiAddress = 'http://api.douban.com/v2/movie/in_theaters';
    // 测试$http服务
    // 在Angular中使用JSONP的方式做跨域请求,
    // 就必须给当前地址加上一个参数 callback=JSON_CALLBACK
    $http.jsonp(doubanApiAddress+'?callback=JSON_CALLBACK').then(function(res) {
      // 此处代码是在异步请求完成过后才执行(需要等一段时间)
      if (res.status == 200) {
    

    实现跨域
    定义jsonp函数传入url, data, callback三个参数,首先生成1个随机数组合成window对象的callback函数名,等于传入的回调函数,data参数包括了传入的查询参数,添加到'?'后面(判断url中是否有'?'),并附加上callback参数,然后组成url,创建script标签,使得src=url + querystring。

    (function(window, document, undefined) {
      'use strict';
      // url = http://ssss?dsf=sdfs&
      var jsonp = function(url, data, callback) {
        // 1. 挂载回调函数
        var fnSuffix = Math.random().toString().replace('.', '');
        var cbFuncName = 'my_json_cb_' + fnSuffix;
        window[cbFuncName] = callback;
        // window.my_json_cb_02132817213 = callback;
    
        // 2. 将data转换为url字符串的形式
        //  {id:1,name:'zhangsan'} => id=1&name=zhangsan
        var querystring = url.indexOf('?') == -1 ? '?' : '&';
        for (var key in data) {
          querystring += key + '=' + data[key] + '&';
          //  id    =        1        &
        }
        // querystring =  ?id=1&name=zhangsan&
    
        // 3. 处理url中的回调参数
        //  url += callback=sdjhkfsdjwe
        querystring += 'callback=' + cbFuncName;
        // querystring =  ?id=1&name=zhangsan&cb=my_json_cb_02132817213
    
        // 4. 创建一个script标签
        var scriptElement = document.createElement('script');
        scriptElement.src = url + querystring;
        // -- 注意此时还不能将其append到页面上
    
        // 5. 将script标签放到页面中
        document.body.appendChild(scriptElement);
        // append过后页面会自动对这个地址发送请求,请求完成以后自动执行
      };
    
      window.$jsonp = jsonp;
    
    })(window, document);
    

    页面调用时,添加一个div,调用jsonp函数,将数据显示到这个div。jsonp相当于在script中添加了window.my_json_cb_02132817213=my_json_cb_02132817213({json data})通过function(data) 获取json data

    <body>
      <div id="result"></div>
      <script src="http.js"></script>
      <script>
        (function() {
          $jsonp(
            'http://api.douban.com/v2/movie/in_theaters', {
              count: 10,
              start: 5
            },
            function(data) {
              document.getElementById('result').innerHTML = JSON.stringify(data);
            });
        })();
      </script>
    </body>
    

    自定义JSONP Angular实现
    创建http.js文件,实现jsonp

    'use strict';
    
    (function(angular) {
      // 由于默认angular提供的异步请求对象不支持自定义回调函数名
      // angular随机分配的回调函数名称不被豆瓣支持
      var http = angular.module('moviecat.services.http', []);
      http.service('HttpService', ['$window', '$document', function($window, $document) {
        // url : http://api.douban.com/vsdfsdf -> <script> -> html就可自动执行
        this.jsonp = function(url, data, callback) {
          var fnSuffix = Math.random().toString().replace('.', '');
          var cbFuncName = 'my_json_cb_' + fnSuffix;
          // 不推荐
          $window[cbFuncName] = callback;
          var querystring = url.indexOf('?') == -1 ? '?' : '&';
          for (var key in data) {
            querystring += key + '=' + data[key] + '&';
          }
          querystring += 'callback=' + cbFuncName;
          var scriptElement = $document[0].createElement('script');
          scriptElement.src = url + querystring;
          $document[0].body.appendChild(scriptElement);
        };
      }]);
    })(angular);
    

    controller中引入新建的http service var module = angular.module('moviecat.in_theaters', ['ngRoute','moviecat.services.http']);在index.html中引入JS <script src="components/http.js"></script>,在controller方法中,传入HttpService并调用jsonp

    module.controller('InTheatersController', ['$scope','HttpService',function($scope,HttpService) {
        $scope.subjects = [];
        $scope.message = '';
          HttpService.jsonp(
            'http://api.douban.com/v2/movie/in_theaters', {},
            function(data) {
              $scope.subjects = data.subjects;
              $scope.$apply();
              // $apply的作用就是让指定的表达式重新同步
            });
    }]);    
        
    

    JSONP更新
    每次点击按钮都生成一个新的script标签,我们应该每次加载完后删除这个script标签


    调整$window[cbFuncName] = callback;的顺序在scriptElement.src = url + querystring;之后,callback之后,移除这个标签
          scriptElement.src = url + querystring;
          $window[cbFuncName] = function(data) {
            callback(data);
            $document[0].body.removeChild(scriptElement);
          };   
    

    step-04 Loading加载动画设计

    参考http://tobiasahlin.com/spinkit/ 动画,插入到view.html与app.css中


    添加mask class,并添加ng-show指令,在controller中,先默认$scope.loading = true;在回调函数中,再改成false
    <div class="mask" ng-show="loading">
      <div class="spinner">
        <div class="dot1"></div>
        <div class="dot2"></div>
      </div>
    </div>
    

    添加mask的css

    .mask {
      position: fixed;
      left: 0;
      top: 0;
      right: 0;
      bottom: 0;
      background-color: rgba(0, 0, 0, .4);
      z-index: 2000;
    }
    

    step-05 实现分页功能

    分页功能
    修改路由$routeProvider.when('/in_theaters/:page', {controller中,添加参数$routeParams,定义page的相关参数设置如下,在jsonp的参数中,添加{start: start, count: count}

        var count = 5; // 每一页的条数
        var page = parseInt($routeParams.page); // 当前第几页
        var start = (page - 1) * count; // 当前页从哪开始
        # 回调函数中
         $scope.totalCount = data.total;
         $scope.totalPages = Math.ceil($scope.totalCount / count);
    

    在view.html中,添加

    <div ng-show="!loading">
        <p>总共:{{totalCount}}条记录,第{{currentPage}}/{{totalPages}}页</p>
    </div>  
    

    访问时,需添加/1等后缀
    分页按钮
    <div ng-show="!loading">标签中添加分页按钮

      <nav>
        <ul class="pager">
          <li ng-class="{disabled:currentPage<=1}"><a ng-click="go(currentPage - 1)">« 上一页</a></li>
          <li ng-class="{disabled:currentPage>=totalPages}"><a ng-click="go(currentPage + 1)">下一页 »</a></li>
        </ul>
    

    在controller中,引入$route,并暴露一个上一页下一页的行为

          $scope.go = function(page) {
            // 传过来的是第几页我就跳第几页
            // 一定要做一个合法范围校验
            if (page >= 1 && page <= $scope.totalPages)
              $route.updateParams({ page: page });
          };
    

    step-06 抽象公共列表页

    实现公共列表页
    由于3个模块的API结构形式都相同,因此我们建立成一个movie_list的公共列表页,复制in_theaters文件夹成movie_list,修改模块名moviecat.movie_list,配置路由$routeProvider.when('/:category/:page', {,修改controller名 'MovieListController',修改url为'http://api.douban.com/v2/movie/'+ $routeParams.category
    app.js中引入'moviecat.movie_list',index中引入<script src="movie_list/controller.js"></script>
    导航栏切换1
    修改导航栏,交由NavController来管理

      <ul class="nav nav-sidebar" ng-controller="NavController">
          <li ng-class="{active:type=='in_theaters'}"><a href="#/in_theaters/1">正在热映</a></li>
          <li ng-class="{active:type=='coming_soon'}"><a href="#/coming_soon/1">即将上映</a></li>
          <li ng-class="{active:type=='top250'}"><a href="#/top250/1">TOP</a></li>
      </ul>
    

    app.js中定义NavController,利用$scope.$watch监测路径值的变化

    .controller('NavController', [
         '$scope',
         '$location',
         function($scope, $location) {
           $scope.$location = $location;
           $scope.$watch('$location.path()', function(now) {
             if (now.startsWith('/in_theaters')) {
               $scope.type = 'in_theaters';
             } else if (now.startsWith('/coming_soon')) {
               $scope.type = 'coming_soon';
             } else if (now.startsWith('/top250')) {
               $scope.type = 'top250';
             }
             console.log($scope.type);
           });
         }
       ]);
    

    导航栏切换2
    自定义指令auto-focus来实现,回到原来状态,加上auto-focus指令

    <ul class="nav nav-sidebar">
      <li auto-focus><a href="#/in_theaters/1">正在热映</a></li>
      <li auto-focus><a href="#/coming_soon/1">即将上映</a></li>
      <li auto-focus><a href="#/top250/1">TOP</a></li>
    </ul>
    

    新建auto-focus.js文件,定义modulemoviecat.directives.auto_focus及自定义指令autoFocus

    (function(angular) {
      angular.module('moviecat.directives.auto_focus', [])
        .directive('autoFocus', ['$location', function($location) {
          // Runs during compile
          var path = $location.path(); // /coming_soon/1
          return {
            restrict: 'A', // E = Element, A = Attribute, C = Class, M = Comment
            link: function($scope, iElm, iAttrs, controller) {
                var aLink = iElm.children().attr('href');
                var type = aLink.replace(/#(\/.+?)\/\d+/,'$1'); // /coming_soon
                if(path.startsWith(type)){
                    // 访问的是当前链接
                    iElm.addClass('active');
                }
              iElm.on('click', function() {
                iElm.parent().children().removeClass('active');
                iElm.addClass('active');
              });
            }
          };
        }]);
    })(angular);
    

    分别在index及app.js中引入<script src="components/auto-focus.js"></script>'moviecat.directives.auto_focus',
    刚开始访问时,$location.path() 为空,可用下面代码来避免

          return {
            restrict: 'A', // E = Element, A = Attribute, C = Class, M = Comment
            link: function($scope, iElm, iAttrs, controller) {
    
              $scope.$location = $location;
              $scope.$watch('$location.path()', function(now) {
                // 当path发生变化时执行,now是变化后的值
                var aLink = iElm.children().attr('href');
                var type = aLink.replace(/#(\/.+?)\/\d+/, '$1'); // /coming_soon
                if (now.startsWith(type)) {
                  // 访问的是当前链接
                  iElm.parent().children().removeClass('active');
                  iElm.addClass('active');
                }
              })
    

    异步加载
    异步加载需要使用script.js模块,通过bower install script.js --save安装,在index文件中引入<script src="bower_components/script.js/dist/script.js"></script>。通过$script异步加载,加载完成后执行回调函数

      <script>
        $script([
          './bower_components/angular/angular.js',
          './bower_components/angular-route/angular-route.js',
          './movie_list/controller.js',
          './components/http.js',
          './components/auto-focus.js',
          './app.js' // 由于这个包比较小,下载完成过后就直接执行
        ], function() {
          console.log(angular);
          angular.bootstrap(document, ['moviecat']);
          // console.log(jQuery);
        });
      </script>
    

    由于app.js文件比较小,加载完成后执行函数,前面的依赖没有加载完,会报错,通过bower install angular-loader --save安装,在header中引入<script src="bower_components/angular-loader/angular-loader.js"></script>自动控制依赖顺序。

    step-07 搜索模块

    index.html修改navbar form,form添加ng-controller与ng-submit,输入框添加ng-model

              <form class="navbar-form navbar-right" ng-controller="SearchController" ng-submit="search()">
                <input type="text" class="form-control" placeholder="Search..." ng-model="input">
              </form>
    

    app.js中创建controller,暴露input数据及search行为,search中,往url后缀参数添加了p=input

      .controller('SearchController', [
        '$scope',
        '$route',
        'AppConfig',
        function($scope, $route, AppConfig) {
          $scope.input = ''; // 取文本框中的输入
          $scope.search = function() {
            // console.log($scope.input);
            $route.updateParams({ category: 'search', q: $scope.input });
          };
        }
      ]);
    

    在controller.js中,jsonp加入q参数{start: start, count: count,q: $routeParams.q}

    step-08 详细页模块

    复制movie_list为新的movie_detail,在index.html及app.js中分别引入js文件及模块名,路由样式/detail/26748673,同时也与movie_list的路由匹配,因此需将'moviecat.movie_detail',放在上面
    view的样式如下

    <div class="jumbotron">
      <h1>{{movie.title}}</h1>
      ![]({{movie.images.large}})
      <p>{{movie.summary}}</p>
    </div>
    
    <div class="mask" ng-show="loading">
      <div class="spinner">
        <div class="dot1"></div>
        <div class="dot2"></div>
      </div>
    </div>
    

    controller中,调用API

          var id = $routeParams.id;
          var apiAddress = 'http://api.douban.com/v2/movie/subject/' + id;
          HttpService.jsonp(apiAddress, {}, function(data) {
            $scope.movie = data;
            $scope.loading = false;
            $scope.$apply();
          });
    

    movie_list中,行的超链接指向detail页面href="#/detail/{{item.id}}"

    为模块定义常量
    在app.js中,定义常量

      .constant('AppConfig', {
        pageSize: 5,
        listApiAddress: 'http://api.douban.com/v2/movie/',
        detailApiAddress: 'http://api.douban.com/v2/movie/subject/'
      })
    

    在controller中,引入并使用

      module.controller('MovieListController', [
        '$scope',
        '$route',
        '$routeParams',
        'HttpService',
        'AppConfig',
        function($scope, $route, $routeParams, HttpService, AppConfig) {
          var count = AppConfig.pageSize; // 每一页的条数
    

    相关文章

      网友评论

        本文标题:AngularJS豆瓣电影项目实战

        本文链接:https://www.haomeiwen.com/subject/hacctxtx.html