Next.js 跨域问题的各种解法

发布于:2024-05-09 ⋅ 阅读:(18) ⋅ 点赞:(0)

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

跨域是使用 Next.js 常遇到的问题,问题主要分为两类:

  1. 如何调用一个跨域接口?
  2. 如何实现一个跨域接口?

本篇我们会先从跨域的基础知识开始讲起,然后讲解 Next.js 下这两大类问题的各种解决方案,帮你系统解决跨域问题!快收藏、点赞这篇文章留做备忘吧!

PS:系统学习 Next.js,欢迎入手小册。基础篇、实战篇、源码篇、面试篇四大篇章带你系统掌握 Next.js!

1. 基础知识

我们先复习一下跨域与 CORS 的基础知识。

1.1. 跨域

跨域是浏览器的行为。 出于安全性,浏览器会限制脚本内发起的跨源 HTTP 请求。XMLHttpRequest 和 Fetch API 都遵循同源策略。

所谓同源策略指的是两个 URL 的协议/主机名/端口一致。 例如,https://www.taobao.com,它的协议是 https,主机名是 www.taobao.com,端口是 443。协议、主机名、端口三者完全一致才算是同源。

这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。

比如我们在掘金主页,打开浏览器控制台,请求淘宝的接口(比如 https://www.taobao.com,便会出现 CORS 错误提示:

截屏2024-05-08 17.44.15.png

但请求同源的接口,就比如掘金自己的接口(https://juejin.cn/frontend),则可以正常请求:

截屏2024-05-08 17.46.05.png

1.2. CORS

为了能够跨域请求,便有了 CORS(Cross-Origin Resource Sharing,跨源资源共享),它是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。

简单的来说,CORS 也是浏览器实现的机制,它允许跨域请求 HTTP 资源。

浏览器会判断发送的请求是否跨域,如果跨域,请求头会带上 Origin 属性,数据返回后,检查其响应头中的 Access-Control-Allow-Origin 是否匹配,如果不匹配,则报 CORS 错误。

比如我们在掘金主页 https://juejin.cn/,控制台中请求 https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot,因为主机名不一致,所以是跨域请求:

image.png

但因为设置了 CORS,所以可以正常请求。我们查看下请求头和响应头:

截屏2024-05-08 15.31.05.png

请求头中的 Origin 为 https://juejin.cn ,而响应头中的Access-Control-Allow-Origin 就是 https://juejin.cn,两者匹配,所以可以正常跨域拿到数据。

当然这是针对简单的请求,对于复杂的请求, 比如携带自定义请求头的 POST,浏览器则会发送用于预检的 OPTIONS 请求。

简单来说就是,浏览器在发送正式的请求之前也发送一个用于检查是否可以跨域请求的 OPTIONS 类型的请求。如果请求通过,再正式发送请求。

比如我们在控制台发送一个携带自定义请求头的 POST 请求:

fetch("https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot", { method: "POST", headers: {
  "x-custom": "yayu"
}});

效果如下:

image.png

此时浏览器会先发送一个 OPTIONS 请求,它的请求头中会携带:

Access-Control-Request-Headers: x-custom,x-secsdk-csrf-token
Access-Control-Request-Method: POST
Origin: https://juejin.cn

Origin 表示请求源是 https://juejin.cn,标头 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。标头 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求标头字段:x-custom 和 x-secsdk-csrf-token。服务器据此决定,该实际请求是否被允许。

服务器返回响应头:

Access-Control-Allow-Headers: x-custom,x-secsdk-csrf-token
Access-Control-Allow-Methods: POST
Access-Control-Allow-Origin: https://juejin.cn

意思就是:行,都接受了!接着浏览器就会发送实际请求。

关于跨域和 CORS 的基础知识就这些,那我们该如何 Next.js 在处理跨域呢?

我们先说在 Next.js 中如何调用跨域接口。

2. 调用一个跨域接口?

我们先模拟一个跨域问题。使用官方脚手架,创建一个 Next.js 项目:

npx create-next-app@latest

修改 app/page.js,代码如下:

'use client'

import { useEffect, useState } from "react";

export default function Home() {

  const [html, setHTML] = useState([]);

  useEffect(() => {

    const fetchArticleList = async () => {
      const response = await fetch("https://www.juejin.com");
      const data = await response.text()
      setHTML(data)
    }

    fetchArticleList()

  }, [])

  return (
    <div>{html}</div>
  );
}

运行 npm run dev 开启开发模式。本机地址为 http://localhost:3000/,请求接口 https://www.juejin.com,因为协议和主机名都不一致,所以会出现 CORS 错误:

image.png

那么该如何避免出现 CORS 错误呢?

2.1. 使用服务端组件

第一种解决方案是改为使用服务端组件。

跨域错误是浏览器的行为,改为服务端组件,本质是改成 Node 后端调用,自然不会出现跨域问题。

修改 app/page.js,代码如下:

export default async function Home() {

  const response = await fetch("https://www.juejin.cn");
  const html = await response.text()

  return (
    <div>{html}</div>
  );
}

浏览器效果如下:

image.png

2.2. 使用后端接口转发

如果不能改为使用服务端组件,我们也可以使用 Next.js 自定义一个接口,前端改为调用此接口。

新建 app/api/juejin/route.js,代码如下:

export async function GET() {
  const res = await fetch('https://www.juejin.cn')
  const data = await res.text()
  
  return Response.json({ data })
}

这样我们就实现了一个 http://localhost:3000/api/juejin 的 GET 接口。

修改 app/page.js,依然是使用客户端组件,改为调用此接口。代码如下:

'use client'

import { useEffect, useState } from "react";

export default function Home() {

  const [html, setHTML] = useState([]);

  useEffect(() => {

    const fetchArticleList = async () => {
      const response = await fetch("http://localhost:3000/api/juejin");
      const data = await response.json()
      setHTML(data.data)
    }

    fetchArticleList()

  }, [])

  return (
    <div>{html}</div>
  );
}

浏览器效果如下:

image.png

使用服务端组件的时候,因为改为后端调用,所以浏览器中并不会查看到该请求。而使用这种方法,本质还是在浏览器端发送请求,所以可以在浏览器中查看到请求。

2.3. 使用 rewrites 配置项

Next.js 其实提供了 rewrites 配置项用于重写请求。这算是解决跨域问题常用的一种方式。

重写会将传入的请求路径映射到其他目标路径。你可以把它理解为代理,并且它会屏蔽目标路径,使得用户看起来并没有改变其在网站上的位置。

修改 next.config.mjs,代码如下:

/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/juejin',
        destination: 'https://juejin.cn',
      }
    ]
  }
};

