AngularJS 项目流程

豆瓣电影列表

项目源码

豆瓣电影列表小项目源码

豆瓣电影列表项目说明

使用 AngularJS + Bootstrap + Node.js 构建的一个电影列表展示单页应用

启动项目

1
$ nodemon add.js

或者:

1
$ node add.js

豆瓣开发接口 API

项目骨架

  • app
    • app.js
    • app.css
    • index.html
    • in_theaters
      • view.html
      • module.js
    • coming_soon
      • view.html
      • module.js
    • top250
      • view.html
      • module.js
  • READMO.md
  • .gitignore
  • .editorconfig
  • bower.json
  • .bowerrc

页面开发流程:

(一)、构建页面(bootstrap)

选择模板 –> 列表组 –> 媒体组件

模板页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<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 href="node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet">
<link href="css/main.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<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="#">关于</a></li>
</ul>
<form class="navbar-form navbar-right" >
<input type="text" class="form-control" placeholder="Search...">
</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">
<li class="active"><a href="#">正在热映</a></li>
<li><a href="#">即将上映</a></li>
<li><a href="#">Top250</a></li>
</ul>
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!-- 每一页不同的部分 -->
</div>
</div>
</div>
</body>
</html>

(二)、模块划分

中间变化部分每一个对应一个视图: view.html 和 模块 module.js

in-theaters 正在热映视图、正在热映控制器、正在热映对应的模型代码

coming-soon 即将上映视图、即将上映控制器、即将上映对应的模型代码

top250 top250视图、top250控制器、top250模型代码

局部 HTML 发生变化。

路由:

  • Node 中的路由:后台接收请求,渲染了了不同的页面。
  • 前端路由,当点击一个连接的时候,显示不同的页面,不需要后台,前端也可以渲染,无非前端的路由变成了哈希值了。

主模块:–> view 里放变化的中间的内容:怎么放?怎么加载?点击即将上映,局部 HTML 发生变化。–> 前端路由

1
2
3
<div ng-view class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!-- 每一页不同的部分 -->
</div>

这回是前端路由,前端的路由变成了 hash 值。

view.html 中放中间变化的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- 每页不同部分 -->
<h1 class="page-header">header</h1>
<!-- 列表组 -->
<div class="list-group">
<a href="#/subject/{{movie.id}}" class="list-group-item" >
<!-- 媒体组件 -->
<div class="media">
<div class="media-left">
<img src="..." alt="...">
</div>
<div class="media-body">
<h4 class="media-heading">{{ movie.title }}</h4>
<p>类型:剧情、爱情、同性 </p>
<p>导演:<span>陈凯歌</span></p>
<p>主演:<span>张国荣</span></p>
</div>
</div>
</a>
</div>
<p>总共:1条记录,第1/8页</p>
<nav>
<ul class="pager">
<li><a href="#">上一页</a></li>
<li><a href="#">下一页</a></li>
</ul>
</nav>

★★★★★★★★★★★★★★★★★★

在 angular 中使用路由,ng 官方提供了一个 ngRoute 模块。

(1)、视图(v)-页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- ... -->
<body ng-app="DemoApp">
<header>
<h1>ng 中的路由模块 ngRoute 的使用</h1>
</header>
<article>
<div>
<!-- 这里是主页面中需要改变的内容 -->
</div>
</article>
<footer>
<p>这里是页面脚部</p>
</footer>
</body>

(2). 安装包

1
2
$ npm install --save angular
$ npm install --save angular-route

(3). 在视图页面中引包

1
2
<script src="node-modules/angular/angular.js"></script>
<script src="node-modules/angular-route/angular-route.js"></script>

(4). 创建模块

使用 Angular 的依赖模块 ngRoute

