Skip to content

Commit 8eb6b10

Browse files
committed
feat(network): remove ofetch dependency and use native fetch
- Replace `ofetch` with native `fetch` API in http utilities - Add `timeout` option to `request` function with default value of 5000ms - Update `fetchWithTimeout` to support timeout properly - Improve `parallelRequests` implementation to maintain input order - Refactor `checkNetworkStatus` and remove outdated compatibility code - Update documentation to reflect new `timeout` parameter - Remove `ofetch` from dependencies and related configurations - Update tests to align with new implementations and remove ofetch mocks
1 parent ce9800d commit 8eb6b10

9 files changed

Lines changed: 115 additions & 88 deletions

File tree

build.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ export default defineBuildConfig({
1515
'dayjs',
1616
'decimal.js',
1717
'query-string',
18-
'ofetch',
1918
],
2019
rollup: {
2120
emitCJS: true,

docs/api/network/functions/checkNetworkStatus.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,3 @@ function checkNetworkStatus(): Promise<boolean>;
1111
## Returns
1212

1313
`Promise`\<`boolean`\>
14-
15-
是否在线

docs/api/network/functions/request.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ function request<T>(
77
method,
88
url,
99
data?,
10-
headers?): Promise<T>;
10+
headers?,
11+
timeout?): Promise<T>;
1112
```
1213

1314
封装 GET/POST 请求(自动处理 JSON)
@@ -36,6 +37,10 @@ headers?): Promise<T>;
3637

3738
`Record`\<`string`, `string`\> = `{}`
3839

40+
### timeout?
41+
42+
`number` = `5000`
43+
3944
## Returns
4045

4146
`Promise`\<`T`\>

docs/guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ pnpm build
9191
# docs
9292
cd docs
9393
pnpm run predocs:dev
94+
95+
# add test cases
9496
```
9597

9698
## License

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@
9898
"dependencies": {
9999
"dayjs": "catalog:build",
100100
"decimal.js": "catalog:build",
101-
"ofetch": "catalog:build",
102101
"query-string": "catalog:build"
103102
},
104103
"devDependencies": {

pnpm-lock.yaml

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,3 @@ catalogs:
3232
query-string: ^9.1.2
3333
decimal.js: ^10.5.0
3434
dayjs: ^1.11.13
35-
ofetch: ^1.4.1

src/network/http.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { ofetch } from 'ofetch'
2-
31
/**
42
* 带超时的 fetch 请求
53
* @category Http
@@ -16,7 +14,7 @@ async function fetchWithTimeout(
1614
const timer = setTimeout(() => controller.abort(), timeout)
1715

1816
try {
19-
const response = await ofetch(url, {
17+
const response = await fetch(url, {
2018
...options,
2119
signal: controller.signal,
2220
})
@@ -38,16 +36,17 @@ async function request<T = any>(
3836
url: string,
3937
data?: object,
4038
headers: Record<string, string> = {},
39+
timeout: number = 5000,
4140
): Promise<T> {
4241
const config: RequestInit = {
4342
method,
4443
headers: { 'Content-Type': 'application/json', ...headers },
4544
}
4645

47-
if (data)
46+
if (data && method !== 'GET')
4847
config.body = JSON.stringify(data)
4948

50-
const response = await fetchWithTimeout(url, config)
49+
const response = await fetchWithTimeout(url, config, timeout)
5150
if (!response.ok)
5251
throw new Error(`HTTP错误 ${response.status}`)
5352

@@ -64,12 +63,13 @@ async function parallelRequests<T>(
6463
requests: (() => Promise<T>)[],
6564
concurrency: number = 5,
6665
): Promise<T[]> {
67-
const results: T[] = []
66+
const results: T[] = Array.from({ length: requests.length })
6867
const executing = new Set<Promise<any>>()
6968

70-
for (const req of requests) {
69+
for (let i = 0; i < requests.length; i++) {
70+
const req = requests[i]
7171
const p = req().then((res) => {
72-
results.push(res)
72+
results[i] = res // 按输入顺序写入
7373
executing.delete(p)
7474
})
7575
executing.add(p)
@@ -115,16 +115,13 @@ async function getClientIP(): Promise<string> {
115115
* @returns IndexedDB 缓存对象
116116
*/
117117
async function getIndexedDBCache(dbName: string, storeName: string) {
118-
const openDB = (): Promise<IDBDatabase> => {
119-
return new Promise((resolve, reject) => {
118+
const openDB = (): Promise<IDBDatabase> =>
119+
new Promise((resolve, reject) => {
120120
const request = indexedDB.open(dbName, 1)
121-
request.onupgradeneeded = () => {
122-
request.result.createObjectStore(storeName)
123-
}
121+
request.onupgradeneeded = () => request.result.createObjectStore(storeName)
124122
request.onsuccess = () => resolve(request.result)
125123
request.onerror = reject
126124
})
127-
}
128125

129126
const db = await openDB()
130127

@@ -153,15 +150,13 @@ async function getIndexedDBCache(dbName: string, storeName: string) {
153150
/**
154151
* 检测网络连接状态
155152
* @category Http
156-
* @returns 是否在线
157153
*/
158154
function checkNetworkStatus(): Promise<boolean> {
159155
return new Promise((resolve) => {
160156
if (navigator.onLine !== undefined) {
161157
resolve(navigator.onLine)
162158
}
163159
else {
164-
// 兼容性方案
165160
const img = new Image()
166161
img.onload = () => resolve(true)
167162
img.onerror = () => resolve(false)

test/network/http.test.ts

Lines changed: 96 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,132 @@
1+
// tests/network.http.test.ts
12
import { beforeEach, describe, expect, it, vi } from 'vitest'
2-
import { ofetch } from 'ofetch'
3-
import { fetchWithTimeout, parallelRequests } from '../../src/network/http'
4-
5-
// Mocking ofetch
6-
vi.mock('ofetch', () => ({
7-
ofetch: vi.fn(),
8-
}))
3+
import {
4+
checkNetworkStatus,
5+
fetchWithTimeout,
6+
getClientIP,
7+
parallelRequests,
8+
request,
9+
} from '../../src/network/http'
910

11+
// ====== fetchWithTimeout 测试 ======
1012
describe('fetchWithTimeout', () => {
11-
beforeEach(() => {
12-
vi.clearAllMocks()
13-
})
13+
beforeEach(() => vi.restoreAllMocks())
1414

15-
it('should successfully fetch data within the timeout period', async () => {
16-
const mockResponse = { success: true }
17-
vi.mocked(ofetch).mockResolvedValueOnce(mockResponse)
18-
19-
const response = await fetchWithTimeout('http://example.com', {}, 3000)
20-
expect(response).toEqual(mockResponse)
21-
expect(ofetch).toHaveBeenCalledWith('http://example.com', expect.objectContaining({
22-
signal: expect.any(AbortSignal),
15+
it('successfully fetches data', async () => {
16+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
17+
ok: true,
18+
json: async () => ({ success: true }),
2319
}))
20+
const response = await fetchWithTimeout('http://example.com', {}, 3000)
21+
expect(response.ok).toBe(true)
22+
const data = await response.json()
23+
expect(data).toEqual({ success: true })
2424
})
2525

26-
it('should throw an error when the request times out', async () => {
27-
vi.mocked(ofetch).mockRejectedValueOnce(new Error('Request timed out'))
28-
26+
it('throws error when fetch fails', async () => {
27+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')))
2928
await expect(fetchWithTimeout('http://example.com', {}, 1000))
3029
.rejects
31-
.toThrow('请求超时或失败: Request timed out')
30+
.toThrow('请求超时或失败: fail')
3231
})
32+
})
3333

34-
it('should use default timeout if not provided', async () => {
35-
const mockResponse = { success: true }
36-
vi.mocked(ofetch).mockResolvedValueOnce(mockResponse)
34+
// ====== request 测试 ======
35+
describe('request', () => {
36+
beforeEach(() => vi.restoreAllMocks())
3737

38-
await fetchWithTimeout('http://example.com')
39-
expect(ofetch).toHaveBeenCalledWith(
38+
it('gET request returns parsed JSON', async () => {
39+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
40+
ok: true,
41+
json: async () => ({ message: 'ok' }),
42+
}))
43+
const data = await request('GET', 'http://example.com')
44+
expect(data).toEqual({ message: 'ok' })
45+
})
46+
47+
it('pOST request sends JSON body', async () => {
48+
const spy = vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) })
49+
vi.stubGlobal('fetch', spy)
50+
await request('POST', 'http://example.com', { foo: 'bar' })
51+
expect(spy).toHaveBeenCalledWith(
4052
'http://example.com',
4153
expect.objectContaining({
54+
method: 'POST',
55+
body: JSON.stringify({ foo: 'bar' }),
56+
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
4257
signal: expect.any(AbortSignal),
4358
}),
4459
)
4560
})
4661
})
4762

48-
/**
49-
* Mocks a promise-returning function that resolves after a delay.
50-
* @param value The value to resolve the promise with.
51-
* @param delay The time in milliseconds before the promise resolves.
52-
*/
53-
function createDelayedPromise<T>(value: T, delay: number = 0): () => Promise<T> {
54-
return () => new Promise(resolve => setTimeout(() => resolve(value), delay))
55-
}
56-
63+
// ====== parallelRequests 测试 ======
5764
describe('parallelRequests', () => {
58-
it('should execute requests sequentially when concurrency is 1', async () => {
65+
function createDelayedPromise<T>(value: T, delay: number = 0) {
66+
return () => new Promise<T>(resolve => setTimeout(() => resolve(value), delay))
67+
}
68+
69+
it('executes sequentially when concurrency = 1', async () => {
5970
const mockRequests = [
6071
createDelayedPromise('first', 100),
6172
createDelayedPromise('second', 50),
6273
createDelayedPromise('third', 200),
6374
]
64-
6575
const results = await parallelRequests(mockRequests, 1)
66-
6776
expect(results).toEqual(['first', 'second', 'third'])
6877
})
6978

70-
it('should execute requests concurrently up to the specified limit', async () => {
71-
// Arrange mocks where the first request takes the longest,
72-
// demonstrating concurrent execution.
79+
it('executes concurrently up to concurrency limit', async () => {
7380
const mockRequests = [
74-
createDelayedPromise('last', 300),
75-
createDelayedPromise('first', 100),
76-
createDelayedPromise('second', 200),
81+
createDelayedPromise('first', 200),
82+
createDelayedPromise('second', 100),
83+
createDelayedPromise('third', 150),
7784
]
78-
79-
const startTime = Date.now()
85+
const start = Date.now()
8086
const results = await parallelRequests(mockRequests, 2)
81-
const duration = Date.now() - startTime
87+
const duration = Date.now() - start
88+
expect(duration).toBeLessThan(350)
89+
expect(results).toEqual(['first', 'second', 'third'])
90+
})
91+
})
92+
93+
// ====== getClientIP 测试 ======
94+
describe('getClientIP', () => {
95+
beforeEach(() => vi.restoreAllMocks())
96+
97+
it('returns IP from successful request', async () => {
98+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
99+
ok: true,
100+
json: async () => ({ ip: '127.0.0.1' }),
101+
}))
102+
const ip = await getClientIP()
103+
expect(ip).toBe('127.0.0.1')
104+
})
105+
106+
it('returns "unknown" if request fails', async () => {
107+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')))
108+
const ip = await getClientIP()
109+
expect(ip).toBe('unknown')
110+
})
111+
})
112+
113+
// ====== checkNetworkStatus 测试 ======
114+
describe('checkNetworkStatus', () => {
115+
it('returns navigator.onLine if available', async () => {
116+
Object.defineProperty(navigator, 'onLine', { value: true, configurable: true })
117+
const status = await checkNetworkStatus()
118+
expect(status).toBe(true)
119+
})
82120

83-
// Ensure that it took less than if they were run sequentially
84-
expect(duration).toBeLessThan(600) // Total sequential time would be 600ms
85-
expect(results).toEqual(['first', 'last', 'second'])
121+
it('falls back to image load if navigator.onLine undefined', async () => {
122+
Object.defineProperty(navigator, 'onLine', { value: undefined, configurable: true })
123+
vi.stubGlobal('Image', class {
124+
onload: () => void = () => { }
125+
onerror: () => void = () => { }
126+
// eslint-disable-next-line accessor-pairs
127+
set src(_val: string) { this.onload() }
128+
} as any)
129+
const status = await checkNetworkStatus()
130+
expect(status).toBe(true)
86131
})
87132
})

0 commit comments

Comments
 (0)