记一次koa服务搭建过程
2025-01-06

背景

在使用和风天气 API 开发天气查询网站时,API 请求需要传递 apikey 作为参数。当前项目中,apikey 是直接写在代码里的,而项目仓库是开源的,这带来了 apikey 泄露的风险。

解决方案

为了解决 apikey 直接暴露的问题,需要将其从项目代码中剥离,存放到更安全的位置。针对这一需求,考虑了以下两种方案:

  1. 搭建独立后端服务进行请求中转
  2. 将接口托管到云函数平台(如 LAF、Netlify)

方案分析

  • 方案一
    搭建独立的后端服务,通过中转方式管理 apikey 和接口。这种传统的做法虽略显繁琐,但能够完全掌控接口能力,如灵活配置鉴权机制、缓存优化和压缩策略等,同时也降低了对第三方平台的依赖。

  • 方案二
    在云函数平台上,将 apikey 配置为环境变量,通过函数的形式供前端调用。这种方案部署简单且无需维护服务器,是一个相对省心的选择。然而,其局限性在于平台的稳定性和灵活性(例如自定义鉴权、缓存策略和请求压缩等)。

最终选择

综合考虑后,我选择了方案一,即通过搭建独立后端服务来管理 apikey 和请求接口。这不仅提升了项目的安全性,还为未来扩展功能预留了足够的空间。

技术选择

这个项目只是中转接口调用,并且接口数量不多,不涉及数据库操作,所以偏向选择简单易用,易扩展的框架,最终选择了 koa 作为后端的框架。

实现过程

刚开始时,不会 koa 这个框架,直接看了官网,不太看得懂,转到了隔壁的阮一峰写的 koa 入门教程。

从监听开始

app.listen(3000, () => {
  console.log("start");
});

启动了监听服务器之后,感觉这件事情完了一半,再把接口挨着挨着写出来好像就结束了

const main = async (ctx) => {
  if (ctx.request.path === "/getWeather") {
    const res = axios.get('https://xx.xx')
    ...
    ctx.response.body = {
      code: 200,
      ...
    }
  }
}
app.use(main)

这样根据 path 判断请求的路径,然后调用真实的请求,拿到返回值,返回给前端,开发完成啦!! 但是这事情真完了吗,看着这种代码不经泛起了恶心,经过一阵眩晕,不觉间想起了 mvc,什么 vc,mvc!

核心问题也没有得到解决,apikey 的管理。我们一个一个来。

对于 apikey 的处理

要存起来最快能想到的是通过文件存储,创建一个文件,名字叫.env,将 apikey 和其他需要的变量写进去。

API_KEY = "xxxx";
PORT = 3000;

之后在使用 dotenv 这个库将这些值处理为环境变量

import { fileURLToPath } from "url";
import dotenv from "dotenv";
import { dirname, resolve } from "path";

// Convert the module URL to a file path
const __filename = fileURLToPath(import.meta.url);

// Get the directory name
const __dirname = dirname(__filename);

dotenv.config({ path: resolve(__dirname, "../../.env") });

export const apiKey = process.env.API_KEY;
export const port = process.env.PORT || 3000;

这样在调用请求时可以直接引入 apiKey 进行调用

主要原因是 apikey 本身需要显式声明,但其管理方式需要更加安全。为此,可以将 apikey 存放在 .env 文件中,并将该文件添加到 .gitignore,以防止其被提交到代码仓库。

使用 dotenv 的目的是将 apikey 作为环境变量获取,避免直接在代码中硬编码。在服务器部署时,只需添加对应的 .env 文件即可完成配置管理,甚至可以通过创建不同的环境文件(如 .env.production.env.development)来区分生产环境和开发环境的配置。这种方式不仅便于管理,还能根据环境需求动态调整配置。

需要注意的是,这是一种常见的管理方式。例如,在许多 AI 项目中,通常会预留 apikey 字段,让开发者自己填写。

路由结合 mvc

当接口多了之后 if else 的判断可读性和可维护性都比较差,其实后端也需要很好的管理每个接口,让接口排排坐,就像前端管理一个个组件,这个时候需要引入路由,根据请求路径,执行对应的 代码,最后发起真实请求,这里就有了 mvc 的味儿了。

附上伪码

// src/routes/apiRoutes.js
const Router = require("koa-router");
const apiController = require("../controllers/apiController");

const router = new Router({
  prefix: "/api",
});

router.get("/data", apiController.getData);
router.get("/info", apiController.getInfo);
module.exports = router;
// src/controllers/apiController.js
const apiService = require("../services/apiService");

exports.getData = async (ctx) => {
  try {
    const data = await apiService.fetchData();
    ctx.body = data;
  } catch (error) {
    ctx.status = 500;
    ctx.body = { message: "Internal Server Error" };
  }
};
// src/services/apiService.js
const axios = require("axios");
const config = require("../config");

exports.fetchData = async () => {
  const response = await axios.get("https://third-party-api.com/data", {
    headers: {
      Authorization: `Bearer ${config.apiKey}`,
    },
  });
  return response.data;
};
app.use(apiRoutes.routes()).use(apiRoutes.allowedMethods());