ngRoute 官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 加载专门处理路由的 ngRoute 模块
angular.module('DemoApp', ['ngRoute'])
.config(['$routeProvider', function ($routeProvider) {
$routeProvider
// 请求这个路径的时候,访问 a.html,把哪个控制器作用的这个视图上
.when('/in_theaters:id', {
// 渲染哪个页面
templateUrl: 'a.html',
// 把哪个控制器作用到了视图上
controller: 'DemoAController'
})
.when('/coming_soon', {
templateUrl: 'b.html',
controller: 'DemoBController'
})
.when('/top250', {
templateUrl: 'c.html',
controller: 'DemoCController'
})
// 当匹配不到以上路径的时候,让它跳转
.otherwise({
redirectTo: '/in_theaters'
});
}])
.controller('DemoAController', ['$scope', function ($scope) {
$scope.title = 'AAA';
}])
.controller('DemoBController', ['$scope', function ($scope) {
$scope.movie = {
src: 'https://img3.doubanio.com/view/movie_poster_cover/ipst/public/p2392444121.jpg',
name: '神奇动物在哪里'
};
}])
.controller('DemoCController', ['$scope', function ($scope) {
$scope.fruits = [
'苹果',
'香蕉',
'橘子',
'菠萝'
];
}]);

(5). 控制器作用到视图

a.html 页面需要一个 title

  • <h2>AngularJS 项目流程</h2>

b.html 页面需要一个src

  • <img ng-src="" alt="" />
  • <h4></h4>

c.html 页面需要列表

  • <li ng-repeat="fruit in fruits track by $index"></li>

(6). 作用到主页面视图-ng-view

1
2
3
4
5
6
7
8
9
10
11
12
13
<body ng-app="DemoApp">
<header>
<h1>ng 中的路由模块 ngRoute 的使用</h1>
</header>
<article>
<div ng-view>
<!-- 这里是主页面中需要改变的内容 -->
</div>
</article>
<footer>
<p>这里是页面脚部</p>
</footer>
</body>

★★★★★★★★★★★★★★★★★★

(三)、模块划分-控制器-路由

通过主模块加载 3 个小模块

主页面: index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<!-- 指定入口标识 -->
<body ng-app="movie.main">
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<!-- ... -->
</nav>
<div class="container-fluid">
<div class="row">
<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">Top250</a></li>
</ul>
</div>
<!-- 作用到当前视图 -->
<div ng-view class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!-- 每一页不同的部分 -->
</div>
</div>
</div>
<!-- 引包 -->
<script src="node_modules/angular/angular.js"></script>
<script src="node_modules/angular-route/angular-route.js"></script>
<!-- 页面中引入包 -->
<script src="in_theaters/module.js"></script>
<script src="coming_soon/module.js"></script>
<script src="top250/module.js"></script>
<script src="app.js"></script>
</body>
</html>

主模块: app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 主模块加载其它模块,就可以使用别的模块中的控制器了
angular.module('movie.main', ['movie.in_theaters', 'movie.comming_soon', 'movie.top250', 'movie.detail',])
// 设置路由
.config(['$routeProvider', function($routeProvider){
$routeProvider
.when('/in_theaters', {
// 渲染这个视图
templateUrl: 'in_theaters/view.html',
// 调这个控制器
controller: 'InTheaterController'
})
.when('/coming_soon',{
templateUrl: 'coming_soon/view.html',
controller: 'ComingSoonController'
})
.when('/top250',{
templateUrl: 'top250/view.html',
controller: 'Top250Controller'
})
// 是指跳转到当前路由了
.otherwise({
redirectTo: '/in_theaters'
})
}])

正在上映模块:/in_theaters/module.js

1
2
3
4
5
6
(function (angular) {
angular.module('movie.in_theaters', [])
.controller('InTheaterController', ['$scope',function ($scope) {
$scope.title = 'Loading...';
}]);
})(angular);

即将上映模块:/comming_soon/module.js

1
2
3
4
5
6
(function (angular) {
angular.module('movie.comming_soon',[])
.controller('ComingSoonController', ['$scope', function($scope){
$scope.title = '即将上映';
}]);
})(angular);

top250 模块 /top250/module.js

1
2
3
4
5
6
(function (angular) {
angular.module('movie.top250',[])
.controller('Top250Controller', ['$scope', function($scope){
$scope.title = 'Top250';
}]);
})(angular);

(四)、绑定假数据

