通过JSONP方式搜索豆瓣图书

前言

众所周知,用 AJAX 不可避免的面临两个问题:

  1. AJAX 以何种格式来交换数据?
  2. 跨域的需求如何解决?

其实这两个问题都有不用的解决方案,比方数据格式可以自定义字符串或 XML 来描述,跨域通过服务器端代理来解决。

但目前为止的首选方案还是 用 JSON 来传数据,靠 JSONP 来跨域

两者虽然只差一个字母,但他们根本不是一回事: JSON 是一种数据交换格式,而 JSONP 是一种依靠开发人员的聪明才智创造出的一种非官方跨域数据交换协议。举例来说 JSON 是要运输的东西(快递?), JSONP 是通过走什么样的路(开车还是飞机?)送到。

本文不过多解释 JSON 和 AJAX,主要是一个用 JSONP 调用接口的例子,更多介绍可以看 这篇文章

JSONP

众所周知的额问题,Ajax 直接请求普通文件存在跨域无权访问的问题,别管什么内容,只要你是跨域请求,一律不准。

而我们又发现了,Web 页面上直接调用其他域名下面的 JS 文件是不受跨域影响的(不仅如此,我们还发现凡是拥有“src”这个属性的标签都拥有跨域的能力,比如<script><img><iframe>)。

于是我们想通过跨域访问数据只有一种可能,那就是在远程服务器上将数据装进 JS 格式的文件里,供客户端调用的进一步处理。恰巧 JSON 的纯字符数据格式可以简洁地描述复杂结构,还被 JS 原生支持,所以可以随心所欲的处理这种格式的数据。

Web 客户端通过与调用脚本一模一样的方式,去调用跨域服务器上动态生成的 JS 格式文件(一般以 JSON 为后缀),服务器之所以要动态生成 JSON 文件,就是要把客户端需要的数据装入进去。

客户端拿到数据之后,就可以按照自己的需求处理和展现了。为了方便客户端使用,逐渐形成了一种非正式传输协议,人们把它称为 JSONP,该协议的一个要点就是 允许用户传递一个 callback 参数给服务端 然后服务端返回数据时会将这个 callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来处理返回数据了。

关于 callback 的解释

如下代码会发生什么?

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="demo.js"></script>
</head>
<body>
</body>
</html>
1
2
// demo.js
alert('hello');

会弹出 hello 吧?

那如果 demo.js 里的内容是下面这样呢?

1
2
// demo.js
{ code: 200 }

没有反应对不对?

那如果代码是这样的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<script>
function callback(data) {
console.log(data.code);
}
</script>
<script src="demo.js"></script>
</head>
<body>
</body>
</html>
1
2
// demo.js
callback({ code: 200 });

是不是就能在控制台打印出 200 了?

其实,demo.js 就是服务器生成的动态文件,如果直接返回一个 JSON 虽然不至于报错,但咱们(Web 客户端)没法接收和处理,然后咱们在引入文件的时候带上一个 callback 参数,让服务器用这个参数的值包裹起 JSON 数据,Web 客户端中再声明一个这个名字的函数,不就可以接收并处理了么(执行了这个函数)?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
<script>
function fn(data) {
console.log(data.code);
}
</script>
<script src="demo.php?callback=fn&user=达达"></script>
</head>
<body>
</body>
</html>
1
2
3
4
// demo.php
$callback = $_GET('callback');
$user = $_GET('user');
echo $callback.'({code:200,user:'.$user.'})';

注意两个问题:

  • 目前为止只能通过 GET 方式请求,jQuery 写的 POST 也会转成 GET 方式
  • 在IE下,中文需要先对域名进行 encodeURI 操作,否则乱码

豆瓣实例

点此查看 DEMO,点此查看 豆瓣开发文档,点此查看 artTemplate 模板引擎。

HTML 代码请直接 F12 DEMO 文件。

GET https://api.douban.com/v2/book/search

参数 意义 备注
q 查询关键字 q和tag必传其一
tag 查询的tag q和tag必传其一
start 取结果的offset 默认为0
count 取结果的条数 默认为20,最大为100

封装函数获取数据

搜索“达达”,从第 1 条开始取 5 条数据成功,返回结果显示一共有 108 条搜索结果。

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
// 搜索关键词
var str = '达达';
// 当前页数
var currentPage = 1;
// 每页条数
var countPage = 5;
// 调用数据
jsonp();
// JSONP生成函数
function jsonp() {
// 当前页数-1 * 每页的显示条数
var start = (currentPage - 1) * countPage;
// 创建 script 标签
var script = $("<script><\/script>");
script.attr(
"src",
'https://api.douban.com/v2/book/search?callback=callback&q=' + str + "&count=" + countPage + '&start=' + start
);
// 插入网页当中
$("body").append(script);
}
// JSONP回调函数
function callback(data) {
console.log(data);
}

