1. ajax:
- 使用js来提交数据到服务器,服务器返回数据给js,然后js局部刷新显示在浏览器。js可以实现异步刷新浏览器界面。
- ajax无法跨域访问 {即无法直接跳转至当前的模块外部,需要另写重定向函数及重定向路由}
2. ajax改造todo:
ajax()的执行流程:{下面3、4的顺序可以交换}
- 创建ajax对象:XMLHttpRequest()
- 连接服务器:open()
- 发送请求:send()
- 监听响应并接收返回值:onreadystatechange事件、readyState属性:请求状态、status属性:请求结果、responseText
- 首先编写后端API,待会儿在前端JS里面写AJAX,通过AJAX向后端API发起HTTP请求,服务端对请求进行解析{即路由解析},返回HTTP响应被AJAX捕获并解析进行某些操作。
- 前端AJAX的写法流程:
- 第一,写个gua.js作为底层函数,这里面有3个逻辑:1,写好ajax()用于正在发送请求和接收响应,并调用监听函数{也即回调函数};2,定义好发送CURD的HTTP请求的API{即操作ajax()向服务器发起HTTP请求};3,写一些辅助函数
- 第二,另定义一个event.js,这里面调用gua.js里面写好的CURD的API,并传入回调函数{其实是定义一个回调函数,然后以匿名函数的方式作为实参传入CURD的API中}
# server.py # 接收HTTP请求,解析路由并执行相应的函数
# todo.py
from routes.session import session
from utils import (
log,
redirect,
template,
http_response,
)
def main_index(request):
return redirect('/todo/index')
# 直接写函数名字不写 route 了
def index(request):
"""
主页的处理函数, 返回主页的响应
"""
body = template('todo_index.html')
return http_response(body)
route_dict = {
'/': main_index,
'/todo/index': index,
}
<!-- todo_index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web10 todo ajax</title>
</head>
<body>
<input id='id-input-todo'>
<button id='id-button-add'>add</button>
<div class="todo-list">
</div>
<!-- 这是我们处理静态文件的套路 -->
<!-- gua.js 放了公共的函数 -->
<!-- 按顺序引入 2 个 js 文件, 后面的 js 文件就能使用前面的文件中的函数了 -->
<script src='/static?file=gua.js'></script>
<script src='/static?file=todo.js'></script>
</body>
</html>
/* gua.js */
var log = function() {
console.log.apply(console, arguments)
}
var e = function(sel) {
return document.querySelector(sel)
}
/*
ajax 函数
*/
var ajax = function(method, path, data, responseCallback) {
var r = new XMLHttpRequest()
// 设置请求方法和请求地址
r.open(method, path, true)
// 设置发送的数据的格式为 application/json
// 这个不是必须的
r.setRequestHeader('Content-Type', 'application/json')
// 注册响应函数 {当请求得到响应,就自动激发}
r.onreadystatechange = function() {
if(r.readyState === 4) { //4表示服务器响应已完成
// r.response 存的就是服务器发过来的放在 HTTP BODY 中的数据
responseCallback(r.response) //把服务器响应内容给了回调函数做实参,故形参也是一个{接收实参}
}
}
// 把数据转换为 json 格式字符串
data = JSON.stringify(data)
// 发送请求
r.send(data)
}
// TODO API {向服务器发起请求的API}, 处理响应的回调函数写在todo.js里面
// 获取所有 todo
var apiTodoAll = function(callback) { //当本函数执行完,请求得到响应后,系统自动执行回调函数callback
var path = '/api/todo/all'
ajax('GET', path, '', callback)
}
// 增加一个 todo
var apiTodoAdd = function(form, callback) {
var path = '/api/todo/add'
ajax('POST', path, form, callback)
}
// 删除一个 todo
var apiTodoDelete = function(id, callback) {
var path = '/api/todo/delete?id=' + id
ajax('GET', path, '', callback)
// get(path, callback)
}
// 更新一个 todo
var apiTodoUpdate = function(form, callback) {
var path = '/api/todo/update'
ajax('POST', path, form, callback)
// post(path, form, callback)
}
// load weibo all
var apiWeiboAll = function(callback) {
var path = '/api/weibo/all'
ajax('GET', path, '', callback)
}
// 增加一个 todo
var apiWeiboAdd = function(form, callback) {
var path = '/api/weibo/add'
ajax('POST', path, form, callback)
}
/* todo.js */
var timeString = function(timestamp) {
t = new Date(timestamp * 1000)
t = t.toLocaleTimeString()
return t
}
var todoTemplate = function(todo) {
var title = todo.title
var id = todo.id
var ut = timeString(todo.ut)
// data-xx 是自定义标签属性的语法
// 通过这样的方式可以给任意标签添加任意属性
// 假设 d 是 这个 div 的引用
// 这样的自定义属性通过 d.dataset.xx 来获取
// 在这个例子里面, 是 d.dataset.id
var t = `
<div class="todo-cell" id='todo-${id}' data-id="${id}">
<button class="todo-edit">编辑</button>
<button class="todo-delete">删除</button>
<span class='todo-title'>${title}</span>
<time class='todo-ut'>${ut}</time>
</div>
`
return t
/*
上面的写法在 python 中是这样的
t = """
<div class="todo-cell">
<button class="todo-delete">删除</button>
<span>{}</span>
</div>
""".format(todo)
*/
}
var insertTodo = function(todo) {
var todoCell = todoTemplate(todo)
// 把todoCell 插入 todo-list
var todoList = e('.todo-list') //找到已有的todolist,把新按钮所在的div挂在.todo-list下
todoList.insertAdjacentHTML('beforeend', todoCell)
}
var insertEditForm = function(cell) {
var form = `
<div class='todo-edit-form'>
<input class="todo-edit-input">
<button class='todo-update'>更新</button>
</div>
`
cell.insertAdjacentHTML('beforeend', form)
}
var loadTodos = function() {
// 调用 ajax api 来载入数据
apiTodoAll(function(r) { //r接收实参{这个实参来自服务器的响应,其定义见gua.js中对回调函数输入的实参}
// console.log('load all', r)
// 解析为 数组
var todos = JSON.parse(r)
// 循环添加到页面中
for(var i = 0; i < todos.length; i++) {
var todo = todos[i]
insertTodo(todo)
}
})
}
var bindEventTodoAdd = function() { //编写事件监听器
var b = e('#id-button-add')
// 注意, 第二个参数可以直接给出定义函数
b.addEventListener('click', function(){
var input = e('#id-input-todo')
var title = input.value
log('click add', title)
var form = {
'title': title,
}
apiTodoAdd(form, function(r) { //定义回调函数,并作为参数传入gua.js中定义的API中
// 收到返回的数据, 插入到页面中
var todo = JSON.parse(r)
insertTodo(todo)
})
})
}
var bindEventTodoDelete = function() { //和bindEventTodoAdd的逻辑刚刚相反
var todoList = e('.todo-list') //当时add一个todo的div时,就是挂在.todo-list下,所以现在删也是要到.todo-list
// 注意, 第二个参数可以直接给出定义函数
todoList.addEventListener('click', function(event){ //事件监听委托给 爷爷节点 todoList
var self = event.target //找到click动作的目标位置 {即要找到删除按钮},因为这个按钮的父亲是todo{一个div},todo的父亲是todo-list{也是一个div}
if(self.classList.contains('todo-delete')){ //找到删除按钮了
// 删除这个 todo
var todoCell = self.parentElement // 其实是要删除“删除按钮”所在的那个todo{即一个div}
var todo_id = todoCell.dataset.id //但是add一个todo的div时,曾经自定义了一个data-id属性,现在从这个属性中取到当时add时存的值
apiTodoDelete(todo_id, function(r){ //又去gua.js中调用ajax(),发送删除的HTTP请求到服务器
log('删除成功', todo_id)
todoCell.remove() //把这个todo从本地浏览器的静态页上删除掉
})
}
})
}
var bindEventTodoEdit = function() {
var todoList = e('.todo-list')
// 注意, 第二个参数可以直接给出定义函数
todoList.addEventListener('click', function(event){ // 又是把监听事件委托给了爷爷todolist
var self = event.target
if(self.classList.contains('todo-edit')){ //找到了"编辑按钮"
var todoCell = self.parentElement //找到 "编辑按钮"的父亲{某一个todo,本质是一个div}
insertEditForm(todoCell) //把一个用于编辑的div插入到todoCell最末尾{让其紧跟着这个待编辑的todo}
}
})
}
var bindEventTodoUpdate = function() {
var todoList = e('.todo-list') // 又是委托{这个静态页面中恒存在的div}来监听事件
// 注意, 第二个参数可以直接给出定义函数
todoList.addEventListener('click', function(event){
var self = event.target //当前被点击的对象
if(self.classList.contains('todo-update')){ // 找到了“update按钮”
log('点击了 update ')
//
var editForm = self.parentElement //拿到整个editForm
// querySelector 是 DOM 元素的方法
// document.querySelector 中的 document 是所有元素的祖先元素
var input = editForm.querySelector('.todo-edit-input') //找到editForm中的input框
var title = input.value
// 用 closest 方法可以找到最近的直系祖先
var todoCell = self.closest('.todo-cell') // 找到“update按钮”最近的一个class名为.todo-cell的祖先
var todo_id = todoCell.dataset.id // 从这个to中拿到自定义data-id的值
var form = {
'id': todo_id,
'title': title,
}
apiTodoUpdate(form, function(r){ // 拿form去调用ajax(),以及即将执行下面的回调函数
log('更新成功', todo_id)
var todo = JSON.parse(r) //解析apiTodoUpdate后得到的HTTP响应
var selector = '#todo-' + todo.id //根据最开始{add todo} 时定义的id属性来
var todoCell = e(selector) //找需要更新的todo
var titleSpan = todoCell.querySelector('.todo-title') //根据 这个todo的 {'.todo-title'的这个class属性}去找这个todo的一个子节点
titleSpan.innerHTML = todo.title //更新titleSpan的内嵌的html内容,同理可以修改17行的${ut}
// todoCell.remove()
})
}
})
}
var bindEvents = function() {
bindEventTodoAdd()
bindEventTodoDelete()
bindEventTodoEdit()
bindEventTodoUpdate()
}
var __main = function() {
bindEvents()
loadTodos()
}
__main()
// 例如下图,待会儿在blog中逐个解释每一个CURD背后的逻辑和流程到底是怎样的?
// 可以很好地梳理清楚js和ajax的运行过程及效果
/*
给 删除 按钮绑定删除的事件
1, 绑定事件
2, 删除整个 todo-cell 元素
*/
// var todoList = e('.todo-list')
// // 事件响应函数会被传入一个参数, 就是事件本身
// todoList.addEventListener('click', function(event){
// // log('click todolist', event)
// // 我们可以通过 event.target 来得到被点击的元素
// var self = event.target
// // log('被点击的元素是', self)
// // 通过比较被点击元素的 class 来判断元素是否是我们想要的
// // classList 属性保存了元素的所有 class
// // 在 HTML 中, 一个元素可以有多个 class, 用空格分开
// // log(self.classList)
// // 判断是否拥有某个 class 的方法如下
// if (self.classList.contains('todo-delete')) {
// log('点到了 删除按钮')
// // 删除 self 的父节点
// // parentElement 可以访问到元素的父节点
// self.parentElement.remove()
// } else {
// // log('点击的不是删除按钮******')
// }
// })
# api_todo.py
import json
from routes.session import session
from utils import (
log,
redirect,
http_response,
json_response,
)
from models.todo import Todo
from models.weibo import Weibo
def all_weibo(request):
"""
返回所有 todo
"""
ms = Weibo.all()
# 要转换为 dict 格式才行
data = [m.json() for m in ms]
return json_response(data)
def add_weibo(request):
"""
接受浏览器发过来的添加 weibo 请求
添加数据并返回给浏览器
"""
form = request.json()
# 创建一个 model
m = Weibo.new(form)
# 把创建好的 model 返回给浏览器
return json_response(m.json())
# 本文件只返回 json 格式的数据
# 而不是 html 格式的数据
def all(request):
"""
返回所有 todo
"""
todo_list = Todo.all()
# 要转换为 dict 格式才行
todos = [t.json() for t in todo_list]
return json_response(todos)
def add(request):
"""
接受浏览器发过来的添加 todo 请求
添加数据并返回给浏览器
"""
# 得到浏览器发送的 json 格式数据
# 浏览器用 ajax 发送 json 格式的数据过来
# 所以这里我们用新增加的 json 函数来获取格式化后的 json 数据
form = request.json() # 字符串格式转化成dict
# 创建一个 todo
t = Todo.new(form)
# 把创建好的 todo 返回给浏览器
return json_response(t.json())
def delete(request):
"""
通过下面这样的链接来删除一个 todo
/delete?id=1
"""
todo_id = int(request.query.get('id'))
t = Todo.delete(todo_id)
return json_response(t.json())
def update(request):
form = request.json()
todo_id = int(form.get('id'))
t = Todo.update(todo_id, form)
return json_response(t.json())
route_dict = {
'/api/todo/all': all,
'/api/todo/add': add,
'/api/todo/delete': delete,
'/api/todo/update': update,
# weibo api
'/api/weibo/all': all_weibo,
'/api/weibo/add': add_weibo,
}
分析lesson_10中,地址栏中输入localhost:3000/todo/index后程序是怎么走的:
-
首先,浏览器向localhost:3000/todo/index后,
Server.py解析路由,根据routes包下面的todo.py找到了/todo/index这个路由信息,
定位到todo.py下的index(),
然后调用jinja2渲染todo_index.html,然后把首页返回给浏览器。
而todo_index.html中又引入了gua.js和todo.js -
todo.js的__main()执行会导致bindEvents()执行和loadTodos()执行;
-
一方面,bindEvents()执行会导致
bindEventTodoAdd()
bindEventTodoDelete()
bindEventTodoEdit()
bindEventTodoUpdate()
四个函数执行,会依次激发后续操作,以bindEventTodoAdd()为例:
bindEventTodoAdd()会定义一个对id-button-add的按钮的点击事件的监听器,如果此按钮被点击,就会调用监听器,
收集id-input-todo中的值,并把这个值和一个匿名的回调
函数传入apiTodoAdd(),并调用apiTodoAdd(),{这个函数的
定义体apiTodoAdd也在gua.js},然后apiTodoAdd首先会调用
ajax()发送一个HTTP请求到/api/todo/add,然后得到响应后,自动把response送到apiTodoAdd上一步匿名传入的回调函数中,然后执行回调函数,然后就会把当前的response中的todo插入到todo_index.html中。
{其余监听器的执行流程类似,只是需要理解js的执行逻辑和代码执行后的前端效果} -
另一方面,bindEvents()执行会导致loadTodos()执行,todos()执行会引发apiTodoAll(监听器)执行,而apiTodoAll的定义体在gua.js,它会调用ajax()首先发送一个HTTP请求到/api/todo/all,然后得到一个HTTP response作为实参传入到回调函数的新参,然后自动激发回调函数,然后就会把response中每一个todo依次插入到todo_index.html中。
待会儿再详细重构下本篇文字:
专门把server.py、todo.py、todo_index.html、gua.js、todo.js、api_todo.py 的源码单独列出来,结合代码中注释和上述文字分析,彻底把ajax的实现过程讲清楚。