一個實(shí)戰(zhàn)Demo,讓你理解"前端+Node后端+AI"的全棧項(xiàng)目如何實(shí)現(xiàn)
共 15441字,需瀏覽 31分鐘
·
2024-12-03 22:39
前言
在數(shù)字時代的浪潮下,前端、后端和人工智能(AI)的融合發(fā)展已經(jīng)成為技術(shù)創(chuàng)新和應(yīng)用的關(guān)鍵驅(qū)動力。它們各自扮演著不同的角色,卻又緊密相連,共同構(gòu)建起了現(xiàn)代軟件系統(tǒng)的核心架構(gòu)。前端負(fù)責(zé)與用戶進(jìn)行交互,提供直觀、友好的界面體驗(yàn);后端則負(fù)責(zé)處理業(yè)務(wù)邏輯、數(shù)據(jù)存儲和與前端的數(shù)據(jù)交換;而AI則通過智能算法和模型,為系統(tǒng)賦予了學(xué)習(xí)和決策的能力。
為此,小編將通過一個實(shí)戰(zhàn)demo,向大家展示如何實(shí)現(xiàn)一個融合了前端、后端和AI技術(shù)的全棧項(xiàng)目。
讓我們一同踏上這場全棧之旅,探索從前端走向后端和AI的無限可能!
項(xiàng)目文件結(jié)構(gòu)
首先我們來了解一下這個項(xiàng)目的文件結(jié)構(gòu),因?yàn)橹挥幸粋€頁面,所以前端代碼沒有細(xì)分css,html,js文件!當(dāng)然,不要小看這一個頁面,觸類旁通,學(xué)會這一個頁面,你就可以走向AI賦能的全棧工程師了!
前端界面
界面長這樣,比較簡陋,小伙伴不要介意,咱們看看過程!
html代碼
<div class="container">
<div class="information">
<h1>AI能力驅(qū)動的userData</h1>
<table class="table table-striped" id="user_table">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>家鄉(xiāng)</th>
</tr>
</thead>
<tbody>
<!-- <tr>
<td>1</td>
<td>代童</td>
<td>贛州</td>
</tr>
<tr>
<td>2</td>
<td>羅昭發(fā)</td>
<td>贛州</td>
</tr> -->
</tbody>
</table>
<div class="send">
<form name="aiForm">
<div class="form-group">
<label for="questionInput">向AI助理提問</label>
<input type="text" name="question" class="form-control" id="questionInput" placeholder="請輸入你想問的users相關(guān)問題">
</div>
<button type="submit" class="btn btn-default">提交</button>
</form>
</div>
<div class="answer" id="message"></div>
</div>
</div>
這是一個非常簡單的html,但是小小的html也有大大的考點(diǎn)(細(xì)節(jié)):
label(for) 和 input(id) 同名
<label for="questionInput">向AI助理提問</label>
<input type="text" id="questionInput" name="question" class="form-control" placeholder="請輸入你想問的users相關(guān)問題">
在HTML表單中,使用 <label> 元素與對應(yīng)的 <input> 元素關(guān)聯(lián)起來有幾個重要的功能和目的,特別是為了增強(qiáng)用戶體驗(yàn)感,以便更好地服務(wù)各種用戶群體,包括有視力障礙的用戶。
可點(diǎn)擊的標(biāo)簽:
label元素的for屬性值是"questionInput",這與input元素的id屬性值相同。這樣,當(dāng)用戶點(diǎn)擊“向AI助理提問”標(biāo)簽時,光標(biāo)會自動跳轉(zhuǎn)到輸入框中。這對于那些使用鼠標(biāo)或觸摸屏的用戶非常方便。輔助技術(shù)支持:
這種關(guān)聯(lián)對于使用屏幕閱讀器的用戶尤其重要。屏幕閱讀器會讀取
<label>元素的內(nèi)容,并告知用戶該輸入框的用途。例如,盲人用戶在使用屏幕閱讀器時會聽到“向AI助理提問”,并知道他們應(yīng)該在此輸入框中輸入問題。提升表單的可用性:
通過這種關(guān)聯(lián),用戶可以更加直觀地理解每個輸入框的用途,減少混淆和錯誤輸入。例如,當(dāng)用戶看到“向AI助理提問”時,他們會清楚地知道應(yīng)該在下面的輸入框中輸入問題。
HTML5 中的 placeholder 屬性
提示性文本:
placeholder屬性提供了一個灰色的提示文本(通常為淺灰色),在輸入框?yàn)榭諘r顯示。這個文本可以幫助用戶理解該輸入框的預(yù)期輸入內(nèi)容。例如,"請輸入你想問的相關(guān)問題" 或 "Email 地址" 等。自動消失:
當(dāng)用戶開始在輸入框中輸入內(nèi)容時,
placeholder提示文本會自動消失,讓用戶專注于輸入自己的內(nèi)容。這使得用戶體驗(yàn)更加流暢,不需要手動清除提示文本。
當(dāng)前端小伙伴完成了前端頁面的搭建和交互邏輯后,通常會開始等待后端接口來獲取動態(tài)數(shù)據(jù),并將這些數(shù)據(jù)寫入數(shù)據(jù)庫中的user_table。然而,為了不讓開發(fā)進(jìn)程受到等待的限制,我們就可以自己編寫一個后端服務(wù)。通過自己編寫后端服務(wù),你可以更好地掌控?cái)?shù)據(jù)的處理過程和數(shù)據(jù)結(jié)構(gòu),更靈活地滿足前端的需求,并且加深對整個項(xiàng)目的理解和把握。
后端數(shù)據(jù)
前置知識
json-server
對于前端開發(fā)人員來說,我們可以使用json-server,它 是一個非常有用的開發(fā)工具,可以讓你使用一個 JSON 文件快速創(chuàng)建一個完整的 RESTful API 服務(wù)器,幫助你在沒有后端服務(wù)器的情況下進(jìn)行前端開發(fā)和測試。
JSON文件
JSON是一種用于數(shù)據(jù)交換的輕量級格式。它使用鍵值對來表示對象,并使用有序列表來表示數(shù)組。鍵是字符串,必須用雙引號包圍,值可以是字符串、數(shù)字、布爾值、數(shù)組、對象或 null。
JSON 格式易于人類閱讀和編寫,也易于機(jī)器解析和生成,廣泛應(yīng)用于Web開發(fā)中的數(shù)據(jù)傳輸、配置文件和數(shù)據(jù)存儲。需要注意的是,JSON 中的鍵和值必須遵循嚴(yán)格的語法規(guī)則,如鍵名必須是字符串,不能包含多余的逗號等。
backend創(chuàng)建過程
Step 1: 初始化項(xiàng)目
首先,在終端中導(dǎo)航到backend文件打開終端輸入命令(初始化為后端項(xiàng)目)。這將生成一個 package.json 文件,默認(rèn)配置會使用 -y 標(biāo)志。
npm init -y
這個命令會創(chuàng)建一個默認(rèn)的 package.json 文件,內(nèi)容如下:
{
"name": "ai_server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^16.4.5",
"openai": "^4.47.1"
}
}
Step 2: 安裝 json-server
在backend后端中安裝 json-server庫。
npm i json-server
Step 3: 創(chuàng)建 JSON 數(shù)據(jù)文件
在backend目錄下創(chuàng)建一個名為 users.json 的文件。這個文件里的數(shù)據(jù)將作為我們的模擬數(shù)據(jù)庫。內(nèi)容如下:
{
"users": [
{
"id": 1,
"name": "蒲熠星",
"hometown": "綿陽"
},
{
"id": 2,
"name": "郭文韜",
"hometown": "青海"
},
{
"id": 3,
"name": "積米者",
"hometown": "潮汕"
},
{
"id": 4,
"name": "小旺車",
"hometown": "豐城"
}
]
}
Step 4: 修改配置文件
在 package.json 文件中添加一個新的腳本命令,在 scripts 部分添加以下內(nèi)容:
{
"scripts": {
"dev": "json-server users.json"
}
}
該配置的主要目的是可以使用 json-server 啟動一個本地服務(wù)器,并利用 users.json 文件作為數(shù)據(jù)源。
Step 5: 運(yùn)行開發(fā)服務(wù)器
在終端中運(yùn)行以下命令來啟動 json-server:
npm run dev
這將會啟動一個在 http://localhost:3000 上運(yùn)行的開發(fā)服務(wù)器,并使用 users.json 作為數(shù)據(jù)源。
啟動成功后終端顯示如下:
在利用 json-server搭建了后端之后,我們可以通過發(fā)送 Fetch 請求到 http://localhost:3000/users[1] 接口來獲取用戶數(shù)據(jù),然后將這些數(shù)據(jù)渲染到用戶表格中。接下來,我們需要將用戶輸入的問題傳遞給 AI 模型進(jìn)行處理,實(shí)現(xiàn)向 AI 提問的功能。
AI賦能
前置知識
OpenAI調(diào)用AI模型
具體如何帶調(diào)用OpenAI 提供的 API 來調(diào)用 AI 模型可以先看看這篇文章
如何將OpenAI集成到項(xiàng)目中 - 掘金 (juejin.cn)[2]
HTTP模塊
http 模塊是Node.js 一個內(nèi)置模塊,用于創(chuàng)建 HTTP 服務(wù)器和客戶端。它提供了一組 API,使得在 Node.js 環(huán)境中能夠方便地處理 HTTP 請求和響應(yīng)。
以下是關(guān)于項(xiàng)目中使用 http 模塊的介紹:
創(chuàng)建 HTTP 服務(wù)器
使用 http.createServer() 方法可以創(chuàng)建一個 HTTP 服務(wù)器。這個方法接受一個回調(diào)函數(shù)作為參數(shù),回調(diào)函數(shù)會在每次收到 HTTP 請求時被調(diào)用?;卣{(diào)函數(shù)的參數(shù)包括代表請求的 request 對象和代表響應(yīng)的 response 對象。
const http = require('http');
const server = http.createServer((req, res) => {
// 處理請求邏輯
});
server.listen(8888, function () {
console.log('server is running')
})
處理 HTTP 請求和響應(yīng)
在 HTTP 請求的回調(diào)函數(shù)中,可以通過 req 對象獲取請求的信息,通過 res 對象發(fā)送響應(yīng)。
req.url: 請求的 URL。 req.method: 請求的方法,如 GET、POST 等。 req.headers: 請求頭部信息。 req.on('data', callback): 用于處理 POST 請求的請求體數(shù)據(jù)。
URL模塊
url 模塊是Node.js 一個內(nèi)置模塊,用于處理 URL 字符串的解析、格式化和操作。它提供了一組 API,使得在 Node.js 環(huán)境中能夠方便地解析和操作 URL 字符串。
以下是關(guān)于項(xiàng)目中使用 url介紹:
URL 查詢參數(shù)解析
URL 查詢參數(shù)可以通過兩種方式進(jìn)行解析:使用 url.parse() 方法解析后的 URL 對象的 query 屬性,或者使用 url.parse() 方法的第二個參數(shù)設(shè)置為 true,將查詢參數(shù)解析成一個對象。
// 使用 url 模塊的 parse 方法解析 HTTP 請求的 URL,將其轉(zhuǎn)換為一個 URL 對象
const urlParams = url.parse(req.url, true);
// 解析后的 URL 對象中的 query 屬性包含了 URL 中的查詢參數(shù),其中 true 參數(shù)表示將查詢字符串解析成對象
// 對象解構(gòu)語法從查詢參數(shù)對象中提取出 question 和 users 參數(shù)的值
const { question, users } = urlParams.query;
Dotenv模塊
dotenv 是一個 Node.js 的第三方模塊,用于加載環(huán)境變量。它的作用是從一個名為 .env 的文件中加載環(huán)境變量,并將這些變量添加到 Node.js 的 process.env 對象中,使得在應(yīng)用程序中可以輕松地訪問這些環(huán)境變量。
以下是關(guān)于項(xiàng)目中使用 dotenv的介紹:
// 導(dǎo)入 OpenAI 模塊
const OpenAI = require('openai');
// 導(dǎo)入 dotenv 模塊,并加載環(huán)境變量
require('dotenv').config();
// 創(chuàng)建 OpenAI 客戶端實(shí)例
const client = new OpenAI({
// 從環(huán)境變量中獲取 OpenAI API 密鑰
apiKey: process.env.OPENAI_API_KEY,
// 設(shè)置 OpenAI API 的基礎(chǔ) URL
baseURL: 'https://api.chatanywhere.tech/v1'
});
ai_server創(chuàng)建過程
Step 1: 初始化項(xiàng)目
首先,在終端中導(dǎo)航到ai_server文件打開終端輸入命令(初始化為后端項(xiàng)目)。實(shí)現(xiàn)AI提問功能本質(zhì)上也是后端功能
npm init -y
Step 2: 創(chuàng)建 main.js 入口文件
在ai_server目錄下創(chuàng)建一個名為 main.js 的文件。
內(nèi)容如下:
//引入其他模塊
const http = require('http');
const url = require('url');
const OpenAI = require('openai');
require('dotenv').config();
// 創(chuàng)建 OpenAI 客戶端實(shí)例
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: 'https://api.chatanywhere.tech/v1'
})
const server = http.createServer(async function (req, res) {
// 解析請求的 URL,獲取查詢參數(shù)
const urlParams = url.parse(req.url, true);
const { question, users } = urlParams.query;
// 構(gòu)建對話 prompt,模擬用戶發(fā)送問題
const prompt = `${users}請根據(jù)以上JSON數(shù)據(jù),回答${question}這個問題,如果回答不了就回答不清楚!`;
// 使用 OpenAI 客戶端發(fā)送對話請求,獲取回復(fù)
const response = await client.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: "user", content: prompt }],
temperature: 0, // 控制輸出的隨機(jī)性,0表示更確定的輸出
});
// 從回復(fù)中提取內(nèi)容
const result = response.choices[0].message.content || '';
// 構(gòu)造返回給客戶端的信息
let info = {
message: result
};
// 設(shè)置 CORS 頭部信息,允許跨域請求
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 設(shè)置 HTTP 響應(yīng)頭部信息和狀態(tài)碼,并發(fā)送 JSON 格式的響應(yīng)
res.statusCode = 200;
res.setHeader('Content-Type', 'text/json');
res.end(JSON.stringify(info));
});
// 啟動 HTTP 服務(wù)器,監(jiān)聽指定端口
server.listen(8888, function () {
console.log('server is running');
});
這段代碼實(shí)現(xiàn)了一個基于 Node.js 的 HTTP 服務(wù)器,通過與 OpenAI 的對話生成服務(wù)交互,提供了向 AI 助理提問的功能。當(dāng)客戶端發(fā)送 HTTP 請求到 http://localhost:8888 端口時,可以通過在請求中傳入問題和用戶數(shù)據(jù)參數(shù),服務(wù)器會解析這些參數(shù)并將其作為對話的內(nèi)容發(fā)送給 OpenAI。OpenAI 使用 GPT-3.5 模型生成對應(yīng)的回答,并將回答返回給服務(wù)器,最終服務(wù)器將回答作為 JSON 格式的數(shù)據(jù)發(fā)送回客戶端。
這樣,通過訪問服務(wù)器提供的端點(diǎn),用戶可以與 AI 助理進(jìn)行實(shí)時的對話交互,向其提出問題并獲取回答,從而在項(xiàng)目中實(shí)現(xiàn)了一個簡單的 AI 對話功能。
前端JS代碼
通過上面實(shí)現(xiàn)了后端和AI服務(wù)功能后,我們獲取到了后端數(shù)據(jù)和AI功能請求接口,當(dāng)前端通過向后端數(shù)據(jù)接口發(fā)送請求獲取數(shù)據(jù),再通過向AI功能接口發(fā)送請求實(shí)現(xiàn)智能功能,就實(shí)現(xiàn)了前端+后端+AI的全棧項(xiàng)目流程。
接下我們再來寫一下前端JS邏輯!
向后端發(fā)送請求獲取用戶數(shù)據(jù)
// 選擇表格中 tbody 元素,用于后續(xù)操作
const oBody = document.querySelector("#user_table tbody");
// 初始化一個空數(shù)組,用于存儲從服務(wù)器獲取的用戶數(shù)據(jù)
let usersData = [];
// 使用 fetch API 從后端獲取用戶數(shù)據(jù)
fetch('http://localhost:3000/users')
// 解析響應(yīng)的數(shù)據(jù)為 JSON 格式
.then(data => data.json())
// 處理解析后的用戶數(shù)據(jù)
.then(users => {
usersData = users;
// 將用戶數(shù)據(jù)生成 HTML 表格行,并插入到 tbody 中
oBody.innerHTML = users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.hometown}</td>
</tr>
`).join("");
});
細(xì)節(jié): 面試官問你可不可以不使用join("")
如果不使用 join(""),在 oBody.innerHTML 中會發(fā)生隱式轉(zhuǎn)換。隱式轉(zhuǎn)換會將數(shù)組中的每個元素和字符串相加,然后將結(jié)果拼接成一個字符串,形式是每個數(shù)組元素之間以逗號分隔。因此,如果不使用 join(""),則 oBody.innerHTML 的值將是一個以逗號分隔的字符串,每個元素都是一個包含 <tr> 元素的字符串。
<tr>
<td>1</td>
<td>蒲熠星</td>
<td>綿陽</td>
</tr>,
<tr>
<td>2</td>
<td>郭文韜</td>
<td>青海</td>
</tr>,
<tr>
<td>3</td>
<td>積米者</td>
<td>潮汕</td>
</tr>
這將會導(dǎo)致渲染錯誤,因?yàn)槎禾柌皇?HTML 表格結(jié)構(gòu)的一部分,而且會導(dǎo)致 DOM 解析器解析錯誤。
向AI接口發(fā)送請求獲取AI回復(fù)功能
const oMessage = document.querySelector("#message");
const oForm = document.forms['aiForm'];
// 監(jiān)聽表單提交事件
oForm.addEventListener('submit', function (event) {
// 阻止表單默認(rèn)提交行為
event.preventDefault();
// 獲取表單中 name 屬性為 "question" 的輸入框的值,并去除首尾空格
const question = this["question"].value.trim();
// 如果問題不為空
if (question) {
// 發(fā)起 HTTP 請求,向AI服務(wù)發(fā)送問題和用戶數(shù)據(jù)
fetch(`http://localhost:8888/users?question=${question}&users=${JSON.stringify(usersData)}`)
.then(data => data.json()) // 將響應(yīng)數(shù)據(jù)解析為 JSON 格式
.then(res => {
console.log(res); // 打印響應(yīng)數(shù)據(jù)到控制臺
// 將響應(yīng)中的消息顯示在頁面中 id 為 "message" 的元素中
document.querySelector("#message").innerHTML = res.message;
})
}
})
細(xì)節(jié): 為什么要用this
const question = this["question"].value.trim();
在事件處理函數(shù)中,this 關(guān)鍵字指向觸發(fā)事件的元素。在這個特定的例子中,事件處理函數(shù)是通過 addEventListener 方法添加到表單元素上的,因此事件處理函數(shù)內(nèi)部的 this 指向的就是這個表單元素。
通過 this["question"],我們可以方便地獲取到表單中名為 "question" 的輸入框元素,并獲取其值。這種寫法在代碼可讀性和維護(hù)性上比直接使用 document.getElementById 更好,因?yàn)樗@式地表明了我們想要操作當(dāng)前表單元素的某個屬性或者子元素。
最后
至此,AI全棧實(shí)現(xiàn)就此完成,講解表達(dá)可能不夠清晰,有疑問的小伙伴歡迎評論區(qū)留言!
這是一個簡單的demo,卻是我們走向AI全棧的一大步,繼續(xù)學(xué)習(xí),與君共勉
源碼倉庫地址:高小莊/fullstack-aiusers (gitee.com)[3]
http://localhost:3000/users: http://localhost:3000/users
[2]https://juejin.cn/post/7368820207593570313: https://juejin.cn/post/7368820207593570313
[3]https://gitee.com/gaoxiaozhuang11111/fullstack-aiusers: https://gitee.com/gaoxiaozhuang11111/fullstack-aiusers
