花1块钱让你的网站支持 ChatGPT

news2024/11/29 20:52:10

点击上方卡片“前端司南”关注我

您的关注意义重大

2a472984a97b642955b514c21bae75e4.png

原创@前端司南

最近 ChatGPT 在技术圈子可太火了,票圈也被刷屏。我也决定来凑个热闹,给自己的博客加一个 ChatGPT 对话功能。

先附上体验链接[1],源码在底部也可以找到。

27c5337d92dd30a6d9637459e8339395.gif

体验 ChatGPT

ChatGPT[2] 是 Open AI 训练的一个 AI 对话模型,可以支持在多种场景下进行智能对话。

98930765714a600c96bfc01e04ca6141.png

想体验 ChatGPT,首先要注册[3]账户,但是这个产品在国内网络并不能直接用,需要自行解决网络问题。

3b6497fe82b514e9bca53e6e07a16efd.png

搞定网络问题后,注册时会让你提供邮箱验证,

e34d2b83740878497b4a50e334eeaf88.png

接着要验证手机号,但是很遗憾国内手机号用不了。

594b7ac959fca6abf7bb29d050d4b7a8.png

你也可以选择用 Google 账号登录,但是最终还是要验证手机号。

所以我们需要先找一个国外的能接收短信验证码的手机号,此时可以上SMS-ACTIVATE[4]

这是一个在这个星球上数以百万计的服务中注册帐户的网站。 我们提供世界上大多数国家的虚拟号码,以便您可以在线接收带有确认代码的短信。在我们的服务中,还有虚拟号码的长期租赁,转发连接,电话验证等等。

SMS-ACTIVATE 上的价格是卢布,我们需要使用手机号码做短信验证,经过查询可以发现,最便宜的是印度地区的手机号,零售价格是 10.5 卢布。

e29189dfa4a16fb5f2db366ebe3e5b13.png

按照汇率算了一下,大概是1块多RMB。

be8f76d05c69e444f2dccb9ad1e16c28.png

SMS-ACTIVATE 支持用某宝充值,我买了一个印度号,就可以收到来自 Open AI 的验证码了。

cb9c7ff355069066544c7ccc62a5debd.png

注意,这个号码只是租用,是有期限的,所以我们要抓紧时间把注册流程搞完,20分钟过了,这个号码就不是你的了。

注册完 Open AI 的账号后,就可以到 ChatGPT 的 Web工作台[5]体验一把 AI 对话了。

fc0ff5057bb4d98568b20c0da91577a2.gif

通过 API 接入 Open AI 能力

体验完 ChatGPT 之后,对于搞技术的我们来说,可能会想着怎么把这个能力接入到自己的产品中。

快速上手

ChatGPT 是 Open AI 训练出来的模型,Open AI 也提供了 API 给开发者们调用,文档[6]和案例[7]也比较全面。

机器学习很重要的一个步骤就是调参,但对于前端开发者来说,大部分人肯定是不知道怎么调参的,那我们就参考官方提供的最契合我们需求的案例就好了,这个 Chat 的案例就非常符合我们的场景需要。

db4e4efda574e2a5462d4ef4133bfd06.png

官方有提供一个 nodejs 的 starter,我们可以基于此快速上手测试一把。

git clone https://github.com/openai/openai-quickstart-node.git

它的核心代码是这么一部分,其中用到的openai[8]是官方封装好的 NodeJS Library。

const completion = await openai.createCompletion({
    model: "text-davinci-003",
    prompt: '提问内容',
    temperature: 0.9,
    max_tokens: 150,
    top_p: 1,
    frequency_penalty: 0,
    presence_penalty: 0.6,
});

在调用 API 之前需要先在你的 Open AI 账户中生成一个 API Key[9]

目前官方给到的免费额度是 18 刀,超过的部分就需要自己付费了。计费是根据 Token 来算的,至于什么是 Token,可以参考Key concepts[10]

d88b19859f7bddacb1f3e1165a640811.png

我们把上面那个 Chat 案例的参数拿过来直接用上,基本上也有个七八分 AI 回答问题的样子了,这个可以自己去试一试效果,并不复杂。

接着就是研究一下怎么把这个 starter 的关键代码集成到自己的产品中。

