目录
一.同源政策
1.同源政策介绍
- Ajax请求限制:Ajax 只能向自己的服务器发送请求,无法向非同源地址请求,如果发送,浏览器会报错
- 现在有一个A网站、有一个B网站,A网站中的 HTML文件 只能向 A网站服务器中发送 Ajax请求,B网站中的 HTML文件 只能向 B网站中发送 Ajax请求;A网站不能向B网站发送 Ajax请求,B同理
- 同源:如果两个页面拥有相同的 协议、域名和端口,那么这两个页面就属于同一个源
- Example:
- 同源政策:为了保证用户信息的安全,防止恶意的网站窃取数据
2.使用 JSONP 解决同源限制问题
- JSONP 是 json with padding 的缩写,它不属于 Ajax请求,但可以模拟 Ajax请求
- JSON 是描述信息的格式,JSONP 是信息传递双方约定的方法(非标准)
- 浏览器存在同源限制问题,而服务器不存在同源限制问题,即服务器可以请求别的服务器的信息而不报错
- JSONP是写在客户端的,不采用Ajax,进而避免了同源限制问题,实现了获取跨域信息
- JSONP 使用步骤:
- 不同源的服务器端请求地址写在 script标签的 src属性中
- 服务器端响应数据必须是 函数调用,发送客户端的数据作为 函数调用的参数
- 在客户端全局作用域下定义函数 fn:function fn (data) { }
- 在 fn函数内部对服务器端返回的数据进行处理:function fn (data) { console.log(data); }
- 封装JSONP方法:
function jsonp (options) { // 动态创建script标签 var script = document.createElement('script'); // 拼接字符串的变量 var params = ''; for (var attr in options.data) { params += '&' + attr + '=' + options.data[attr]; } // myJsonp0124741 给函数随机命名 fnName是我们给window对象定义的属性 var fnName = 'myJsonp' + Math.random().toString().replace('.', ''); window[fnName] = options.success; // 为script标签添加src属性 script.src = options.url + '?callback=' + fnName + params; // 将script标签追加到页面中 document.body.appendChild(script); // 为script标签添加onload事件 script.onload = function () { document.body.removeChild(script); }}
- 使用JSONP 获取腾讯天气分析:
- 引入jsonp.js实现跨域获取信息,引入template-web.js实现客户端模板拼接
- 设置客户端模板,格式化服务器返回信息,遍历服务器返回信息并拼接到模板中
- 调用jsonp(),根据接口文档传入客户端参数,拼接显示模板内容
- template.defaults.imports......:向模板中开放外部方法,这方法可以在模板中调用
// 客户端代码 .html <table class="table table-hover" id="box"></table> <script src="/js/jsonp.js"></script> <script src="/js/template-web.js"></script> <script type="text/html" id="tpl"> // 设置模板 记住:<script type="text/html" <tr> <th>时间</th> <th>温度</th> <th>天气</th> <th>风向</th> <th>风力</th> </tr> {{each info}} // 循环遍历服务器端返回的回答 <tr> <td>{{dateFormat($value.update_time)}}</td> <td>{{$value.degree}}</td> //... {{/each}} </script> <script> var box = document.getElementById('box'); // 获取table标签 template.defaults.imports.dateFormat = dateFormat; // 向模板中开放外部变量 自己命名 function dateFormat(date) { // 定义自己命名的函数,这函数负责格式化服务器返回的时间 var year = date.substr(0, 4); // 从第0个字符开始,截取4个长度,表示年 // ... var seconds = date.substr(12, 2); return year + '年' + month + '月' + day + '日' + hour + '时' + minute + '分' + seconds + '秒'; } jsonp({ // 向服务器端获取天气信息 根据接口文档书写data信息 url: 'https://wis.qq.com/weather/common', data: { source: 'pc', weather_type: 'forecast_1h|forecast_24h', province: '山东省', city: '威海市' }, success: function (data) { // 请求成功将获取跨域资源data,拼接字符串并展示 var html = template('tpl', {info: data.data.forecast_1h}); box.innerHTML = html; }}) </script>
3.CORS 跨域资源共享
- CORS 全称为 Cross-origin resource sharing,即 跨域资源共享
- CORS 允许浏览器向跨域服务器发送 Ajax请求,但是服务器端必须设置 响应头
- 区分:
- JSONP 是在客户端调用jsonp(),服务器不变,实现跨域资源访问,没有克服同源限制
- CORS 是在客户端调用ajax(),同时服务器书写对应的响应头,实现跨域资源访问,克服了同源限制
- Node 服务器端设置响应头示例代码:
// 3000端口 客户端文件.html var btn = document.getElementById('btn'); btn.onclick = function () { ajax({ // 调用ajax() type: 'get', url: 'http://localhost:3001/cross', //注意这里,跨域请求访问 3001端口服务器 success: function (data) { console.log(data) }})}; // 3001端口 服务器端文件.js // 拦截所有请求 app.use((req, res, next) => { // 这是CORS对应的服务器端响应头 res.header('Access-Control-Allow-Origin', '*') // 允许哪些客户端访问我 res.header('Access-Control-Allow-Methods', 'get,post') // 允许哪些请求方法访问我 res.header('Access-Control-Allow-Credentials', true); // 允许客户端发送跨域请求时携带cookie next(); });
4.访问非同源服务器端数据解决方案
- 同源政策是浏览器给予 Ajax技术的限制,服务器端 不存在同源政策限制
- Ajax发送跨域请求时,默认情况下,不会在请求中携带 cookie信息
- withCredentials属性:指定在涉及到跨域请求时,是否携带cookie信息,默认值为false,不携带
- Access-Control-Allow-Credentials:true 允许客户端发送请求时 携带cookie
- 实现跨域登陆功能分析:
// 3000端口 客户端.html <form id="loginForm"> <input type="text" name="username" placeholder="请输入用户名"> <input type="password" name="password" placeholder="请输入用密码"> <input type="button" value="登录" id="loginBtn"> <input type="button" value="检测用户登录状态" id="checkLogin"> </form> <script type="text/javascript"> var loginBtn = document.getElementById('loginBtn'); // 获取登录按钮 var checkLogin = document.getElementById('checkLogin'); // 获取检测登录状态按钮 var loginForm = document.getElementById('loginForm'); // 获取登录表单 loginBtn.onclick = function () { // 为登录按钮添加点击事件 // 将html表单转换为formData表单对象 var formData = new FormData(loginForm); var xhr = new XMLHttpRequest(); // 创建ajax对象 xhr.open('post', 'http://localhost:3001/login'); // 对ajax对象进行配置 注意跨域 xhr.withCredentials = true; // 当发送跨域请求时,携带cookie信息 xhr.send(formData); // 发送请求并传递请求参数 xhr.onload = function () { // 监听服务器端给予的响应内容 console.log(xhr.responseText); }} checkLogin.onclick = function () { // 当检测用户状态按钮被点击时 var xhr = new XMLHttpRequest(); xhr.open('get', 'http://localhost:3001/checkLogin'); // 注意跨域 xhr.withCredentials = true; // 当发送跨域请求时,携带cookie信息 xhr.send(); xhr.onload = function () { console.log(xhr.responseText); }} </script> // 3001端口 服务器端.js app.post('/login', (req, res) => { // 登录按钮 var form = formidable.IncomingForm(); // 创建表单解析对象 form.parse(req, (err, fields, file) => { // 解析表单 // 接收客户端传递过来的用户名和密码 const { username, password } = fields; // 用户名密码比对 if (username == '茶茶子' && 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: '处于未登录状态'}) }});
二.jQuery 中的 Ajax
1.$.ajax()
- 发送Ajax请求:
<button id="btn">发送请求</button> <script src="/js/jquery.min.js"></script> <script> var params = {name: 'wangwu', age: 300} $('#btn').on('click', function () { $.ajax({ type: 'post', // 请求方式 url: '/base', // 请求地址 data: JSON.stringify(params), // 向服务器端发送的请求参数 contentType: 'application/json', // 指定参数的格式类型 beforeSend: function () { // 在请求发送之前调用 alert('请求不会被发送') return false; // 请求不会被发送 组织程序向下进行 }, success: function (response) { // 请求成功以后函数被调用 // response为服务器端返回的数据 // 方法内部会自动将json字符串转换为json对象 console.log(response); }, error: function (xhr) { // 请求失败以后函数被调用 console.log(xhr) }})}); </script> app.get('/user', (req, res) => { res.send(req.query); }); app.post('/user', (req, res) => { res.send(req.body) }); app.post('/base', (req, res) => { res.status(400).send({ name: 'zhaoliu', age: 35 })});
- 发送 JSONP请求:
- serialize()方法:将表单中的数据,自动拼接成字符串类型的参数
- var params = $('#form').serialize(); // name=zhangsan&age=30
$('#form').on('submit', function () { serializeObject($(this)); return false; }); // 将表单中用户输入的内容转换为对象类型 function serializeObject (obj) { var result = {}; var params = obj.serializeArray(); $.each(params, function (index, value) { // 循环数组 将数组转换为对象类型 result[value.name] = value.value; }) return result;}
2.$.get、$.post
- $.get('路径', 'xx=xx&xx=xx', function (response) ):用于发送 get请求
- $.post('路径', {xx=xx,xx=xx}, function (response) ):用于发送 post请求
<button id="btn">发送请求</button> <script src="/js/jquery.min.js"></script> <script> $('#btn').on('click', function () { // $.get('/base', 'name=TeaMeow&age=21', function (response) { // console.log(response) // }) $.post('/base', {name='TeaMeow',age=21}, function (response) { console.log(response) })}); </script>
3.全局事件
- 页面中有 Ajax请求 被发送,对应的全局事件就会被触发
- .ajaxStart():当请求开始发送时触发
- .ajaxComplete():当请求完成时触发
4.nprogress进度条插件
- 纳米级进度条,使用逼真的涓流动画,告诉用户正在发生的事情
- <link rel='stylesheet' href='nprogress.css'/>:引入 nprogress.css
- <script src='nprogress.js'></script>:引入 nprogress.js
- NProgress.start();:进度条开始运动
- NProgress.done();:进度条结束运动
三.RESTful 和 XML
1.RESTful 风格的 API
- RESTful API 概述:设计请求地址 的规范
- 请求方式不仅仅GET、POST,还有下面的:
- GET:获取数据
- POST:添加数据
- PUT:修改数据
- DELETE:删除数据
- Example:
- RESTful风格特点:请求地址相同,请求方式不同,所做的事情也不同
- 请求地址不要出现动词,全是名词即可,观察下面代码的 请求路径及请求方式:
- '/users/:id'
- const id = req.params.id; // 获取客户端传递过来的用户id
// 客户端.html $.ajax({ // 获取用户列表信息 type: 'get', url: '/users', success: function (response) { console.log(response) }}) $.ajax({ //删除id为1的用户信息 type: 'delete', url: '/users/10',... }}) $.ajax({ // 更新id为1的用户信息 type: 'put', url: '/users/10',... }}) // 服务器端.js app.get('/users', (req, res) => { // 获取用户列表信息 res.send('当前是获取用户列表信息的路由'); }); app.get('/users/:id', (req, res) => { // 获取某一个用户具体信息的路由 :id const id = req.params.id; // 获取客户端传递过来的用户id res.send(`当前我们是在获取id为${id}用户信息`); }); app.delete('/users/:id', (req, res) => { // 删除某一个用户 const id = req.params.id; res.send(`当前我们是在删除id为${id}用户信息`); });
2.XML 基础
- XML 可扩展标记语言:用于 传输和存储数据
- 都是标记语言,XML注重的是标签内容,HTML注重的是标签样式
- Example:
- XML DOM = XML 文档对象模型:操作 XML文档对象的 API,就是一套方法,可以被调用实现增删改查
- 浏览器会将 XML文档 解析成 文档对象模型,其方法和 document差不多
- var xmlDocument = xhr.responseXML:获取服务器端返回的 xml数据
- var title = xmlDocument.getElementsByTagName('title')[0].innerHTML; // 获取消息标题
- 服务器端一定要写:res.header('content-type', 'text/xml');,指定客户端如何解析服务器端的响应
// 客户端.html var btn = document.getElementById('btn'); var container = document.getElementById('container'); btn.onclick = function () { var xhr = new XMLHttpRequest(); xhr.open('get', '/xml'); xhr.send(); xhr.onload = function () { // xhr.responseXML 获取服务器端返回的xml数据 var xmlDocument = xhr.responseXML; var title = xmlDocument.getElementsByTagName('title')[0].innerHTML; container.innerHTML = title; }} // 服务器端.js app.get('/xml', (req, res) => { res.header('content-type', 'text/xml'); // 一定要写,不然客户端获取不了 xml格式数据 res.send('<message><title>消息标题</title><content>消息内容</content></message>') });
四.TodoExample
1.为todo数据库添加账号
- 使用mongo命令进入mongodb数据库
- 使用use admin命令进入到admin数据中
- 使用db.auth(‘root’, ‘root’)命令登录数据库
- 使用use todo命令切换到todo数据库
- 使用db.createUser({user: ‘itcast’, pwd: ‘itcast’, roles: [‘readWrite’]})创建todo数据库账号
- 使用exit命令退出mongodo数据库
2.展示任务列表
- 准备一个放置任务列表的数组
- 向服务器端发送请求,获取已存在的任务
- 将已存在的任务存储在任务列表数组中
- 通过模板引擎将任务列表数组中的任务显示在页面中
3.添加任务
- 为文本框绑定键盘抬起事件,在事件处理函数中判断当前用户敲击的是否是回车键
- 当用户敲击回车键的时候,判断用户在文本框中是否输入了任务名称
- 向服务器端发送请求,将用户输入的任务名称添加到数据库中,同时将任务添加到任务数组中
- 通过模板引擎将任务列表数组中的任务显示在页面中
4.删除任务
- 为删除按钮添加点击事件
- 在事件处理函数中获取到要删任务的id
- 向服务器端发送请求,根据ID删除任务,同时将任务数组中的相同任务删除
- 通过模板引擎将任务列表数组中的任务重新显示在页面中
5.更改任务状态
- 为任务复选框添加onchange事件
- 在事件处理函数中获取复选框是否选中
- 向服务器端发送请求,将当前复选框的是否选中状态提交到服务器端
- 将任务状态同时也更新到任务列表数组中
- 通过模板引擎将任务列表数组中的任务重新显示在页面中并且根据任务是否完成为li元素添加completed类名
6.修改任务名称
- 为任务名称外层的label标签添加双击事件,同时为当前任务外层的li标签添加editing类名,开启编辑状态
- 将任务名称显示在文本框中并让文本框获取焦点
- 当文本框离开焦点时,将用户在文本框中输入值提交到服务器端,并且将最新的任务名称更新到任务列表数组中
- 使用模板引擎重新渲染页面中的任务列表
7.计算未完成的任务量
- 准备一个用于存储未完成任务数量的变量
- 将未完成任务从任务数组中过滤出来
- 将过滤结果数组的长度赋值给任务数量变量
- 将结果更新到页面中
8.显示未完成任务
- 为active按钮添加点击事件
- 从任务列表数组中将未完成任务过滤出来
- 使用模板引擎将过滤结果显示在页面中
9.清除已完成任务
- 为clear completed按钮添加点击事件
- 向服务器端发送请求将数据库中的已完成任务删除掉
- 将任务列表中的已完成任务删除调用
- 使用模板引擎将任务列表中的最后结果显示在页面中
10.index.html
<title>Todo List</title> <link rel="stylesheet" href="assets/css/base.css"> <link rel="stylesheet" href="assets/css/index.css"> <link rel="stylesheet" href="/js/nprogress/nprogress.css"> // 进度条插件 <body> <section class="todoapp"> <header class="header"> <h1>todos</h1> <input type="text" class="new-todo" autofocus id="task"> // 输入框 </header> <section class="main"> <input class="toggle-all" type="checkbox"> // 复选框 <ul class="todo-list" id="todo-list"></ul> // 任务列表容器 </section> <footer class="footer"> <span class="todo-count"><strong id="count">0</strong> item left</span> <ul class="filters"> <li><a class="selected" href="javascript:;">All</a></li> <li><a href="javascript:;">Active</a></li> <li><a href="javascript:;">Completed</a></li> </ul> <button class="clear-completed">Clear completed</button> </footer> </section> <script src="/js/jquery.min.js"></script> <script src="/js/template-web.js"></script> <script src="/js/nprogress/nprogress.js"></script> <!-- 任务列表模板 --> <script type="text/html" id="taskTpl"> {{each tasks}} // 遍历服务器数据库任务数组 <li class="{{$value.completed ? 'completed' : ''}}"> // 当编辑的时候edit盒子在上,view盒子隐藏 <div class="view"> <input class="toggle" type="checkbox" {{$value.completed ? 'checked' : ''}}> <label>{{$value.title}}</label> // 代做事项内容 // 删除按钮 动态添加删除id:$value._id <button class="destroy" data-id="{{$value._id}}"></button> </div> <input class="edit"> </li> {{/each}} </script> <script type="text/javascript"> var taskAry = []; // 用于存放任务列表的数组 var taskBox = $('#todo-list'); // 选择任务列表容器 var taskInp = $('#task'); // 输入框 var strong = $('#count'); // 存储未完成任务数量的strong标签 // 当页面中有ajax请求发送时触发 $(document).on('ajaxStart', function () { NProgress.start() // 进度条开始 }) // 当页面中有ajax请求完成时触发 $(document).on('ajaxComplete', function () { NProgress.done() // 进度条结束 }) // 向服务器端发送请求 获取已经存在的任务 $.ajax({ url: '/todo/task', type: 'get', success: function (response) { taskAry = response; // 将已存在的任务存储在taskAry变量中 render(); // 拼接字符串 将拼接好的字符串显示在页面中 calcCount () // 计算未完成任务数量 }}) // 拼接字符串 将拼接好的字符串显示在页面中 function render() { // 字符串拼接 var html = template('taskTpl', { tasks: taskAry }); taskBox.html(html); // 将拼接好的字符串显示在ul标签中 } // 获取文本框并且添加键盘抬起事件 taskInp.on('keyup', function (event) { if (event.keyCode == 13) { // 如果用户敲击的是回车键 var taskName = $(this).val(); // 将文本框中的值存储在taskName中 if (taskName.trim().length == 0) { // 如果用户没有在文本框中输入内容 alert('请输入任务名称') return; // 阻止代码向下执行 } $.ajax({ // 向服务器端发送请求 添加任务 type: 'post', url: '/todo/addTask', contentType: 'application/json', data: JSON.stringify({title: taskName}), success: function (response) { taskAry.push(response); // 将任务添加到任务列表中 render(); // 拼接字符串并显示 taskInp.val(''); // 清空文本框中的内容 calcCount () // 计算未完成任务数量 }})}}); // 当用户点击删除按钮时,触发ul标签身上的点击事件 事件委托 taskBox.on('click', '.destroy', function () { var id = $(this).attr('data-id'); // 要删除的任务的id $.ajax({ // 向服务器端发送请求删除 任务 url: '/todo/deleteTask', type: 'get', data: { _id: id }, success: function (response) { // 筛选到已经删除掉的任务的索引 var index = taskAry.findIndex(item => item._id == id); taskAry.splice(index, 1); // 将任务从数组中删除 从哪删,删几个 render(); // 重新将任务数组中的元素显示在页面中 calcCount () // 计算未完成任务数量 }})}); // 当用户改变任务名称前面的复选框状态时触发 taskBox.on('change', '.toggle', function () { // 代表复选框是否选中 true 选中 false 未选中的 const status = $(this).is(':checked'); // 当前点击任务的id const id = $(this).siblings('button').attr('data-id'); $.ajax({ // 向服务器端发送请求 更改任务状态 type: 'post', url: '/todo/modifyTask', data: JSON.stringify({_id: id, completed: status}), contentType: 'application/json', success: function (response) { // 将任务状态同步到任务数组中 var task = taskAry.find(item => item._id == id); // 更改任务状态 task.completed = response.completed; render(); calcCount () }})}); // 当双击事件名称的时候触发 taskBox.on('dblclick', 'label', function () { // 让任务处于编辑状态 $(this).parent().parent().addClass('editing'); // 将任务名称显示在文本框中 $(this).parent().siblings('input').val($(this).text()) // 让文本框获取焦点 $(this).parent().siblings('input').focus(); }) // 当文本框离开焦点的时候 taskBox.on('blur', '.edit', function () { // 最新的任务名称 var newTaskName = $(this).val(); // 编辑任务的id var id = $(this).siblings().find('button').attr('data-id'); $.ajax({ // 向服务器端发送请求 修改任务名称 url: '/todo/modifyTask', type: 'post', data: JSON.stringify({_id: id, title: newTaskName}), contentType: 'application/json', success: function (response) { // 将当期任务的最新状态同步到任务数组中 var task = taskAry.find(item => item._id == id); // 修改任务名称 task.title = response.title; render(); calcCount () }})}); // 计算未完成任务的数量 function calcCount () { var count = 0; // 存储结果的变量 // 将未完成的任务过滤到一个新的数组中 var newAry = taskAry.filter(item => item.completed == false); count = newAry.length; // 将新数组的长度赋值给count strong.text(count) // 将未完成的任务数量显示在页面中 } </script> </body>
11.app.js
// app.js // 数据库连接 mongoose.connect('mongodb://username:TeaMeow@localhost:27017/todo', {useNewUrlParser: true }) // 导入todo路由案例 const todoRouter = require('./route/todo') // 当客户端的请求路径以/todo开头时 app.use('/todo', todoRouter); // todo.js const express = require('express'); // 引入express框架 const _ = require('lodash'); // 工具库 const Joi = require('joi'); // 对象校验 const todoRouter = express.Router(); // 创建todo案例路由 const Task = require('../model/task'); // 导入todo集合构造函数 // 获取任务列表 todoRouter.get('/task', async (req, res) => { const task = await Task.find(); res.send(task); // 响应 }); // 添加任务 todoRouter.post('/addTask', async (req, res) => { const { title } = req.body; // 接收客户端传递过来的任务名称 const schema = { // 验证规则 title: Joi.string().required().min(2).max(30) }; const { error } = Joi.validate(req.body, schema); // 验证客户端传递过来的请求参数 if (error) { // 验证失败 将错误信息响应给客户端 return res.status(400).send({message: error.details[0].message}) } // 创建任务实例 const task = new Task({title: title, completed: false}); await task.save(); // 执行插入操作 setTimeout(() => { // 响应 res.send(task); }, 2000) }); // 删除任务 todoRouter.get('/deleteTask', async (req, res) => { const { _id } = req.query; // 要删除的任务id const schema = { // 验证规则 _id: Joi.string().required().regex(/^[0-9a-fA-F]{24}$/)} const { error } = Joi.validate(req.query, schema); // 验证客户端传递过来的请求参数 if (error) { // 验证失败 将错误信息响应给客户端 return res.status(400).send({message: error.details[0].message}) } const task = await Task.findOneAndDelete({_id: _id}); // 删除任务 res.send(task); // 响应 }); // 清除已完成任务 todoRouter.get('/clearTask', async (req, res) => { const result = await Task.deleteMany({completed: true}); // 执行清空操作 res.send(result); // 返回清空数据 }); // 修改任务 todoRouter.post('/modifyTask', async (req, res) => { // 执行修改操作 const task = await Task.findOneAndUpdate({_id: req.body._id}, _.pick(req.body, ['title', 'completed']),{new: true}) res.send(task); // 响应 }); // 查询未完成任务数量 todoRouter.get('/unCompletedTaskCount', async (req, res) => { const result = await Task.countDocuments({completed: false}); // 执行查询操作 res.send({num: result}) // 响应 }); // 更改任务全部状态 todoRouter.get('/changeAllTasksComplete', async (req, res) => { const { status } = req.query; // 状态 const result = await Task.updateMany({}, {completed: status}); // 执行更改状态操作 res.send(result); // 响应 }); module.exports = todoRouter; // 将todo案例路由作为模块成员进行导出