首先説明,我不是專業的前端工程師。
但這次,我一個人完成了一個包含聊天窗口、WebSocket 實時推送、多語言翻譯、複雜 UI 狀態管理的前端項目。
説實話,如果沒有 AI,這個項目我大概率會延期,甚至放棄一些體驗上的細節。
這是我第一次,在一個真實、長期維護、並且已經上線使用的項目中,深度引入 AI 參與開發。 不是 Demo,不是練手,而是一個我必須為穩定性、性能和可維護性負責的系統。
一、前言
前端時間接了一個前端聊天+後端管理後台的項目,兩個項目都是我自己一個人完成。
説起來後端還好,但是前端html+css那套我最開始入行的時候學了一點,但是後面正式工作後主要還是圍繞後端語言來展開,前端的那套樣式語法就漸漸地放下了;
但這次是一個全新的機會,也是一個新的挑戰,需要自己寫前端。那如何快速寫前端項目,並快速交付呢?於是我想到了AI這個幫手,之前總拿它來排查問題,但是寫一個項目行不行呢? 我抱着懷疑的態度開始了這項“挑戰”,並最終“有驚無險”的落地完成,順利完成交付;
本篇文章,我想詳細的覆盤下這次經歷:如何與AI溝通? 如何合理利用AI完成代碼的實現?以及舉例一些聊天系統中實現的業務關鍵點!
在使用前,先想一下:
AI 到底能幫我們做到什麼?我們又該如何與 AI 協作,才能真的提高生產力,而不是製造技術債?
二、與AI對話
2.1 為什麼我會把 AI 真正引入一個“正經項目”?
先説結論: 不是因為“新技術”,而是因為“現實問題”。
我的真實情況
這個項目是一個 客服聊天系統,核心特點包括:
- Laravel 後端處理接口數據
- jQuery + Bootstrap 前端(我比較熟悉的是這套組合拳)
- 多賬號、多好友
- WebSocket 實時推送
- 消息類型複雜(文本 / 圖片 / 視頻 / 語音)
- SaaS 場景(多租户)
我面臨的真實問題是:
- 後端我非常熟
- 但前端交互複雜、狀態多、樣式細
- 每一個“小交互”都很耗時間
- 而項目又在持續迭代,不能停下來重構
👉 這時,AI 不再是“錦上添花”,而是降低邊際成本的工具。
2.2 AI 開發入門:不要幻想“全自動”,要追求“人機協作”
2.2.1 AI 最適合做什麼?
在這次項目中,我給 AI 的定位非常清晰:
AI = 前端協作工程師
基於我當時的情況,我給它的定時是,輔助幫我寫前端代碼,包括但不限於以下:
- UI 結構拆解
- JS 事件邏輯補全
- CSS 微調與重構
- 複雜 DOM 操作的示例實現
- 重複性、模式化代碼生成
結合我使用之後的感覺,我認為他可能不太適合:
- 定業務邊界
- 定核心數據結構
- 決定架構選型
- 性能極限設計
這些必須由人來做。
2.2.2 心態非常重要:你不是“用 AI”,而是在“帶 AI”
如果你把 AI 當成:
- “自動寫代碼工具”
- “一句話生成系統”
那你一定會失望。
但如果你把 AI 當成:
- 一個不抱怨的工程師
- 一個願意反覆改的搭子
- 一個可以隨時請教的助手
你會發現它非常好用。
2.3 如何與 AI 溝通,才能真的把前端項目做出來?
這一節,是我整篇文章裏最想聊的部分。
2.3.1 關鍵原則一:給 AI “現有代碼”,而不是“空需求”
❌ 錯誤方式:
幫我寫一個聊天窗口
✅ 正確方式:
這是我現有的 HTML 結構 這是我的 JS 方法 這是我的業務規則 請在不破壞現有結構的前提下,實現功能 X
AI 的代碼質量,嚴重依賴上下文完整度。
PS:如果你是從0開始讓AI幫你完成項目,那最好在同一個人聊天窗口下,如果切換了聊天窗口,那可能會導致以前的消息可能無法產生關聯;如果要優化,最好貼上之前的代碼!
2.3.2 關鍵原則二:需求要“具象”,不要“抽象”
比如我會這樣描述 UI:
聊天窗口頂部: 左側是頭像 + 暱稱 右側是三個點按鈕 點擊後,從“聊天窗口右側”滑出信息面板 而不是整個頁面
你會發現:我描述的是“畫面”,不是“功能名詞”。
2.3.3 關鍵原則三:有問題就“精準反饋”,不要一句否定
我在項目中經常這樣和 AI 互動:
- “三個點按鈕沒有靠右”
- “事件綁定不到,因為是動態元素”
- “滑出層相對於 body 了,不是 chat-panel”
這種反饋,會讓 AI 快速修正,而不是推倒重來。
2.3.4 一個我踩過的坑:AI 會“自信地寫錯”
AI不是萬能的,它也有可能出錯,你的描述詞不清晰,代碼未提供完整,就可能導致:
- CSS 看起來對,但層級錯了
- JS 邏輯跑得通,但狀態沒覆蓋
- WebSocket 示例是 Demo 級,不是生產級
所以我後來形成了一個習慣:AI負責“給方案”,我負責“兜底校驗”!
三、項目功能關鍵點拆解示例
3.1 關鍵功能點一:前後端聊天消息推送
3.1.1 後端整體設計思路
我採用的是:
- Workerman / GatewayWorker
- 後端消息統一入庫
- 再推送 WebSocket 給前端
核心原則是:
消息以“後端為準”,前端只是展示層
我設計的流程是,後端採用腳本監聽第三方消息服務,監聽到有消息之後推送到job,job中處理消息,代碼如下:
<?php
namespace App\Jobs;
use App\Repositories\YkAccountFriendChatRecordRepository;
use App\Repositories\YkAccountFriendRepository;
use App\Repositories\YkAccountRepository;
use App\Services\GatewayService;
use App\Services\InstagramMessageService;
use App\Services\TranslateService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* 處理mqtt消息
*/
class ProcessIncomingMqttMessage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $payload;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(array $payload)
{
$this->payload = $payload;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
try {
if (!$this->payload['PK']) {
throw new \Exception('缺少PK!');
}
$accountKey = $this->payload['PK'];
$account = app(YkAccountRepository::class)->firstWhere(['account_key' => $accountKey, 'status' => YkAccountRepository::STATUS_ENABLE]);
if (empty($account)) {
throw new \Exception("account_key:{$accountKey}對應的account數據不存在");
}
if (!is_array($this->payload['Payload']) || !count($this->payload['Payload'])) {
throw new \Exception('payload數據異常!');
}
foreach ($this->payload['Payload'] as $value) {
/**
* UserId 發送方id
* RealTimeOp 類型
* Text 文本類型是內容字段
* Media 媒體類型時字段
*/
if (empty($value['UserId'])) {
Log::info('payload中沒有UserId:' . json_encode($value));
continue;
}
$friend = app(YkAccountFriendRepository::class)->firstWhere(['pks' => $value['UserId'], 'account_id' => $account['id']]);
if (empty($friend)) {
Log::info("pks:{$value['UserId']}在好友表中不存在!");
continue;
}
$contentType = InstagramMessageService::transContentType($value);
if (empty($contentType)) {
Log::warning('ProcessIncomingMqttMessage - handle 未知的消息類型' . json_encode($value));
continue;
}
$sendTime = transMicrosecondTimestamp($value['TimeStampUnix']);
$data = [
'customer_id' => $account['belong_customer_id'],
'account_id' => $account['id'],
'friend_id' => $friend['id'],
'content' => $value['Text'] ?? null,
'content_type' => $contentType,
'attachment' => InstagramMessageService::getAttachment($contentType, $value),
'item_id' => $value['ItemId'],
'send_time' => transMicrosecondTimestamp($value['TimeStampUnix']),
'send_status' => YkAccountFriendChatRecordRepository::STATUS_SUCCESS
];
// 如果是文本類型 獲取翻譯之後的數據
if ($contentType == YkAccountFriendChatRecordRepository::CONTENT_TYPE_TEXT) {
$transContent = TranslateService::getChatMessageTranslate($account['belong_customer_id'], $value['Text']);
if ($transContent && ($transContent != $value['Text'])) {
$data['is_translate'] = YkAccountFriendChatRecordRepository::CONTENT_TRANSLATE;
$data['content_translate'] = $transContent;
}
}
DB::beginTransaction();
try {
$record = app(YkAccountFriendChatRecordRepository::class)->updateOrCreate(['item_id' => $data['item_id']], $data);
app(YkAccountFriendRepository::class)->updateLastChatTime($friend['id'], $sendTime);
app(YkAccountRepository::class)->updateLastChatTime($account['id'], $sendTime);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
Log::info('ProcessIncomingMqttMessage handle 落庫失敗,異常原因:' . $e->getMessage());
continue;
}
$data['message_id'] = $record->id;
GatewayService::pushMessageToClient($data, $friend);
// 自動回覆消息
dispatch(new SendAutoReplyMessageJob($record->id))->onConnection('redis')->onQueue('SendAutoReplyMessageSqs');
}
} catch (\Throwable $e) {
Log::error('ProcessIncomingMqttMessage fail line:' . $e->getLine() . ' 報錯信息:' . $e->getMessage(), ['payload' => $this->payload]);
}
}
}
GatewayService類的pushMessageToClient方法代碼如下:
public static function pushMessageToClient($data, $friend)
{
$gatewayHost = config('services.gateway.host', '127.0.0.1');
$gatewayPort = config('services.gateway.port', '1238');
Gateway::$registerAddress = sprintf('%s:%s', $gatewayHost, $gatewayPort);
$sendData = [
'account_id' => $data['account_id'],
'friend_id' => $data['friend_id'],
'friend_name' => $friend['username'],
'friend_avatar' => $friend['avatar'],
'send_time' => format_time($data['send_time']),
'timestamp' => format_time(null),
'content' => $data['content'],
'attachment' => $data['attachment'],
'content_type' => $data['content_type'],
'message_id' => $data['message_id'],
'is_me' => $data['is_me'] ?? false,
'is_auto_reply' => $data['is_auto_reply'] ?? false,
];
if (!$sendData['is_me']) {
// 不管客服有沒有在線 先標記賬號和好友 有未讀數據(從前端去處理已讀)
app(YkAccountFriendRepository::class)->update(['is_have_un_read_msg' => YkAccountFriendRepository::HAVE_UN_READ_MSG], $friend['id']);
app(YkAccountRepository::class)->update(['is_have_un_read_msg' => YkAccountFriendRepository::HAVE_UN_READ_MSG], $data['account_id']);
}
// 判斷當前客服是否在線
if (Gateway::isUidOnline($data['customer_id'])) {
Log::info('推送到客户端信息', ['customer_id' => $data['customer_id'], 'data' => json_encode([
'type' => 'new_message',
'data' => $sendData
])]);
// 發送消息給客服
Gateway::sendToUid($data['customer_id'], json_encode([
'type' => 'new_message',
'data' => $sendData
]));
} else {
Log::info("客服id:{$data['customer_id']},未在線~", ['send_data' => $sendData]);
}
}
這個方法多個地方都可以調用,比如:
- 接收到消息推送到前端
- 前端發送消息,後端推送到第三方成功,發送到前端回顯
- 自動回覆消息成功,發送到前端回顯
- ...
3.1.2 後端推送的數據結構
{
"account_id": 123456,
"friend_id": 789012,
"friend_name": "張三",
"friend_avatar": "https://example.com/avatar.jpg",
"send_time": "2023-10-15 14:30:25",
"timestamp": "2023-10-15 16:45:10",
"content": "你好,最近怎麼樣?",
"attachment": "image_001.jpg",
"content_type": "text",
"message_id": "msg_20231015143025_123456",
"is_me": false,
"is_auto_reply": false
}
3.1.3 前端接收消息
綁定並監聽websocket
// 綁定 WebSocket
function connectWebSocket() {
if (!CUSTOMER_ID) {
console.error('未設置客服ID,無法連接WebSocket');
return;
}
// 清除之前的重連定時器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
ws = new WebSocket(window.WEBSECKET_HOST); // 改成你的服務地址
ws.onopen = function () {
console.log('WebSocket 已連接');
lastPongTime = Date.now(); // 連接建立時重置時間
reconnectAttempts = 0; // 重置計數器
// 綁定客服登錄用户ID
ws.send(JSON.stringify({
type: 'bind',
uid: CUSTOMER_ID
}));
// 啓動心跳檢測
startHeartbeatCheck();
};
ws.onmessage = function (event) {
console.log('收到消息:', event.data);
let msg = {};
try {
msg = JSON.parse(event.data);
} catch (e) {
console.warn('收到非法消息', event.data);
return;
}
if (msg.type === 'ping') {
// 服務器心跳包,更新最後活躍時間並回復pong
lastPongTime = Date.now();
// 服務器心跳包,回覆pong
ws.send(JSON.stringify({type: 'pong'}));
return;
}
if (msg.type === 'new_message') {
console.log('收到new_message消息:', msg.data);
handleIncomingMessage(msg.data);
}
if (msg.type === 'account_online_status') {
const data = msg.data;
console.log('收到account_online_status消息:', msg.data);
updateAccountOnlineStatus(data);
}
if (msg.type === 'send_message_status') {
console.log('收到send_message_status消息:', msg.data);
const data = msg.data;
// updateFriendOnlineStatus(data);
updateMessageSendStatus(data)
}
};
ws.onclose = function () {
reconnectAttempts++;
// 漸進式重連:前3次快速重連,後續採用退避策略
const delay = reconnectAttempts <= 3 ?
BASE_DELAY :
Math.min(BASE_DELAY * Math.pow(1.5, reconnectAttempts - 3), MAX_DELAY);
console.warn(`[第${reconnectAttempts}次重連] ${delay}ms後嘗試...`);
setTimeout(connectWebSocket, delay);
};
ws.onerror = function (e) {
console.error('WebSocket 發生錯誤');
console.error('WS錯誤代碼:', e.code);
console.error('WS錯誤原因:', e.reason);
ws.close();
};
}
/**
* 新消息處理
* @param msg
*/
function handleIncomingMessage(msg) {
console.log('handleIncomingMessage', msg);
const currentAccountId = state.currentAccountId;
const currentFriendId = state.currentFriendId;
if (msg.account_id === currentAccountId) {
if (msg.friend_id === currentFriendId) {
// 當前聊天窗口好友,追加消息
appendMessage({
me: msg.is_me || false,
auto_reply: msg.is_auto_reply || false,
name: msg.friend_name,
avatar: msg.friend_avatar,
timestamp: msg.timestamp,
send_time: msg.send_time,
content: msg.content,
attachment: msg.attachment,
id: msg.message_id,
type: getContentType(msg.content_type)
});
// 如果當前消息是當前聊天好友的,標記好友狀態為已讀
setFriendUnReadStatus(msg.friend_id, 0);
translateVisibleMessages($('select[name="input_target_lang"]').val(), 'left');
} else {
// 當前賬號的其他好友,標紅點
markFriendUnreadDot(msg.friend_id, msg.account_id);
}
} else {
// 非當前賬號,賬號頭像標紅點
markAccountUnreadDot(msg.account_id);
}
}
這個這段邏輯主要包括:
- 當前聊天窗口實時追加消息
- 非當前好友 → 好友紅點
- 非當前賬號 → 賬號紅點
這裏之所以要在前端判斷account\_id /friend\_id,而不是後端分多鐘類型推是因為:
- 降低後端推送負責度,而且我覺得在前端判斷會更好
- 保證前端狀態一致性
- 方便後續擴展更多UI狀態
3.2 關鍵功能點二:中英文切換與“批量翻譯”的實現思路
這是一個讓我非常滿意、也非常適合 AI 協作的功能。
3.2.1 我的需求不是“翻譯一條消息”
而是:
- 聊天窗口已有歷史消息
- 切換語言後
- 原文不變
- 原文下方顯示譯文
- 支持再次切換目標語言
3.2.2 前端結構設計(AI 協助)
<div class="chat-message-bubble">
<div class="original-text">Hello</div>
<div class="translated-text text-muted small">你好</div>
</div>
AI 在這裏給了我一個很重要的建議:
翻譯內容不要覆蓋原文,而是“附加”
如果一開始就讓 AI 覆蓋原文,後期做多語言切換、撤銷翻譯、重新翻譯,都會非常痛苦。這讓體驗和可維護性都好很多。
3.2.3 切換語言時的 JS 邏輯
// Tab 切換事件
$("#collapseTranslate").on('change', '.translate-select', function () {
let name = $(this).attr('name');
let value;
if (name === 'chat_message_auto_translate' || name === 'input_auto_translate') {
const isChecked = $(this).is(':checked');
value = isChecked ? 1 : 0;
} else {
value = $(this).val();
}
if (name === 'input_target_lang') {
translateVisibleMessages(value);
}
if (name === 'chat_message_target_lang') {
translateVisibleMessages(value, 'left');
}
$.post('/chat/change_translate_config', {
name: name,
value: value
}, function (res) {
if (res.code !== 0) {
layer.msg(res.msg)
}
});
});
/**
* 聊天框內容翻譯
* @param targetLang
* @param trans_message_type
*/
function translateVisibleMessages(targetLang = 'en', trans_message_type = 'right') {
const messagesToTranslate = [];
const selector = '.chat-message-wrapper.chat-message-' + trans_message_type;
console.log('selector', selector); // 輸出拼接的選擇器
console.log('匹配到元素數量:', $(selector).length);
$(selector).each(function () {
const $wrapper = $(this);
const messageId = $wrapper.data('id');
const $bubble = $wrapper.find('.chat-message-bubble');
const content = $wrapper.find('.chat-message-bubble .original-text').text().trim();
const cacheKey = `${messageId}_${targetLang}`;
if (!messageId || !content) return;
// 若已緩存則直接渲染
if (translationCache[cacheKey]) {
applyTranslatedMessage(messageId, translationCache[cacheKey]);
} else {
// 顯示 loading 佔位
insertLoadingPlaceholder($bubble);
// 準備發送的內容
messagesToTranslate.push({ message_id: messageId, content });
}
});
console.log('messagesToTranslate', messagesToTranslate);
if (messagesToTranslate.length === 0) return;
$.ajax({
url: '/chat/translate/batch',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
messages: messagesToTranslate,
target_lang: targetLang
}),
success: function (res) {
if (res.code === 0 && Array.isArray(res.data)) {
res.data.forEach(item => {
const cacheKey = `${item.message_id}_${targetLang}`;
translationCache[cacheKey] = item.translate;
applyTranslatedMessage(item.message_id, item.translate);
});
}
}
});
}
/**
* 翻譯內容顯示
* @param messageId
* @param translation
*/
function applyTranslatedMessage(messageId, translation) {
const $wrapper = $(`.chat-message-wrapper[data-id="${messageId}"]`);
const $bubble = $wrapper.find('.chat-message-bubble');
const $translated = $bubble.find('.translated-text');
if ($translated.length) {
$translated.text(translation);
} else {
$bubble.append(`
<div class="divider"></div>
<div class="translated-text text-muted small">${translation}</div>
`);
}
}
這段代碼是 AI 在我給出 DOM 結構後幫我補全的。
3.3 關鍵功能點三:複雜 UI 交互(微信式體驗)如何落地?
比如:
- 消息氣泡寬度
- 時間顯示位置
- 自己 / 對方對齊方式
- 動態按鈕事件綁定
3.3.1 動態元素事件綁定問題
我一開始寫的是:
$('#toggle-friend-info').on('click', ...)
AI 很快指出問題:
這是動態生成的 DOM,需要事件委託
修正後:
$(document).on('click', '#toggle-friend-info', function () {
$('#friend-info-panel').toggleClass('show');
});
這是一個非常典型的“AI 幫你查漏補缺”場景。
3.3.2 滑出面板相對聊天窗口,而不是頁面
AI 在我反饋問題後,幫我調整為:
.chat-panel {
position: relative;
overflow: hidden;
}
.friend-info-panel {
position: absolute;
right: -260px;
transition: right .3s;
}
.friend-info-panel.show {
right: 0;
}
3.3.3 發送消息、接收消息左右分隔
.chat-message-wrapper {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
width: 100%;
}
.chat-message-left {
flex-direction: row;
}
.chat-message-right {
flex-direction: row-reverse;
}
.message-block {
display: inline-flex;
flex-direction: column;
max-width: 75%; /* 讓消息區最大佔75%寬 */
word-wrap: break-word;
}
/* 自己發的消息(右側) */
.chat-message-right .message-block {
display: flex;
flex-direction: column;
align-items: flex-end; /* 時間右對齊 */
}
/* 對方的消息(左側) */
.chat-message-left .message-block {
display: flex;
flex-direction: column;
align-items: flex-start; /* 時間左對齊 */
}
.chat-message-bubble {
max-width: 95%;
padding: 10px 14px;
border-radius: 16px;
word-wrap: break-word;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
line-height: 1.5;
}
以上這些,基本上都是AI幫我完成調優的。
除了以上幾個功能點之外,我還實現了
- 退出登錄聊天窗記憶功能:退出登錄,記錄上次聊天的對象,再次登錄後自動打開最後一次聊天對象所在的分組、tab、聊天窗口、聊天記錄
- 多語言切換:通過配置切換當前要展示的語言
- 快捷回覆:添加(文本、圖片、視頻、語音)、刪除、快捷發送
- 自動回覆:接收到消息自動回覆
- 翻譯配置:支持發送出去的消息和接收到的消息,可以分別配置源語言、翻譯為目標語言。比如發出去的是中文,實際對方接收到的是英文;接收到的是英文、韓文,聊天窗顯示中文
- 未讀分組、tab、好友紅點標記等
太多了,具體細節就不一一介紹了,光聊天一個窗口交互就複雜的一批(感覺要錢要少了,orz...
四、效果展示
登錄
聊天首頁
翻譯配置
自動回覆配置
五、總結
5.1 AI寫代碼的優缺點
基於我這次和AI對話的實戰來看,優點是非常明顯的
- 前端效率提升巨大
- UI 微調不再痛苦
- 可以快速試錯
- 非前端工程師也能做出“像樣界面”
但是缺點也很明顯:
- 不會主動考慮性能極限
- 容易“寫得能用,但不夠優雅”
- 架構必須你來定
- 需要你具備基本判斷能力
5.2 其他
AI的使用遠不限於此,如果你願意學習如何使用:
- 描述需求
- 提供上下文
- 精準反饋
- 與 AI 協作
你會發現:一個人,可以完成過去一個小團隊才能完成的事情。
六、寫到最後
對我來説,AI 並不是讓我“變成前端工程師”,而是讓我在有限時間內,把一個本來可能妥協的項目,做到自己滿意為止。
從某些方面來説,AI讓我變的更高效,從之前排查問題靠百度、靠在社區提問,到現在有問題問AI,AI的準確率還不錯;從之前靠自己經驗寫出一段邏輯代碼,到現在請AI幫我優化,大大提高了我代碼的質量;AI是個好東西,咱們程序員還是要擅於利用它,這樣才有更多的時間“摸魚"(提高自己)啊
你有沒有用 AI 真正寫過項目呢?歡迎評論聊聊。