产品分析

我之前有在自己的博客中做过一个简单的 WebSocket 聊天功能[11],而在 AI 对话这个需求中,前端 UI 部分基本上可以参考着WebSocket 聊天功能改改,工作量不是很大,主要工作量还是在前后端的逻辑和对接上面。

97e1d9429adee9340c454863c143e591.png

ChatGPT 的这个产品模式,它不是一个常规的 WebSocket 全双工对话,而是像我们平常调接口一样,发生用户输入后,客户端发送请求到服务端,等待服务端响应,最后反馈给用户,它仅仅是从界面上看起来像是聊天,实际上不是一个标准的聊天过程。所以前后端交互主要还是靠 HTTP 接口对接。

核心要素 Prompt

openai.createCompletion调用时有一个很重要的参数prompt,它是对话的上下文信息,只有这个信息足够完整,AI 才能正确地做出反馈。

举个例子,假设在对话过程中有2个回合。

// 回合1
你:爱因斯坦是谁?
AI: 爱因斯坦(Albert Einstein)是20世纪最重要的物理学家,他被誉为“时空之父”。他发现了相对论,并获得诺贝尔物理学奖。

第一个回合中,传参prompt爱因斯坦是谁?,机器人很好理解,马上能给出符合实际的回复。

// 回合2
你:他做了什么贡献?
AI: 他为社会做出了许多贡献,例如改善公共卫生、建立教育基础设施、提高农业生产能力、促进经济发展等。

第二个回合传参prompt他做了什么贡献?,看到机器人的答复,你可能会觉得有点离谱,因为这根本就是牛头不对马嘴。但是仔细想想,这是因为机器人不知道上下文信息,所以机器人不能理解代表的含义,只能通过他做了什么贡献?整句话去推测,所以从结果上看就是符合语言的逻辑,但是不符合我们给出的语境。

如果我们把第二个回合的传参prompt改成你: 爱因斯坦是谁?\nAI: 爱因斯坦(Albert Einstein)是20世纪最重要的物理学家,他被誉为“时空之父”。他发现了相对论,并获得诺贝尔物理学奖。\n你: 他做了什么贡献?\nAI:,机器人就能够理解上下文信息,给出接下来的符合逻辑的答复。

// 改进后的回合2
你:他做了什么贡献?
AI: 爱因斯坦对科学有着重大的贡献,他发明了相对论,改变了人们对世界、物理定律和宇宙的认识,并为量子力学奠定了基础。他还发现了...

所以,我们的初步结论是:prompt参数应该包含此次对话主题的较完整内容,才能保证 AI 给出的下一次回答符合我们的基本认知。

前后端交互

对于前端来说,我们通常关注的是,我给后端发了什么数据,后端反馈给我什么数据。所以,前端关注点之一就是用户的输入,用上面的例子说,爱因斯坦是谁?他做了什么贡献?这两个内容,应该分别作为前端两次请求的参数。而且,对于前端来说,我们也不需要考虑后端传给 Open AI 的prompt是不是完整,只要把用户输入的内容合理地传给后端就够了。

对于后端来说,我们要关注 session 问题,每个用户应该有属于自己和 AI 的私密对话空间,不能和其他的用户对话串了数据,这个可以基于 session 实现。前端每次传过来的信息只有简单的用户输入,而后端要关注与 Open AI 的对接过程,结合用户的输入以及会话中保留的一些信息,合并成一个完整的prompt传给 Open AI,这样才能得到正常的对话过程。

所以基本的流程应该是这个样子:

1d67a93d6d8dd9e75642274b236b97f1.png

我们根据这个流程输出第一版代码。

后端V1版本代码

router.get('/chat-v1', async function(req, res, next) {
    // 取得用户输入
    const wd = req.query.wd;
    // 构造 prompt 参数
    if (!req.session.chatgptSessionPrompt) {
        req.session.chatgptSessionPrompt = ''
    }
    const prompt = req.session.chatgptSessionPrompt + `\n提问:` + wd + `\nAI:`
    try {
        const completion = await openai.createCompletion({
            model: "text-davinci-003",
            prompt,
            temperature: 0.9,
            max_tokens: 150,
            top_p: 1,
            frequency_penalty: 0,
            presence_penalty: 0.6,
            stop: ["\n提问:", "\nAI:"],
        });
        // 调用 Open AI 成功后,更新 session
        req.session.chatgptSessionPrompt = prompt + completion.data
        // 返回结果
        res.status(200).json({
            code: '0',
            result: completion.data.choices[0].text
        });
    } catch (error) {
        console.error(error)
        res.status(500).json({
            message: "Open AI 调用异常"
        });
    }
});

