美文网首页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