export default nextConfig;

通过配置 next.config.js 中的 rewrites 配置项,当请求 /api/juejin 的时候,代理请求 https://juejin.cn

修改 app/page.js,代码如下:

'use client'

import { useEffect, useState } from "react";

export default function Home() {

  const [html, setHTML] = useState([]);

  useEffect(() => {

    const fetchArticleList = async () => {
      const response = await fetch("http://localhost:3000/api/juejin");
      const data = await response.text()
      setHTML(data)
    }

    fetchArticleList()

  }, [])

  return (
    <div>{html}</div>
  );
}

浏览器效果如下:

image.png

2.4. 使用中间件

不止 next.config.js 可以配置重写,你也可以在中间件中实现重写。

根目录新建 middleware.js,代码如下:

import { NextResponse } from 'next/server';

export function middleware(request) {
  if (request.nextUrl.pathname.startsWith('/api/juejin')) {
    return NextResponse.rewrite(new URL('https://juejin.cn'))
  }
}
 
export const config = {
  matcher: '/api/:path*',
}

修改 app/page.js,代码如下:

'use client'

import { useEffect, useState } from "react";

export default function Home() {

  const [html, setHTML] = useState([]);

  useEffect(() => {

    const fetchArticleList = async () => {
      const response = await fetch("http://localhost:3000/api/juejin");
      const data = await response.text()
      setHTML(data)
    }

    fetchArticleList()

  }, [])

  return (
    <div>{html}</div>
  );
}

此时浏览器效果不变:

image.png

3. 实现一个跨域接口?

如果我们要实现一个接口,允许其他源访问呢?