前端V1版本关键代码

const sendChatContentV1 = async () => {
    // 先显示自己说的话
    msgList.value.push({
        time: format(new Date(), "HH:mm:ss"),
        user: "我说",
        content: chatForm.chatContent,
        type: "mine",
        customClass: "mine",
    });
    loading.value = true;
    try {
        // 调 chat-v1 接口,等结果
        const { result } = await chatgptService.chatV1({ wd: chatForm.chatContent });
        // 显示 AI 的答复
        msgList.value.push({
            time: format(new Date(), "HH:mm:ss"),
            user: "Chat AI",
            content: result,
            type: "others",
            customClass: "others",
        });
    } finally {
        loading.value = false;
    }
};

fdffd50bc37c71ee5516809988257a2a.gif

基本的对话能力已经有了,但是最明显的缺点就是一个回合等得太久了,我们希望他速度更快一点,至少在交互上看起来快一点。

流式输出(服务器推 + EventSource)

还好 Open AI 也支持 stream 流式输出,在前端可以配合 EventSource 一起用。

You can also set the stream parameter to true for the API to stream back text (as data-only server-sent events[12]).

基本的数据流是这个样子的:

678543bc20ce51913f0ec4b7815e0179.png

后端改造如下:

router.get('/chat-v2', async function(req, res, next) {
    // ...省略部分代码
    try {
        const completion = await openai.createCompletion({
            // ...省略部分代码
            // 增加了 stream 参数
            stream: true
        }, { responseType: 'stream' });
        // 设置响应的 content-type 为 text/event-stream
        res.setHeader("content-type", "text/event-stream")
        // completion.data 是一个 ReadableStream,res 是一个 WritableStream,可以通过 pipe 打通管道,流式输出给前端。
        completion.data.pipe(res)
    }
    // ...省略部分代码
});

前端放弃使用 axios 发起 HTTP 请求,而是改用 EventSource。

const sendChatContent = async () => {
    // ...省略部分代码
    // 先显示自己说的话
    msgList.value.push({
        time: format(new Date(), "HH:mm:ss"),
        user: "我说",
        content: chatForm.chatContent,
        type: "mine",
        customClass: "mine",
    });
    
    // 通过 EventSource 取数据
    const es = new EventSource(`/api/chatgpt/chat?wd=${chatForm.chatContent}`);

    // 记录 AI 答复的内容
    let content = "";
    
    // ...省略部分代码

    es.onmessage = (e) => {
        if (e.data === "[DONE]") {
            // [DONE] 标志数据结束,调用 feedback 反馈给服务器
            chatgptService.feedback(content);
            es.close();
            loading.value = false;
            updateScrollTop();
            return;
        }
        // 从数据中取出文本
        const text = JSON.parse(e.data).choices[0].text;
        if (text) {
            if (!content) {
                // 第一条数据来了,先显示
                msgList.value.push({
                    time: format(new Date(), "HH:mm:ss"),
                    user: "Chat AI",
                    content: text,
                    type: "others",
                    customClass: "others",
                });
                // 再拼接
                content += text;
            } else {
                // 先拼接
                content += text;
                // 再更新内容,实现打字机效果
                msgList.value[msgList.value.length - 1].content = content;
            }
        }
    };
};

从代码中可以发现前端在 EventSource message 接收结束时,还调用了一个 feedback 接口做反馈。这是因为在使用 Pipe 输出时,后端没有记录 AI 答复的文本,考虑到前端已经处理了文本,这里就由前端做一次反馈,把本次 AI 答复的内容完整回传给后端,后端再更新 session 中存储的对话信息,保证对话上下文的完整性。

feedback 接口的实现比较简单:

