我有一个应用程序,前端是用Vue实现的,后端使用FastAPI框架。通信是通过websocket实现的。
目前,前端允许用户输入一个术语,该术语被发送到后端以生成自动完成,并对返回 JSON 对象的 URL 执行搜索。其中,我将此 JSON 对象保存在前端文件夹中。之后,后端将相关术语的自动完成数据返回给前端。前端显示 aucomplete 以及 json 数据。
但是,当我进一步研究时,我注意到有一种方法可以将请求 URL 返回的 JSON 发送到 Vue(前端),而不必保存在本地,避免给出不允许执行此过程的错误不止一次。
我当前的代码如下。对于 FastAPI(后端):
@app.websocket("/")
async def predict_question(websocket: WebSocket):
await websocket.accept()
while True:
input_text = await websocket.receive_text()
autocomplete_text = text_gen.generate_text(input_text)
autocomplete_text = re.sub(r"[\([{})\]]", "", autocomplete_text)
autocomplete_text = autocomplete_text.split()
autocomplete_text = autocomplete_text[0:2]
resp = req.get('www.description_url_search_='+input_text+'')
datajson = resp.json()
with open('/home/user/backup/AutoComplete/frontend/src/data.json', 'w', encoding='utf-8') as f:
json.dump(datajson, f, ensure_ascii=False, indent=4)
await websocket.send_text(' '.join(autocomplete_text))
文件 App.vue(前端):
<template>
<div class="main-container">
<h1 style="color:#0072c6;">Title</h1>
<p style="text-align:center; color:#0072c6;">
Version 0.1
<br>
</p>
<Autocomplete />
<br>
</div>
<div style="color:#0072c6;">
<JsonArq />
</div>
<div style="text-align:center;">
<img src="./components/logo-1536.png" width=250 height=200 alt="Logo" >
</div>
</template>
<script>
import Autocomplete from './components/Autocomplete.vue'
import JsonArq from './components/EstepeJSON.vue'
export default {
name: 'App',
components: {
Autocomplete,
JsonArq: JsonArq
}
}
</script>
<style>
.main-container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: 'Fredoka', sans-serif;
}
h1 {
font-size: 3rem;
}
@import url('https://fonts.googleapis.com/css2?family=Fredoka&display=swap');
</style>
组件目录下的Autocomplete.vue文件:
<template>
<div class="pad-container">
<div tabindex="1" @focus="setCaret" class="autocomplete-container">
<span @input="sendText" @keypress="preventInput" ref="editbar" class="editable" contenteditable="true"></span>
<span class="placeholder" contenteditable="false">{{autoComplete}}</span>
</div>
</div>
</template>
<script>
export default {
name: 'Autocomplete',
data: function() {
return {
autoComplete: "",
maxChars: 75,
connection: null
}
},
mounted() {
const url = "ws://localhost:8000/"
this.connection = new WebSocket(url);
this.connection.onopen = () => console.log("connection established");
this.connection.onmessage = this.receiveText;
},
methods: {
setCaret() {
const range= document.createRange()
const sel = window.getSelection();
const parentNode = this.$refs.editbar;
if (parentNode.firstChild == undefined) {
const emptyNode = document.createTextNode("");
parentNode.appendChild(emptyNode);
}
range.setStartAfter(this.$refs.editbar.firstChild);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
},
preventInput(event) {
let prevent = false;
// handles capital letters, numbers, and punctuations input
if (event.key == event.key.toUpperCase()) {
prevent = true;
}
// exempt spacebar input
if (event.code == "Space") {
prevent = false;
}
// handle input overflow
const nChars = this.$refs.editbar.textContent.length;
if (nChars >= this.maxChars) {
prevent = true;
}
if (prevent == true) {
event.preventDefault();
}
},
sendText() {
const inputText = this.$refs.editbar.textContent;
this.connection.send(inputText);
},
receiveText(event) {
this.autoComplete = event.data;
}
}
}
</script>
组件目录下的EstepeJSON.ue文件:
<template>
<div width="80%" v-for="regList in myJson" :key="regList" class="container">
<table>
<thead>
<tr>
<th>Documento</th>
</tr>
</thead>
<tbody>
<tr v-for="countryList in regList[2]" :key="countryList">
<td style="visibility: visible">{{ countryList}}</td>
</tr>
</tbody>
</table>
</div>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"
/>
</template>
<script>
import json from "@/data.json";
export default {
name: "EstepeJson",
data() {
return {
myJson: json,
};
},
};
</script>
URL返回的JSON示例:
[
{
"Title": "SOFT-STARTER",
"Cod": "Produto: 15775931",
"Description": "A soft-starter SSW7000 permite o controle de partida/parada e proteção de motores.",
"Technical_characteristics": ["Corrente nominal", "600 A", "Tensão nominal", "4,16 kV", "Tensão auxiliar", "200-240 V", "Grau de proteção", "IP41", "Certificação", "CE"]
},
{
"Title": "SOFT-STARTER SSW",
"Cod": "Produto: 14223395",
"Description": "A soft-starter SSW7000 permite o controle de partida/parada e proteção de motores de indução trifásicos de média tensão.",
"Technical_characteristics": ["Corrente nominal", "125 A", "Tensão nominal", "6,9 kV", "Tensão auxiliar", "200-240 V", "Grau de proteção", "IP54/NEMA12", "Certificação", "CE"]
}
]
只需使用
json.dumps(mydata)
将数据转换为 json 字符串即可
使用 @Chris 关于 HTTP 的技巧,经过一些研究,我设法解决了我的问题。以下是决议。
在我的后端 FastAPI 文件中,我实现了 HTTPX 异步(来自 @Chris 的提示)。返回 JSON 后,我将自动完成术语添加到 JSON 的第一个位置。从而将带有自动完成功能和 HTTPX 数据的 JSON 返回给 Vue(前端)。
文件快速API:
async def predict_question(websocket: WebSocket):
await manager.connect(websocket)
input_text = await websocket.receive_text()
if not input_text:
await manager.send_personal_message(json.dumps([]), websocket)
else:
autocomplete_text = text_gen.generate_text(input_text)
autocomplete_text = re.sub(r"[\([{})\]]", "", autocomplete_text)
autocomplete_text = autocomplete_text.split()
autocomplete_text = autocomplete_text[0:2]
resp = client.build_request("GET", 'www.description_url_search_='+input_text+'')
r = await client.send(resp)
datajson = r.json()
datajson.insert(0, ' '.join(autocomplete_text))
await manager.send_personal_message(json.dumps(datajson), websocket)
在 Autocomplete.vue 文件中我做了一些小的更改。 首先我将EstepeJson.vue文件合并到Autocomplete.vue中,特别是html中的json读取部分。 其次,在 data: function(){} 中我又添加了一个对象,名为 myJson: []。
第三,在receiveText方法中我改变了从websocket接收数据的方式。从现在开始,我有 JSON.parse 将 event.data 转换为 JSON。然后我使用 shift 方法获取 json 中的第一个位置并从文件中删除该数据。最后,将 json 返回到 myjson 变量。
文件自动完成.vue:
<template>
<div class="pad-container">
<div tabindex="1" @focus="setCaret" class="autocomplete-container">
<span @input="sendText" @keypress="preventInput" ref="editbar" class="editable" contenteditable="true"></span>
<span class="placeholder" data-ondeleteId="#editx" contenteditable="false">{{autoComplete}}</span>
</div>
</div>
<div v-for="regList in myJson" :key="regList" class="container" >
<table>
<thead>
<tr>
<th>Documento</th>
</tr>
</thead>
<tbody>
<tr v-for="countryList in regList[2]" :key="countryList">
<td style="visibility: visible">{{ countryList}}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
...
data: function() {
return {
autoComplete: "",
maxChars: 75,
connection: null,
myJson: []
}
},
.....
...
receiveText(event) {
let result = JSON.parse(event.data)
this.autoComplete = result.shift();
this.myJson = result
}
</script>
首先,我强烈建议使用
requests
,而不是使用 Python httpx
库在 FastAPI 应用程序中发出 HTTP 请求(这 会阻止事件循环- 有关更多详细信息,请参阅这个答案) ,它还提供了
async
API,请查看 thisanswer 和 thisanswer 了解更多详细信息和工作示例。
其次,要将
data
作为 JSON 发送,您需要使用 await websocket.send_json(data)
,如 Starlette 文档 中所述。如 Starlette 的 websockets
源代码所示,Starlette/FastAPI 将使用 text = json.dumps(data)
,以便在调用 data
函数时序列化您正在发送的 send_json()
。因此,您需要传递一个 Python dict
对象。与 requests
类似,在 httpx
中,您可以调用响应对象上的 .json()
方法来获取作为 dict
对象的响应数据,然后将该字典传递给 send_json()
函数。
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from websockets.exceptions import ConnectionClosed
from fastapi.responses import HTMLResponse
from contextlib import asynccontextmanager
import httpx
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>
"""
@asynccontextmanager
async def lifespan(app: FastAPI):
# Initialise the Client on startup and add it to the state
async with httpx.AsyncClient() as client:
yield {'client': client}
# The Client closes on shutdown
@app.get('/')
async def get():
return HTMLResponse(html)
@app.websocket('/ws')
async def websocket_endpoint(request: Request, websocket: WebSocket):
await websocket.accept()
client = request.state.client
try:
while True:
data = await websocket.receive_text()
# here, use httpx to issue a request, as demonstrated in the linked answers above
r = await client.get('http://httpbin.org/get')
await websocket.send_json(r.json())
except (WebSocketDisconnect, ConnectionClosed):
print("Client disconnected")