动机、参考资料、涉及内容

(1) 先记录 javascript 事件相关的内容 (websocket, event, fastapi example)

(2) python 的 logging 模块

(3) langchain 的 astream_eventsastream_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 的流程是触发 WebSocketsend 函数, 这里 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