router.post('/feedback', function(req, res, next) {
    if (req.body.result) {
        req.session.chatgptSessionPrompt += req.body.result
        res.status(200).json({
            code: '0',
            msg: "更新成功"
        });
    } else {
        res.status(400).json({
            msg: "参数错误"
        });
    }
});

我这里只是给出一种简单的做法,实际产品中可能要考虑的会更多,或者应该在后端自行处理 session 内容,而不是依靠前端的反馈。

最终的效果大概是这个样子:

f68da259dc4960af395d6440a427ff81.gif


限制访问频次

由于 Open AI 也是有免费额度的,所以在调用频率和次数上也应该做个限制,防止被恶意调用,这个也可以通过 session 来处理。我这里也提供一种比较粗糙的处理方式,具体请往下看。实际产品中可能会写 Redis,写库,加定时任务之类的,这方面我也不够专业,就不多说了。

针对访问频率,我暂定的是 3 秒内最多调用一次,我们可以在调用 Open AI 成功之后,在 session 中记录时间戳。

req.session.chatgptRequestTime = Date.now()

当一个新的请求过来时,可以用当前时间减去上次记录的chatgptRequestTime,判断一下是不是在 3 秒内,如果是,就返回 HTTP 状态码 429;如果不在 3 秒内,就可以继续后面的逻辑。

if (req.session.chatgptRequestTime && Date.now() - req.session.chatgptRequestTime <= 3000) {
    // 不允许在3s里重复调用
    return res.status(429).json({
        msg: "请降低请求频次"
    });
}

关于请求次数也是同样的道理,我这里也写得很简单,实际上还应该有跨天清理等逻辑要做。我这里偷懒了,暂时没做这些。

if (req.session.chatgptTimes && req.session.chatgptTimes >= 50) {
    // 实际上还需要跨天清理,这里先偷懒了。
    return res.status(403).json({
        msg: "到达调用上限,欢迎明天再来哦"
    });
}

同一个话题也不能聊太多,否则传给 Open AI 的 prompt 参数会很大,这就可能会耗费很多 Token,也有可能超过 Open AI 参数的限制。

if (req.session.chatgptTopicCount && req.session.chatgptTopicCount >= 10) {
    // 一个话题聊的次数超过限制时,需要强行重置 chatgptSessionPrompt,换个话题。
    req.session.chatgptSessionPrompt = ''
    req.session.chatgptTopicCount = 0
    return res.status(403).json({
        msg: "这个话题聊得有点深入了,不如换一个"
    });
}

切换话题

客户端应该也有切换话题的能力,否则 session 中记录的信息可能会包含多个话题的内容,可能导致与用户的预期不符。那我们做个接口就好了。

router.post('/changeTopic', function(req, res, next) {
    req.session.chatgptSessionPrompt = ''
    req.session.chatgptTopicCount = 0
    res.status(200).json({
        code: '0',
        msg: "可以尝试新的话题"
    });
});

结语

总的来说,Open AI 开放出来的智能对话能力可以满足基本需求,但是还有很大改进空间。我在文中给出的代码仅供参考,不保证功能上的完美。

附上源码地址[13],可以点个 star 吗,球球了[认真脸]。

参考

[1]

体验链接: https://blog.wbjiang.cn/chatgpt

[2]

ChatGPT: https://openai.com/

[3]

注册: https://beta.openai.com/login/

[4]

SMS-ACTIVATE: https://sms-activate.org/cn

[5]

Web工作台: https://chat.openai.com/chat

[6]

文档: https://beta.openai.com/

[7]

案例: https://beta.openai.com/examples

[8]

openai: https://www.npmjs.com/package/openai

[9]

生成一个 API Key: https://beta.openai.com/account/api-keys

[10]

Key concepts: https://beta.openai.com/docs/introduction/key-concepts

[11]

WebSocket 聊天功能: https://blog.wbjiang.cn/chat

[12]

data-only server-sent events: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format

[13]

源码地址: https://github.com/cumt-robin/vue3-ts-blog-frontend

END

689b2f6d671aa08acdaa35dcc09a9bf2.png

如果觉得这篇文章还不错

点击下面卡片关注我

