step-01 构建项目结构
- 克隆项目骨架
bash $ git clone --depth=1 https://github.com/Micua/angular-boilerplate.git moviecat $ cd moviecat
- 安装项目依赖
bash $ bower install bootstrap --save
.editorconfig -- 统一不同开发者的不同开发工具的不同开发配置 在Sublime中使用需要安装一个EditorConfig的插件
- 为NG做一个项目骨架的目的是为了快速开始一个新的项目
- angular-seed
npm 在 package.json中的script节点中可以定义脚本任务,
API的概念: Application Programxxx Interface 应用程序编程接口
有哪些常见的API
WebAPI 通过WEB方式提供结构叫做 WEBAPI
Math.random() -- api?
所有有输入有输出的事物都可以是API 都是函数
测试WebAPI的工具: POSTMAN
step-02 抽象数据成员,以假数据的方式设计控制器和视图
step-03 图片链接数据绑定BUG
step-04 豆瓣API介绍,加入$http服务对象完成AJAX请求数据
step-05 加载提示,Loading状态设计
step-06 修改字符数组的展示形式
step-07 实现分页功能
step-07 抽象公共的列表页
step-08 搜索功能模块
step-09 详细页模块设计展示
<!-- package.json -->
{
"name": "angular-boilerplate",
"version": "1.0.0",
"description": "iceStone angular boilerplate",
"scripts": {
"postinstall": "bower install",
"prestart": "npm install",
"start": "./node_modules/.bin/hs -a localhost -p 9000 -o",
"pretest": "npm install",
"test": "./node_modules/.bin/browser-sync start --server app --files 'app/index.html' --no-notify"
},
"keywords": [
"iceStone",
"Micua",
"Angular",
"Boilerplate"
],
"author": "iceStone <ice@wedn.net> (http://wedn.net/)",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"browser-sync": "^2.11.1",
"http-server": "^0.8.5"
}
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN" ng-app="moviecat">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>豆瓣电影</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="app.css">
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">豆瓣电影</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Dashboard</a></li>
</ul>
<form class="navbar-form navbar-right" ng-controller="SearchController" ng-submit="search()">
<input type="text" class="form-control" placeholder="Search..." ng-model="input">
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<!-- <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> -->
<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>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main" ng-view></div>
</div>
</div>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<!-- 主模块 -->
<script src="app.js"></script>
<script src="components/http.js"></script>
<script src="components/auto-focus.js"></script>
<!-- 正在热映子模块 -->
<script src="movie_list/controller.js"></script>
<script src="movie_detail/controller.js"></script>
<!-- 其他子模块 -->
</body>
</html>
<!-- app.js -->
'use strict';
// Declare app level module which depends on views, and components
angular.module('moviecat', [
'ngRoute',
'moviecat.movie_detail',
'moviecat.movie_list',
'moviecat.directives.auto_focus',
])
// 为模块定义一些常量
.constant('AppConfig', {
pageSize: 10,
listApiAddress: 'http://api.douban.com/v2/movie/',
detailApiAddress: 'http://api.douban.com/v2/movie/subject/'
})
.config(['$routeProvider', function($routeProvider) {
$routeProvider.otherwise({ redirectTo: '/in_theaters/1' });
}])
.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('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);
// });
// }
// ])
<!-- app.css -->
/*
* Base structure
*/
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
}
/*
* Global add-ons
*/
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
/*
* Top navigation
* Hide default border to remove 1px line.
*/
.navbar-fixed-top {
border: 0;
}
/*
* Sidebar
*/
/* Hide for mobile, show later */
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto;
/* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
border-right: 1px solid #eee;
}
}
/* Sidebar navigation */
.nav-sidebar {
margin-right: -21px;
/* 20px padding + 1px border */
margin-bottom: 20px;
margin-left: -20px;
}
.nav-sidebar > li > a {
padding-right: 20px;
padding-left: 20px;
}
.nav-sidebar > .active > a,
.nav-sidebar > .active > a:hover,
.nav-sidebar > .active > a:focus {
color: #fff;
background-color: #428bca;
}
/*
* Main content
*/
.main {
padding: 20px;
}
@media (min-width: 768px) {
.main {
padding-right: 40px;
padding-left: 40px;
}
}
.main .page-header {
margin-top: 0;
}
/*
* Placeholder dashboard ideas
*/
.placeholders {
margin-bottom: 30px;
text-align: center;
}
.placeholders h4 {
margin-bottom: 0;
}
.placeholder {
margin-bottom: 20px;
}
.placeholder img {
display: inline-block;
border-radius: 50%;
}
.list-group .media {
margin-top: 0;
}
.spinner {
margin: 100px auto;
width: 40px;
height: 40px;
position: relative;
text-align: center;
-webkit-animation: sk-rotate 2.0s infinite linear;
animation: sk-rotate 2.0s infinite linear;
}
.dot1,
.dot2 {
width: 60%;
height: 60%;
display: inline-block;
position: absolute;
top: 0;
background-color: #333;
border-radius: 100%;
-webkit-animation: sk-bounce 2.0s infinite ease-in-out;
animation: sk-bounce 2.0s infinite ease-in-out;
}
.dot2 {
top: auto;
bottom: 0;
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
}
@-webkit-keyframes sk-rotate {
100% {
-webkit-transform: rotate(360deg)
}
}
@keyframes sk-rotate {
100% {
transform: rotate(360deg);
-webkit-transform: rotate(360deg)
}
}
@-webkit-keyframes sk-bounce {
0%,
100% {
-webkit-transform: scale(0.0)
}
50% {
-webkit-transform: scale(1.0)
}
}
@keyframes sk-bounce {
0%,
100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
}
50% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}
.mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, .4);
z-index: 2000;
}
.pager a{
cursor: pointer;
}
<!-- movie_list/view.html -->
<h1 class="page-header">{{title}}</h1>
<h2 ng-if="message">{{message}}</h2>
<div class="list-group">
<a ng-repeat="item in subjects" href="#/detail/{{item.id}}" class="list-group-item">
<span class="badge">{{item.rating.average}}</span>
<div class="media">
<div class="media-left">
<img class="media-object" ng-src="{{item.images.small}}" alt="item.title" height="128">
</div>
<div class="media-body">
<h3 class="media-heading">{{item.title}}</h3>
<p>
类型:<span>{{item.genres.join('、')}}</span> 上映年份:
<span>{{item.year}}</span></p>
<p>
捣眼:
<span ng-repeat="d in item.directors">
{{d.name + ($last?'':'、')}}
<!-- <span ng-if="!$last">、</span> -->
</span>
</p>
<p>
主演:
<span ng-repeat="c in item.casts">
{{c.name}}{{$last?'':'、'}}
<!-- <span ng-if="!$last">、</span> -->
</span>
</p>
</div>
</div>
</a>
</div>
<div ng-show="!loading">
<p>总共:{{totalCount}}条记录,第{{currentPage}}/{{totalPages}}页</p>
<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>
</nav>
</div>
<div class="mask" ng-show="loading">
<div class="spinner">
<div class="dot1"></div>
<div class="dot2"></div>
</div>
</div>
<!-- movie_list/controler.js -->
(function(angular) {
'use strict';
// 创建正在热映模块
var module = angular.module(
'moviecat.movie_list', [
'ngRoute',
'moviecat.services.http'
]);
// 配置模块的路由
module.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/:category/:page', {
templateUrl: 'movie_list/view.html',
controller: 'MovieListController'
});
}]);
module.controller('MovieListController', [
'$scope',
'$route',
'$routeParams',
'HttpService',
'AppConfig',
function($scope, $route, $routeParams, HttpService, AppConfig) {
var count = AppConfig.pageSize; // 每一页的条数
var page = parseInt($routeParams.page); // 当前第几页
var start = (page - 1) * count; // 当前页从哪开始
// 控制器 分为两步: 1. 设计暴露数据,2. 设计暴露的行为
$scope.loading = true; // 开始加载
$scope.subjects = [];
$scope.title = 'Loading...';
$scope.message = '';
$scope.totalCount = 0;
$scope.totalPages = 0;
$scope.currentPage = page;
HttpService.jsonp(
AppConfig.listApiAddress + $routeParams.category,
// $routeParams 的数据来源:1.路由匹配出来的,2.?参数
{ start: start, count: count, q: $routeParams.q },
function(data) {
$scope.title = data.title;
$scope.subjects = data.subjects;
$scope.totalCount = data.total;
$scope.totalPages = Math.ceil($scope.totalCount / count);
$scope.loading = false;
$scope.$apply();
// $apply的作用就是让指定的表达式重新同步
});
// 暴露一个上一页下一页的行为
$scope.go = function(page) {
// 传过来的是第几页我就跳第几页
// 一定要做一个合法范围校验
if (page >= 1 && page <= $scope.totalPages)
$route.updateParams({ page: page });
};
}
]);
})(angular);
// 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) {
// $scope.subjects = res.data.subjects;
// } else {
// $scope.message = '获取数据错误,错误信息:' + res.statusText;
// }
// }, function(err) {
// console.log(err);
// $scope.message = '获取数据错误,错误信息:' + err.statusText;
// });
<!-- movie_detail/view.html -->
<div class="jumbotron">
<h1>{{movie.title}}</h1>
<img ng-src="{{movie.images.large}}" alt="{{movie.title}}">
<p>{{movie.summary}}</p>
</div>
<div class="mask" ng-show="loading">
<div class="spinner">
<div class="dot1"></div>
<div class="dot2"></div>
</div>
</div>
<!-- movie_detail/controler.js -->
(function(angular) {
'use strict';
// 创建正在热映模块
var module = angular.module(
'moviecat.movie_detail', [
'ngRoute',
'moviecat.services.http'
]);
// 配置模块的路由
module.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/detail/:id', {
templateUrl: 'movie_detail/view.html',
controller: 'MovieDetailController'
});
}]);
module.controller('MovieDetailController', [
'$scope',
'$route',
'$routeParams',
'HttpService',
'AppConfig',
function($scope, $route, $routeParams, HttpService, AppConfig) {
$scope.movie = {};
$scope.loading = true;
var id = $routeParams.id;
var apiAddress =
AppConfig.detailApiAddress + id;
// 跨域的方式
HttpService.jsonp(apiAddress, {}, function(data) {
$scope.movie = data;
$scope.loading = false;
$scope.$apply();
});
}
]);
})(angular);
<!-- conponents/auto_focus.js -->
(function(angular) {
angular.module('moviecat.directives.auto_focus', [])
.directive('autoFocus', ['$location', function($location) {
// Runs during compile
// var path = $location.path(); // /coming_soon/1
// console.log(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');
}
})
// iElm.on('click', function() {
// iElm.parent().children().removeClass('active');
// iElm.addClass('active');
// });
}
};
}]);
})(angular);
<!-- conpontents/http.js -->
'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) {
// if (typeof data == 'function') {
// callback = data;
// }
var querystring = url.indexOf('?') == -1 ? '?' : '&';
for (var key in data) {
querystring += key + '=' + data[key] + '&';
}
var fnSuffix = Math.random().toString().replace('.', '');
var cbFuncName = 'my_json_cb_' + fnSuffix;
querystring += 'callback=' + cbFuncName;
var scriptElement = $document[0].createElement('script');
scriptElement.src = url + querystring;
// 不推荐
$window[cbFuncName] = function(data) {
callback(data);
$document[0].body.removeChild(scriptElement);
};
$document[0].body.appendChild(scriptElement);
};
}]);
})(angular);