点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
原文链接: https://juejin.cn/post/7372523264067764233
作者:积米者
前言
在数字时代的浪潮下,前端、后端和人工智能(AI)的融合发展已经成为技术创新和应用的关键驱动力。它们各自扮演着不同的角色,却又紧密相连,共同构建起了现代软件系统的核心架构。前端负责与用户进行交互,提供直观、友好的界面体验;后端则负责处理业务逻辑、数据存储和与前端的数据交换;而AI则通过智能算法和模型,为系统赋予了学习和决策的能力。
为此,小编将通过一个实战demo,向大家展示如何实现一个融合了前端、后端和AI技术的全栈项目。
让我们一同踏上这场全栈之旅,探索从前端走向后端和AI的无限可能!
项目文件结构
首先我们来了解一下这个项目的文件结构,因为只有一个页面,所以前端代码没有细分css,html,js文件!当然,不要小看这一个页面,触类旁通,学会这一个页面,你就可以走向AI赋能的全栈工程师了!
前端界面
界面长这样,比较简陋,小伙伴不要介意,咱们看看过程!
html代码
<div class="container">
<div class="information">
<h1>AI能力驱动的userData</h1>
<table class="table table-striped" id="user_table">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>家乡</th>
</tr>
</thead>
<tbody>
<!-- <tr>
<td>1</td>
<td>代童</td>
<td>赣州</td>
</tr>
<tr>
<td>2</td>
<td>罗昭发</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相关问题">
</div>
<button type="submit" class="btn btn-default">提交</button>
</form>
</div>
<div class="answer" id="message"></div>
</div>
</div>
这是一个非常简单的html,但是小小的html也有大大的考点(细节):
label(for) 和 input(id) 同名
<label for="questionInput">向AI助理提问</label>
<input type="text" id="questionInput" name="question" class="form-control" placeholder="请输入你想问的users相关问题">
在HTML表单中,使用 <label>
元素与对应的 <input>
元素关联起来有几个重要的功能和目的,特别是为了增强用户体验感,以便更好地服务各种用户群体,包括有视力障碍的用户。
可点击的标签:
label
元素的for
属性值是"questionInput"
,这与input
元素的id
属性值相同。这样,当用户点击“向AI助理提问”标签时,光标会自动跳转到输入框中。这对于那些使用鼠标或触摸屏的用户非常方便。辅助技术支持:
这种关联对于使用屏幕阅读器的用户尤其重要。屏幕阅读器会读取
<label>
元素的内容,并告知用户该输入框的用途。例如,盲人用户在使用屏幕阅读器时会听到“向AI助理提问”,并知道他们应该在此输入框中输入问题。提升表单的可用性:
通过这种关联,用户可以更加直观地理解每个输入框的用途,减少混淆和错误输入。例如,当用户看到“向AI助理提问”时,他们会清楚地知道应该在下面的输入框中输入问题。
HTML5 中的 placeholder
属性
提示性文本:
placeholder
属性提供了一个灰色的提示文本(通常为浅灰色),在输入框为空时显示。这个文本可以帮助用户理解该输入框的预期输入内容。例如,"请输入你想问的相关问题" 或 "Email 地址" 等。自动消失:
当用户开始在输入框中输入内容时,
placeholder
提示文本会自动消失,让用户专注于输入自己的内容。这使得用户体验更加流畅,不需要手动清除提示文本。
当前端小伙伴完成了前端页面的搭建和交互逻辑后,通常会开始等待后端接口来获取动态数据,并将这些数据写入数据库中的user_table。然而,为了不让开发进程受到等待的限制,我们就可以自己编写一个后端服务。通过自己编写后端服务,你可以更好地掌控数据的处理过程和数据结构,更灵活地满足前端的需求,并且加深对整个项目的理解和把握。
后端数据
前置知识
json-server
对于前端开发人员来说,我们可以使用json-server
,它 是一个非常有用的开发工具,可以让你使用一个 JSON 文件快速创建一个完整的 RESTful API 服务器,帮助你在没有后端服务器的情况下进行前端开发和测试。
JSON文件
JSON是一种用于数据交换的轻量级格式。它使用键值对来表示对象,并使用有序列表来表示数组。键是字符串,必须用双引号包围,值可以是字符串、数字、布尔值、数组、对象或 null。
JSON 格式易于人类阅读和编写,也易于机器解析和生成,广泛应用于Web开发中的数据传输、配置文件和数据存储。需要注意的是,JSON 中的键和值必须遵循严格的语法规则,如键名必须是字符串,不能包含多余的逗号等。
backend创建过程
Step 1: 初始化项目
首先,在终端中导航到backend
文件打开终端输入命令(初始化为后端项目)。这将生成一个 package.json
文件,默认配置会使用 -y
标志。
npm init -y
这个命令会创建一个默认的 package.json
文件,内容如下:
{
"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: 创建 JSON 数据文件
在backend
目录下创建一个名为 users.json
的文件。这个文件里的数据将作为我们的模拟数据库。内容如下:
{
"users": [
{
"id": 1,
"name": "蒲熠星",
"hometown": "绵阳"
},
{
"id": 2,
"name": "郭文韬",
"hometown": "青海"
},
{
"id": 3,
"name": "积米者",
"hometown": "潮汕"
},
{
"id": 4,
"name": "小旺车",
"hometown": "丰城"
}
]
}
Step 4: 修改配置文件
在 package.json
文件中添加一个新的脚本命令,在 scripts
部分添加以下内容:
{
"scripts": {
"dev": "json-server users.json"
}
}
该配置的主要目的是可以使用 json-server
启动一个本地服务器,并利用 users.json
文件作为数据源。
Step 5: 运行开发服务器
在终端中运行以下命令来启动 json-server
:
npm run dev
这将会启动一个在 http://localhost:3000
上运行的开发服务器,并使用 users.json
作为数据源。
启动成功后终端显示如下:
在利用 json-server搭建了后端之后,我们可以通过发送 Fetch 请求到 http://localhost:3000/users[1] 接口来获取用户数据,然后将这些数据渲染到用户表格中。接下来,我们需要将用户输入的问题传递给 AI 模型进行处理,实现向 AI 提问的功能。
AI赋能
前置知识
OpenAI调用AI模型
具体如何带调用OpenAI 提供的 API 来调用 AI 模型可以先看看这篇文章
如何将OpenAI集成到项目中 - 掘金 (juejin.cn)[2]
HTTP模块
http
模块是Node.js 一个内置模块,用于创建 HTTP 服务器和客户端。它提供了一组 API,使得在 Node.js 环境中能够方便地处理 HTTP 请求和响应。
以下是关于项目中使用 http
模块的介绍:
创建 HTTP 服务器
使用 http.createServer()
方法可以创建一个 HTTP 服务器。这个方法接受一个回调函数作为参数,回调函数会在每次收到 HTTP 请求时被调用。回调函数的参数包括代表请求的 request
对象和代表响应的 response
对象。
const http = require('http');
const server = http.createServer((req, res) => {
// 处理请求逻辑
});
server.listen(8888, function () {
console.log('server is running')
})
处理 HTTP 请求和响应
在 HTTP 请求的回调函数中,可以通过 req
对象获取请求的信息,通过 res
对象发送响应。
req.url: 请求的 URL。 req.method: 请求的方法,如 GET、POST 等。 req.headers: 请求头部信息。 req.on('data', callback): 用于处理 POST 请求的请求体数据。
URL模块
url
模块是Node.js 一个内置模块,用于处理 URL 字符串的解析、格式化和操作。它提供了一组 API,使得在 Node.js 环境中能够方便地解析和操作 URL 字符串。
以下是关于项目中使用 url
介绍:
URL 查询参数解析
URL 查询参数可以通过两种方式进行解析:使用 url.parse()
方法解析后的 URL 对象的 query
属性,或者使用 url.parse()
方法的第二个参数设置为 true
,将查询参数解析成一个对象。
// 使用 url 模块的 parse 方法解析 HTTP 请求的 URL,将其转换为一个 URL 对象
const urlParams = url.parse(req.url, true);
// 解析后的 URL 对象中的 query 属性包含了 URL 中的查询参数,其中 true 参数表示将查询字符串解析成对象
// 对象解构语法从查询参数对象中提取出 question 和 users 参数的值
const { question, users } = urlParams.query;
Dotenv模块
dotenv
是一个 Node.js 的第三方模块,用于加载环境变量。它的作用是从一个名为 .env
的文件中加载环境变量,并将这些变量添加到 Node.js 的 process.env
对象中,使得在应用程序中可以轻松地访问这些环境变量。
以下是关于项目中使用 dotenv
的介绍:
// 导入 OpenAI 模块
const OpenAI = require('openai');
// 导入 dotenv 模块,并加载环境变量
require('dotenv').config();
// 创建 OpenAI 客户端实例
const client = new OpenAI({
// 从环境变量中获取 OpenAI API 密钥
apiKey: process.env.OPENAI_API_KEY,
// 设置 OpenAI API 的基础 URL
baseURL: 'https://api.chatanywhere.tech/v1'
});
ai_server创建过程
Step 1: 初始化项目
首先,在终端中导航到ai_server
文件打开终端输入命令(初始化为后端项目)。实现AI提问功能本质上也是后端功能
npm init -y
Step 2: 创建 main.js 入口文件
在ai_server
目录下创建一个名为 main.js
的文件。
内容如下:
//引入其他模块
const http = require('http');
const url = require('url');
const OpenAI = require('openai');
require('dotenv').config();
// 创建 OpenAI 客户端实例
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,获取查询参数
const urlParams = url.parse(req.url, true);
const { question, users } = urlParams.query;
// 构建对话 prompt,模拟用户发送问题
const prompt = `${users}请根据以上JSON数据,回答${question}这个问题,如果回答不了就回答不清楚!`;
// 使用 OpenAI 客户端发送对话请求,获取回复
const response = await client.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: "user", content: prompt }],
temperature: 0, // 控制输出的随机性,0表示更确定的输出
});
// 从回复中提取内容
const result = response.choices[0].message.content || '';
// 构造返回给客户端的信息
let info = {
message: result
};
// 设置 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');
// 设置 HTTP 响应头部信息和状态码,并发送 JSON 格式的响应
res.statusCode = 200;
res.setHeader('Content-Type', 'text/json');
res.end(JSON.stringify(info));
});
// 启动 HTTP 服务器,监听指定端口
server.listen(8888, function () {
console.log('server is running');
});
这段代码实现了一个基于 Node.js 的 HTTP 服务器,通过与 OpenAI 的对话生成服务交互,提供了向 AI 助理提问的功能。当客户端发送 HTTP 请求到 http://localhost:8888
端口时,可以通过在请求中传入问题和用户数据参数,服务器会解析这些参数并将其作为对话的内容发送给 OpenAI。OpenAI 使用 GPT-3.5 模型生成对应的回答,并将回答返回给服务器,最终服务器将回答作为 JSON 格式的数据发送回客户端。
这样,通过访问服务器提供的端点,用户可以与 AI 助理进行实时的对话交互,向其提出问题并获取回答,从而在项目中实现了一个简单的 AI 对话功能。
前端JS代码
通过上面实现了后端和AI服务功能后,我们获取到了后端数据和AI功能请求接口,当前端通过向后端数据接口发送请求获取数据,再通过向AI功能接口发送请求实现智能功能,就实现了前端+后端+AI的全栈项目流程。
接下我们再来写一下前端JS逻辑!
向后端发送请求获取用户数据
// 选择表格中 tbody 元素,用于后续操作
const oBody = document.querySelector("#user_table tbody");
// 初始化一个空数组,用于存储从服务器获取的用户数据
let usersData = [];
// 使用 fetch API 从后端获取用户数据
fetch('http://localhost:3000/users')
// 解析响应的数据为 JSON 格式
.then(data => data.json())
// 处理解析后的用户数据
.then(users => {
usersData = users;
// 将用户数据生成 HTML 表格行,并插入到 tbody 中
oBody.innerHTML = users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.hometown}</td>
</tr>
`).join("");
});
细节: 面试官问你可不可以不使用join("")
如果不使用 join("")
,在 oBody.innerHTML
中会发生隐式转换。隐式转换会将数组中的每个元素和字符串相加,然后将结果拼接成一个字符串,形式是每个数组元素之间以逗号分隔。因此,如果不使用 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>
这将会导致渲染错误,因为逗号不是 HTML 表格结构的一部分,而且会导致 DOM 解析器解析错误。
向AI接口发送请求获取AI回复功能
const oMessage = document.querySelector("#message");
const oForm = document.forms['aiForm'];
// 监听表单提交事件
oForm.addEventListener('submit', function (event) {
// 阻止表单默认提交行为
event.preventDefault();
// 获取表单中 name 属性为 "question" 的输入框的值,并去除首尾空格
const question = this["question"].value.trim();
// 如果问题不为空
if (question) {
// 发起 HTTP 请求,向AI服务发送问题和用户数据
fetch(`http://localhost:8888/users?question=${question}&users=${JSON.stringify(usersData)}`)
.then(data => data.json()) // 将响应数据解析为 JSON 格式
.then(res => {
console.log(res); // 打印响应数据到控制台
// 将响应中的消息显示在页面中 id 为 "message" 的元素中
document.querySelector("#message").innerHTML = res.message;
})
}
})
细节: 为什么要用this
const question = this["question"].value.trim();
在事件处理函数中,this
关键字指向触发事件的元素。在这个特定的例子中,事件处理函数是通过 addEventListener
方法添加到表单元素上的,因此事件处理函数内部的 this
指向的就是这个表单元素。
通过 this["question"]
,我们可以方便地获取到表单中名为 "question" 的输入框元素,并获取其值。这种写法在代码可读性和维护性上比直接使用 document.getElementById
更好,因为它显式地表明了我们想要操作当前表单元素的某个属性或者子元素。
最后
至此,AI全栈实现就此完成,讲解表达可能不够清晰,有疑问的小伙伴欢迎评论区留言!
这是一个简单的demo,却是我们走向AI全栈的一大步,继续学习,与君共勉
源码仓库地址:高小庄/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
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一波👍