前端SSE流式請(qǐng)求的三種實(shí)現(xiàn)方案
一、什么是 SSE?
SSE(Server-Sent Events)是 HTML5 標(biāo)準(zhǔn)的一部分,允許服務(wù)器主動(dòng)向客戶端推送數(shù)據(jù)。與 WebSocket 雙向通信不同,SSE 是單向的(服務(wù)器 → 客戶端),但實(shí)現(xiàn)更簡(jiǎn)單,且基于 HTTP 協(xié)議。
SSE 的典型應(yīng)用場(chǎng)景
- AI 對(duì)話流式輸出:ChatGPT、Claude 等 AI 助手的打字機(jī)效果
- 實(shí)時(shí)通知推送:消息提醒、系統(tǒng)通知
- 實(shí)時(shí)數(shù)據(jù)更新:股票行情、體育比分
- 進(jìn)度監(jiān)控:文件上傳進(jìn)度、任務(wù)執(zhí)行狀態(tài)
SSE 協(xié)議格式
data: 消息內(nèi)容\n\n
完整格式包含多個(gè)字段:
event: message\n id: 1\n retry: 3000\n data: 消息內(nèi)容\n\n
data:- 消息內(nèi)容(必需)event:- 事件類型(可選,默認(rèn)為 message)id:- 消息 ID(可選,用于斷線重連)retry:- 重連間隔(可選,單位毫秒)\n\n- 事件分隔符(兩個(gè)換行符)
二、三種實(shí)現(xiàn)方案對(duì)比
方案一:原生 EventSource API
基本用法
const eventSource = new EventSource('/api/sse');
eventSource.onmessage = (event) => {
console.log('收到消息:', event.data);
};
eventSource.onerror = (error) => {
console.error('連接錯(cuò)誤:', error);
eventSource.close();
};
// 手動(dòng)關(guān)閉連接
eventSource.close();
完整封裝示例
class SSEClient {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.eventSource = null;
this.isCompleted = false;
}
connect() {
this.eventSource = new EventSource(this.url);
this.eventSource.onmessage = (event) => {
// 檢查結(jié)束信號(hào)
if (event.data === '[DONE]') {
this.close('done');
return;
}
// 觸發(fā)消息回調(diào)
this.options.onMessage?.(event.data);
};
this.eventSource.onerror = () => {
if (!this.isCompleted) {
this.options.onError?.(new Error('SSE 連接異常'));
}
this.close('error');
};
}
close(reason = 'manual') {
if (this.isCompleted) return;
this.isCompleted = true;
this.eventSource?.close();
this.options.onComplete?.(reason);
}
}
// 使用示例
const client = new SSEClient('/api/chat', {
onMessage: (data) => {
console.log('收到:', data);
},
onComplete: (reason) => {
console.log('結(jié)束:', reason);
},
onError: (err) => {
console.error('錯(cuò)誤:', err);
},
});
client.connect();
// 手動(dòng)停止
client.close();
優(yōu)勢(shì)
? 零依賴:瀏覽器原生 API,無(wú)需引入任何庫(kù)
? 自動(dòng)解析:瀏覽器自動(dòng)處理 SSE 協(xié)議解析
? 自動(dòng)重連:連接斷開后自動(dòng)重連(可配置 retry 時(shí)間)
? 穩(wěn)定可靠:瀏覽器原生實(shí)現(xiàn),經(jīng)過(guò)充分測(cè)試
? 代碼簡(jiǎn)潔:核心邏輯僅需 20-30 行代碼
劣勢(shì)
? 僅支持 GET 請(qǐng)求:無(wú)法發(fā)送 POST 請(qǐng)求體
? 無(wú)法自定義 Header:除了 Cookie,無(wú)法添加其他 Header(如 Authorization)
? 認(rèn)證限制:只能通過(guò) URL 參數(shù)或 Cookie 傳遞認(rèn)證信息
適用場(chǎng)景
- 后端接口支持 GET 請(qǐng)求
- 認(rèn)證信息通過(guò) Cookie 自動(dòng)攜帶
- 追求零依賴和最簡(jiǎn)實(shí)現(xiàn)
- 不需要自定義 Header
常見踩坑
1. 跨域問(wèn)題
// 錯(cuò)誤:跨域請(qǐng)求會(huì)失敗
const es = new EventSource('https://other-domain.com/sse');
// 解決方案 1:服務(wù)器設(shè)置 CORS
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Credentials: true
// 解決方案 2:使用代理
const es = new EventSource('/api/proxy/sse');
2. 自動(dòng)重連導(dǎo)致重復(fù)請(qǐng)求
// 問(wèn)題:連接斷開后會(huì)自動(dòng)重連,可能導(dǎo)致重復(fù)請(qǐng)求 // 解決方案:服務(wù)器端使用 id 字段去重 // event: message // id: 123 // data: 消息內(nèi)容 // 客戶端重連時(shí)會(huì)自動(dòng)發(fā)送 Last-Event-ID header
3. 無(wú)法傳遞 POST 數(shù)據(jù)
// 錯(cuò)誤:EventSource 不支持 POST
const es = new EventSource('/api/chat', {
method: 'POST', // 無(wú)效!
body: JSON.stringify({ message: 'Hello' })
});
// 解決方案:先通過(guò) POST 保存數(shù)據(jù),再用 GET 建立 SSE 連接
async function sendMessage(message) {
// 1. POST 保存消息
const res = await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify({ message }),
});
const { messageId } = await res.json();
// 2. GET 建立 SSE 連接
const es = new EventSource(`/api/sse?messageId=${messageId}`);
}
方案二:Fetch API + ReadableStream
基本用法
async function fetchSSE(url, options = {}) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
},
body: JSON.stringify({ message: 'Hello' }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
options.onComplete?.();
break;
}
// 解碼二進(jìn)制數(shù)據(jù)
buffer += decoder.decode(value, { stream: true });
// 按行分割(SSE 以 \n\n 分隔事件)
const lines = buffer.split('\n\n');
buffer = lines.pop() || ''; // 保留不完整的行
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
options.onComplete?.();
return;
}
options.onMessage?.(data);
}
}
}
}
// 使用示例
fetchSSE('/api/chat', {
onMessage: (data) => {
console.log('收到:', data);
},
onComplete: () => {
console.log('完成');
},
});
核心原理詳解
1. ReadableStream 流式讀取
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// value 是 Uint8Array 類型的二進(jìn)制數(shù)據(jù)
console.log(value);
}
2. TextDecoder 解碼
const decoder = new TextDecoder();
// { stream: true } 很重要!
// 處理多字節(jié)字符被拆分到不同 chunk 的情況
const text = decoder.decode(value, { stream: true });
為什么需要 { stream: true }?
// 假設(shè) "你好" 的 UTF-8 編碼被拆分到兩個(gè) chunk
// chunk1: [0xE4, 0xBD] // "你" 的前兩個(gè)字節(jié)
// chunk2: [0xA0, 0xE5, 0xA5, 0xBD] // "你" 的最后一個(gè)字節(jié) + "好"
const decoder = new TextDecoder();
// 不使用 stream: true(錯(cuò)誤)
decoder.decode(chunk1); // 亂碼或報(bào)錯(cuò)
decoder.decode(chunk2); // 亂碼或報(bào)錯(cuò)
// 使用 stream: true(正確)
decoder.decode(chunk1, { stream: true }); // "" (等待完整字符)
decoder.decode(chunk2, { stream: true }); // "你好"
3. Buffer 緩沖區(qū)管理
let buffer = '';
// 收到 chunk1: "data: Hello\n"
buffer += "data: Hello\n";
const lines = buffer.split('\n\n'); // ["data: Hello\n"]
buffer = lines.pop(); // buffer = "data: Hello\n"
// 收到 chunk2: "\ndata: World\n\n"
buffer += "\ndata: World\n\n"; // "data: Hello\n\ndata: World\n\n"
const lines = buffer.split('\n\n'); // ["data: Hello", "data: World", ""]
buffer = lines.pop(); // buffer = ""
// 處理完整的事件
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
onMessage(data); // "Hello", "World"
}
}
4. 支持中斷
const controller = new AbortController();
fetch(url, {
signal: controller.signal,
});
// 中斷請(qǐng)求
controller.abort();
完整封裝示例
class FetchSSEClient {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.controller = new AbortController();
}
async connect() {
try {
const response = await fetch(this.url, {
method: this.options.method || 'POST',
headers: this.options.headers || {},
body: this.options.body,
signal: this.controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
this.options.onComplete?.('done');
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
this.options.onComplete?.('done');
return;
}
this.options.onMessage?.(data);
}
}
}
} catch (error) {
if (error.name === 'AbortError') {
this.options.onComplete?.('abort');
} else {
this.options.onError?.(error);
this.options.onComplete?.('error');
}
}
}
abort() {
this.controller.abort();
}
}
// 使用示例
const client = new FetchSSEClient('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
},
body: JSON.stringify({ message: 'Hello' }),
onMessage: (data) => {
console.log('收到:', data);
},
onComplete: (reason) => {
console.log('結(jié)束:', reason);
},
onError: (err) => {
console.error('錯(cuò)誤:', err);
},
});
client.connect();
// 手動(dòng)停止
client.abort();
優(yōu)勢(shì)
? 支持 POST 請(qǐng)求:可以發(fā)送請(qǐng)求體
? 自定義 Header:可以添加任意 Header(如 Authorization)
? 完全控制:可以自定義超時(shí)、重試等邏輯
? 零依賴:瀏覽器原生 API
? 支持中斷:使用 AbortController 中斷請(qǐng)求
劣勢(shì)
? 需要手動(dòng)解析 SSE 協(xié)議:需要處理 data:、event:、id: 等字段
? 需要處理 chunk 邊界:一個(gè)事件可能被拆分到多個(gè) chunk
? 需要管理 buffer:手動(dòng)拼接不完整的數(shù)據(jù)
? 代碼復(fù)雜度高:核心邏輯需要 50+ 行代碼
? 容易出錯(cuò):邊界情況處理不當(dāng)會(huì)導(dǎo)致數(shù)據(jù)丟失或重復(fù)
適用場(chǎng)景
- 需要 POST 請(qǐng)求和自定義 Header
- 需要完全控制請(qǐng)求行為
- 后端返回的不是標(biāo)準(zhǔn) SSE 格式
- 需要讀取響應(yīng) Header 或狀態(tài)碼
常見踩坑
1. 忘記使用 { stream: true }
// 錯(cuò)誤:多字節(jié)字符會(huì)亂碼
const text = decoder.decode(value);
// 正確
const text = decoder.decode(value, { stream: true });
2. buffer 管理錯(cuò)誤
// 錯(cuò)誤:直接 split 會(huì)丟失不完整的數(shù)據(jù)
const lines = buffer.split('\n\n');
for (const line of lines) {
// 處理...
}
buffer = ''; // 錯(cuò)誤!最后一個(gè)可能不完整
// 正確:保留最后一個(gè)元素
const lines = buffer.split('\n\n');
buffer = lines.pop() || ''; // 保留不完整的部分
for (const line of lines) {
// 處理...
}
3. 沒(méi)有處理中斷
// 錯(cuò)誤:沒(méi)有處理 AbortError
try {
await fetch(url, { signal });
} catch (error) {
console.error(error); // AbortError 也會(huì)被當(dāng)作錯(cuò)誤
}
// 正確:區(qū)分 AbortError
try {
await fetch(url, { signal });
} catch (error) {
if (error.name === 'AbortError') {
console.log('用戶取消');
} else {
console.error('請(qǐng)求失敗:', error);
}
}
4. 代理層緩沖問(wèn)題
// 問(wèn)題:Nginx 等代理可能會(huì)緩沖響應(yīng),導(dǎo)致消息延遲
// 解決方案:服務(wù)器端配置
// Nginx:
// proxy_buffering off;
// proxy_cache off;
// proxy_set_header Connection '';
// proxy_http_version 1.1;
// chunked_transfer_encoding off;
// Node.js:
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders(); // 立即發(fā)送 headers
方案三:@microsoft/fetch-event-source
基本用法
npm install @microsoft/fetch-event-source
import { fetchEventSource } from '@microsoft/fetch-event-source';
fetchEventSource('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
},
body: JSON.stringify({ message: 'Hello' }),
onmessage(event) {
if (event.data === '[DONE]') {
return;
}
console.log('收到:', event.data);
},
onclose() {
console.log('連接關(guān)閉');
},
onerror(err) {
console.error('錯(cuò)誤:', err);
throw err; // 拋出錯(cuò)誤會(huì)停止重連
},
openWhenHidden: true, // 頁(yè)面隱藏時(shí)保持連接
});
高級(jí)用法
class MSFetchSSEClient {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.ctrl = new AbortController();
}
async connect() {
await fetchEventSource(this.url, {
method: this.options.method || 'POST',
headers: this.options.headers || {},
body: this.options.body,
signal: this.ctrl.signal,
onmessage: (event) => {
if (event.data === '[DONE]') {
this.abort();
return;
}
this.options.onMessage?.(event.data);
},
onclose: () => {
this.options.onComplete?.('done');
},
onerror: (err) => {
this.options.onError?.(err);
// 拋出錯(cuò)誤會(huì)停止重連
throw err;
},
openWhenHidden: true,
});
}
abort() {
this.ctrl.abort();
}
}
// 使用示例
const client = new MSFetchSSEClient('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
},
body: JSON.stringify({ message: 'Hello' }),
onMessage: (data) => {
console.log('收到:', data);
},
onComplete: (reason) => {
console.log('結(jié)束:', reason);
},
onError: (err) => {
console.error('錯(cuò)誤:', err);
},
});
client.connect();
// 手動(dòng)停止
client.abort();
優(yōu)勢(shì)
? 支持 POST 請(qǐng)求:可以發(fā)送請(qǐng)求體
? 自定義 Header:可以添加任意 Header
? 自動(dòng)解析 SSE 協(xié)議:無(wú)需手動(dòng)處理
? 內(nèi)置自動(dòng)重連:連接斷開后自動(dòng)重連
? 頁(yè)面隱藏時(shí)保持連接:openWhenHidden 選項(xiàng)
? 體積小:~2KB gzip
? TypeScript 支持:完整的類型定義
劣勢(shì)
? 需要引入外部依賴:增加打包體積
? 自動(dòng)重連可能不符合需求:需要手動(dòng)控制
? 錯(cuò)誤處理復(fù)雜:需要拋出錯(cuò)誤才能停止重連
? 中斷機(jī)制不直觀:需要使用 AbortController
適用場(chǎng)景
- 需要 POST 請(qǐng)求和自定義 Header
- 需要自動(dòng)重連功能
- 不介意引入外部依賴
- 追求開發(fā)效率
常見踩坑
1. 自動(dòng)重連導(dǎo)致重復(fù)請(qǐng)求
// 問(wèn)題:連接斷開后會(huì)自動(dòng)重連,可能導(dǎo)致重復(fù)請(qǐng)求
// 解決方案 1:拋出錯(cuò)誤停止重連
onerror(err) {
console.error(err);
throw err; // 停止重連
}
// 解決方案 2:使用 AbortController
const ctrl = new AbortController();
fetchEventSource(url, {
signal: ctrl.signal,
onerror(err) {
ctrl.abort(); // 停止重連
},
});
2. 無(wú)法獲取完成狀態(tài)
// 問(wèn)題:onclose 回調(diào)無(wú)法區(qū)分正常結(jié)束還是錯(cuò)誤結(jié)束
// 解決方案:自己維護(hù)狀態(tài)
let isCompleted = false;
fetchEventSource(url, {
onmessage(event) {
if (event.data === '[DONE]') {
isCompleted = true;
ctrl.abort();
}
},
onclose() {
if (isCompleted) {
console.log('正常結(jié)束');
} else {
console.log('異常結(jié)束');
}
},
});
3. 頁(yè)面隱藏時(shí)連接斷開
// 問(wèn)題:默認(rèn)情況下,頁(yè)面隱藏時(shí)連接會(huì)斷開
// 解決方案:設(shè)置 openWhenHidden
fetchEventSource(url, {
openWhenHidden: true, // 頁(yè)面隱藏時(shí)保持連接
});
三、方案對(duì)比總結(jié)
| 維度 | EventSource | Fetch + ReadableStream | @microsoft/fetch-event-source |
|---|---|---|---|
| 支持 POST | ? 僅 GET | ? | ? |
| 自定義 Header | ? 僅 Cookie | ? | ? |
| SSE 協(xié)議解析 | ? 瀏覽器原生 | ? 需手動(dòng)實(shí)現(xiàn) | ? 自動(dòng)處理 |
| 自動(dòng)重連 | ? 瀏覽器原生 | ? 需手動(dòng)實(shí)現(xiàn) | ? 內(nèi)置 |
| 手動(dòng)中斷 | ? close() | ? AbortController | ?? AbortController + throw |
| 代碼復(fù)雜度 | 低(~30行) | 高(~50行) | 中(~10行) |
| 外部依賴 | 零依賴 | 零依賴 | ~2KB |
| 瀏覽器兼容性 | IE 10+ | 現(xiàn)代瀏覽器 | 現(xiàn)代瀏覽器 |
| 學(xué)習(xí)成本 | 低 | 高 | 中 |
| 適用場(chǎng)景 | GET + Cookie 認(rèn)證 | 完全自定義 | POST + 自動(dòng)重連 |
四、選型建議
選擇 EventSource 的情況
- ? 后端接口支持 GET 請(qǐng)求
- ? 認(rèn)證信息通過(guò) Cookie 自動(dòng)攜帶
- ? 追求零依賴和最簡(jiǎn)實(shí)現(xiàn)
- ? 不需要自定義 Header
選擇 Fetch + ReadableStream 的情況
- ? 需要完全控制請(qǐng)求行為
- ? 后端返回的不是標(biāo)準(zhǔn) SSE 格式
- ? 需要讀取響應(yīng) Header 或狀態(tài)碼
- ? 需要自定義超時(shí)、重試邏輯
選擇 @microsoft/fetch-event-source 的情況
- ? 需要 POST 請(qǐng)求和自定義 Header
- ? 需要自動(dòng)重連功能
- ? 不介意引入外部依賴
- ? 追求開發(fā)效率
五、實(shí)戰(zhàn)經(jīng)驗(yàn)總結(jié)
1. 如何處理認(rèn)證
方案 A:Cookie(推薦 EventSource)
// 服務(wù)器設(shè)置 Cookie
res.cookie('token', 'xxx', { httpOnly: true });
// 客戶端自動(dòng)攜帶
const es = new EventSource('/api/sse');
方案 B:URL 參數(shù)(適用 EventSource)
const token = 'xxx';
const es = new EventSource(`/api/sse?token=${token}`);
方案 C:Authorization Header(需要 Fetch)
fetch('/api/sse', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
2. 如何處理超時(shí)
EventSource(無(wú)法設(shè)置超時(shí))
// 解決方案:手動(dòng)實(shí)現(xiàn)超時(shí)
const es = new EventSource('/api/sse');
const timeout = setTimeout(() => {
es.close();
console.log('超時(shí)');
}, 30000);
es.onmessage = () => {
clearTimeout(timeout);
};
Fetch(使用 AbortController)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
fetch('/api/sse', {
signal: controller.signal,
});
3. 如何處理重連
EventSource(自動(dòng)重連)
// 服務(wù)器端設(shè)置重連間隔
res.write('retry: 3000\n\n'); // 3 秒后重連
// 客戶端無(wú)法禁用自動(dòng)重連
// 只能通過(guò) close() 關(guān)閉連接
Fetch(手動(dòng)重連)
async function connectWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
await fetchSSE(url, { /* ... */ });
break; // 成功,退出循環(huán)
} catch (error) {
if (i === maxRetries - 1) {
throw error; // 最后一次重試失敗
}
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}
4. 如何處理代理層緩沖
問(wèn)題: Nginx 等代理可能會(huì)緩沖響應(yīng),導(dǎo)致消息延遲
解決方案:
# Nginx 配置
location /api/sse {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}
// Node.js 服務(wù)器
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // 禁用 Nginx 緩沖
res.flushHeaders(); // 立即發(fā)送 headers
5. 如何處理跨域
CORS 配置:
// 服務(wù)器端
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
代理方案:
// 開發(fā)環(huán)境使用代理
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://backend.com',
changeOrigin: true,
},
},
},
};
六、總結(jié)
SSE 是實(shí)現(xiàn)服務(wù)器推送的簡(jiǎn)單而強(qiáng)大的技術(shù),三種實(shí)現(xiàn)方案各有優(yōu)劣:
- EventSource:零依賴、最簡(jiǎn)單,適合 GET 請(qǐng)求場(chǎng)景
- Fetch + ReadableStream:完全控制、最靈活,適合復(fù)雜場(chǎng)景
- @microsoft/fetch-event-source:功能完善、開箱即用,適合快速開發(fā)
選擇哪種方案取決于具體需求,建議優(yōu)先考慮 EventSource,只有在需要 POST 或自定義 Header 時(shí)才使用其他方案。
以上就是前端SSE流式請(qǐng)求的三種實(shí)現(xiàn)方案的詳細(xì)內(nèi)容,更多關(guān)于前端SSE流式請(qǐng)求實(shí)現(xiàn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript設(shè)計(jì)模式 – 代理模式原理與用法實(shí)例分析
這篇文章主要介紹了javascript設(shè)計(jì)模式 – 代理模式,結(jié)合實(shí)例形式分析了javascript代理模式相關(guān)概念、原理、用法及操作注意事項(xiàng),需要的朋友可以參考下2020-04-04
php的派發(fā)機(jī)制實(shí)現(xiàn)方法
PHP是一種動(dòng)態(tài)類型的編程語(yǔ)言,它支持面向?qū)ο缶幊?在PHP中,派發(fā)指在運(yùn)行時(shí)確定要調(diào)用的方法或函數(shù)的過(guò)程,派發(fā)機(jī)制允許根據(jù)實(shí)際對(duì)象的類型來(lái)選擇要執(zhí)行的方法,這種靈活性使得PHP可以實(shí)現(xiàn)多態(tài)性,本文將給大家介紹php的派發(fā)機(jī)制是怎么實(shí)現(xiàn)的,需要的朋友可以參考下2023-10-10
JavaScript 函數(shù)式編程實(shí)踐(來(lái)自IBM)
說(shuō)到函數(shù)式編程,人們的第一印象往往是其學(xué)院派,晦澀難懂,大概只有那些蓬頭散發(fā),不修邊幅,甚至有些神經(jīng)質(zhì)的大學(xué)教授們才會(huì)用的編程方式。2010-06-06
layui之select的option疊加問(wèn)題的解決方法
下面小編就為大家分享一篇layui之select的option疊加問(wèn)題的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03