来个【分享、点赞、在看】三连支持一下吧585be4cce6307c6001e94b6c914ad690.png

   “分享、点赞、在看” 支持一波 40a7d4b9dcc2d124fc9a87a5e542d7df.png 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/87754.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1区SCI潜力刊,中科院分区即将更新,有望冲击2区

1区 计算机物联网类SCI&EI 01 期刊详情 【出版社】Elsevier 【指标情况】自引率6.30% 【期刊简介】IF:5.5-6.0&#xff0c;JCR1区&#xff0c;中科院分区预计月底公布 【检索情况】SCI&EI 双检&#xff0c;正刊 【参考周期】3-4个月左右录用 【截稿日期】2023.2.…

cv2.circle()函数报错(tensor 转 array 感觉是bug)

前言 我不理解为啥opencv-python可视化一堆报错&#xff0c;同一个三通道图像&#xff0c;cv2.imshow()没有问题&#xff0c;cv2.circle()就一直有问题&#xff0c;搞了一晚&#xff0c;心态炸了&#xff01;&#xff01;&#xff01; cv2需要的图片矩阵&#xff08;H, W, C&…

FKM规范 针对非焊接构件疲劳强度评估的实例介绍(下篇)

本文主要结合FKM规范及FKM inside ANSYS软件针对非焊接构件的手动计算实例及软件计算实例进行介绍&#xff0c;希望大家对FKM规范在非焊接构件疲劳强度评估中的手动及软件计算过程有基本的了解。 一、写在前面 FKM Inside ANSYS软件&#xff0c;该软件是在FKM 规范的基础上&a…

不想写日报、周报,这个报表自动化软件太牛了,仅需三分钟

昨天看到一个哥们发帖说IT部门负责做报表的同事阳了&#xff0c;再加上年底各个业务部门报表需求旺盛&#xff0c;现在他们是忙的饭都吃不上&#xff0c;天天凌晨才能回家。京东的人倒是被解放了&#xff0c;毕竟强东说汇报只能1页ppt。但对于万千其他公司的朋友们来说&#xf…

【POJ No. 2114】 游船之旅 Boatherds

【POJ No. 2114】 游船之旅 Boatherds 北大OJ 题目地址 【题意】 河流总是形成一棵树&#xff08;以村庄为节点&#xff09;&#xff0c;超过两条河流时可以在交叉路口汇入。游船的定价政策非常简单&#xff1a;两个村庄之间的每条河流都有一个价格&#xff08;两个方向的价格…

向现实「低头」,大众「调战略」

越来越多的汽车制造商&#xff0c;在修正自己的战略。 作为全球智能电动汽车领头羊&#xff0c;特斯拉针对欺诈指控为自己辩护&#xff0c;解释称其自动驾驶技术只是未能实现自己设定的目标&#xff0c;而不是误导消费者。2019年4月&#xff0c;其首席执行官埃隆马斯克声称&…

电脑技巧:推荐几款装机必备的软件

目录 1、火绒安全 2、Geek Uninstaller 卸载神器 3、Potplayer 4、Edge微软官方浏览器 5、Snipaste 截图神器 6、Everything 本地搜索神器 7、Honeyview 超好用的图片查看工具 8、Bandizip 解压缩工具 9、傲梅分区助手 分区必备神器 10、ToDesk 远程协助工具 11、剪…

数字化采购浪潮下,MRO工业品采购商城系统如何助力企业深化智能升级

工业品作为工程建设、机械制造等行业发展过程中不可缺少的一部分&#xff0c;市场前景十分广阔。根据艾瑞咨询数据显示&#xff0c;2020年我国工业品市场规模为10.6万亿元&#xff0c;其中MRO市场占比为20%。而在万亿规模下&#xff0c;我国工业品市场上下游存在诸多行业痛点&a…

(八)并发集合——阻塞队列

阻塞队列---BlockQueue BlockingQueue是带阻塞功能的队列&#xff0c;继承了Queue接口&#xff0c;当执行入队操作时&#xff0c;如果队列满了&#xff0c;则阻塞调用者&#xff1b;当执行出队操作时&#xff0c;如果队列是空的&#xff0c;也阻塞调用者。 public interface Bl…

RK3568平台开发系列讲解(Linux系统篇)内存映射与虚拟内存

