目录
1.课程目标及知识要点
## 课堂目标
- 掌握jsonp的原理
- 会搭建node服务器创建jsonp接口
- 学会封装jsonp
- 学会jsonp的实际运用
## 知识要点
- 跨域解决
- jsonp原理及封装
- jsonp服务器搭建
- jsonp实际运用
2.ajax问题
2.1问题重现
在非同源下,请求不到接口
示例:在非同源情况下,如使用ajax,在3000端口上,请求4000端口上服务器接口,就会请求不到。
html:
<body>
<button>发送请求</button>
<script>
document.querySelector("button").onclick = function(){
let xhr = new XMLHttpRequest();
xhr.open("get","http://localhost:4000/getInfo",true);
xhr.onload = function(){
console.log(xhr.responseText);
};
xhr.send();
};
</script>
</body>
jsonp3000.js
const Koa = require("koa");
const Router = require("koa-router");
const static = require("koa-static");
const koaBody = require("koa-body");
let app = new Koa();
let router = new Router();
app.use(koaBody());
app.use(static("static"));
router.get("/",(ctx,next)=>{
ctx.body = "请求成功";
});
router.get("/getInfo",(ctx,next)=>{
ctx.body = "getInfo请求成功";
});
app.use(router.routes());
app.listen("3000");
jsonp4000.js
const Koa = require("koa");
const Router = require("koa-router");
const static = require("koa-static");
const koaBody = require("koa-body");
let app = new Koa();
let router = new Router();
app.use(koaBody());
app.use(static("static"));
router.get("/",(ctx,next)=>{
ctx.body = "请求成功";
});
router.get("/getInfo",(ctx,next)=>{
ctx.body = "getInfo请求成功";
});
app.use(router.routes());
app.listen("4000");
在3000端口下访问http://localhost:3000/jsonp.html进行请求:报以下错误
此处即jsonp.html页面实在3000端口上运行,但是去访问了4000端口的getInfo请求,所以就导致了跨域问题。
2.2浏览器同源策略
- 同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源
- 源 :协议(http/https/ftp/tcp等)、域名和端口号
2.3跨域
跨域即非同源,非跨域即同源
区分:协议,域名,端口,都相同就是同源,其中有一个不同就是非同源。
2.4不受跨域问题(同源策略)影响的资源的引入
以下资源引入不收同源策略影响:
- <script src="..."></script>
- <img>
- <link>
- <iframe>
3.jsonp
JSONP*(JSON with Padding)解决跨域问题;可以让网页从别的域名(网站)那获取资料,即跨域读取数据。
3.1jsonp原理
jsonp就是通过script标签不受跨域问题影响去实现的。
返还时,返还可执行的js变量或数据;
3.2通过script来实现跨域;
<script src="http://localhost:4000/getInfo?name=张三"></script>
jsonp4000.js:
const Koa = require("koa");
const Router = require("koa-router");
const static = require("koa-static");
const koaBody = require("koa-body");
let app = new Koa();
let router = new Router();
app.use(koaBody());
app.use(static("static"));
router.get("/",(ctx,next)=>{
ctx.body = "请求成功";
});
router.get("/getInfo",(ctx,next)=>{
// 推送变量(使用script直接引入问题:直接推送变量会造成变量污染)
let a = 20;
ctx.body = "var a = 20";
// ctx.body = "getInfo请求成功";
});
app.use(router.routes());
app.listen("4000");
jsonp-script.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jsop使用script标签实现跨域</title>
</head>
<body>
<script src="http://localhost:4000/getInfo?name=张三"></script>
<script>
console.log(a);
</script>
</body>
</html>
问题:
- 使用script直接引入问题:直接推送变量会造成变量污染(html页面中可能js也定义了a变量);
- 使用script标签(querystring)提交数据时,参数如果是变量时不好带参数变量(?name=zs)
- 可能会导致异步问题
3.3动态创建script实现请求
通过执行回调的方式发送请求数据,以解决变量污染和异步问题。
步骤:
- 通过let spt = createElement("script")创建script标签;
- 通过spt.src = "http://localhost:4000/getInfo"发送请求;
- document.querySelector(head").appendChild(spt)加到DOM中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>动态创建script实现请求</title>
</head>
<body>
<button>发送请求</button>
<script>
let btn = document.querySelector("button");
btn.onclick = function(){
//点击button时,动态创建script标签
let spt = document.createElement("script");
spt.src = "http://localhost:4000/getInfo";
// 将创建的script标签append到head标签中
document.querySelector("head").appendChild(spt);
}
console.log(a);
</script>
</body>
</html>
jsonp4000.js同上;
router.get("/getInfo",(ctx,next)=>{
// 推送变量(使用script直接引入问题:直接推送变量会造成变量污染)
let a = 20;
ctx.body = "var a = 20";
// ctx.body = "getInfo请求成功";
});
结果:
问题:以上处理方式会涉及到异步问题(会返回数据a,但客户端获取不到变量a的值)
解决方式一:客户端通过onload加载完成后才能获得变量(但是通过这种方式并不好)。
<body>
<button>发送请求</button>
<script>
let btn = document.querySelector("button");
btn.onclick = function(){
//点击button时,动态创建script标签
let spt = document.createElement("script");
spt.src = "http://localhost:4000/getInfo";
// 将创建的script标签append到head标签中
document.querySelector("head").appendChild(spt);
spt.onload = function(){
console.log(a);
}
}
</script>
</body>
解决方式二:通过回调函数解决。客户端定义执行函数,并通过queryString设置回调函数cb;把需要的数据通过参数带到服务端,服务端通过ctx.query.cb进行接收,服务端不会执行这个函数,而是通过ctx.body=`${cb}(a)`重新发回到客户端。
注意:服务端发回到客户端的数据必须是字符串。如果数据是对象,需要通过JSON.stringfy(obj)转为字符串后才能传递。
<body>
<button>发送请求</button>
<script>
// 客户端定义回调函数
function cbfn(param){
console.log(param);
}
let btn = document.querySelector("button");
btn.onclick = function(){
//点击button时,动态创建script标签
let spt = document.createElement("script");
spt.src = "http://localhost:4000/getInfo?cb=cbfn";
// 将创建的script标签append到head标签中
document.querySelector("head").appendChild(spt);
}
</script>
</body>
router.get("/getInfo",(ctx,next)=>{
// 推送变量(使用script直接引入问题:直接推送变量会造成变量污染)
let a = 20;
// 服务端通过ctx.query.cb接收到回调函数
let cb = ctx.query.cb;
// 再通过ctx.body返回给客户端执行(注意a的取值方式)
ctx.body = `${cb}(${a})`;
});
传递对象:
// 注意返回数据必须是字符串,如果返回数据是对象,需要通过JSON.stringfy()转为字符串后才能进行传递
let obj = {
a:20,
b:30
}
let cb = ctx.query.cb;
ctx.body = `${cb}(${JSON.stringify(obj)})`;
4.jsonp封装
在ajax封装基础上,再封装jsonp(封装动态创建script标签,回调函数等);jsonp只支持get请求方式;
- 回调函数cb=cbfn单独封装,如jsonp:"callback";
- 通过dataType区分是普通ajax还是jsonp;
- ajax中jsonp默认值为cb;
- 处理jsonp请求(实际上和ajax没有关联)。如果有dataType且值为jsonp时才进行特殊处理(组装url:http://localhost:4000/getInfo?cb=cbFn)jsonpFn(opts.url,opts.data,opts.jsonp,opts.success);然后直接retern false;
- 封装jsonpFn(url,data,cbName,cbFn)。data需要从对象转为queryString方式;cbName即为cb或callback;
- cbFn回调函数不能直接获取(直接获取得到的是整个方法体),而是需要通过函数名和函数进行绑定 let fnName = "LMF_"+Math.random().toString().sustr(2),然后通过window[fnName]=cbFn挂载到window上;
- 动态创建script标签,设置src属性和appendchild()
封装josnp.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>封装jsonp</title>
</head>
<body>
<button>发送请求</button>
<script type="module">
import ajax from './js/ajax_jsonp.js';
let btn = document.querySelector("button");
btn.onclick = function(){
// 发送jsonp请求
ajax({
url:"http://localhost:4000/getInfo",
// jsonp只能使用get请求方式
data:{
name:"zs",
age:20
},
// 通过dataType区分是普通ajax请求还是jsonp请求
dataType:"jsonp",
// 回调函数名
jsonp:"callback",
success(res) {
console.log(res);
}
})
}
</script>
</body>
</html>
jsonp4000.js:
const Koa = require("koa");
const Router = require("koa-router");
const static = require("koa-static");
const koaBody = require("koa-body");
let app = new Koa();
let router = new Router();
app.use(koaBody());
app.use(static("static"));
router.get("/",(ctx,next)=>{
ctx.body = "请求成功";
});
router.get("/getInfo",(ctx,next)=>{
// 注意返回数据必须是字符串,如果返回数据是对象,需要通过JSON.stringfy()转为字符串后才能进行传递
let obj = {
a:20,
b:30
}
let cb = ctx.query.callback;
ctx.body = `${cb}(${JSON.stringify(obj)})`;
});
app.use(router.routes());
app.listen("4000");
ajax_jsonp.js:注意ajax的封装,和ajax与jsonp结合的封装
class MyAjax {
constructor(options) {
// 将传入的配置和默认配置进行合并
this.options = options;
this.ajax(options);
}
ajax(options) {
let xhr = new XMLHttpRequest();
// 注意assign()方法是将后面的输入配置合并前面的默认配合,且需要使用参数接收,否则合并不了
let opts = Object.assign({
// 因为get/post请求不同带参方式不同,所以需要拼接url
url: '',
method: 'get',
data: '',
headers:{
"content-type":"application/x-www-form-urlencoded",
},
// 回调函数默认值cb
jsonp:"cb",
success(res) {}
}, options);
// 处理jsonp请求
if(opts.dataType === "jsonp"){
jsonpFn(opts.url,opts.data,opts.jsonp,opts.success);
return false;
}
// 判断get请求时拼接url
if (opts.method === "get") {
opts.url = olUrl(opts);
}
// 发送请求
xhr.open(opts.method, opts.url, true);
// 循环设置头部(可能会有不同的头部信息,不只是content-type),注意设置头部必须在open()方法之后
for(let header in opts.headers){
xhr.setRequestHeader(header, opts.headers[header]);
}
// 根据不同头部信息,发送请求不同
let sendData;
switch(opts.headers['content-type']){
// 如果请求头content-type为application/x-www-form-urlencoded直接拼接参数
case 'application/x-www-form-urlencoded':
sendData = olUrl(opts);
break;
case 'application/json':
sendData = JSON.stringify(opts.data);
break;
}
// 获取返回数据(需要设置到options中的success,在调用时才能使用success()获得返回数据)
xhr.onload = function(){
// 用户名密码都正确时会返回页面
if(xhr.responseText.startsWith("<!DOCTYPE html>")){
opts.success({
msg:"校验成功",
code:4
});
}else{
opts.success(JSON.parse(xhr.responseText));//返回数据全部转为对象
}
}
// 判断如果是post请求需要将请求数据一并发送
if (opts.method === "get") {
xhr.send();
} else {
xhr.send(sendData);
}
// 拼接url(只有get请求需要拼接)
function olUrl(options) {
let keys = Object.keys(options.data);
let values = Object.values(options.data);
let queryString = keys.map((key, index) => {
return key + '=' + values[index];
}).join("&");
return options.url + "?" + queryString;
}
// jsonp处理函数
function jsonpFn(url,data,cbName,cbFn){
// 回调函数cbFn不能直接拼接(options.success获得的是整个函数体)
let fnName = "LMF_"+Math.random().toString().substr(2);//LMF_用于区分
// 通过window[fnName]=cbFn将cbFn函数挂载到window上
window[fnName]=cbFn;
// 拼接url
let path = olUrl(options) + "&" + cbName + "=" + fnName;
// 创建动态script标签
let spt = document.createElement("script");
spt.src = path;
document.querySelector("head").appendChild(spt);
}
}
}
// 通过定义函数new MyAjax(options),返回MyAjax对象
function ajax(options) {
return new MyAjax(options);
}
export default ajax;
结果:
5.请求百度接口
https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=hello&cb=succFn
需求: 将请求到的结果显示到div中
ajax_jsonp.js同上
baidu_jsonp.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>请求百度接口</title>
</head>
<body>
<input type="text">
<div class="exchange"></div>
<script type="module">
import ajax from './js/ajax_jsonp.js'
document.querySelector("input").onblur = function(){
ajax({
url:"https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su",
data:{
wd:this.value
},
jsonp:"cb",
dataType:"jsonp",
success(res){
console.log(res.s);
let exchange = document.querySelector(".exchange");
let inner = "<ul>"
res.s.forEach(s => {
inner += `<li>${s}</li>`;
});
exchange.innerHTML = inner + "</ul>";
}
});
}
</script>
</body>
</html>
结果:
6.蘑菇街案例实现
6.1使用jsonp实现动态数据的获取及渲染
https://list.mogu.com/search?callback=jQuery2110599693622515429_1558943916971&_version=8193&ratio=3%3A4&cKey=15&page=1&sort=pop&ad=0&fcid=52014&action=food
6.2实现滚动底部数据的重新获取及更新(下拉加载)
- 可视区域高度:document.documentElement.clientHeight
- 内容高度:document.documentElement.offsetHeight
- 滚动高度:内容高度 - 可视区域高度
- 当前滚动高度:document.documentElement.scrollTop
- 当当前滚动高度 >=(滚动高度-10) 就再次进行获取。再次获取动态数据时需要page需要更改
案例完整实现:
ajax_jsonp.js同上
index.css:
a{
text-decoration:none
}
.wrap{
width: 960px;
margin: 0 auto;
}
.headerContainer{
height: 100px;
/* background: gray; */
display: flex;
justify-content: center;
}
.headerContainer img{
width: 130px;
float: left;
}
.catalog{
height: 100px;
width: 50px;
display: block;
float: left;
line-height: 100px;
text-align: center;
margin-left: 30px;
}
.searchInput{
float: left;
width: 462px;
height: 48px;
line-height: 48px;
border: none;
font-size: 14px;
background: #f7f7f7;
color: #666;
padding: 0 15px;
}
.searchContainer{
height: 100px;
width: 552px;
display: flex;
float: left;
align-items: center;
margin-left: 10px;
}
.btn{
float: right;
width: 58px;
height: 48px;
border: none;
cursor: pointer;
border-radius: 0 5px 5px 0;
background-image: url(https://s10.mogucdn.com/mlcdn/c45406/181205_2llkjh1g0fe27h51ahh5k5f27gkk9_116x96.png);
background-repeat: no-repeat;
background-size: 100%;
}
.sub_title{
line-height: 32px;
font-size: 24px;
font-weight: 400;
color: #333;
margin-top: 30px;
margin-bottom: 8px;
}
.nav_box{
border: 1px solid #e5e5e5;
height: 24px;
padding: 7px 0;
line-height: 24px;
overflow: hidden;
margin-top: 10px;
}
.txt{
float: left;
width: 46px;
height: 22px;
line-height: 22px;
display: block;
border: 1px solid #dadada;
font-size: 12px;
}
.txt span{
float: left;
color: #999;
}
.divid{
float: left;
margin: 0 5px;
font-size: 12px;
}
.confirm_btn{
background: 0 0;
border: 1px solid #dadada;
color: #333;
width: 45px;
height: 22px;
line-height: 22px;
display: inline-block;
text-align: center;
margin-left: 10px;
font-size: 12px;
}
.nav_box{
padding-left: 15px;
margin-bottom: 30px;
}
/* 每个商品的样式 */
.item{
width: 220px;
height: 374px;
border: 1px solid #f2f2f2;
position: relative;
margin: 0 13px 25px 0;
display: inline-block
}
.item img{
width: 220px;
height: 293px;
}
.bottom-describe{
padding: 0 8px;
color: #666;
font-size: 14px;
}
.priceContainer{
height: 25px;
line-height: 25px;
color: #333;
width: 100%;
position: absolute;
bottom: 0px;
}
.priceContainer img{
height: 15px;
width: 16px;
}
.describe{
position: absolute;
width: 200px;
height: 40px;
overflow: hidden;
bottom: 20px;
}
.mystar{
float: right;
margin-right: 20px;
color:#999;
font-size: 12px;
}
.mystar img{
vertical-align: middle;
}
.oldPrice{
text-decoration: line-through;
}
.iconShow{
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 20px;
align-items: center;
}
.iconMessage{
width: 24px;
height: 24px;
display: block;
background-image: url(https://s10.mogucdn.com/mlcdn/c45406/181016_626h76eek6jgl5621492ck0438af3_48x44.png);
background-repeat: no-repeat;
background-size: 100%;
}
.iconCollect{
width: 24px;
height: 24px;
display: block;
background-image: url(https://s10.mogucdn.com/mlcdn/c45406/181016_1e18fbc53dab5f8f6c45h6e57954d_44x44.png);
background-repeat: no-repeat;
background-size: 100%;
}
mogu.js:
const Koa = require("koa");
const App = require("koa-router");
const static = require("koa-static");
const body = require("koa-body");
let app = new Koa();
let router = new App();
app.use(static(__dirname+"/static"));
router.get("/",(ctx,next)=>{
ctx.body = "页面成功";
});
app.use(router.routes());
app.listen("8989");
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>蘑菇街</title>
<link rel="stylesheet" href="css/index.css" />
</head>
<body>
<div class="wrap">
<div class="headerContainer">
<img src="https://s10.mogucdn.com/mlcdn/c45406/190102_088f4i166l4gkl08k297h5kk8690i_260x200.png">
<div class="catalog">
目录
</div>
<div class="searchContainer">
<input type="text" class="searchInput" />
<button class="btn"></button>
</div>
<div class="iconShow">
<span class="iconMessage"></span>
消息
</div>
<div class="iconShow">
<span class="iconCollect"></span>
收藏
</div>
</div>
<h3 class="sub_title">
全部食品
</h3>
<div class="nav_box">
<div class="txt">
<span>¥</span>
</div>
<span class="divid">-</span>
<div class="txt">
<span>¥</span>
</div>
<a class="confirm_btn" href="javascript:;">确定</a>
</div>
<div class="itemContainer">
<!-- 商品呈现 -->
<!-- <div class="item">
<img
src="https://s11.mogucdn.com/mlcdn/c45406/180830_0ggfhcfd757g3jg8k8fjaclc3h123_640x960.jpg_440x587.v1cAC.40.webp" />
<div class="bottom-describe">
<p class="describe">
海底捞火锅1盒懒人自煮自热方便速食牛油麻辣即食小火锅麻辣火锅
</p>
<div class="priceContainer">
<b>¥26.8</b>
<span class="oldPrice">¥53.6</span>
<span class="mystar">
<img src="https://s18.mogucdn.com/p2/160908/upload_27g4f1ch6akie83hacb676j622b9l_32x30.png"
alt="" />
2585
</span>
</div>
</div>
</div> -->
</div>
</div>
</body>
<script type="module">
import ajax from "./js/ajax_jsonp.js";
// https://list.mogu.com/search?callback=jQuery2110599693622515429_1558943916971&_version=8193&ratio=3%3A4&cKey=15&page=1&sort=pop&ad=0&fcid=52014&action=food
// page默认为1,根据加载成功次数+1
let page = 1;
getData();
function getData(){
ajax({
url:"https://list.mogu.com/search",
data:{
_version:8193,
ratio:"3%3A4",
cKey:15,
page:page,
sort:"pop",
ad:0,
fcid:52014,
action:"food"
},
dataType:"jsonp",
jsonp:"callback",
success:function(res){
console.log(res);
// 当获取成功时,渲染页面
if(res.status.code === 1001){
let foods = res.result.wall.docs;
page+=1;
foods.forEach(item => {
renderFood(item);
});
}
}
});
}
// 坚挺鼠标滚动事件,滚动到一定位置,重新加载数据
document.onscroll = function(){
// 获取可视区域高度
let clientHeight = document.documentElement.clientHeight;
// 获取内容高度
let contentHeight = document.documentElement.offsetHeight;
// 滚动高度
let scrollHeight = contentHeight - clientHeight;
// 获取当前滚动高度
let scrollTop = document.documentElement.scrollTop;
// 如果当前滚动高度所在位置 > 滚动高度
if(scrollTop >= (scrollHeight-10)){
getData();
}
}
// 渲染页面
function renderFood(item){
let itemDiv = document.createElement("div");
itemDiv.classList.add("item");
itemDiv.innerHTML = `
<img
src="${item.img}" />
<div class="bottom-describe">
<p class="describe">
${item.title}
</p>
<div class="priceContainer">
<b>¥${item.price}</b>
<span class="oldPrice">¥${item.orgPrice}</span>
<span class="mystar">
<img src="https://s18.mogucdn.com/p2/160908/upload_27g4f1ch6akie83hacb676j622b9l_32x30.png"
alt="" />
${item.cfav}
</span>
</div>
</div>
`;
let itemContainer = document.querySelector(".itemContainer");
itemContainer.appendChild(itemDiv);
}
</script>
</html>
如果是真实项目需要做预加载处理。
7.jsonp问题
- 只能使用get请求
- 安全性问题(get请求只能使用queryString进行传参,所以会有安全性问题)
8.总结
- jsonp原理
- jsonp封装
- 会搭建node服务器创建jsonp接口
- jsonp实际运用