主页面视图:static/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body ng-app="movie.main">
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<!-- ... -->
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="#">关于</a></li>
</ul>
<!-- 搜索功能 -->
<form ng-submit="search()" ng-controller="searchController" class="navbar-form navbar-right" >
<input type="text" ng-model="search_text" class="form-control" placeholder="Search...">
</form>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<!-- ng 指令是用来操作这个 ul 的,所以将指令加给它比较合适,指令这给主模块就可以了 -->
<ul class="nav nav-sidebar" movie-active>
<li class="active"><a href="#/in_theaters">正在热映</a></li>
<li><a href="#/coming_soon">即将上映</a></li>
<li><a href="#/top250">Top250</a></li>
</ul>
</div>
<div ng-view class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!-- 需要改变的内容 -->
</div>
</div>
</div>
<!-- 页面中引入包 -->
<!-- ... -->
<script src="movie_detail/module.js"></script>
<script src="app.js"></script>
</body>
</html>

其他模块视图:in_theaters/view.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<h1 class="page-header">{{ title }}</h1>
<div class="list-group">
<a ng-repeat="movie in movie_list.subjects track by $index" href="#/subject/{{movie.id}}" class="list-group-item" >
<div class="media">
<div class="media-left">
<img ng-src="{{ movie.images.small }}" alt="{{ movie.alt }}">
</div>
<div class="media-body">
<h4 class="media-heading">{{ movie.title }}</h4>
<p>类型:{{ movie.genres.join('、') }} </p>
<p>导演:<span ng-repeat="director in movie.directors track by $index">{{ director.name }} {{ $last?"":"、" }} </span></p>
<p>主演:<span ng-repeat="cast in movie.casts track by $index">{{ cast.name }} {{ $last?"":"、" }} </span></p>
</div>
</div>
</a>
</div>
<p>总共:{{ total }}条记录,第{{ page }}/{{ totalPage }}页</p>
<nav>
<ul class="pager">
<li ng-class="{disabled:page === 1}"><a ng-click="go(page-1)" href="">上一页</a></li>
<li ng-class="{disabled:page === totalPage}"><a ng-click="go(page+1)" href="">下一页</a></li>
</ul>
</nav>

请求 API,将数据绑定到视图预备知识

★★★★★★★★★★★★★★★★★★

ng 中的 $http 请求资源

问题:豆瓣 API 不支持:$htpp.jsonp('http://api.douban.com/v2/movie/in_theaters?count=5&callback=JSON_CALLBACK')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function (angular) {
angular.module('movie.in_theaters', [])
.controller('InTheaterController', ['$scope', '$http', function ($scope, $http) {
$scope.title = 'Loading...';
$http
// 跨域请求,直接用get 报错
// .get('http://api.douban.com/v2/movie/in_theaters?count=5')
// ng 中的 jsonp
.jsonp('http://api.douban.com/v2/movie/in_theaters?count=5&callback=JSON_CALLBACK')
// URL 变成了:http://api.douban.com/v2/movie/in_theaters?count=5&callback=angular.callbacks._0
// 数据回来了,但是报了常见语法错误,
// 返回的数据并没有拼接上: angular.callbacks._0,把里面的 . 改成下划线 _ 就可以拼接到数据前面了。
// 原因是豆瓣 API 不支持,豆瓣 API 中只包含数字、字母、下划线,长度不大于 50
// 解决:自己写一个 jsonp 方法
// 另一种方法:前后端分离:用 node 来请求数据,把项目跑到 node 里面,完全不用考虑跨域。
// node 做一个中间层,专门来做 UI 渲染
.then( function(data) {
console.log(data);
});
}
]);
})(angular);

前后端分离

前后端分离

a) 前后端分离

(1)、V + C 前端 视图 + 控制器
(2)、M 后台 操作数据库
(3)、C 可以实现加载任意 V,在 V 里通过 XMLHttpRequest 发送请求向,索取数据
(4)、C 靠JS、CSS、HTML 是不能实现的
(5)、为了实现 C 前端团队需要依赖于 Nodejs、PHP、Python 等后端语言
(6)、前后端分离可以实现前后端完全解藕,使得后端数据更加稳定统一
(7)、可能会引起跨域问题,解决办法:jsonp

b) 前后端不分离

1) V = 前端 C + M = 后端

暴漏了 static 静态资源,所有的资源都可以直接访问

