将 API 从 Express 迁移到无服务器功能时,如何构建代码?

2025-05-24

将 API 从 Express 迁移到无服务器功能时,如何构建代码?

有很多文章介绍了如何将无服务器函数用于各种用途。其中很多文章介绍了如何入门,而且非常有用。但是,当你想像整理 Node.js Express API 那样更有效地组织它们时,该怎么做呢?

关于这个话题有很多值得讨论的地方,但在本文中,我想特别关注一种组织代码的方法。欢迎在评论区留言,告诉我你还对哪些方面感兴趣,我会考虑在以后陆续更新。

以下是我推荐的一些入门资源:

为什么要结构化你的代码?

你可以将所有函数逻辑放在一个文件中。但你想这样做吗?共享逻辑怎么办?测试?调试?可读性?这时,模式或结构就能派上用场了。有很多方法可以做到这一点。除了上面提到的方法之外,一致性是我主要关注的附加方面。

这是一个函数应用程序的非常标准的表示:

FunctionApp
 | - host.json
 | - myfirstfunction
 | | - function.json
 | | - index.js
 | | - ...
 | - mysecondfunction
 | | - function.json
 | | - index.js
 | | - ...
 | - sharedCode
Enter fullscreen mode Exit fullscreen mode

这是我的结构,仅用于英雄 API。

heroes-serverless API 文件夹结构

你的入口点

函数的入口点位于与index.js函数同名的文件夹中的文件中。

这个函数本身非常直观。调用此函数时,会将上下文传递给它。上下文可以访问请求和响应对象,这非常方便。然后我调用异步操作来获取数据并设置响应。

// heroes-get/index.js
const { getHeroes } = require('../shared/hero.service');

module.exports = async function(context) {
  context.log(
    'getHeroes: JavaScript HTTP trigger function processed a request.'
  );
  await getHeroes(context);
};
Enter fullscreen mode Exit fullscreen mode

另一种方法是通过context.res和/或context.req进入服务。如何选择由您决定。我更喜欢通过reqres进入,因为它对我来说更熟悉。但通过context也允许访问其他功能,例如context.log。这里没有对错之分,选择你的冒险并保持一致。

数据访问服务

创建第一个 Azure 函数时,“hello world”函数通常会返回一条静态字符串消息。在大多数 API 中,你都需要与另一个数据库或 Web 服务通信,以获取/操作数据,然后再返回响应。

就我而言,我获取的是英雄列表。因此,我将大部分数据访问服务推迟到一个名为 的 Node.js 模块中hero.service.js。为什么这样做?简单来说,是为了组织我的代码(在本例中是数据访问服务),使其遵循 DRY(不要重复自己),隔离职责,使其更易于扩展、调试和测试。

hero.service.js模块首先获取对容器(包含数据库数据的存储单元)的引用。为什么要抽象它?好问题……我将其抽象为一个共享模块,以便可以重用该逻辑。我需要获取所有类型的容器,而获取容器需要使用一些特定于数据库的连接 API 访问数据库。我们稍后会详细讨论这一点。

getHeroes服务接受上下文,并使用解构将响应对象取出并放入一个变量中res。然后,它尝试获取英雄列表,成功后就将它们添加到响应中。失败时,它会返回一个错误。

// shared/hero.service.js
const { heroes: container } = require('./index').containers;

async function getHeroes(context) {
  let { req, res } = context;
  try {
    const { result: heroes } = await container.items.readAll().toArray();
    res.status(200).json(heroes);
  } catch (error) {
    res.status(500).send(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

共享数据库模块

数据访问服务模块hero.service.js从共享数据库模块导入。这个模块是连接数据库的关键所在。在本例中,我通过 npm 中的 Node.js SDK 使用 Azure 的 CosmosDB

请注意,代码通过 Node.js 环境变量读取机密信息。然后,它只需从相应的数据库中导出容器即可。我可以使用不同的环境变量,而无需更改代码。

// shared/index.js
const cosmos = require('@azure/cosmos');

const endpoint = process.env.CORE_API_URL;
const masterKey = process.env.CORE_API_KEY;
const databaseDefName = 'vikings-db';
const heroContainerName = 'heroes';
const villainContainerName = 'villains';
const { CosmosClient } = cosmos;

const client = new CosmosClient({ endpoint, auth: { masterKey } });

const containers = {
  heroes: client.database(databaseDefName).container(heroContainerName),
  villains: client.database(databaseDefName).container(villainContainerName)
};

module.exports = { containers };
Enter fullscreen mode Exit fullscreen mode

你的路线是什么?

我不希望我的 API 出现这种情况,/api/heroes-get而是更喜欢/api/heroes在执行GET操作时这样做,所以我修改了它。我的函数位于路径中/heroes-get/index.js,在同一个文件夹中有一个function.json文件。这个文件是配置函数行为的地方。我想要更改的关键是路由别名。请注意,我通过route: heroes在下面的代码块中进行设置来更改它。

现在我的终点是api/heroes

// function.json
{
  "disabled": false,
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get"],
      "route": "heroes"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

重点是什么?

整理代码、隔离逻辑只有在带来切实的积极影响时才能让你的工作更轻松,所以让我们来探讨一下。在编写下一个更新英雄数据的函数时,该函数可能类似于以下代码。

const { putHero } = require('../shared/hero.service');

module.exports = async function(context) {
  context.log('putHero: JavaScript HTTP trigger function processed a request.');
  await putHero(context);
};
Enter fullscreen mode Exit fullscreen mode

你注意到它和获取英雄的函数非常相似吗?它们之间形成了一种模式,这很好。这里最大的区别在于代码是在模块putHero中调用的hero.service.js。让我们仔细看看。

英雄更新逻辑是隔离的。隔离是 的主要工作之一,hero.service.js获取英雄的逻辑也是如此。

展望未来,删除、插入以及任何其他操作的逻辑也可以放在此模块中,并导出以供其他函数使用。这使得将此结构扩展到其他操作和模型变得相对简单。

// shared/hero.service.js
const { heroes: container } = require('./index').containers;

async function getHeroes(context) {
  // ...
}

async function putHero(context) {
  const { req, res } = context;
  const hero = {
    id: req.params.id,
    name: req.body.name,
    description: req.body.description
  };

  try {
    const { body } = await container.items.upsert(hero);
    res.status(200).json(body);
    context.log('Hero updated successfully!');
  } catch (error) {
    res.status(500).send(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

更多无服务器

分享你的兴趣,我会在写更多关于无服务器的文章时记住你的兴趣!同时,以下是一些资源,如果你需要一些入门资料,可以参考:

致谢

感谢Marie Hoeger审阅本文内容并采纳我的反馈。你一定要在 Twitter 上关注 Marie

文章来源:https://dev.to/azure/how-do-you-struct-your-code-when-moving-your-api-from-express-to-serverless-functions-30bc
PREV
我如何撰写在线文章
NEXT
TypeScript 中回调、Promises 和 Async Await 的比较