🚀返回专栏总目录 文章目录 一、内存映射二、内存保护三、内存锁定3.1、锁定指定的内存段3.2、锁定进程的所有内存页沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章讲介绍几个对虚拟内存进行系统级控制的系统调用,合理使用它们,能极大地提高应用的执行效率,…

C++ Reference: Standard C++ Library reference: Containers: map: map: at

C官网参考链接&#xff1a;https://cplusplus.com/reference/map/map/at/ 公有成员函数 <map> std::map::at mapped_type& at (const key_type& k); const mapped_type& at (const key_type& k) const;访问元素 返回对键k标识的元素的映射值的引用。 如…

1、CSS基础之初识

文章目录一、简介二、基础语法语法格式注释举个例子三、CSS的数值与单位颜色值长度值四、CSS样式类型内联式&#xff08;行内嵌&#xff09;内嵌式&#xff08;内部样式表&#xff09;外链式&#xff08;外部样式表&#xff09;五、文字排版六、格式化列表七、格式化链接八、We…

网络实验之RIPV2协议(一)

一、RIPV2协议简介 RIP (Routing Information Protocol) 路由协议是一种相对古老&#xff0c;在小型以及同介质网络中得到了广泛应用的一种路由协议。RIP 采用距离向量算法&#xff0c;是一种距离向量协议。RIP-1是有类别路由协议&#xff08;Classful Routing Protocol&#x…

[R语言]RMarkdown: 入门与操作

Rmarkdown入门汇总 R Markdown介绍 RMarkdown为我们的数据分析创建一个清晰美观的报告&#xff0c;它能够包含代码&#xff0c;图&#xff0c;表&#xff0c;说明&#xff0c;查看的人能够很清楚地理解。 RMarkdown利用Markdown的语法&#xff0c;能够被方便的转成其他类型(…

超店有数推出TikTok达人批量触达工具,让海量达人同时带货不再是梦

近年来&#xff0c;TikTok的全球化步伐越来越快。作为全球知名的短视频应用&#xff0c;TikTok目前日活跃用户超过8亿。电子商务业务规模保持快速增长。今年TikTok Shop黑五大促已于11月28日正式收官&#xff0c;TikTok Shop在黑五大促期间GMV增长126%&#xff0c;订单量增长13…

汇编前四章学习笔记

汇编学习笔记&#xff08;by 小白奋斗ing&#xff09; 参考《汇编语言&#xff08;第三版&#xff09;》王爽著 1.基础知识 1.1 机器语言 二进制编码 1.2 汇编语言 1.3 汇编语言的组成 1.4进制表示符 二进制&#xff08;B&#xff09;&#xff0c;十六进制&#xff08;H&a…

2022计算机毕业设计选题推荐 - 计算机毕业设计题目大全

文章目录0 前言1 java web 管理系统 毕设选题2 java web 平台/业务系统 毕设选题3 游戏设计、动画设计类 毕设选题 (适合数媒的同学)4 算法开发5 数据挖掘 毕设选题6 大数据处理、云计算、区块链 毕设选题7 网络安全 毕设选题8 通信类/网络工程 毕设选题9 嵌入式 毕设选题10 开…

电脑怎么恢复数据?恢复数据的3个技巧分享

在日常工作或生活中&#xff0c;电脑都是不可或缺的工具和设备。电脑里面保存着很多文件数据&#xff0c;如果重要的数据文件丢失&#xff0c;那就很麻烦。如果电脑数据丢失&#xff0c;怎么恢复数据&#xff1f;让我们一起来看看下面的3个恢复电脑数据的技巧吧&#xff01; 数…

阿里最新分享 SpringCloudAlibaba 实战小抄(第五版),一代更比一代强

为什么要学习 SpringCloud Alibaba&#xff1f; Spring Cloud Alibaba 为分布式应用开发提供了一站式解决方案。它包含开发分布式应用程序所需的所有组件&#xff0c;可以轻松地使用 Spring Cloud 开发应用程序。 使用 Spring Cloud Alibaba&#xff0c;只需添加一些注解和少…

web前端期末大作业——用HTML+CSS做一个漂亮简单的电影主题网站

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 精彩专栏推荐&#x1f4…