1
2
3
4
5
6
7
8
9
10
11
var express = require('express'), path = require('path');
var app = express();
// 走前台不需要后台路由
// app.get('/', funciton(req, res) {
// res.send('后台路由');
// });
// 由于 static 目录中有一个 index.html,当去访问 '/' 的时候,express 会自动将 static 目录下的 index.html 渲染
app.use(experss.static(path.join(__dirname, static)));
app.listen(4000, function() {
console.log('Server is running at port 4000');
});

node 不仅仅能作为一个服务器接收请求,还能主动去请求别人的服务器,完全不受跨域影响。让 Node 做一个中转层。

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 node 发送请求
var http = require('http');
// 不使用原生的 http,有更好的包 request 来解决
http.get('http://api.douban.com/v2/movie/in_theaters?count=5', function(res) {
var rawData = '';
res.on('data', function(chunk) {
return rawData += chunk
});
res.on('end', function() {
console.log(JSON.parse(rawData));
});
});

request 包

1
$ npm install --save request

★★★★★★★★★★★★★★★★★★

(五)、请求 API,将数据绑定到视图

用 node 来请求数据,把项目跑到 node 里面,完全不用考虑跨域,node 做一个中间层,专门来做 UI 渲染。

安装 express

1
$ npm install express request --save

Node 中转请求接口 app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var express = require('express');
var config = require('./config');
var request = require('request');
// querystring.parse() 将查询字符串转成对象;querystring.stringify() 将对象转成查询字符串
var qstring = require('querystring');
var path = require('path');
var app = express();
// 暴漏静态资源
// 由于 static 目录中有一个 index.html,所以访问 / 的时候,express 会自动将 static 目录下的 index.html 渲染
app.use(express.static(config.staticPath));
app.use(express.static(path.join(__dirname, 'node_modules')));
// Node中转暴漏一个接口给前台
// app.get('/in_theaters', function(req, response, next) {
// // 从豆瓣 API 拿数据,使用 request 包,不使用原生的 http
// request('https://api.douban.com/v2/movie/in_theaters', function(err, res, body) {
// if (err) {
// throw err;
// }
// // 拿到数据,响应给前台请求
// if (res.statusCode === 200) {
// response.send(body);
// }
// });
// });
// 上面这种方式可以变成下面这种方式:
// Request 请求到的数据就是一个可读流,可以通过 pipe 管道顺着可读流发送数据
// app.get('/in_theaters', function (req, res, next) {
// // 拿到了查询字符串
// // console.log(req.query);
// request(`https://api.douban.com/v2/movie/in_theaters?${qstring.stringify(req.query)}`).pipe(res);
// });
// 获得所有页面的数据
app.get('/movie/:category', function (req, res, next) {
// 拿到了查询字符串,req.query 是一个对象
// console.log(req.query);
// qstring.stringify() 将一个对象转成查询字符串
request(`https://api.douban.com/v2/movie/${req.params.category}?${qstring.stringify(req.query)}`).pipe(res);
});
// 后台请求 电影条目 API 接口
app.get('/movie/subject/:id', function(req, res, next) {
request(`https://api.douban.com/v2/movie/subject/${req.params.id}`).pipe(res);
});
// 监听端口
app.listen(config.port, config.host, function() {
console.log(`Server is running at port ${config.port}`);
});

