(P1) Event
动机、参考资料、涉及内容
(1) 先记录 javascript 事件相关的内容 (websocket, event, fastapi example)
(2) python 的 logging 模块
(3) langchain 的 astream_events
与 astream_log
接口
mdn/Web-API/DOM/Event: https://developer.mozilla.org/en-US/docs/Web/API/Event mdn/Web-API/WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
FAQ
Q: mdn 文档中所指的 web api 是什么?
A: 浏览器只认 html, css, javascript 这三种语言, 因此浏览器可以视作是这三门语言的解释器, 这就好比 python 解释器能运行 .py
代码一样. 然而浏览器与 javascript 并不是同一标准的, 因此浏览器可以对 javascript 做扩展, 譬如说 gcc 编译器中有个内置函数 __builtin_popcount
用于统计一个整数的二进制表示中有多少个 1, 这个函数在使用 gcc 编译时能被正确编译, 而 clang 编译器进行编译时会出现编译错误, 而这个函数并非 C 标准的一部分, 作为编译器来说, 其首要目标是完备地支持 C 标准, 也就是对于 C 标准规定的部分, 必须按 C 标准的规定进行实现, 在此基础上编译器还可以自行新增一些功能以方便开发者, 从跨平台的要求来说, C 标准规定的部分总是可移植的. 回到 web api, 这些接口并非 javascript 语言的一部分, 而是浏览器对 javascript 的扩展, 使用起来就像是 javascript 的内置函数一样, 而不同的浏览器扩展程度不一样, 但一般都会遵循 Web 标准组织例如 W3C 的规定, 而这些规定里的一部分就是 web api. 常见的 web api 就包括 DOM, 而 Event 相关的 API 也属于 DOM.
Q: Event “继承”关系
A:
Event
- UIEvent
- MouseEvent
- PointerEvent # document.getElementById('outer').addEventListener('click', function(event) {...}) 里 event 实参的变量类型
- MessageEvent # WebSocket.onmessage=function(event){...} 里 event 实参的变量类型
缘起: FastAPI & websocket
本文写作最初来源于 FastAPI 实现 websocket 的官方例子: https://fastapi.tiangolo.com/advanced/websockets/
# main.py
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
app = FastAPI()
html = """
<!DOCTYPE html>
<html>
<head>
<title>Chat</title>
</head>
<body>
<h1>WebSocket Chat</h1>
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="messageText" autocomplete="off"/>
<button>Send</button>
</form>
<ul id='messages'>
</ul>
<script>
var ws = new WebSocket("ws://localhost:8000/ws");
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
function sendMessage(event) {
var input = document.getElementById("messageText")
ws.send(input.value)
input.value = ''
event.preventDefault()
}
</script>
</body>
</html>
"""
@app.get("/")
async def get():
return HTMLResponse(html)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
运行与交互方式: 运行实际上指的是 websocket 服务端代码 (也就是 main.py
, 尤其是 websocket_endpoint
函数), 交互实质上指的是 websocket 客户端代码 (主要就是 main.py
函数中的 script
标签部分)
运行方式:
#
# 新版本的 fastapi 可以使用这种方式运行(估计应该是内置了一套 ASGI web 服务器), 当然, 也可以使用其他 ASGI web 服务器例如 uvicorn 来启动
fastapi dev main.py
交互方式:
(1) 首先使用浏览器打开 http://localhost:8000
页面, 这时会触发普通的 http 请求, 浏览器端这时获取到了 main.py
中的 html
字符串并展示给用户, 注意浏览器在解释执行这段字符串时, 由于 script
标签也被执行了, 所以 websocket 连接已经建立 (备注: 这种做法是常见的, 交互之前先建立连接). 并且为这个 WebSocket 对象建立了事件监听器, 注意也可以写作(官方文档):
var ws = new WebSocket("ws://localhost:8000/ws");
ws.addEventListener("message", function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
});
(2) 用户在输入框输入信息并点击提交按钮时, 首先触发了这段客户端代码:
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="messageText" autocomplete="off"/>
<button>Send</button>
</form>
<script>
function sendMessage(event) {
var input = document.getElementById("messageText")
ws.send(input.value)
input.value = ''
event.preventDefault()
}
</script>
这里 action 参数用于指定将表单数据交给哪个 URL, 这里留空, 表示提交给当前页面, 也就是交给 form 元素指定的属性 onsubmit="sendMessage(event)"
. 注意 onsubmit
绑定的函数只能是无参数或者单参数的, 单参数的情况下, 入参是 Event 对象, 这也就是本文的主角. 而 sendMessage
的流程是触发 WebSocket
的 send
函数, 这里 event.preventDefault()
用于阻止 form 元素的默认行为即发送表单, 因此提交表单数据的处理完全就变成了这三行代码:
var input = document.getElementById("messageText")
ws.send(input.value)
input.value = ''
这里简要提一下 websocket 的交互过程:
(1) 前面已提及, 在打开页面时, 客户端与服务端已建立 websocket 连接:
客户端执行代码:
var ws = new WebSocket("ws://localhost:8000/ws");
服务端执行代码:
await websocket.accept()
while True:
data = await websocket.receive_text() # 第一次进入循环, 卡在此处, 等待
(2) 在点击表单提交按钮时:
客户端通过表单绑定的 onsubmit
属性触发 ws.send(input.value)
这样便使得服务端执行代码:
while True:
data = await websocket.receive_text() # 接收到客户端数据
await websocket.send_text(f"Message text was: {data}") # 发送完数据, 无须得知客户端接收到消息, 就立刻进入下一个循环, 等待在 await websocket.receive_text() 处
客户端接收到服务端的数据后, 触发 ws.onmessage
绑定的函数
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
Event
Event Propagation
事件总是从 DOM 的根节点传播到目标节点, 然后再从目标节点传播回 DOM 的根节点, 更书面地说是三个过程: Capturing Phase (捕获过程), At Target (到达目标), Bubbling Phase (冒泡过程), 可以使用 event.eventPhase
来获取这些状态, 注意 event.eventPhase
是只读属性, 且类型是整数:
Event.NONE (0)
Event.CAPTURING_PHASE (1)
Event.AT_TARGET (2)
Event.BUBBLING_PHASE (3)
用一段代码解释:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Event Propagation</title>
</head>
<body>
<div id="outer" style="padding: 50px; background-color: lightblue;">
Outer Div
<div id="inner" style="padding: 50px; background-color: lightcoral;">
Inner Div
<button id="button">Click Me</button>
</div>
</div>
<script>
document.getElementById('outer').addEventListener('click', function(event) {
console.log('Outer Div Clicked (Capturing)', event.eventPhase);
}, true); // Use capturing
document.getElementById('inner').addEventListener('click', function(event) {
console.log('Inner Div Clicked (Capturing)', event.eventPhase);
}, true); // Use capturing
document.getElementById('button').addEventListener('click', function(event) {
console.log('Button Clicked', event.eventPhase);
});
document.getElementById('inner').addEventListener('click', function(event) {
console.log('Inner Div Clicked (Bubbling)', event.eventPhase);
});
document.getElementById('outer').addEventListener('click', function(event) {
console.log('Outer Div Clicked (Bubbling)', event.eventPhase);
});
</script>
</body>
</html>
使用浏览器打开 html 文件, 点击页面按钮, 打开开发者工具, 可以看到这些输出:
Outer Div Clicked (Capturing) 1
test.html:22 Inner Div Clicked (Capturing) 1
test.html:26 Button Clicked 2
test.html:30 Inner Div Clicked (Bubbling) 3
test.html:34 Outer Div Clicked (Bubbling) 3
首先, 这里的 addEventListener 使用的是接口 https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener. 备注: document.getElementById('inner')
是继承自 EventTarget
的.
addEventListener(type, listener)
addEventListener(type, listener, options)
addEventListener(type, listener, useCapture) // useCapture 默认是 false, 即默认只处理冒泡过程的 event
这里的 type
是字符串类型, 取值不能任意取, 只能预定义的选项. listener 可以是单参数输入函数或者是无参数输入函数.
Back to FastAPI & websocket
简述一些关于 Exception 的做法和 websocket manager