无服务器 TypeScript API:使用 Azure Functions 在 MongoDB 中存储数据
创建一个 Azure 函数 API,以便使用 Mongoose API 将数据存储在 Azure Cosmos DB 中,然后将 Function 应用程序部署到 Azure 云,以便使用公共 HTTP 终结点进行托管。
准备开发环境
安装以下软件:
- 创建一个免费的 Azure 订阅
- 安装 Node.js LTS v18+
- TypeScript v4+
- 全局安装的 Azurite 用于本地开发存储
- Azure Functions 运行时 v4.16+
- Azure Functions Core Tools v4.0.5095+ (如果在本地运行)全局安装,用于本地开发
- 安装 Visual Studio Code 并使用以下扩展:
1.在 Visual Studio Code 中登录到 Azure
如果你已使用 Azure 服务扩展,则应该已经登录,可以跳过此步骤。
在 Visual Studio Code 中安装 Azure 服务扩展后,需要登录到 Azure 帐户。
在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
在 “资源 ”部分中,选择“ 登录到 Azure”,然后按照提示进行操作。
登录后,验证 Azure 帐户的电子邮件地址是否显示在“状态栏”中,以及订阅是否显示在 Azure 资源管理器中:
2.创建 Azure 资源组
资源组是基于区域的资源集合。 先创建资源组,再创建该组中的资源,这样在本教程结束时就可以删除资源组,而不必单独删除每个资源。
在本地系统上创建新文件夹,用作 Azure Functions 项目的根目录。
在 Visual Studio Code 中打开此文件夹。
在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
在“资源”下找到订阅,然后选择+图标,然后选择“创建资源组”。
使用下表,根据提示完成操作:
Prompt 值 输入新资源组的名称。 azure-tutorial
选择新资源的位置。 选择一个离你近的地理区域。
3.创建本地 Functions 应用
创建一个本地 Azure Functions(无服务器)应用程序,其中包含一个 HTTP 触发器函数。
在 Visual Studio Code 中,打开命令面板(Ctrl + Shift + P)。
搜索并选择 “Azure Functions:创建新项目 ”。
使用下表,完成本地 Azure 函数项目的创建:
Prompt 值 说明 选择将包含函数项目的文件夹 选择当前(默认)文件夹。 选择一种语言 TypeScript 选择 TypeScript 编程模型 模型 V4(预览版) 为项目的第一个函数选择模板 HTTP 触发器 API 是使用 HTTP 请求调用的。 提供函数名称 blogposts
API 路由是 /api/blogposts
当 Visual Studio Code 创建项目时,请在
./src/functions/blogposts.ts
文件中查看 API 代码。import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; export async function blogposts(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log(`Http function processed request for url "${request.url}"`); const name = request.query.get('name') || await request.text() || 'world'; return { body: `Hello, ${name}!` }; }; app.http('blogposts', { methods: ['GET', 'POST'], authLevel: 'anonymous', handler: blogposts });
此代码是新的 v4 编程模型中的标准样板。 它并不是指示使用 POST 和 GET 编写 API 层的唯一方法。
将前面的代码替换为以下代码,以仅允许 GET 请求返回所有博客文章。
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; // curl --location 'http://localhost:7071/api/blogposts' --verbose export async function getBlogPosts(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log(`Http function getBlogPosts processed request for url "${request.url}"`); // Empty array for now ... will fix later const blogposts = []; return { status: 200, jsonBody: { blogposts } }; }; app.get('getBlogPosts', { route: "blogposts", authLevel: 'anonymous', handler: getBlogPosts });
应注意以下几个 Azure Functions Node.js v4 编程模型更改 :
- 指示它是 GET 请求的函数名称
getBlobPosts
将帮助你隔离日志中的函数。 - 属性
route
设置为blogposts
,这是提供/api/blogposts
的默认 API 路由的一部分。 methods
该属性已被删除,并且是不必要的,因为app
该对象的使用get
指示这是 GET 请求。 下面列出了方法函数。 如果你有其他方法,则可以返回使用属性methods
。deleteRequest()
get()
patch()
post()
put()
- 指示它是 GET 请求的函数名称
4.启动 Azurite 本地存储模拟器
在本地计算机上开发函数需要存储模拟器(免费)或Azure 存储帐户(付费)。
在单独的终端中 ,启动 Azurite 本地存储模拟器。
azurite --silent --location ./azurite --debug ./azurite/debug.log
需要使用本地Azure 存储模拟器在本地运行 Azure Functions。 本地存储模拟器在local.settings.json
文件中指定,其值为 AzureWebJobs存储 属性UseDevelopmentStorage=true
。
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
}
}
子 azurite
文件夹已添加到 .gitignore
文件。
5.运行本地无服务器函数
在本地运行 Azure Functions 项目,以在部署到 Azure 之前对其进行测试。
在 Visual Studio Code 中,在 getBlogPosts 函数末尾设置
return
语句的断点。在 Visual Studio Code 中,按 F5 启动调试器并附加到 Azure Functions 主机。
还可以使用“调试”>“启动调试”菜单命令。
输出将显示在 终端 面板中。
在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
在“工作区”部分中,找到并展开“本地项目 -Functions ->>getBlogPosts”。
右键单击函数名称 getBlogPosts,然后选择“复制函数 URL”。
在浏览器中,粘贴 URL 并选择 Enter,或在终端中使用以下 cURL 命令:
curl http://localhost:7071/api/blogposts --verbose
空博客文章数组的响应将返回为:
* Trying 127.0.0.1:7071... * Connected to localhost (127.0.0.1) port 7071 (#0) > GET /api/blogposts HTTP/1.1 > Host: localhost:7071 > User-Agent: curl/7.88.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/json < Date: Mon, 08 May 2023 17:35:24 GMT < Server: Kestrel < Transfer-Encoding: chunked < {"blogposts":[]}* Connection #0 to host localhost left intact
在 VS Code 中,停止调试器 Shift + F5。
6. 在 Visual Studio Code 中创建 Azure 函数应用
在本部分中,将在 Azure 订阅中创建函数应用云资源和相关资源。
在 Visual Studio Code 中,打开命令面板(Ctrl + Shift + P)。
搜索并选择“Azure Functions:在 Azure 中创建函数应用”(高级)。
根据提示提供以下信息:
Prompt 选择 输入函数应用的全局唯一名称 键入 URL 路径中有效的名称,例如 first-function
。 Postpend 3 个字符以使 URL 全局唯一。 将对你键入的名称进行验证,以确保其在 Azure Functions 中是唯一的。选择一个运行时堆栈 选择 Node.js 18 LTS 或较新版本。 选择 OS 选择 Linux。 选择新资源的资源组 创建名为 azure-tutorial-first-function 的新资源组。 此资源组最终将有多个资源:Azure Function、Azure 存储 和 Cosmos DB for MongoDB API。 选择托管计划 选择 “消耗”。 选择存储帐户 选择“ 创建新存储帐户 ”并接受默认名称。 为你的应用选择 Application Insights 资源。 选择“ 创建新的 Application Insights”资源 并接受默认名称。 等待通知确认应用已创建。
7. 在 Visual Studio Code 中将 Azure 函数应用部署到 Azure
重要
部署到现有函数应用将始终覆盖该应用在 Azure 中的内容。
- 在“活动”栏中选择 Azure 图标,然后在 “资源 ”区域中,右键单击函数应用资源,然后选择“ 部署到函数应用”。
- 如果系统询问是否确定要部署,请选择“ 部署”。
- 部署完成后,会显示一个通知,其中包含多个选项。 选择“ 查看输出 ”以查看结果。 如果错过了通知,请选择右下角的响铃图标以再次查看。
8. 将应用程序设置添加到云应用
在“活动”栏中选择 Azure 图标,然后在“资源”区域中展开函数应用资源,然后右键单击“应用程序设置”。
选择“ 添加新设置” 并添加以下设置以启用 Node.js v4(预览版)编程模型。
设置 “值” AzureWebJobsFeatureFlags EnableWorkerIndexing
9. 运行远程无服务器函数
在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
在“ 资源 ”部分中,展开 Azure 函数应用资源。 右键单击函数名称,然后选择“ 复制函数 URL”。
将 URL 粘贴到浏览器中。 返回与在本地运行函数时相同的空数组。
{"blogposts":[]}
10. 添加 Azure Cosmos DB for MongoDB API 集成
Azure Cosmos DB 提供 MongoDB API 来提供熟悉的集成点。
在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
在 “资源 ”部分中,选择 + “ 创建数据库服务器”。 使用下表完成提示,以创建新的 Azure Cosmos DB 资源。
Prompt 值 说明 选择 Azure 数据库服务器 “用于 MongoDB 的 Azure Cosmos DB”API 提供 Azure Cosmos DB 帐户名称。 cosmosdb-mongodb-database
添加三个字符以创建唯一名称。 该名称为 API 的 URL 的一部分。 选择容量模型。 无服务器 选择新资源的资源组。 azure-tutorial-first-function 选择在上一部分中创建的资源组。 选择新资源的位置。 选择建议的区域。
11. 安装 mongoose 依赖项
在 Visual Studio Code 终端中,按 Ctrl + Shift + `,然后安装 npm 包:
npm install mongoose
12. 为博客文章添加 mongoose 代码
在 Visual Studio Code 中,创建一个名为 lib 的
./src/
子目录,创建一./database.ts
个名为文件,并将以下代码复制到其中。import { Schema, Document, createConnection, ConnectOptions, model, set } from 'mongoose'; const connectionString = process.env.MONGODB_URI; console.log('connectionString', connectionString); const connection = createConnection(connectionString, { useNewUrlParser: true, useUnifiedTopology: true, autoIndex: true } as ConnectOptions); export interface IBlogPost { author: string title: string body: string } export interface IBlogPostDocument extends IBlogPost, Document { id: string created: Date } const BlogPostSchema = new Schema({ id: Schema.Types.ObjectId, author: String, title: String, body: String, created: { type: Date, default: Date.now } }); BlogPostSchema.set('toJSON', { transform: function (doc, ret, options) { ret.id = ret._id; delete ret._id; delete ret.__v; } }); export const BlogPost = model<IBlogPostDocument>('BlogPost', BlogPostSchema); connection.model('BlogPost', BlogPostSchema); export default connection;
在 Visual Studio Code 中,打开
./src/functions/blogposts
文件,将整个文件的代码替换为以下内容:import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; import connection from '../lib/database'; // curl --location 'http://localhost:7071/api/blogposts' --verbose export async function getBlogPosts(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log(`Http function getBlogPosts processed request for url "${request.url}"`); const blogposts = await connection.model('BlogPost').find({}); return { status: 200, jsonBody: { blogposts } }; }; app.get('getBlogPosts', { route: "blogposts", authLevel: 'anonymous', handler: getBlogPosts });
13. 将连接字符串添加到本地应用
在 Visual Studio Code 的 Azure 资源管理器中,选择 “Azure Cosmos DB ”部分,然后展开以右键单击选择新资源。
选择“复制连接字符串。
在 Visual Studio Code 中,使用文件资源管理器打开
./local.settings.json
。添加名为
MONGODB_URI
“”的新属性,并粘贴连接字符串的值。{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "", "FUNCTIONS_WORKER_RUNTIME": "node", "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", "MONGODB_URI": "mongodb://...." } }
文件中的
./local.settings.json
机密:- 不会部署到 Azure,因为它包含在
./.funcignore
文件中。 - 未检查到源代码管理中,因为它包含在
./.gitignore
文件中。
- 不会部署到 Azure,因为它包含在
在本地运行应用程序,并使用上一部分中的相同 URL 测试 API。
14. 将连接字符串添加到远程应用
- 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
- 在“ 资源 ”部分中,找到 Azure Cosmos DB 实例。 右键单击资源,然后选择“复制连接字符串”。
- 在同一 “资源 ”部分中,找到 Function App 并展开节点。
- 右键单击“应用程序设置”,然后选择“添加新设置”。
- 输入应用设置名称,
MONGODB_URI
然后选择 Enter。 - 粘贴复制的值,然后按 Enter。
15. 添加用于创建、更新和删除博客文章的 API
在 Visual Studio Code 中,使用命令面板查找并选择 Azure Functions:创建函数。
选择 HTTP 触发器 并将其命名
blogpost
(单一)。将以下代码复制到文件中。
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; import connection, { IBlogPost, IBlogPostDocument } from '../lib/database'; // curl -X POST --location 'http://localhost:7071/api/blogpost' --header 'Content-Type: application/json' --data '{"author":"john","title":"my first post", "body":"learn serverless node.js"}' --verbose export async function addBlogPost(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log(`Http function addBlogPost processed request for url "${request.url}"`); const body = await request.json() as IBlogPost; const blogPostResult = await connection.model('BlogPost').create({ author: body?.author, title: body?.title, body: body?.body }); return { status: 200, jsonBody: { blogPostResult } }; }; // curl -X PUT --location 'http://localhost:7071/api/blogpost/64568e727f7d11e09eab473c' --header 'Content-Type: application/json' --data '{"author":"john jones","title":"my first serverless post", "body":"Learn serverless Node.js with Azure Functions"}' --verbose export async function updateBlogPost(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log(`Http function updateBlogPost processed request for url "${request.url}"`); const body = await request.json() as IBlogPost; const id = request.params.id; const blogPostResult = await connection.model('BlogPost').updateOne({ _id: id }, { author: body?.author, title: body?.title, body: body?.body }); if(blogPostResult.matchedCount === 0) { return { status: 404, jsonBody: { message: 'Blog post not found' } }; } return { status: 200, jsonBody: { blogPostResult } }; }; // curl --location 'http://localhost:7071/api/blogpost/6456597918547e37d515bda3' --verbose export async function getBlogPost(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log(`Http function getBlogPosts processed request for url "${request.url}"`); console.log('request.params.id', request.params.id) const id = request.params.id; const blogPost = await connection.model('BlogPost').findOne({ _id: id }); if(!blogPost) { return { status: 404, jsonBody: { message: 'Blog post not found' } }; } return { status: 200, jsonBody: { blogPost } }; }; // curl --location 'http://localhost:7071/api/blogpost/6456597918547e37d515bda3' --request DELETE --header 'Content-Type: application/json' --verbose export async function deleteBlogPost(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log(`Http function deleteBlogPost processed request for url "${request.url}"`); const id = request.params.id; const blogPostResult = await connection.model('BlogPost').deleteOne({ _id: id }); if(blogPostResult.deletedCount === 0) { return { status: 404, jsonBody: { message: 'Blog post not found' } }; } return { status: 200, jsonBody: { blogPostResult } }; }; app.get('getBlogPost', { route: "blogpost/{id}", authLevel: 'anonymous', handler: getBlogPost }); app.post('postBlogPost', { route: "blogpost", authLevel: 'anonymous', handler: addBlogPost }); app.put('putBlogPost', { route: "blogpost/{id}", authLevel: 'anonymous', handler: updateBlogPost }); app.deleteRequest('deleteBlogPost', { route: "blogpost/{id}", authLevel: 'anonymous', handler: deleteBlogPost });
再次使用调试器启动本地函数。 以下 API 可用:
deleteBlogPost: [DELETE] http://localhost:7071/api/blogpost/{id} getBlogPost: [GET] http://localhost:7071/api/blogpost/{id} getBlogPosts: [GET] http://localhost:7071/api/blogposts postBlogPost: [POST] http://localhost:7071/api/blogpost putBlogPost: [PUT] http://localhost:7071/api/blogpost/{id}
使用 cURL 命令中的
blogpost
(单一) API 添加一些博客文章。curl -X POST --location 'http://localhost:7071/api/blogpost' --header 'Content-Type: application/json' --data '{"author":"john","title":"my first post", "body":"learn serverless node.js"}' --verbose
使用 cURL 命令中的
blogposts
(plural) API 获取博客文章。curl http://localhost:7071/api/blogposts --verbose
16. 使用适用于 Azure Cosmos DB 的 Visual Studio Code 扩展查看所有数据
在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
在“资源”部分中,右键单击 Azure Cosmos DB 数据库并选择“刷新”。
展开 测试 数据库和 博客文章 集合节点以查看文档。
选择列出的项之一以查看 Azure Cosmos DB 实例中的数据。
17. 重新部署函数应用以包含数据库代码
- 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
- 在“资源”部分中,右键单击 Azure 函数应用,然后选择“部署到函数应用”。
- 在弹出窗口中,询问是否确实要部署,请选择“ 部署”。
- 等到部署完成后再继续。
18. 使用基于云的 Azure 函数
- 仍在 Azure 资源管理器的 Functions 区域中,选择并展开函数,然后 选择 Functions 节点,其中列出了 API
- 右键单击其中一个 API,然后选择“ 复制函数 URL”。
- 编辑以前的 cURL 命令以使用远程 URL而不是本地 URL。 运行命令以测试远程 API。
19. 查询 Azure 函数日志
若要搜索日志,请使用 Azure 门户。
在 Visual Studio Code 中,选择 Azure 资源管理器,然后在 Functions 下右键单击函数应用,然后在门户中选择“打开”。
这将打开 Azure 门户指向你的 Azure 函数。
在设置中,依次选择“Application Insights”、“查看 Application Insights 数据”。
此链接将转到在使用 Visual Studio Code 创建 Azure 函数时为你创建的单独的指标资源。
从“监视”部分,选择“日志”。 如果出现“查询”弹出窗口,请选择弹出窗口右上角的 X 将其关闭。
在“新查询 1”窗格中的“表”选项卡上,双击“跟踪”表。
这会在查询窗口中输入 Kusto 查询
traces
。编辑查询来搜索自定义日志:
traces | where message startswith "***"
选择“运行”。
如果日志未显示任何结果,可能是因为 HTTP 请求与 Kusto 中的日志可用性之间存在几分钟延迟。 等待几分钟,然后再次运行查询。
你无需额外执行任何操作即可获取此日志记录信息:
- 代码使用了
context.log
函数框架提供的函数。 通过使用context
日志记录,console
可以将日志记录筛选为特定的单个函数。 如果函数应用具有许多函数,这非常有用。 - 函数应用为你添加了 Application Insights。
- Kusto 查询工具包含在Azure 门户中。
- 可以选择
traces
,而无需了解如何编写 Kusto 查询 ,以便从日志中获取最小信息。
- 代码使用了
20. 清理资源
由于使用了单个资源组,因此可以通过删除资源组来删除所有资源。
- 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
- 搜索并选择 “Azure:按资源组分组”。
- 右键单击选择资源组,然后选择“ 删除资源组”。
- 输入资源组名称以确认删除。
可用的源代码
此 Azure 函数应用的完整源代码:
后续步骤
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