Scott

不使用axios nextjs如何发送http请求 8 months ago

nextjs
8032个字符
共有81人围观

博客大纲

客户端

首先创建一个 API 工具函数:

这里以src/utils/api_client.tsx为例:

interface FetchConfig extends RequestInit {
    timeout?: number;
}

export async function fetchApi<T>(url: string, config: FetchConfig = {}): Promise<T> {
    const { timeout = 8000, ...restConfig } = config;

    try {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);

        const response = await fetch(url, {
            ...restConfig,
            signal: controller.signal,
            headers: {
                'Content-Type': 'application/json',
                ...restConfig.headers,
            },
        });

        clearTimeout(timeoutId);

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        return data as T;
    } catch (error) {
        if (error instanceof Error) {
            throw new Error(`请求失败: ${error.message}`);
        }
        throw error;
    }
} 

然后创建一个示例组件来展示如何使用:

这里以src/components/examples/HttpExample.tsx为例:

'use client'

import { useState, useEffect } from 'react'
import { fetchApi } from '@/utils/api_client'

interface Post {
    id: number;
    title: string;
    body: string;
}

export default function HttpExample() {
    const [posts, setPosts] = useState<Post[]>([])
    const [loading, setLoading] = useState(false)
    const [error, setError] = useState<string | null>(null)

    // GET 请求示例
    const fetchPosts = async () => {
        try {
            setLoading(true)
            setError(null)
            const data = await fetchApi<Post[]>('https://jsonplaceholder.typicode.com/posts')
            setPosts(data.slice(0, 5)) // 只取前5条
        } catch (err) {
            setError(err instanceof Error ? err.message : '请求失败')
        } finally {
            setLoading(false)
        }
    }

    // POST 请求示例
    const createPost = async (title: string, body: string) => {
        try {
            const newPost = await fetchApi<Post>('https://jsonplaceholder.typicode.com/posts', {
                method: 'POST',
                body: JSON.stringify({
                    title,
                    body,
                    userId: 1,
                }),
            })
            setPosts(prev => [newPost, ...prev])
        } catch (err) {
            setError(err instanceof Error ? err.message : '创建失败')
        }
    }

    useEffect(() => {
        fetchPosts()
    }, [])

    return (
        <div className="p-4">
            <h2 className="text-2xl font-bold mb-4">HTTP 请求示例</h2>

            {/* 创建新帖子的表单 */}
            <div className="mb-6">
                <button
                    onClick={() => createPost('新文章', '这是文章内容')}
                    className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
                >
                    创建新文章
                </button>
            </div>

            {/* 加载状态 */}
            {loading && (
                <div className="text-gray-500">加载中...</div>
            )}

            {/* 错误提示 */}
            {error && (
                <div className="text-red-500 mb-4">{error}</div>
            )}

            {/* 文章列表 */}
            <div className="space-y-4">
                {posts.map(post => (
                    <div key={post.id} className="border p-4 rounded">
                        <h3 className="font-bold">{post.title}</h3>
                        <p className="text-gray-600">{post.body}</p>
                    </div>
                ))}
            </div>
        </div>
    )
} 

展示:

import HttpExample from '@/components/examples/HttpExample'

export default function Home() {
    return (
        <main className="container mx-auto px-4 py-8">
            <HttpExample />
        </main>
    )
}

其他常用请求方法示例:

// PUT 请求
const updatePost = async (id: number, data: Partial<Post>) => {
    return fetchApi<Post>(`https://jsonplaceholder.typicode.com/posts/${id}`, {
        method: 'PUT',
        body: JSON.stringify(data),
    })
}

// DELETE 请求
const deletePost = async (id: number) => {
    return fetchApi(`https://jsonplaceholder.typicode.com/posts/${id}`, {
        method: 'DELETE',
    })
}

服务端

此示例为一个获取和展示用户列表的demo:

1, 首先创建类型定义文件

src/types/api.tsx

export interface User {
    id: number;
    name: string;
    email: string;
    company: {
        name: string;
    };
}

export interface ApiResponse<T> {
    data: T;
    status: number;
    message: string;
}

src/utils/api_server.tsx

import { ApiResponse } from '@/types/api'

export async function fetchServer<T>(url: string, init?: RequestInit): Promise<ApiResponse<T>> {
    try {
        const response = await fetch(url, {
            ...init,
            headers: {
                'Content-Type': 'application/json',
                ...init?.headers,
            },
            next: { revalidate: 3600 }, // 1小时缓存
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        
        return {
            data: data as T,
            status: response.status,
            message: 'success'
        };
    } catch (error) {
        return {
            data: [] as T,
            status: 500,
            message: error instanceof Error ? error.message : '请求失败'
        };
    }
}

src/components/examples/UserList.tsx

import { User } from '@/types/api'
import { fetchServer } from '@/utils/api_server'

async function getUsers() {
    const { data } = await fetchServer<User[]>('https://jsonplaceholder.typicode.com/users');
    return data;
}

export default async function UserList() {
    const users = await getUsers();

    return (
        <div className="max-w-4xl mx-auto p-4">
            <h2 className="text-2xl font-bold mb-6">用户列表</h2>
            
            <div className="grid gap-4 md:grid-cols-2">
                {users.map(user => (
                    <div 
                        key={user.id} 
                        className="p-4 border rounded-lg shadow-sm hover:shadow-md transition-shadow"
                    >
                        <h3 className="font-semibold text-lg">{user.name}</h3>
                        <p className="text-gray-600">{user.email}</p>
                        <p className="text-gray-500 text-sm mt-2">
                            公司: {user.company.name}
                        </p>
                    </div>
                ))}
            </div>
        </div>
    );
}

loading.tsx

export default function Loading() {
    return (
        <div className="max-w-4xl mx-auto p-4">
            <div className="h-8 w-48 bg-gray-200 rounded mb-6 animate-pulse" />
            
            <div className="grid gap-4 md:grid-cols-2">
                {[...Array(6)].map((_, index) => (
                    <div 
                        key={index}
                        className="p-4 border rounded-lg"
                    >
                        <div className="h-6 w-3/4 bg-gray-200 rounded animate-pulse mb-2" />
                        <div className="h-4 w-1/2 bg-gray-200 rounded animate-pulse" />
                    </div>
                ))}
            </div>
        </div>
    );
}

error.tsx

'use client'

interface ErrorProps {
    error: Error & { digest?: string };
    reset: () => void;
}

export default function ErrorComponent({ reset }: ErrorProps) {
    return (
        <div className="max-w-4xl mx-auto p-4">
            <div className="text-center">
                <h2 className="text-2xl font-bold text-red-500 mb-4">
                    加载失败
                </h2>
                <button
                    onClick={reset}
                    className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
                >
                    重试
                </button>
            </div>
        </div>
    );
}