第二部分 Ajax 编程扩展
学习内容及目标: 模板引擎,案例,FormData,同源政策
1,模板引擎
在传统网站中,客户端向服务器端发送请求,服务器将html + 数据拼接好 发送给客户端,现在使用Ajax技术向服务器发送请求,服务器大多数都是用json 数据格式作为响应内容,也就是说,原本数据+html 的拼接在服务器端完成,现在在客户端完成,在客户端做数据拼接 我们也需要使用模板引擎;现在看的是客户端模板引擎, art-template也有客户端版本
1.1 模板引擎 概述
作用:使用模板引擎提供的模板语法,可以将数据和 HTML 拼接起来。
官方地址: https://aui.github.io/art-template/zh-cn/index.html
下载到我们的项目文件夹里 template-web.js
1.2 使用步骤
1). 下载 art-template 模板引擎库文件并在 HTML 页面中引入
<script src="./js/template-web.js"></script>
2). 准备 art-template 模板:利用模板语法将HTML+数据拼接起来的地方;注意,客户端JS没有读取文件的能力,所以在客户端不是一个单独的文件,而是HTML文件中一个代码片段,并用 script包裹,需要有一个id作为模板的唯一标识,在 script标签内部,写HTML语法,但是由于 script内部编辑器会作为 JS语法解析,也灭有高亮提示?【如何解决:】 script标签上添加type属性 type="text/html"
<script id="tpl" type="text/html">
<div class="box"></div>
</script>
3). 告诉模板引擎将哪一个模板和哪个数据进行拼接,如何告诉呢? Template()方法返回拼接好的字符串
var html = template('tpl=模板id', {username: 'zhangsan', age: '20'}=对象,模板中展示的数据,可以在HTML中可以通过对象。属性 获取属性对应的值);
4). 将拼接好的html字符串添加到页面中,通过DOM 方法获取页面中的存储容器,并将拼接好的HTML字符串放在容器中,这里还挺简单的哈
document.getElementById('container').innerHTML = html;
5). 通过模板语法告诉模板引擎,数据和html字符串要如何拼接,重要的一步!!和node中一样
<script id="tpl" type="text/html">
<div class="box"> {{ username }} </div>
</script>
| 看一下实例代码哈~:D????
|
2.案例
2.1 验证邮箱地址唯一性
- 获取文本框并为 其添加离开焦点事件
- 离开焦点时,检测用户输入的邮箱地址是否符合规则
- 如果不符合规则,阻止程序向下执行并给出提示信息
- 向服务器端发送请求,检测邮箱地址是否被别人注册
- 根据服务器端返回值决定客户端显示何种提示信息
- /^[A-Za-z\d]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,4}$/
| ajax.js 发送请求 封装函数代码 O(∩_∩)O~~ function ajax (options) { // 默认值 var defaults = { type: 'get', url: '', async: true, data: {}, header: { 'Content-Type': 'application/x-www-form-urlencoded' }, success: function () {}, error: function () {} } // 使用用户传递的参数替换默认值参数 Object.assign(defaults, options); // 创建ajax对象 var xhr = new XMLHttpRequest(); // 参数拼接变量 var params = ''; // 循环参数 for (var attr in defaults.data) { // 参数拼接 params += attr + '=' + defaults.data[attr] + '&'; // 去掉参数中最后一个& params = params.substr(0, params.length-1) } // 如果请求方式为get if (defaults.type == 'get') { // 将参数拼接在url地址的后面 defaults.url += '?' + params; }
// 配置ajax请求 xhr.open(defaults.type, defaults.url, defaults.async); // 如果请求方式为post if (defaults.type == 'post') { // 设置请求头 xhr.setRequestHeader('Content-Type', defaults.header['Content-Type']); // 如果想服务器端传递的参数类型为json if (defaults.header['Content-Type'] == 'application/json') { // 将json对象转换为json字符串 xhr.send(JSON.stringify(defaults.data)) }else { // 发送请求 xhr.send(params); } } else { xhr.send(); } // 请求加载完成 xhr.onload = function () { // 获取服务器端返回数据的类型 var contentType = xhr.getResponseHeader('content-type'); // 获取服务器端返回的响应数据 var responseText = xhr.responseText; // 如果服务器端返回的数据是json数据类型 if (contentType.includes('application/json')) { // 将json字符串转换为json对象 responseText = JSON.parse(responseText); } // 如果请求成功 if (xhr.status == 200) { // 调用成功回调函数, 并且将服务器端返回的结果传递给成功回调函数 defaults.success(responseText, xhr); } else { // 调用失败回调函数并且将xhr对象传递给回调函数 defaults.error(responseText, xhr); } } // 当网络中断时 xhr.onerror = function () { // 调用失败回调函数并且将xhr对象传递给回调函数 defaults.error(xhr); } }
|
| |
| 02.验证邮箱地址唯一性.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>验证邮箱地址是否已经注册</title> <link rel="stylesheet" href="/assets/bootstrap/dist/css/bootstrap.min.css"> <style type="text/css"> p:not(:empty) { padding: 15px; } .container { padding-top: 100px; } </style> </head> <body> <div class="container"> <div class="form-group"> <label>邮箱地址</label> <input type="email" class="form-control" placeholder="请输入邮箱地址" id="email"> </div> <!-- 错误 bg-danger 正确 bg-success --> <p id="info"></p> </div> <script src="/js/ajax.js"></script> <script> // 1, 获取页面中的元素 var emailInp = document.getElementById('email'); var info = document.getElementById('info');
// 2, 当文本框离开焦点以后 onblur emailInp.onblur = function () { // 2.1 获取用户输入的邮箱地址 this 代表当前 发生事件的元素 = 文本框 var email = this.value; // 2.2 验证邮箱地址的正则表达式 var reg = /^[A-Za-z\d]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,4}$/; // 2.3 如果用户输入的邮箱地址不符合规则 if (!reg.test(email)) { // 给出用户提示 info.innerHTML = '请输入符合规则的邮箱地址'; // 让提示信息显示为错误提示信息的样式 info.className = 'bg-danger'; // 阻止程序向下执行 return; }
// 向服务器端发送请求,引入Ajax发送请求函数,直接调用ajax函数 ajax({ type: 'get', url: 'http://localhost:3000/verifyEmailAdress', data: { email: email // 参数Email给定的参数名,用户输入的邮箱地址验证邮箱 }, success: function (result) { console.log(result); info.innerHTML = result.message; info.className = 'bg-success'; }, error: function (result) { console.log(result) info.innerHTML = result.message; info.className = 'bg-danger'; } }); } </script> </body> </html>
|
| App.js // 邮箱地址验证 app.get('/verifyEmailAdress', (req, res) => { // 接收客户端传递过来的邮箱地址 const email = req.query.email; // 判断邮箱地址注册过的情况 if (email == '[email protected]') { // 设置http状态码并对客户端做出响应 res.status(400).send({message: '邮箱地址已经注册过了, 请更换其他邮箱地址'}); } else { // 邮箱地址可用的情况 // 对客户端做出响应 res.send({message: '恭喜, 邮箱地址可用'}); } });
|
2.2 搜索框内容自动提示
- 获取搜索框并为其添加用户输入事件
- 获取用户输入的关键字
- 向服务器端发送请求并携带关键字作为请求参数
- 将响应数据显示在搜索框底部
| |
| 【问题1】 请求的发送和携带的参数,第一次携带参数是c , 第二次是ch, 第三次chu, 这就是不合理的,发送了很多无意义的请求?如何解决? 用户连续输入,输入字符比较快,时间间隔比较短;利用时间间隔解决? 在输入事件oninput 触发是,包裹一个延时定时器,让他不要连续触发; 连续输入,oninput 会被持续触发,先将上一次开启的延时定时器清除,上一次输入请求不会发送,在开启一个新的定时器,用于当前输入发送,循环往复,知道输入完成,才会发送 大大减少了无意义请求的次数; 【问题2】 清除搜索框,下边的提示文字,也要隐藏; 在发生oninput 的时候,判断用户输入的值,如果没有输入内容,要隐藏下拉提示框listBox.style.display = ‘none’; 并且阻止程序向下执行 return;
|
| 03.搜索框内容自动提示.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>搜索框输入文字自动提示</title> <link rel="stylesheet" href="/assets/bootstrap/dist/css/bootstrap.min.css"> <style type="text/css"> .container { padding-top: 150px; } .list-group { display: none; } </style> </head> <body> <div class="container"> <div class="form-group"> <input type="text" class="form-control" placeholder="请输入搜索关键字" id="search"> <ul class="list-group" id="list-box">
</ul> </div> </div> <script src="/js/ajax.js"></script> <script src="/js/template-web.js"></script> <script type="text/html" id="tpl"> {{each result}} <li class="list-group-item">{{$value}}</li> {{/each}} </script> <script> // 1,获取搜索框 用getElementById,返回一个searchInp var searchInp = document.getElementById('search'); // 2,获取提示文字的存放容器,返回值存在listBox var listBox = document.getElementById('list-box'); // 存储定时器的变量 var timer = null;
// 3,当用户在搜索框中输入的时候触发 oninput用户输入事件 searchInp.oninput = function () { // 清除上一次开启的定时器 clearTimeout(timer);
// 3.1 获取用户输入的内容,接下来要向服务器端发送数据了,但是传递的参数都不知道,需要查看接口文档 var key = this.value; // 3.2 如果用户没有在搜索框中输入内容 if (key.trim().length == 0) { // 将提示下拉框隐藏掉 listBox.style.display = 'none'; // 阻止程序向下执行 return; }
// 开启定时器 让请求延迟发送 timer = setTimeout(function () { //4, 向服务器端发送请求:向服务器端索取和用户输入关键字相关的内容 ajax({ type: 'get', //可以不传,默认就是get url: 'http://localhost:3000/searchAutoPrompt', //请求地址 data: { // 当前请求需要我们传入一个参数 key: key }, // success函数,形参是服务器给我们返回的数据,返回值是一个数组,和用户输入的关键字相关的内容 // 数组和li进行拼接,最终把拼接的结果放在 ul 标签里边 success: function (result) { // 使用模板引擎拼接字符串:参数:那个模板+那个数据:就是形参result var html = template('tpl', {result: result}); // 将拼接好的字符串显示在页面中 ul容器listBox listBox.innerHTML = html; // ul 默认是隐藏的,显示ul容器 listBox.style.display = 'block'; } }) }, 800) } </script> </body> </html |
| spp.js |
2.3 省市区三级联动
需求:当用户选择省份后,城市下拉框要显示对应省份的城市信息,当用户选择城市下拉框中某一城市,要显示城市对应县城或者区下拉框信息,
- 通过接口获取 省份信息
- 使用JavaScript获取到省市区下拉框元素
- 将服务器端返回的省份信息显示在下拉框中:模板引擎
- 为下拉框元素添加 表单值改变 事件(onchange):会在用户将下拉框的值改变时触发
- 当用户选择省份时,根据省份id获取城市信息
- 当用户选择城市时,根据城市id获取县城信息
| |
| 【问题】 当第一次选择完成后,当再次选择省份的时候,清空县城下的下拉框数据;使用模板方式;
|
| 04.省市区联动.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>搜索框输入文字自动提示</title> <link rel="stylesheet" href="/assets/bootstrap/dist/css/bootstrap.min.css"> <style type="text/css"> .container { padding-top: 150px; } </style> </head> <body> <div class="container"> <div class="form-inline"> <div class="form-group"> <select class="form-control" id="province"></select> </div> <div class="form-group"> <select class="form-control" id="city"> <option>请选择城市</option> </select> </div> <div class="form-group"> <select class="form-control" id="area"> <option>请选择县城</option> </select> </div> </div> </div> <script src="/js/ajax.js"></script> <script src="/js/template-web.js"></script> <!-- 省份模板 --> <script type="text/html" id="provinceTpl"> <option>请选择省份</option> {{each province}} <option value="{{$value.id}}">{{$value.name}}</option> {{/each}} </script> <!-- 城市模板 --> <script type="text/html" id="cityTpl"> <option>请选择城市</option> {{each city}} <option value="{{$value.id}}">{{$value.name}}</option> {{/each}} </script> <!-- 县城模板 --> <script type="text/html" id="areaTpl"> <option>请选择县城</option> {{each area}} <option value="{{$value.id}}">{{$value.name}}</option> {{/each}} </script> <script> // 2,获取省市区下拉框元素 var province = document.getElementById('province'); var city = document.getElementById('city'); var area = document.getElementById('area'); // 1,获取省份信息 ajax({ type: 'get', url: 'http://localhost:3000/province', success: function (data) { // 将服务器端返回的数据 data 和html进行拼接 var html = template('provinceTpl', {province: data}); // 将拼接好的html字符串显示在页面中 province.innerHTML = html; } });
// 为省份的下拉框添加值改变事件 province.onchange = function () { // 获取省份id var pid = this.value;
// 清空县城下拉框中的数据 var html = template('areaTpl', {area: []}); area.innerHTML = html;
// 根据省份id获取城市信息 ajax({ type: 'get', url: '/cities', data: { id: pid }, success: function (data) { var html = template('cityTpl', {city: data}); city.innerHTML = html; } }) };
// 当用户选择城市的时候 city.onchange = function () { // 获取城市id var cid = this.value; // 根据城市id获取县城信息 ajax({ type: 'get', url: 'http://localhost:3000/areas', data: { id: cid }, success: function(data) { var html = template('areaTpl', {area: data}); area.innerHTML = html; } }) } </script> </body> </html> |
| // 获取省份 app.get('/province', (req, res) => { res.json([{ id: '001', name: '黑龙江省' },{ id: '002', name: '四川省' },{ id: '003', name: '河北省' },{ id: '004', name: '江苏省' }]); });
// 根据省份id获取城市 app.get('/cities', (req, res) => { // 获取省份id const id = req.query.id; // 城市信息 const cities = { '001': [{ id: '300', name: '哈尔滨市' }, { id: '301', name: '齐齐哈尔市' }, { id: '302', name: '牡丹江市' }, { id: '303', name: '佳木斯市' }], '002': [{ id: '400', name: '成都市' }, { id: '401', name: '绵阳市' }, { id: '402', name: '德阳市' }, { id: '403', name: '攀枝花市' }], '003': [{ id: '500', name: '石家庄市' }, { id: '501', name: '唐山市' }, { id: '502', name: '秦皇岛市' }, { id: '503', name: '邯郸市' }], '004': [{ id: '600', name: '常州市' }, { id: '601', name: '徐州市' }, { id: '602', name: '南京市' }, { id: '603', name: '淮安市' }] } // 响应 res.send(cities[id]); });
// 根据城市id获取县城 app.get('/areas', (req, res) => { // 获取城市id const id = req.query.id; // 县城信息 const areas = { '300': [{ id: '20', name: '道里区', }, { id: '21', name: '南岗区' }, { id: '22', name: '平房区', }, { id: '23', name: '松北区' }], '301': [{ id: '30', name: '龙沙区' }, { id: '31', name: '铁锋区' }, { id: '32', name: '富拉尔基区' }] }; // 响应 res.send(areas[id] || []); }); |
3.FormData
3.1 FormData 对象的作用
可以帮助我们解决当前Ajax代码中的一些问题,比如,当如我们要发送请求,参数比较多的时候,参数值的获取,和参数格式的拼接,就像注册用户,需要填写用户名,密码,确认密码,邮箱等,需要挨个获取表单控件以及它的值,还要按照制定的格式进行字符串拼接,代码繁琐
再比如,普通的Ajax是不能传 二进制文件的,比如图片
使用formData 就可以解决这些问题
1)模拟HTML表单,相当于将HTML表单映射成表单对象,自动将表单对象中的数据拼接成请求参数的格式。将表单对象 作为请求参数 传到服务器端,就省去了空间值,表单控件的获取,参数值的拼接
2)异步上传二进制文件;图片,视频等
3.2 FormData 对象的使用
第一种使用场景:转换为表单对象,作为参数传递到服务器
1). 准备 HTML 表单:不需要设置请求地址和请求方式和表单提交按钮,因为不是传统的表单提交,要发送Ajax请求,这些都在Ajax请求中设置,但是在表单控件中一定要有name属性,因为在表单提交时,需要将表单内容作为请求参数给服务器,name作为请求参数的属性名称
<form id="form">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="button"/>
</form>
2). 将 HTML 表单转化为 formData 表单对象: formData本身是一个构造函数,可以创建 formData对象,可以接受一个表单DOM 对象作为参数,构造函数会自动将表单控件中的数据拼接成请求参数所需要的格式,也就是说,需要通过DOM获取表单,获取结果传到formData构造函数中
var form = document.getElementById('form');
var formData = new FormData(form);
3). 提交表单对象:创建的表单对象放在Ajax中的 xhr.send()方法中, 就省去了空间值,表单控件的获取,参数值的拼接
xhr.send(formData);
注意:
- Formdata 对象不能用于 get 请求,因为对象需要被传递到 send 方法中,而 get 请求方式的请求参数 只能放在请求地址的后面。
- 服务器端 bodyParser 模块不能解析 formData 对象表单数据,我们需要使用 formidable 模块进行解析。
| 05.FormData对象的使用方法.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <!-- 1,创建普通的html表单 : 不需要有action和method属性,因为我们做的不是传统的表单提交,而是Ajax表单提交,当表单提交,name属性作为请求参数的属性名--> <form id="form"> <input type="text" name="username"> <input type="password" name="password"> <!-- 不是submit ,因为他有默认的提交行为 --> <input type="button" id="btn" value="提交"> </form> <script type="text/javascript"> // 2,获取按钮,添加事件 var btn = document.getElementById('btn'); // 3,获取表单 var form = document.getElementById('form'); // 4,为按钮添加点击事件 btn.onclick = function () { // 4.1 将普通的html表单转换为表单对象,参数:DOM对象,将哪一个表单转换成formDate对象 var formData = new FormData(form);
// 创建ajax对象:把表单对象提交到服务器 var xhr = new XMLHttpRequest(); // 对ajax对象进行配置:调用xhr.open(); 请求方式一定是post,因为formData 是别放在 send()方法中,get请求参数是url 后边 xhr.open('post', 'http://localhost:3000/formData'); // 发送ajax请求 xhr.send(formData);
// 监听xhr对象下面的onload事件 xhr.onload = function () { // 对象http状态码进行判断 if (xhr.status == 200) { console.log(xhr.responseText); } } } </script> </body> </html> |
| 服务器端代码 app.js const formidable = require('formidable'); app.post('/formData', (req, res) => { // 创建formidable表单解析对象 // 之前使用body-pareser接受客户端传来的post请求参数,但是不能处理formData对象 const form = new formidable.IncomingForm(); // 解析客户端传递过来的FormData对象: fields 里边保存了这个表单中普通请求参数,files保存和文件上传相关信息 form.parse(req, (err, fields, files) => { res.send(fields); }); }); |
| |
3.3 FormData 对象的实例方法
对表单对象中 数据的操作,一般来说,表单数据一般不会被直接提交到服务器端的,用户填写数据以点击提交按钮后,对用户填的数据进行校验,要获取用户输入的值,对其检验或者修改等
1). 获取表单对象中属性的值 formData.get('key')
2). 设置表单对象中属性的值 formData.set('key', 'value'); set()可以用在表单数据的二次处理上,用户发布文章,没有设置时间;用户在文本框输入钱的数,服务器要求保留两位小数;获取用户输入值,为这个值保留两位小数,在设置回表单对象中
3). 删除表单对象中属性的值 formData.delete('key'); 用户注册,必须输入两次密码,必须一致,向服务器只提交一次
4). 向表单对象中追加属性值 formData.append('key', 'value'); 创建 formData可以不传DOM对象做参数,代表创建一个空的 formData对象
注意:set 方法与 append 方法的区别是,在属性名已存在的情况下,set 会覆盖已有键名的值,append会保留两个值。
| // 06.FormData对象下的实例方法.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <!-- 创建普通的html表单 --> <form id="form"> <input type="text" name="username"> <input type="password" name="password"> <input type="button" id="btn" value="提交"> </form> <script type="text/javascript"> // 获取按钮 var btn = document.getElementById('btn'); // 获取表单 var form = document.getElementById('form'); // 为按钮添加点击事件 btn.onclick = function () { // 将普通的html表单转换为表单对象 var formData = new FormData(form); /* get('key') 获取表单对象属性的值 set('key', 'value') 设置表单对象属性的值 delete('key') 删除表单对象属性中的值 */ console.log(formData.get('username'));
// 如果设置的表单属性存在 将会覆盖属性原有的值 formData.set('username', 'itcast'); formData.append('username', 'itheima'); // 如果设置的表单属性不存在 将会创建这个表单属性 formData.set('age', 100); // 删除用户输入的密码 formData.delete('password');
// 创建ajax对象 var xhr = new XMLHttpRequest(); // 对ajax对象进行配置 xhr.open('post', 'http://localhost:3000/formData'); // 发送ajax请求 xhr.send(formData); // 监听xhr对象下面的onload事件 xhr.onload = function () { // 对象http状态码进行判断 if (xhr.status == 200) { console.log(xhr.responseText); } }
// 创建空的表单对象 var f = new FormData(); f.append('sex', '男'); console.log(f.get('sex')); } </script> </body> </html> |
3.4 FormData 二进制文件上传
图片,视频,音频文件
| <input type="file" id="file"/> // 准备文件选择控件,用户选择文件后,我们要提交到服务器
// 获取文件 var file = document.getElementById('file') // 当用户选择文件的时候触发 file.onchange = function () { // 创建空表单对象:存用户选择的文件 var formData = new FormData(); // 将用户选择的二进制文件追加到表单对象中 // files属性 是文件的集合,哪怕你只选了一个文件,他也是一个集合;默认input选择文件控件 只能选择一个文件 formData.append('attrName=数据的属性名称,后端开发人员定义', this.files[0]=具体要追加的数据,用户选择的文件,存在文件选择控件中的 files属性中); // 配置ajax对象,请求方式必须为post xhr.open('post', 'www.example.com'); xhr.send(formData); } |
3.5 FormData 文件上传进度展示
| // 当用户选择文件的时候 file.onchange = function () { // 文件上传过程中持续触发onprogress事件; upload对象下有一个 onprogress属性 xhr.upload.onprogress = function (ev) { // 当前上传文件大小/文件总大小 再将结果转换为百分数 // 将结果赋值给进度条的宽度属性 bar.style.width = (ev.loaded / ev.total) * 100 + '%'; } } |
3.6 FormData 文件上传图片即时预览
在我们将图片上传到服务器端以后,服务器端通常都会将 图片地址做为响应数据传递到客户端,!!客户端可以从响应数据中获取图片地址,然后将图片再显示在页面中。
实际上node中做过图片及时预览功能,但是当时使用的是H5中的文件对象fileread实现的,但是有个问题,在不支持H5浏览器中,这个功能不好使
| xhr.onload = function () { var result = JSON.parse(xhr.responseText); var img = document.createElement('img'); img.src = result.src; img.onload = function () { document.body.appendChild(this); } } |
| App.js // 实现文件上传的路由 app.post('/upload', (req, res) => { // 创建formidable表单解析对象 const form = new formidable.IncomingForm(); // 设置客户端上传文件的存储路径 form.uploadDir = path.join(__dirname, 'public', 'uploads'); // 保留上传文件的后缀名字 form.keepExtensions = true; // 解析客户端传递过来的FormData对象 form.parse(req, (err, fields, files) => { // 将客户端传递过来的文件地址响应到客户端 res.send({ path: files.attrName.path.split('public')[1] }); }); });
|
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <link rel="stylesheet" href="/assets/bootstrap/dist/css/bootstrap.min.css"> <style type="text/css"> .container { padding-top: 60px; } .padding { padding: 5px 0 20px 0; } </style> </head> <body> <div class="container"> <div class="form-group"> <label>请选择文件</label> <input type="file" id="file"> <div class="padding" id="box"> <!--<img src="" class="img-rounded img-responsive">--> </div> <div class="progress"> <div class="progress-bar" style="width: 0%;" id="bar">0%</div> </div> </div> </div> <script type="text/javascript"> // 1,获取文件选择控件 var file = document.getElementById('file'); // 获取进度条元素 var bar = document.getElementById('bar'); // 获取图片容器 var box = document.getElementById('box');
// 2,为文件选择控件添加onchanges事件 在用户选择文件时触发 file.onchange = function () { // 2.1 创建空的formData表单对象:存用户选择的文件 var formData = new FormData(); // 2.2 将用户选择的文件追加到formData表单对象中 formData.append('attrName', this.files[0]); // 2.3 创建ajax对象,发送Ajax请求 var xhr = new XMLHttpRequest(); // 2.4 对ajax对象进行配置xhr.open()只有post方式财可以 进行文件上传 // upload路由 一会再app.js 中要创建它 xhr.open('post', 'http://localhost:3000/upload'); // 在文件上传的过程中持续触发 xhr.upload.onprogress = function (ev) { // ev.loaded 文件已经上传了多少 // ev.total 上传文件的总大小 var result = (ev.loaded / ev.total) * 100 + '%'; // 设置进度条的宽度 bar.style.width = result; // 将百分比显示在进度条中 bar.innerHTML = result; } // 2.5 发送ajax请求,传递到服务器端 xhr.send(formData); // 2.6 监听服务器端响应给客户端的数据 xhr.onload = function () { // (1)如果服务器端返回的http状态码为200,说明请求是成功的 if (xhr.status == 200) { // (2)将服务器端返回的数据显示在控制台中,客户端代码到这里截至完成了, // 服务器还要接受这个文件,如何接受客户端传来的二进制文件呢? var result = JSON.parse(xhr.responseText); // (3)动态创建img标签:直接获取设置src属性,用户体验不好,用户可以看到图片上传过程 var img = document.createElement('img'); // (4)给图片标签设置src属性 img.src = result.path; // (5)当图片加载完成以后,将图片显示在页面中 img.onload = function () { box.appendChild(img); } } }
} </script> </body> </html> |
4.同源政策
4.1 Ajax请求限制
Ajax 只能向自己的服务器发送请求。
比如现在有一个A网站、有一个B网站,A网站中的 HTML 文件只能向A网站服务器中发送 Ajax 请求,B网站中的 HTML 文件只能向 B 网站中发送 Ajax 请求,
但是 A 网站是不能向 B 网站发送 Ajax请求的,同理,B 网站也不能向 A 网站发送 Ajax请求。
比如获取天气信息,只能请求国家气象局
如何解决呢?
为什么只能向自己的服务器发送请求呢?- 是因为有同源政策的限制
4.2 什么是同源
相同的来源,来自同一个地方;
如果两个页面拥有相同的协议、域名和端口,那么这两个页面就属于同一个源,其中只要有一个不相同,就是不同源。没有端口,默认80
http://www.example.com/dir/page.html
http://www.example.com/dir2/other.html :同源
http://example.com/dir/other.html:不同源(域名不同)
http://v2.www.example.com/dir/other.html:不同源(域名不同)
http://www.example.com:81/dir/other.html:不同源(端口不同)
https://www.example.com/dir/page.html:不同源(协议不同)
4.3 同源政策的目的
同源政策是 为了保证用户信息的安全,防止恶意的网站窃取数据。
最初的同源政策是指 A 网站在客户端设置的 Cookie,B网站是不能访问的。比如A是银行,用户登录以后,A网站在用户的机器上设置了一个 cookie , 包含了一些隐私信息,比如存款总额,当用户离开A网站,访问B网站,如果没有同源限制,B网站就可以访问A网站的cookie,用户的隐私就被泄露了
随着互联网的发展,同源政策也越来越严格,在不同源的情况下,其中有一项规定 就是Ajax技术 无法向 非同源地址 发送Ajax 请求,如果请求,浏览器就会报错,并且给出提示。但实际上,请求可以发出去,只不过是浏览器拒绝接受服务器端的返回结果,所以请求还是失败的。
如何验证?用两个不同源的服务器
| <script type="text/javascript" src="/js/ajax.js"></script> <script type="text/javascript"> ajax({ url: 'http://localhost:3001/test', type: 'get', success: function (result) { console.log(result); } }) </script> |
4.4 使用 JSONP 解决同源限制问题
jsonp 是 json with padding 的缩写,将json 数据填充到函数中,就是利用 script 标签的 src 属性的特性;它不属于 Ajax 请求,但它可以模拟 Ajax 请求。这种解决方案 需要前后端人员配合完成
1). 将不同源的服务器端 请求地址写在 script 标签的 src 属性中, src 属性可以发送请求,也不受同源政策的影响,可以写非同源地址
<script src="www.example.com"></script>
<script src=“https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
2). 服务器端响应数据必须是一个函数的调用,真正要发送给客户端的数据需要作为函数调用的参数。当浏览器通过script标签将响应内容加载完成以后,会自动将响应内容作为script代码来执行;实际上,响应内容加载完成之后,会立即调用这个函数,因为加载过来的就是函数调用的代码,在服务器端,这个响应内容必须是字符串,包裹函数调用代码;如果不是字符串,就是真的函数调用,就会在服务器端执行了,我们目的是让他在客户端执行
在服务器端代码中,返回函数调用代码的同时,还要将客户端真正需要的数据写在函数调用实参的地方,函数在客户端被执行的时候,可以通过形参对应
函数在客户端被调用,客户端要先定义,才能被调用
const data = 'fn({name: "张三", age: "20"})';
res.send(data);
3). 在客户端全局作用域下定义函数 fn:写在script上边,函数调用才能找到这个函数的定义部分;服务器在返回函数调用的时候,已经将真正的数据作为函数的实参了,可以写一个形参可以和形参对应了
function fn (data) { }
4). 在 fn 函数内部对服务器端返回的数据进行处理
function fn (data) { console.log(data); }
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script> //2,函数定义 function fn (data) { console.log('客户端的fn函数被调用了') console.log(data); // zhangsan } </script> <!-- 1. 第一步 将非同源服务器端的请求地址写在script标签的src属性中 在这个请求地址内部,返回一个函数调用的代码=script加载完请求地址里的内容以后, 内容是一个函数调用代码,加载之后,函数会被立即调用, 所以在客户端要提前准备函数的定义部分,定义在全局作用域下和script的上边,才能找到 --> <script src="http://localhost:3001/test"></script> </body> </html>
|
| // 2号服务器 app.get('/test', (req, res) => { const result = 'fn({name: "张三"})'; res.send(result); });
|
4.5 JSONP 代码优化
- 客户端需要将函数名称 传递到服务器端。
- 将 script 请求的发送变成动态请求:想发送请求的时候,动态创建script标准,在追加到页面中/ 比如点击按钮发送请求
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <button id="btn">点我发送请求</button> <script> function fn2 (data) { console.log('客户端的fn函数被调用了') console.log(data); } </script> <script type="text/javascript"> // 1,获取按钮 var btn = document.getElementById('btn'); // 2,为按钮添加点击事件 btn.onclick = function () { // 3,创建script标签 var script = document.createElement('script'); // 4,设置src属性 script.src = 'http://localhost:3001/better?callback=fn2'; // 5,将script标签追加到页面中 先获取document.body属性再appendChild document.body.appendChild(script); // 6,为script标签添加onload事件,将body中的script标签删除掉 script.onload = function () { document.body.removeChild(script); } } </script> </body> </html> |
- )封装 jsonp 函数,方便请求发送。
- )服务器端代码优化之 res.jsonp 方法。
| 【问题1】jsonp函数用于发送请求,但是再函数外部其他地方,还要定义fn2函数用于接受服务器返回的数据;=发送一个请求,用两个函数,两个函数还是独立的,就打破了jsonp函数的封装性;不能一眼就看出那个请求和那个函数是关联的,如果可以像Ajax的处理请求结果的函数变成success函数一样,函数的封装性就比较好了 【问题2】在真实的项目中,要发送很多请求,每个请求都要对应一个自己函数,处理服务器返回结果,为很多请求函数起名字也很麻烦。比如每个按钮对应的函数名字不一样,如果一样,后边就会覆盖前边;使用随机命名 |
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <button id="btn1">点我发送请求</button> <button id="btn2">点我发送请求</button> <script type="text/javascript"> // 获取按钮 var btn1 = document.getElementById('btn1'); var btn2 = document.getElementById('btn2'); // 为按钮添加点击事件 btn1.onclick = function () { jsonp({ // 请求地址 url: 'http://localhost:3001/better', data: { name: 'lisi', age: 30 }, success: function (data) { console.log(123) console.log(data) } }) }
btn2.onclick = function () { jsonp({ // 请求地址 url: 'http://localhost:3001/better', // 问题1:已经不是全局函数了?服务器返回函数调用,会找不到它,要把它变成全局函数 / 挂载到window对象? // 问题2:变成匿名函数了 success: function (data) { console.log(456789) console.log(data) } }) }
// 向非同源 服务器 发送请求 function jsonp (options) { // 1,动态创建script标签 var script = document.createElement('script'); // 拼接字符串的变量 var params = '';
for (var attr in options.data) { params += '&' + attr + '=' + options.data[attr]; }
// myJsonp0124741 var fnName = 'myJsonp' + Math.random().toString().replace('.', '');
// 它已经不是一个全局函数了,我们要想办法将它变成全局函数 .后边是不能跟变量的 window[fnName] = options.success; // 2,为script标签添加src属性:请求地址 只有在发送请求,调用jsonp函数,才能知道 script.src = options.url + '?callback=' + fnName + params; // 将script标签追加到页面中 document.body.appendChild(script); // 为script标签添加onload事件 script.onload = function () { document.body.removeChild(script); } } </script> </body> </html> |
| // 服务器端代码 app.js app.get('/better', (req, res) => { // 接收客户端传递过来的函数的名称 //const fnName = req.query.callback; // 将函数名称对应的函数调用代码返回给客户端 //const data = JSON.stringify({name: "张三"}); //const result = fnName + '('+ data +')'; // setTimeout(() => { // res.send(result); // }, 1000)
// jsonp 方法直接可以干上边的事情,j接受客户端发来的数据,对象转为字符串 res.jsonp({name: 'lisi', age: 20}); }) |
【案例】-获取腾讯天气
从腾讯网获取天气信息, 获取未来48小时,并展示到自己的网站中
腾讯天气: https://tianqi.qq.com/
网站内容:当前时间天气,未来48小时,未来7天的
观察网站腾讯网站是如何获取天气信息的?
F12,开发者工具 – network – 重新刷新浏览器 – 找到所有天气的请求 – 其实就是和common相关的 – 点击一个 右键 open in new tab –
返回的就是一个函数的调用,(函数的调用),对象作为参数,对象就是具体的天气信息,就是jsonp形式的服务器端返回值
观察的域名:tianqi.qq.com/ wis.qq.com 是 非同源网站,就是通过jsonp 请求过来的
| |
|
|
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用jsonp获取腾讯天气信息</title> <link rel="stylesheet" href="/assets/bootstrap/dist/css/bootstrap.min.css"> <style type="text/css"> .container { padding-top: 60px; } </style> </head> <body> <div class="container"> <table class="table table-striped table-hover" align="center" id="box"></table> </div> <!-- 封装好的 jsonp 函数 --> <script src="/js/jsonp.js"></script> <script src="/js/template-web.js"></script> <script type="text/html" id="tpl"> <!--2, 模板的内容就是tr --> <tr> <th>时间</th> <th>温度</th> <th>天气</th> <th>风向</th> <th>风力</th> </tr> <!-- 2.1 创建 tr&td- 把内容显示 td-里 : 找到模板地方,告诉模板引擎,数据和 HTML字符串 要怎么拼接 在info 对象下,存的又是一堆对象;循环的方式 --> {{each info}} <tr> <!-- 2.2 value.update_time 获取时间,但是时间20190304120000是一个字符串需要处理成更加容易阅读方式 【解决】定义一个处理时间日期的方法,方法要在模板中去调用;在模板中暴漏一个外部变量,可以是一个函数--> <td>{{dateFormat($value.update_time)}}</td> <td>{{$value.degree}}</td> <td>{{$value.weather}}</td> <td>{{$value.wind_direction}}</td> <td>{{$value.wind_power}}</td> </tr> {{/each}} </script> <script> // 2.3 获取table标签 var box = document.getElementById('box');
// 3.1 定义一个函数,会在模板中调用 ;return 返回什么 页面中就会显示什么 function dateFormat(date) { // console.log(data); 获取到了时间 20190909098060 截取时间需要 var year = date.substr(0, 4); var month = date.substr(4, 2); var day = date.substr(6, 2); var hour = date.substr(8, 2); var minute = date.substr(10, 2); var seconds = date.substr(12, 2); return year + '年' + month + '月' + day + '日' + hour + '时' + minute + '分' + seconds + '秒'; }
// 3,向模板中开放外部变量,它的值必须是一个函数template.defaults.imports固定的 template.defaults.imports.dateFormat = dateFormat;
// 1,向服务器端获取天气信息 jsonp({ // 1.1 请求地址 url: 'https://wis.qq.com/weather/common', // 1.2 需要传递的参数:在接口文档中有 data: { source: 'pc', weather_type: 'forecast_1h', // weather_type: 'forecast_1h|forecast_24h', province: '黑龙江省', city: '哈尔滨市' }, // 1.3 请求成功后,会调用这个函数;并把数据传进来,并用模板引擎展示在页面中 success: function (data) { // console.log(data); 请求到的天气信息 // id='tpl' 第二个参数为对象,info:具体的天气信息-data.data.forecast_1h var html = template('tpl', {info: data.data.forecast_1h}); // 2.4 console.log(html); 很多tr td 追加 table标签中,给它起一个名字 box.innerHTML = html;
} }) </script> </body> </html>
|
4.6 CORS 跨域资源共享
CORS:全称为 Cross-origin resource sharing,即跨域资源共享,非同源跨域共享,jsonp-是绕过了同源限制,发送的也不是Ajax请求;它允许浏览器向 跨域服务器发送 Ajax 请求,克服了 Ajax 只能同源使用的限制。
这种解决办法:主要是在服务器端做一些配置
跨域过程:当客户端向服务器发请求,如果浏览器检测这个请求时跨域的,会自动加 请求头 origin字段,字段的值就是当前的域名信息,当前网站的页面地址;
比如A 网站 向 B 网站发送请求,origin中存的就是A的域名信息,包含协议,域名,端口号,服务器根据字段的值决定是否同意这次请求,不管同意不同意都会给一个响应,如果服务器同意,就会在相应头加入一个字段 Access-Control-Allow-Origin,如果不同意,就不加这个字段;浏览器自动完成,不需要开发人员完成;这个字段存的是 客户端的信息,如果你的信息被存在它服务器中,你就可以访问;这个字段的值是客户端的源信息,或者*号代表所有的客户端允许访问这个服务器。
origin: http://localhost:3000
Access-Control-Allow-Origin: 'http://localhost:3000'
Access-Control-Allow-Origin: '*'
Node 服务器端设置响应头示例代码:客户端-Ajax代码;客户端需要做的浏览器会自动做好;服务器-允许那些客户端访问?设置客户端通过那些请求方式访问服务器端?这两项信息设置在相应头中
| app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST'); next(); }) |
| S1号服务器:点击按钮,向2号服务器发送请求 |
| S2号服务器中app.js app.get('/cross', (req, res) => { // 1.允许哪些客户端访问我 // res.header('Access-Control-Allow-Origin', 'http://localhost:3000') // // 2.允许客户端使用哪些请求方法访问我 // res.header('Access-Control-Allow-Methods', 'get,post')
res.send('ok') }); // 优化代码;真实的项目中需要在每个路由中设置这两项内容妈?可以使用 // 中间件 拦截所有请求,对所有的请求设置这俩;所有路由最上边写上桑app.use (()=>{}) app.use((req, res, next) => { // 1.允许哪些客户端访问我 // * 代表允许所有的客户端访问我 // 注意:如果跨域请求中涉及到cookie信息传递,值不可以为*号 比如是具体的域名信息 res.header('Access-Control-Allow-Origin', 'http://localhost:3000') // 2.允许客户端使用哪些请求方法访问我 res.header('Access-Control-Allow-Methods', 'get,post') // 允许客户端发送跨域请求时携带cookie信息 res.header('Access-Control-Allow-Credentials', true); next(); // 请求往下继续写匹配 });
|
4.7 访问非同源数据 服务器端另一种解决方案
这种解决方案,也是绕过浏览器的同源政策;同源政策是浏览器给予Ajax技术的限制,服务器端是不存在同源政策限制。服务器开发语言,可以直接访问非同源网站的数据;
所以客户端想访问非同源网站数据时候,可以让自己服务器端去获取非同源网站的数据,将数据响应到自己客户端;这样就绕过了同源政策
| 用 1号网站的客户端 访问 1号服务器;1号网站服务器 访问 2号服务器,获取2号服务器的数据;响应给1号网站的客户端 |
| 1号网站的服务器端app.js
|
4.8 cookie复习
当客户端向服务器发送请求的时候,会携带cookie,用以证明自己的身份,正常不跨域请求,这是没有问题的,但是跨域 cookie不会被发送问题;
但是现在有需求,比如有两个服务器都是自己的,想实现跨域登录功能, 就需要cookie技术;但是 因为是跨域发送请求,cookie不会发送到服务器;就无法实现登录功能?
4.9 withCredentials属性
在使用Ajax技术发送跨域请求时,默认情况下不会在请求中携带cookie信息。Ajax下的一个属性,
withCredentials:指定在涉及到跨域请求时,是否携带cookie信息,默认值为false
Access-Control-Allow-Credentials:true 允许客户端发送请求时携带cookie,在响应头中设置
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>实现跨域功能</title> <link rel="stylesheet" href="/assets/bootstrap/dist/css/bootstrap.min.css"> <style type="text/css"> .container { padding-top: 60px; } </style> </head> <body> <div class="container"> <form id="loginForm"> <div class="form-group"> <label>用户名</label> <input type="text" name="username" class="form-control" placeholder="请输入用户名"> </div> <div class="form-group"> <label>密码</label> <input type="password" name="password" class="form-control" placeholder="请输入用密码"> </div> <input type="button" class="btn btn-default" value="登录" id="loginBtn"> <input type="button" class="btn btn-default" value="检测用户登录状态" id="checkLogin"> </form> </div> <script type="text/javascript"> // 获取登录按钮 var loginBtn = document.getElementById('loginBtn'); // 获取检测登录状态按钮 var checkLogin = document.getElementById('checkLogin'); // 获取登录表单 var loginForm = document.getElementById('loginForm'); // 为登录按钮添加点击事件 loginBtn.onclick = function () { // 点击登录,将用户填写的内容通过jsonp提交到服务器 // 将html表单转换为formData表单对象 var formData = new FormData(loginForm); // 创建ajax对象 var xhr = new XMLHttpRequest(); // 对ajax对象进行配置 xhr.open('post', 'http://localhost:3001/login'); // 当发送跨域请求时,携带cookie信息 xhr.withCredentials = true; // 发送请求并传递请求参数 xhr.send(formData); // 监听服务器端给予的响应内容 xhr.onload = function () { console.log(xhr.responseText); } }
// 当检测用户状态按钮被点击时 checkLogin.onclick = function () { // 创建ajax对象 var xhr = new XMLHttpRequest(); // 对ajax对象进行配置 xhr.open('get', 'http://localhost:3001/checkLogin'); // 当发送跨域请求时,携带cookie信息 xhr.withCredentials = true; // 发送请求并传递请求参数 xhr.send(); // 监听服务器端给予的响应内容 xhr.onload = function () { console.log(xhr.responseText); } } </script> </body> </html> |
| app.use((req, res, next) => { // 1.允许哪些客户端访问我 // * 代表允许所有的客户端访问我 // 注意:如果跨域请求中涉及到cookie信息传递,值不可以为*号 比如是具体的域名信息 res.header('Access-Control-Allow-Origin', 'http://localhost:3000') // 2.允许客户端使用哪些请求方法访问我 res.header('Access-Control-Allow-Methods', 'get,post') // 允许客户端发送跨域请求时携带cookie信息 res.header('Access-Control-Allow-Credentials', true); next(); }); app.post('/login', (req, res) => { // 创建表单解析对象 var form = formidable.IncomingForm(); // 解析表单 form.parse(req, (err, fields, file) => { // 接收客户端传递过来的用户名和密码 const { username, password } = fields; // 用户名密码比对 if (username == 'itheima' && password == '123456') { // 设置session req.session.isLogin = true; res.send({message: '登录成功'}); } else { res.send({message: '登录失败, 用户名或密码错误'}); } }) });
app.get('/checkLogin', (req, res) => { // 判断用户是否处于登录状态 if (req.session.isLogin) { res.send({message: '处于登录状态'}) } else { res.send({message: '处于未登录状态'}) } }); |
|
|