对数据进行处理

既然收到了数据,那么我们改造下 callback 函数,对数据进行一些处理。

1
2
3
4
5
6
7
8
9
10
11
12
// 总页码个数
var pageAll = 0;
// JSONP回调函数
function callback(data) {
// 数据赋值
pageAll = Math.ceil(data.total/countPage);
// 加载搜索结果,需要展示不同的内容,没用 template 模板引擎
$('.count').html('共搜索到 <b>' + data.total + '</b> 条数据,共 <b>' + pageAll + '</b> 页,当前第 <b>' + currentPage + '</b> 页。');
// 渲染图书列表,用 template 模板引擎
var listHtml = template("listTemp", data);
$("#list").html(listHtml);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 模板引擎渲染图书列表 -->
<script type="text/html" id="listTemp">
{{each books as value index}}
<div class="item g-cf">
<div class="img g-fl">
<a href="{{value.alt}}" target="_blank">
<img class="g-ac" src="{{value.images.large}}" />
</a>
</div>
<div class="con g-fl">
<h3 class="title"><a href="{{value.alt}}" target="_blank">{{value.title}}</a></h3>
<p class="author">{{value.author.join()}}</p>
<p class="content">{{value.summary.substr(0, 140)}}...</p>
</div>
</div>
{{/each}}
</script>

封装分页函数

我们的图书展示区域就做好了之后,封装一个分页函数,在展示完图书之后调用分页函数生成页码。pageNum 是显示几个页码。

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
// 在 callback 函数最后添加调用
function callback(data) {
// ...
// 生成页码
createPage();
}
// 展示页码个数
var pageNum = 5;
// 生成页码
function createPage() {
// 获取展示页码个数中间值
var pageNumHalf = Math.floor(pageNum/2);
var arr = [];
// 当前页码 < 展示页码个数/2
if (currentPage <= pageNumHalf) {
for (var i = 0; i < pageNum; i++) {
arr.push(i + 1);
}
}
// 当前页码 + 展示页码个数/2 > 总页码个数
else if (currentPage + pageNumHalf >= pageAll) {
for (var i = pageNum; i > 0; i--) {
arr.push(pageAll - i + 1);
}
}
// 中间范围的页码直接 减减 加加
else {
for (var i = 0; i < pageNum; i++) {
arr.push(currentPage - (pageNumHalf - i));
}
}
// 将当前页码和页码数组传递,方便高亮
var obj = { currentPage: currentPage, arr: arr };
// 渲染页码,用 template 模板引擎
var pageHtml = template("pageTemp", obj);
$(".pagination").html(pageHtml);
}
1
2
3
4
5
6
7
8
9
10
<!-- 模板引擎渲染页码 -->
<script type="text/html" id="pageTemp">
<span>首页</span>
<span>上一页</span>
{{each arr as value index}}
<a class="{{if value === currentPage}}current{{/if}}" href="javascript:;" data-id="{{value}}">{{value}}</a>
{{/each}}
<span>下一页</span>
<span>尾页</span>
</script>

增加点击事件

页面渲染完成之后,给搜索和页码添加点击事件。

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
// 搜索按钮点击
$(document).on('keydown', function(e) {
e.keyCode === 13 && $('#b').click();
});
$('#b').on('click', function() {
// 获取输入内容
var val = $('#s').val();
if (!val) {
$('.count').html('请输入搜索内容!');
return;
}
// 加载 loading 文字
$('.count').html('正在搜索中……');
// 进行搜索
str = val;
jsonp();
});
// 页码点击
$(".pagination").on("click", "a", function() {
// 改变页码
currentPage = $(this).data('id');
// 请求数据
jsonp();
});
$(".pagination").on("click", "span", function() {
var spanIndex = $(this).index('.pagination span');
// 首页 && 当前不是第一页
if (spanIndex === 0 && currentPage !== 1) {
currentPage = 1;
}
// 尾页 && 当前不是最后一页
else if (spanIndex === 3 && currentPage !== pageAll) {
currentPage = pageAll;
}
// 上一页 && 当前不是第一页
else if (spanIndex === 1 && currentPage !== 1) {
currentPage--;
}
// 下一页 && 当前不是最后一页
else if (spanIndex === 2 && currentPage !== pageAll) {
currentPage++;
}
// 都没判断成功就说明点击无效 -_-#
else {
return;
}
// 请求数据
jsonp();
});

写在最后

到此整个程序就实现了所有的功能,但文章里并没有叙述太多。主要是代码这个东西太占篇幅,关于 JSON 、 AJAX 和 分页的原理 也没有详细的解释。

不过都加了链接,大家可以访问链接去看看详细的介绍,或者搜索一下更多的文章。分页改天我会单拿出一篇文章来介绍介绍。

坚持原创技术分享,您的支持将鼓励我继续创作!