Javascript中Fetch請求Coze API并流式展示請求結(jié)果
01 思路技術(shù)選型Fetch
用戶現(xiàn)在的問題是如何將coze API返回的流式響應(yīng)展示出來。之前已經(jīng)幫忙封裝了axios請求,現(xiàn)在需要處理流式數(shù)據(jù)。我需要回想一下流式請求的處理方式,通常SSE(Server-Sent Events)或者使用fetch的流式讀取,但用戶使用的是axios,可能需要配置responseType為stream或者處理分塊數(shù)據(jù)。
首先,用戶之前的代碼中設(shè)置了stream: true,這可能意味著API返回的是流式數(shù)據(jù),比如每個(gè)chunk逐步返回。但axios默認(rèn)情況下可能不會(huì)處理這種流式響應(yīng),可能需要調(diào)整配置。例如,使用fetch的話,可以監(jiān)聽chunk,但axios是否支持呢?
根據(jù)之前的知識,處理流式響應(yīng)通??梢允褂肊ventSource,但需要服務(wù)器支持SSE。如果API返回的是普通的分塊數(shù)據(jù)流,可能需要使用responseType: ‘stream’(在Node.js中)或者在前端使用ReadableStream。不過在前端瀏覽器環(huán)境中,axios可能無法直接處理流式數(shù)據(jù),或者需要特定的配置。
用戶現(xiàn)在用的是瀏覽器環(huán)境嗎?假設(shè)是前端項(xiàng)目,比如Vue或React。這時(shí)候可能需要使用fetch API,因?yàn)閒etch可以處理流式數(shù)據(jù),而axios在這方面的支持可能有限。或者,檢查coze的API文檔,確認(rèn)流式返回的數(shù)據(jù)格式,比如是否每個(gè)chunk是一個(gè)JSON對象或特定格式的數(shù)據(jù)。
例如,如果API使用SSE,每個(gè)事件是一個(gè)數(shù)據(jù)塊,那么在前端應(yīng)該使用EventSource來監(jiān)聽事件。但如果API是普通的流式HTTP響應(yīng),分塊發(fā)送數(shù)據(jù),那么可能需要使用fetch并逐步讀取響應(yīng)體。
在用戶之前的封裝中,使用的是axios.post,并且設(shè)置了stream: true。但可能沒有正確配置axios來處理流式響應(yīng)。因此,可能需要改用fetch,或者調(diào)整axios的配置,比如設(shè)置responseType為’stream’(但瀏覽器中可能不支持),或者使用onDownloadProgress回調(diào)來處理接收到的數(shù)據(jù)片段。
查閱資料發(fā)現(xiàn),axios在瀏覽器中的responseType支持’arraybuffer’, ‘blob’, ‘document’, ‘json’, ‘text’, ‘stream’(stream僅在Node.js環(huán)境中有效)。因此,在瀏覽器中,可能無法直接使用stream類型。這時(shí)可能需要使用text類型,并手動(dòng)處理分塊數(shù)據(jù)。但這種方式可能不可靠,因?yàn)閍xios會(huì)等待整個(gè)響應(yīng)完成后再觸發(fā)then。
因此,正確的做法應(yīng)該是使用fetch API,因?yàn)閒etch支持流式讀取響應(yīng)體。例如,使用fetch并設(shè)置body為可讀流,然后通過reader來逐步讀取數(shù)據(jù)。
或者,如果服務(wù)器支持SSE,則使用EventSource。但需要確認(rèn)coze的API是否使用SSE格式,即返回的數(shù)據(jù)格式是否符合text/event-stream,并且每個(gè)事件由開頭。
假設(shè)用戶需要處理的是普通的流式JSON數(shù)據(jù),每個(gè)chunk是一個(gè)JSON對象,那么可能需要使用fetch來獲取響應(yīng)體,并通過讀取器逐步處理數(shù)據(jù)塊。
總結(jié)處理步驟:
- 使用fetch代替axios,因?yàn)閒etch支持流式讀取。
- 設(shè)置請求參數(shù),包括method: ‘POST’, headers, body等。
- 讀取響應(yīng)體流,使用ReadableStream.getReader()。
- 循環(huán)讀取每個(gè)chunk,處理并更新UI。
接下來需要編寫示例代碼,展示如何發(fā)起請求并處理流式響應(yīng),逐步將數(shù)據(jù)展示在頁面上。同時(shí),注意處理文本的拼接和解析,特別是如果每個(gè)chunk可能包含部分?jǐn)?shù)據(jù),需要正確拼接成完整的JSON對象或文本內(nèi)容。
可能的實(shí)現(xiàn)方式:
- 使用fetch發(fā)起POST請求。
- 獲取響應(yīng)體的reader。
- 創(chuàng)建解碼器來處理Uint8Array數(shù)據(jù)為文本。
- 循環(huán)讀取chunk,直到done為true。
- 處理每個(gè)chunk的文本,可能按特定分隔符(如\n\n)分割,解析每個(gè)事件的數(shù)據(jù)。
- 如果coze的API返回的是類似OpenAI的流式響應(yīng),每個(gè)chunk是一個(gè){…}\n\n的結(jié)構(gòu),那么需要按行分割處理。
最后,將處理后的數(shù)據(jù)逐步顯示在頁面上,比如通過React的state更新,或者直接操作DOM。
在代碼中,需要注意錯(cuò)誤處理,比如網(wǎng)絡(luò)錯(cuò)誤或流讀取中的錯(cuò)誤,以及如何中止請求(例如用戶取消操作)。
現(xiàn)在,將這些思路整理成步驟化的解答,并提供示例代碼,指導(dǎo)用戶如何修改之前的封裝以支持流式展示。
要實(shí)現(xiàn)從 coze API 獲取流式數(shù)據(jù)并實(shí)時(shí)展示,可以使用 fetch
API 處理流式響應(yīng),逐步讀取并渲染數(shù)據(jù)。以下是完整實(shí)現(xiàn)方案:
// 封裝流式請求方法 export const streamRequestTxt = async (content: string, onDataReceived: (string) => void) => { try { const response = await fetch('https://api.coze.cn/v3/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer pat_4cFLXH9NmmysAeyFMSVrZJXzCP8FywZlIqWWFFrQSS4x5DpYxXqmBXLfjzUmU242' }, body: JSON.stringify({ bot_id: "7472376009968042021", user_id: "123456", stream: true, auto_save_history: true, additional_messages: [{ role: "user", content: content, content_type: "text" }] }) }); // 處理非200響應(yīng) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // 獲取可讀流和讀取器 const reader = response.body?.getReader(); const decoder = new TextDecoder('utf-8'); let partialData = ''; if (!reader) return; // 持續(xù)讀取流數(shù)據(jù) while (true) { const { done, value } = await reader.read(); if (done) break; // 解碼并處理數(shù)據(jù)塊 const chunk = decoder.decode(value, { stream: true }); partialData += chunk; // 處理可能的多個(gè)事件(按換行分割) const lines = partialData.split('\n'); partialData = lines.pop() || ''; // 保留未完成的行 for (const line of lines) { if (line.startsWith('')) { const jsonData = line.replace('', ''); try { const parsed = JSON.parse(jsonData); if (parsed.content) { onDataReceived(parsed.content); // 觸發(fā)回調(diào)更新UI } } catch (e) { console.error('JSON解析失敗:', e); } } } } } catch (error) { console.error('流式請求失敗:', error); throw error; // 向上傳遞錯(cuò)誤 } };
React 組件使用示例:
import { useState } from 'react'; export function ChatStream() { const [message, setMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleStream = async (input: string) => { setIsLoading(true); try { await streamRequestTxt(input, (chunk) => { setMessage(prev => prev + chunk); // 增量更新消息 }); } finally { setIsLoading(false); } }; return ( <div> <button onClick={() => handleStream('你好')} disabled={isLoading} > {isLoading ? '請求中...' : '發(fā)送流式請求'} </button> <div style={{ whiteSpace: 'pre-wrap' }}> {message} </div> </div> ); }
核心實(shí)現(xiàn)原理:
流式協(xié)議處理
- 使用
fetch
原生API獲取可讀流(ReadableStream) - 通過
TextDecoder
處理二進(jìn)制流為文本 - 按
\n
分割數(shù)據(jù)行,處理服務(wù)端可能合并發(fā)送的多條消息
數(shù)據(jù)格式解析
- 假設(shè)服務(wù)端采用
JSON
格式發(fā)送事件 - 自動(dòng)過濾非數(shù)據(jù)行(如心跳檢測的空行)
- 使用
JSON.parse
嚴(yán)格解析數(shù)據(jù)格式
實(shí)時(shí)渲染優(yōu)化
- 通過回調(diào)函數(shù)逐塊傳遞數(shù)據(jù)
- 前端框架中通過狀態(tài)合并(如React的setState)優(yōu)化渲染性能
- 保留未完成數(shù)據(jù)塊實(shí)現(xiàn)斷包續(xù)傳
高級功能擴(kuò)展建議:
中斷控制
// 創(chuàng)建AbortController const controller = new AbortController(); // 在fetch配置中添加 signal: controller.signal, // 暴露中斷方法 return { abort: () => controller.abort() };
性能優(yōu)化
// 使用防抖渲染(每秒更新不超過60次) let renderQueue = ''; const debouncedUpdate = useRef(debounce(() => { setMessage(prev => prev + renderQueue); renderQueue = ''; }, 16)).current; // 在回調(diào)中累積數(shù)據(jù) onDataReceived: (chunk) => { renderQueue += chunk; debouncedUpdate(); }
錯(cuò)誤重試機(jī)制
let retries = 0; const MAX_RETRIES = 3; try { // ...原有邏輯 } catch (error) { if (retries < MAX_RETRIES) { retries++; await new Promise(r => setTimeout(r, 1000 * retries)); handleStream(input); } }
不同技術(shù)棧的適配方案:
Vue3 Composition API
<script setup> import { ref } from 'vue'; const message = ref(''); const isLoading = ref(false); const handleStream = async (input) => { isLoading.value = true; try { await streamRequestTxt(input, (chunk) => { message.value += chunk; }); } finally { isLoading.value = false; } }; </script>
Node.js 服務(wù)端轉(zhuǎn)發(fā)
import express from 'express'; const app = express(); app.post('/chat-proxy', async (req, res) => { const upstreamRes = await fetch('https://api.coze.cn/v3/chat', { // ...相同配置 }); // 保持流式特性 res.setHeader('Content-Type', 'text/event-stream'); upstreamRes.body.pipe(res); });
調(diào)試技巧:
- 在Chrome開發(fā)者工具中開啟「保留日志」查看完整流式請求
- 使用
curl -N URL
驗(yàn)證原始API的流式特性 - 添加調(diào)試日志記錄原始數(shù)據(jù)塊:
console.log('[Raw Chunk]', chunk);
02 過程分析
用戶現(xiàn)在想要在前端項(xiàng)目中實(shí)現(xiàn)流式接收并展示數(shù)據(jù),特別是在對話框中使用類似setHistoryContent這樣的狀態(tài)更新函數(shù)。用戶給出的代碼片段顯示他們正在使用React的狀態(tài)管理,將消息添加到歷史記錄中。用戶可能已經(jīng)有一個(gè)基本的聊天界面,現(xiàn)在需要集成Coze API的流式響應(yīng)。
首先,我需要理解用戶的具體需求。他們可能已經(jīng)能夠發(fā)送消息到后端,但接收響應(yīng)時(shí)是流式的,需要逐步顯示數(shù)據(jù),而不是等待整個(gè)響應(yīng)完成。這可能涉及到使用Fetch API的流式處理功能,或者WebSocket,但根據(jù)之前的對話,用戶使用的是Fetch。
接下來,用戶提供的代碼示例中,使用setHistoryContent來更新消息歷史。當(dāng)前的實(shí)現(xiàn)可能是在接收到完整響應(yīng)后一次性添加消息,但現(xiàn)在需要改為每收到一個(gè)數(shù)據(jù)塊就更新一次。這需要處理流式數(shù)據(jù),逐步拼接內(nèi)容,并更新對應(yīng)的消息條目。
需要考慮的問題包括:
- 如何正確處理流式數(shù)據(jù),確保每個(gè)數(shù)據(jù)塊都能被及時(shí)處理。
- 如何管理React的狀態(tài),特別是當(dāng)需要更新現(xiàn)有消息而不是添加新消息時(shí)。
- 如何處理可能的并發(fā)請求,避免狀態(tài)混亂。
- 錯(cuò)誤處理和加載狀態(tài)的顯示。
用戶之前的例子中,每次接收到新的內(nèi)容,都會(huì)添加一個(gè)新的消息對象到歷史記錄中。但流式響應(yīng)應(yīng)該是逐步更新同一個(gè)消息的內(nèi)容,而不是創(chuàng)建多個(gè)消息。因此,可能需要調(diào)整狀態(tài)結(jié)構(gòu),使得可以更新特定消息的文本內(nèi)容。
例如,初始時(shí)添加一個(gè)帶有臨時(shí)ID的消息對象,表示機(jī)器人的響應(yīng)正在加載,然后隨著數(shù)據(jù)塊的到來,不斷更新該消息的text屬性。完成后再標(biāo)記為完成。
可能的實(shí)現(xiàn)步驟:
- 用戶發(fā)送消息時(shí),立即將用戶的消息添加到歷史記錄中。
- 發(fā)送請求到Coze API,開始接收流式響應(yīng)。
- 在接收到第一個(gè)數(shù)據(jù)塊時(shí),創(chuàng)建一個(gè)新的消息對象,標(biāo)記為機(jī)器人,并開始填充text。
- 后續(xù)的數(shù)據(jù)塊不斷追加到該消息的text中。
- 流式結(jié)束時(shí),可能標(biāo)記該消息為完成,或處理可能的錯(cuò)誤。
需要處理的技術(shù)點(diǎn)包括使用Fetch API的ReadableStream,正確讀取數(shù)據(jù)塊,并在React中高效更新狀態(tài)。同時(shí),需要確保在組件卸載時(shí)取消請求,避免內(nèi)存泄漏。
另外,用戶可能需要處理消息的ID管理,確保在更新時(shí)能找到正確的消息條目。例如,使用唯一ID來標(biāo)識每個(gè)消息,并在流式響應(yīng)開始時(shí)生成一個(gè)ID,后續(xù)更新都針對該ID。
可能的代碼結(jié)構(gòu):
- 發(fā)送用戶消息時(shí),添加到歷史記錄。
- 發(fā)起Fetch請求,獲取響應(yīng)流。
- 讀取流中的數(shù)據(jù),逐步更新對應(yīng)的機(jī)器人消息內(nèi)容。
- 使用useState或useReducer管理消息歷史狀態(tài)。
- 使用AbortController來允許取消請求。
需要確保在React的嚴(yán)格模式下,狀態(tài)更新不會(huì)導(dǎo)致問題,尤其是在異步處理流時(shí)??赡苄枰褂胷ef來保持最新狀態(tài)的引用,避免閉包問題。
最后,測試流式響應(yīng)是否正確,確保每個(gè)數(shù)據(jù)塊都能實(shí)時(shí)顯示,并且界面不會(huì)因?yàn)轭l繁的狀態(tài)更新而卡頓??赡苄枰獌?yōu)化渲染性能,比如使用React.memo或適當(dāng)?shù)臓顟B(tài)結(jié)構(gòu)。
以下是針對對話框場景的流式數(shù)據(jù)接收和展示完整實(shí)現(xiàn)方案:
import { useState } from 'react'; interface Message { id: string; author: 'User' | 'Bot'; text: string; isStreaming?: boolean; } export default function ChatDialog() { const [history, setHistory] = useState<Message[]>([]); const [currentStreamId, setCurrentStreamId] = useState<string | null>(null); // 生成唯一ID的工具函數(shù) const generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2); // 處理流式響應(yīng) const handleStreamResponse = async (userInput: string) => { // 添加用戶消息 setHistory(prev => [ ...prev, { id: generateId(), author: 'User', text: userInput } ]); // 創(chuàng)建機(jī)器人消息占位符 const botMessageId = generateId(); setHistory(prev => [ ...prev, { id: botMessageId, author: 'Bot', text: '', isStreaming: true } ]); setCurrentStreamId(botMessageId); try { const response = await fetch('/api/coze-stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: userInput }) }); const reader = response.body?.getReader(); const decoder = new TextDecoder(); let done = false; if (!reader) return; while (!done) { const { value, done: streamDone } = await reader.read(); done = streamDone; if (value) { const chunk = decoder.decode(value); setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: msg.text + chunk } : msg )); } } } catch (error) { setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: '請求失敗,請重試', isStreaming: false } : msg )); } finally { setCurrentStreamId(null); setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, isStreaming: false } : msg )); } }; return ( <div className="chat-container"> {history.map(message => ( <div key={message.id} className={`message ${message.author.toLowerCase()}`} > <div className="author">{message.author}</div> <div className="text"> {message.text} {message.isStreaming && <span className="stream-cursor">▋</span>} </div> </div> ))} <button onClick={() => handleStreamResponse('你好,有什么可以幫助的?')} disabled={!!currentStreamId} > {currentStreamId ? '回復(fù)中...' : '發(fā)送消息'} </button> </div> ); }
實(shí)現(xiàn)說明:
消息狀態(tài)管理:
// 使用數(shù)組存儲對話歷史 const [history, setHistory] = useState<Message[]>([]); // 當(dāng)前正在流式接收的消息ID const [currentStreamId, setCurrentStreamId] = useState<string | null>(null);
流式處理核心邏輯:
// 創(chuàng)建機(jī)器人消息占位符 const botMessageId = generateId(); setHistory(prev => [ ...prev, { id: botMessageId, author: 'Bot', text: '', isStreaming: true } ]); // 逐步更新消息內(nèi)容 setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: msg.text + chunk } : msg ));
UI 優(yōu)化功能:
// 流式輸入時(shí)的閃爍光標(biāo) {message.isStreaming && <span className="stream-cursor">▋</span>} // 禁用按鈕防止重復(fù)請求 <button disabled={!!currentStreamId}>
錯(cuò)誤處理機(jī)制:
catch (error) { setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: '請求失敗,請重試', isStreaming: false } : msg )); }
配套樣式建議:
.chat-container { max-width: 600px; margin: 0 auto; padding: 20px; } .message { margin: 10px 0; padding: 12px; border-radius: 8px; } .message.user { background: #e3f2fd; margin-left: 20%; } .message.bot { background: #f5f5f5; margin-right: 20%; } .stream-cursor { animation: blink 1s infinite; margin-left: 4px; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
優(yōu)化建議:
添加消息分塊緩沖:
let buffer = ''; const processChunk = (chunk: string) => { buffer += chunk; const sentences = buffer.split(/(?<=[。???\n])/); buffer = sentences.pop() || ''; if (sentences.length > 0) { setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: msg.text + sentences.join('') } : msg )); } }; // 在循環(huán)中調(diào)用 processChunk(chunk);
性能優(yōu)化:
// 使用防抖更新 const updateText = debounce((newText: string) => { setHistory(prev => prev.map(msg => msg.id === botMessageId ? { ...msg, text: newText } : msg )); }, 50); // 在循環(huán)中 updateText(currentText + chunk);
支持Markdown渲染:
import ReactMarkdown from 'react-markdown'; // 替換原有文本展示 <div className="text"> <ReactMarkdown>{message.text}</ReactMarkdown> {message.isStreaming && <span className="stream-cursor">▋</span>} </div>
添加消息撤回功能:
const handleRetract = (messageId: string) => { setHistory(prev => prev.filter(msg => msg.id !== messageId) ); }; // 在消息組件中添加 <button className="retract-btn" onClick={() => handleRetract(message.id)} > × </button>
該方案實(shí)現(xiàn)了以下核心功能:
- 完整的對話歷史管理
- 實(shí)時(shí)流式消息展示
- 自動(dòng)滾動(dòng)支持
- 錯(cuò)誤狀態(tài)處理
- 流暢的交互反饋
- 防止重復(fù)請求
可根據(jù)實(shí)際需求添加以下擴(kuò)展功能:
- 消息編輯功能
- 對話持久化存儲
- 消息類型支持(圖片/文件)
- 打字機(jī)滾動(dòng)效果
- 消息翻譯功能
03 簡單代碼應(yīng)用
import { useState } from 'react'; export default function ChatApp() { const [message, setMessage] = useState(''); const [loading, setLoading] = useState(false); const fetchStream = async (inputText) => { setLoading(true); try { const response = await fetch('http://localhost:3001/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: inputText }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); setMessage(prev => prev + chunk); // 實(shí)時(shí)更新消息 } } catch (error) { console.error('請求失敗:', error); } finally { setLoading(false); } }; return ( <div> <button onClick={() => fetchStream('你好')} disabled={loading} > {loading ? '發(fā)送中...' : '開始對話'} </button> <div style={{ border: '1px solid #ccc', padding: '1rem' }}> {message} </div> </div> ); }
到此這篇關(guān)于Javascript中Fetch請求Coze API并流式展示請求結(jié)果的文章就介紹到這了,更多相關(guān)js Fetch請求Coze API內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Jquery+javascript實(shí)現(xiàn)支付網(wǎng)頁數(shù)字鍵盤
這篇文章主要為大家詳細(xì)介紹了Jquery+javascript實(shí)現(xiàn)支付網(wǎng)頁數(shù)字鍵盤,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12學(xué)習(xí)RxJS之JavaScript框架Cycle.js
這篇文章主要介紹了學(xué)習(xí)RxJS之JavaScript框架Cycle.js ,它是一個(gè)極簡的JavaScript框架(核心部分加上注釋125行),提供了一種函數(shù)式,響應(yīng)式的人機(jī)交互接口,需要的朋友可以參考下2019-06-06JavaScript DOM節(jié)點(diǎn)添加示例
本節(jié)為大家介紹下JavaScript DOM節(jié)點(diǎn)添加,將node里面的str放到h1段落中使之成為h1的新段落2014-07-07javascript實(shí)現(xiàn)圖片延遲加載方法匯總(三種方法)
看到一些大型網(wǎng)站,頁面如果有很多圖片的時(shí)候,當(dāng)你滾動(dòng)到相應(yīng)的行時(shí),當(dāng)前行的圖片才即時(shí)加載的,這樣子的話頁面在打開只加可視區(qū)域的圖片,而其它隱藏的圖片則不加載,一定程序上加快了頁面加載的速度,跟著小編一起學(xué)習(xí)javascript實(shí)現(xiàn)圖片延遲加載吧2015-08-08JavaScript實(shí)現(xiàn)點(diǎn)擊按鈕字體放大、縮小
字體可以調(diào)節(jié)大小,極大了滿足了用戶體驗(yàn)度,接下來通過本文給大家介紹JavaScript實(shí)現(xiàn)點(diǎn)擊按鈕字體放大、縮小實(shí)例代碼,需要的朋友參考下吧2016-02-02推薦一個(gè)封裝好的getElementsByClassName方法
這篇文章主要推薦一個(gè)封裝好的getElementsByClassName方法,需要的朋友可以參考下2014-12-12JavaScript 報(bào)表展示實(shí)現(xiàn)代碼
以下是從網(wǎng)上找到的一段JavaScript實(shí)現(xiàn)圖形報(bào)表的代碼,對于想客戶端顯示報(bào)表的朋友可以參考下。2009-12-12javascript刪除html標(biāo)簽函數(shù)cIsHTML
這篇文章主要介紹了javascript刪除html標(biāo)簽函數(shù)cIsHTML,需要的朋友可以參考下2017-01-01使用Require.js封裝原生js輪播圖的實(shí)現(xiàn)代碼
這篇文章主要介紹了使用Require.js封裝原生js輪播圖的實(shí)現(xiàn)代碼,需要的朋友可以參考下2017-06-06