实现的关键在于添加 Access-Control-Allow-Origin 响应头,那么在哪里添加这个响应头呢?

其实有很多种方案可以选择:

3.1. 路由处理程序

如果只有一个或者少量接口需要实现跨域,那可以直接写在对应的路由处理程序中。

新建 app/api/blog/route.js,代码如下:

import { NextResponse } from 'next/server'
 
export async function GET() {
  const data = { success: true, data: { name: "yayu"}}
  return NextResponse.json(data)
}

这样我们就实现了一个接口,它的地址是 http://localhost:3000/api/blog

我们打开掘金主页,在浏览器控制台中,请求该地址:

image.png

很明显会出现 CORS 错误。

修改 app/api/blog/route.js,代码如下:

import { NextResponse } from 'next/server'
 
export async function GET() {
  const data = { success: true, data: { name: "yayu"}}
  return NextResponse.json(data, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

此时接口就已经设置了 CORS 支持了跨域请求。我们再在掘金主页请求一次,此时已经能够正常响应:

image.png

3.2. 中间件设置

如果是多个接口,则可以在中间件或者 next.config.js 中配置。

我们先说使用中间件,这算是解决跨域问题最常用的解决方法。

app/api/blog/route.js 代码修改回之前有跨域问题的代码:

import { NextResponse } from 'next/server'
 
export async function GET() {
  const data = { success: true, data: { name: "yayu"}}
  return NextResponse.json(data)
}

修改 middleware.js,代码如下:

import { NextResponse } from 'next/server'
 
const allowedOrigins = ['https://juejin.cn']
 
const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
 
export function middleware(request) {
  // 检查请求的 origin 属性
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)
 
  // 处理预检 OPTIONS 请求
  const isPreflight = request.method === 'OPTIONS'
 
  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }
 
  // 处理普通请求
  const response = NextResponse.next()
 
  if (isAllowedOrigin) {
    response.headers.set('Access-Control-Allow-Origin', origin)
  }
 
  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value)
  })
 
  return response
}
 
export const config = {
  matcher: '/api/:path*',
}

中间件为传入的请求提供了全局控制机制。我们在 middleware 中为 /api/xxx统一进行了 CORS 的判断和处理,此时我们再在掘金主页控制台请求该地址,已经可以正常请求:

image.png

我们再试试预检请求,浏览器控制台运行:

fetch("http://localhost:3000/api/blog", { method: "POST", headers: {
  "Content-Type": "application/xml"
}});

因为自定义了请求头的值,此时会触发用于预检的 OPTIONS 请求。也能够正常请求:

image.png

注:注意我们代码中的 Access-Control-Allow-Headers,如果我们携带了其他请求头,因为请求头不匹配,也会导致 CORS 错误。

3.3. 使用 headers 配置项

除了在 middleware 中设置,还可以借助 next.config.js 的 headers 配置项。

修改 next.config.mjs,代码如下:

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: "/api/:path*",
        headers: [
          {
            key: "Access-Control-Allow-Origin",
            value: "*",
          },
          {
            key: "Access-Control-Allow-Methods",
            value: "GET, POST, PUT, DELETE, OPTIONS",
          },
          {
            key: "Access-Control-Allow-Headers",
            value: "Content-Type, Authorization",
          },
        ],
      },
    ];
  }
};

export default nextConfig;

这算是一种简单的实现方式。如果你需要更高的灵活度,则还是采用中间件的形式。此时也能够正常请求:

image.png

3.4. Vercel 配置项

如果你用 Vercel,你也可以在 vercel.json 中进行配置:

{
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Access-Control-Allow-Credentials", "value": "true" },
        { "key": "Access-Control-Allow-Origin", "value": "*" },
        { "key": "Access-Control-Allow-Methods", "value": "GET,OPTIONS,PATCH,DELETE,POST,PUT" },
        { "key": "Access-Control-Allow-Headers", "value": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" }
      ]
    }
  ]
}

参考链接

PS:系统学习 Next.js,欢迎入手小册。基础篇、实战篇、源码篇、面试篇四大篇章带你系统掌握 Next.js!


网站公告

今日签到

点亮在社区的每一天
去签到