Skip to content

引出问题

起因是因为最近想把这个站点部署到 vercel 上,在经过一些列操作以后,终于可以正常访问了

但是发现之前写的自定义组件的里的请求有问题

因为之前此站点部署在自己的服务器上,当时用 nginx 做了代理,所以可以正常请求我的后台

在改成 vercel 部署以后,发现没有地方让我配置代理或者 nginx,而我又不想动后台代码(加跨域)

于是想基于 vercel 本身的 serverless 能力自己做一个代理的 server 也用 vercel 部署

serverless

这个概念出现的比较早,个人理解就是对于用户来说,感受不到或者不需要服务器,但是实际上是有服务器提供出来用的

而用户只需要关注业务逻辑,而不需要去关心服务的运行情况,运维情况等

Vercel 中的 serverless

在 vercel 中,已经支持 serverless 部署形式,因此只需要提供接口,部署接口,然后就可以正常去请求

这里以自己的需求为例,我需要有一个同时支持 post 和 get 请求的接口

当请求这个接口时,它会拿到我们的参数去转发到我们配置的服务地址,等同于一个简单的代理的效果

因此就有了以下代码:

javascript
// api/proxy.js
const fetch = require('node-fetch');

export default async function handler(req, res) {
    const { method, body, query, headers, url } = req;

    // 基础目标地址,后面会根据请求路径拼接
    const targetBaseUrl = process.env.TARGET_API_URL;

    // 设置允许跨域的 IP 和端口
    const allowedIps = JSON.parse(process.env.ALLOWED_IPS || "[]");

    // 获取请求头中的 Origin
    const origin = headers.origin || headers['x-forwarded-host'];

    // 如果请求来源与允许的来源匹配,设置 CORS 头部
    if ( allowedIps.includes(origin) ) {
        res.setHeader('Access-Control-Allow-Origin', origin);
    } else {
        console.log('Not Allow Origin: ' + origin);
        res.setHeader('Access-Control-Allow-Origin', ''); // 不允许跨域,清空该头
    }

    // 设置 CORS 头部,允许来源
    // res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

    // 预检请求(OPTIONS)处理
    if (method === 'OPTIONS') {
        return res.status(200).end();
    }

    try {

        const { slug = '', ...queryParams } = query; // slug 是 catch-all 路由的捕获部分

        // 动态构建目标 URL
        // 获取请求路径中的子路径,并拼接到目标 URL
        const targetUrl = new URL(slug, targetBaseUrl);  // 将路径拼接到目标基础 URL

        // 拼接查询参数到目标 URL
        Object.keys(queryParams).forEach(key =targetUrl.searchParams.append(key, queryParams[key]));

        // 准备请求选项
        let requestOptions = {
            method: method,
            headers: {
                'Content-Type': 'application/json',
            },
        };

        // 如果是 POST 请求,设置请求体
        if (method === 'POST' && body) {
            requestOptions.body = JSON.stringify(body);
        }

        if (method !== 'GET' && method !== 'POST') {
            requestOptions.body = JSON.stringify(body);
        }

        // 转发请求到外部服务
        const response = await fetch(targetUrl, requestOptions);

        // 如果响应失败,返回外部服务的错误信息
        if (!response.ok) {
            // 尝试解析响应的错误信息
            let errorMessage = 'Error forwarding request'; // 默认错误信息

            try {
                // 如果外部 API 返回的是 JSON 格式的错误信息
                const errorData = await response.json();
                errorMessage = errorData.message || errorData.error || 'Unknown error'; // 获取错误信息
            } catch (err) {
                // 如果解析 JSON 失败,尝试获取文本错误信息
                errorMessage = await response.text() || 'Unknown error';
            }

            return res.status(response.status).json({ message: errorMessage, status: response.status });
        }

        // 获取外部服务的响应数据
        const responseData = await response.json();

        // 返回外部服务的响应给客户端
        return res.status(200).json(responseData);
    } catch (error) {
        // 处理错误
        console.error('Error in request forwarding:', error);
        return res.status(500).json({ message: 'Internal Server Error' });
    }
}

这里值得注意的是,/api/proxy.js 导出一个 handler 的函数

然后去请求 部署后的地址 + /api/proxy 就可以接收到请求了

这里我需要配置环境变量,因此创建了一个 vercel.json 文件(当然也可以在部署的时候在面板上去设置):

JSON
{
    "env": {
        "注释1": "TARGET_API_URL 表示要代理的目标服务地址",
        "注释2": "ALLOWED_IPS 表示允许跨域的地址",
        "TARGET_API_URL": "xxx",
        "ALLOWED_IPS": "[\"https://fyang.fun\"]"
    },
    "rewrites": [
        {
          "注释1": "表示 proxy 后拼接任何 url 都可以匹配请求",
          "注释2": "例如 /api/proxy/get/names, 代码中就可以用 slug 获取到 /get/names 这一段",
          "注释3": "destination 表示当匹配到规则时,使用哪个文件接收请求",
          "source": "/api/proxy/:slug*",
          "destination": "/api/proxy.js"
        }
      ]
}

基于以上规则,如果我们想要继续加一个接口,就可以在 api 文件夹下再加一个 js 文件,例如:

javascript
// api/health.js
export default function handler(req, res) {
    res.status(200).json({ message: "Service is up and running!" });
}

当请求 /api/health 时,就会响应内容

0