主模块 app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 主模块加载其它模块,就可以使用别的模块中的控制器了
angular.module('movie.main', ['movie.in_theaters', 'movie.comming_soon', 'movie.top250', 'movie.detail','ngRoute' ])
// 设置路由
.config(['$routeProvider', function($routeProvider){
$routeProvider
// 请求首页时
.when('/', {
redirectTo: '/in_theaters/1'
})
// 为了避免路径匹配问题,将这个放到前面
.when('/subject/:id', {
templateUrl: 'movie_detail/view.html',
controller: 'DetailController'
})
// 路径中传页码
// ng 中的路由支持这样的形式和后台一样动态处理路由
// page 是定义路径的时候起的一个名字,会自动把模糊匹配路径解析出来,然后挂载到 $routeParams 中
// $routeProvider 支持 when() 方法中传 ? ,表示 0 次或多次
.when('/:category/:page?', {
// 渲染这个视图
templateUrl: 'in_theaters/view.html',
// 调这个控制器
controller: 'InTheaterController'
})
// 是指跳转到当前路由了
.otherwise({
redirectTo: '/in_theaters'
})
}])
// 解决详情页搜索问题
.controller('searchController', ['$scope', '$route',function($scope, $route) {
$scope.search_text = [];
$scope.search = function(){
// console.log($scope.search_text);
$route.updateParams({
category: 'search',
page:'1',
// 路由中,如果没有该路径参数,则更新一个不存在的路由参数,路由自动帮你变成查询字符串
q: $scope.search_text
});
};
}])
// 利用自定义指令解决导航栏状态切换
.directive('movieActive', ['$location', function ($location) {
return {
link: function ($scope, iElm, iAttrs, controller) {
// 获取当前 url ,根据 url 找到对应的 li ,让 li 获得 active 样式,其它 li 去除 active
$scope.$location = $location;
$scope.$watch('$location.url()', function (newVal, oldVal) {
var currentUrl = newVal.split('/')[1];
// Array.from 可以将一个伪数组转换成一个真的数组
Array.from(iElm.find('a')).forEach(function (a) {
angular.element(a).parent().removeClass('active');
if (a.hash.substr(2) === currentUrl) {
angular.element(a).parent().addClass('active');
}
});
});
}
};
}]);

配置文件路径:

1
2
3
4
5
6
const path = require('path');
module.exports = {
port: 4000,
host: '127.0.0.1',
staticPath: path.join(__dirname, 'static')
};

其它模块 module.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
(function (angular) {
angular.module('movie.in_theaters', [])
.controller('InTheaterController', [
'$scope',
'$http',
// ng 中专门拿路由当中的请求参数的
'$routeParams',
// ng 中专门处理路由的
'$route',
function ($scope, $http, $routeParams, $route) {
// 给模型暴露数据
$scope.title = 'Loading...';
$scope.total = 0;
$scope.movie_list = {};
// 上一页、下一页
var pageSize = 10;
$scope.totalPage = 0;
$scope.page = $routeParams.page ? parseInt($routeParams.page) : 1;
// console.log($routeParams);
// $http.get('/in_theaters', {
// // 需要查询字符串,查询第几页,get() 方法不支持这样配置
// data: {
// start: ($scope.page -1) * pageSize,
// count: pageSize
// }
// })
// 以上 $http.get 不支持配置,使用 $http 来配置
$http({
method: 'get',
url:'/movie/' + $routeParams.category,
// get 请求数据使用 params;post 请求使用 data 属性
params: {
// 请求开始的数据
start: ($scope.page -1) * pageSize,
count: pageSize,
// 这里的 q 只是针对 search 有效,如果不是 search,豆瓣会忽略
q: $routeParams.q
}
})
.then(function(data) {
var result = data.data;
$scope.movie_list = result;
$scope.title = result.title;
$scope.total = result.total;
$scope.totalPage = Math.ceil(result.total/pageSize);
});
$scope.go = function(page) {
// console.log(page)
// 如果这个页面小于等于 0 了 或者 > 最大页面时不去处理
if (page <= 0 || page > $scope.totalPage) {
return;
}
// 路径中传页码
// 使用路由提供的 API 更新当前请求路径中的请求参数
// 只要更新了路由中的参数,当前页面中的路由会被重载
$route.updateParams({
page: page
});
}
}
]);
})(angular);

(六)、通过路由实现简单的分页传参功能

路径中传页码,使用路由提供的 API 更新当前请求路径中的请求参数。

(七)、通过配置路由参数实现多模块重用

ng 中和后台原理相似,都支持路由参数

(八)、利用自定义指令解决导航栏状态切换

在需要操作 DOM 时

(九)、解决详情页搜索问题

总结

整个小项目工作流程

  • 前台主模块加载其它模块并设置路由,
  • 前台其它模块 module.js 中的 $http 发起请求
  • 请求中转 app.js
  • 中转 app.js 向后台(豆瓣 API)发起请求
  • 再由中转 app.js 将数据响应给前台 module.js
  • 前台数据绑定,渲染视图
感谢您的支持!