无服务器 TypeScript API:使用 Azure Functions 在 MongoDB 中存储数据

创建一个 Azure 函数 API,以便使用 Mongoose API 将数据存储在 Azure Cosmos DB 中,然后将 Function 应用程序部署到 Azure 云,以便使用公共 HTTP 终结点进行托管。

Flow chart showing path of HTTP request to pass data through Azure Functions and store in Azure Cosmos DB.

准备开发环境

安装以下软件:

1.在 Visual Studio Code 中登录到 Azure

如果你已使用 Azure 服务扩展,则应该已经登录,可以跳过此步骤。

在 Visual Studio Code 中安装 Azure 服务扩展后,需要登录到 Azure 帐户。

  1. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。

  2. “资源 ”部分中,选择“ 登录到 Azure”,然后按照提示进行操作。

    Sign in to Azure through VS Code

  3. 登录后,验证 Azure 帐户的电子邮件地址是否显示在“状态栏”中,以及订阅是否显示在 Azure 资源管理器中

    VS Code Azure explorer showing subscriptions

2.创建 Azure 资源组

资源组是基于区域的资源集合。 先创建资源组,再创建该组中的资源,这样在本教程结束时就可以删除资源组,而不必单独删除每个资源。

  1. 在本地系统上创建新文件夹,用作 Azure Functions 项目的根目录。

  2. 在 Visual Studio Code 中打开此文件夹。

  3. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。

  4. 在“资源”下找到订阅,然后选择+图标,然后选择“创建资源组”。

  5. 使用下表,根据提示完成操作:

    Prompt
    输入新资源组的名称。 azure-tutorial
    选择新资源的位置。 选择一个离你近的地理区域。

3.创建本地 Functions 应用

创建一个本地 Azure Functions(无服务器)应用程序,其中包含一个 HTTP 触发器函数。

  1. 在 Visual Studio Code 中,打开命令面板(Ctrl + Shift + P)。

  2. 搜索并选择 “Azure Functions:创建新项目 ”。

  3. 使用下表,完成本地 Azure 函数项目的创建:

    Prompt 说明
    选择将包含函数项目的文件夹 选择当前(默认)文件夹。
    选择一种语言 TypeScript
    选择 TypeScript 编程模型 模型 V4(预览版)
    为项目的第一个函数选择模板 HTTP 触发器 API 是使用 HTTP 请求调用的。
    提供函数名称 blogposts API 路由是 /api/blogposts
  4. 当 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 层的唯一方法。

  5. 将前面的代码替换为以下代码,以仅允许 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()

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 之前对其进行测试。

  1. 在 Visual Studio Code 中,在 getBlogPosts 函数末尾设置return语句的断点。

  2. 在 Visual Studio Code 中,按 F5 启动调试器并附加到 Azure Functions 主机

    还可以使用“调试”>“启动调试”菜单命令

  3. 输出将显示在 终端 面板中。

  4. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。

  5. “工作区”部分中,找到并展开“本地项目 -Functions ->>getBlogPosts”。

  6. 右键单击函数名称 getBlogPosts,然后选择“复制函数 URL”。

    Partial screenshot of Visual Studio Code, with the Azure Function's button named Copy Function URL highlighted.

  7. 在浏览器中,粘贴 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
    
  8. 在 VS Code 中,停止调试器 Shift + F5。

6. 在 Visual Studio Code 中创建 Azure 函数应用

在本部分中,将在 Azure 订阅中创建函数应用云资源和相关资源。

  1. 在 Visual Studio Code 中,打开命令面板(Ctrl + Shift + P)。

  2. 搜索并选择“Azure Functions:在 Azure 中创建函数应用”(高级)。

  3. 根据提示提供以下信息:

    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 中的内容。

  1. 在“活动”栏中选择 Azure 图标,然后在 “资源 ”区域中,右键单击函数应用资源,然后选择“ 部署到函数应用”。
  2. 如果系统询问是否确定要部署,请选择“ 部署”。
  3. 部署完成后,会显示一个通知,其中包含多个选项。 选择“ 查看输出 ”以查看结果。 如果错过了通知,请选择右下角的响铃图标以再次查看。

8. 将应用程序设置添加到云应用

  1. 在“活动”栏中选择 Azure 图标,然后在“资源”区域中展开函数应用资源,然后右键单击“应用程序设置”。

  2. 选择“ 添加新设置” 并添加以下设置以启用 Node.js v4(预览版)编程模型。

    设置 “值”
    AzureWebJobsFeatureFlags EnableWorkerIndexing

9. 运行远程无服务器函数

  1. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。

  2. 在“ 资源 ”部分中,展开 Azure 函数应用资源。 右键单击函数名称,然后选择“ 复制函数 URL”。

  3. 将 URL 粘贴到浏览器中。 返回与在本地运行函数时相同的空数组。

    {"blogposts":[]}
    

10. 添加 Azure Cosmos DB for MongoDB API 集成

Azure Cosmos DB 提供 MongoDB API 来提供熟悉的集成点。

  1. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。

  2. “资源 ”部分中,选择 +创建数据库服务器”。 使用下表完成提示,以创建新的 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 代码

  1. 在 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;
    
  2. 在 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. 将连接字符串添加到本地应用

  1. 在 Visual Studio Code 的 Azure 资源管理器中,选择 “Azure Cosmos DB ”部分,然后展开以右键单击选择新资源。

  2. 选择“复制连接字符串

  3. 在 Visual Studio Code 中,使用文件资源管理器打开 ./local.settings.json

  4. 添加名为MONGODB_URI“”的新属性,并粘贴连接字符串的值。

    {
      "IsEncrypted": false,
      "Values": {
        "AzureWebJobsStorage": "",
        "FUNCTIONS_WORKER_RUNTIME": "node",
        "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
        "MONGODB_URI": "mongodb://...."
      }
    }
    

    文件中的 ./local.settings.json 机密:

    • 不会部署到 Azure,因为它包含在 ./.funcignore 文件中。
    • 未检查到源代码管理中,因为它包含在./.gitignore文件中。
  5. 在本地运行应用程序,并使用上一部分中的相同 URL 测试 API。

14. 将连接字符串添加到远程应用

  1. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
  2. 在“ 资源 ”部分中,找到 Azure Cosmos DB 实例。 右键单击资源,然后选择“复制连接字符串”。
  3. 在同一 “资源 ”部分中,找到 Function App 并展开节点。
  4. 右键单击“应用程序设置”,然后选择“添加新设置”
  5. 输入应用设置名称, MONGODB_URI 然后选择 Enter。
  6. 粘贴复制的值,然后按 Enter。

15. 添加用于创建、更新和删除博客文章的 API

  1. 在 Visual Studio Code 中,使用命令面板查找并选择 Azure Functions:创建函数

  2. 选择 HTTP 触发器 并将其命名 blogpost (单一)。

  3. 将以下代码复制到文件中。

    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
    });
    
  4. 再次使用调试器启动本地函数。 以下 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}
    
  5. 使用 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
    
  6. 使用 cURL 命令中的 blogposts (plural) API 获取博客文章。

    curl http://localhost:7071/api/blogposts --verbose
    

16. 使用适用于 Azure Cosmos DB 的 Visual Studio Code 扩展查看所有数据

  1. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。

  2. 在“资源”部分中,右键单击 Azure Cosmos DB 数据库并选择“刷新”。

  3. 展开 测试 数据库和 博客文章 集合节点以查看文档。

  4. 选择列出的项之一以查看 Azure Cosmos DB 实例中的数据。

    Partial screenshot of Visual Studio Code, showing the Azure explorer with the Databases with a selected item displayed in the reading pane.

17. 重新部署函数应用以包含数据库代码

  1. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
  2. 在“资源”部分中,右键单击 Azure 函数应用,然后选择“部署到函数应用”。
  3. 在弹出窗口中,询问是否确实要部署,请选择“ 部署”。
  4. 等到部署完成后再继续。

18. 使用基于云的 Azure 函数

  1. 仍在 Azure 资源管理器的 Functions 区域中,选择并展开函数,然后 选择 Functions 节点,其中列出了 API
  2. 右键单击其中一个 API,然后选择“ 复制函数 URL”。
  3. 编辑以前的 cURL 命令以使用远程 URL而不是本地 URL。 运行命令以测试远程 API。

19. 查询 Azure 函数日志

若要搜索日志,请使用 Azure 门户。

  1. 在 Visual Studio Code 中,选择 Azure 资源管理器,然后在 Functions右键单击函数应用,然后在门户中选择“打开”。

    这将打开 Azure 门户指向你的 Azure 函数。

  2. 设置中,依次选择“Application Insights”、“查看 Application Insights 数据”。

    Browser screenshot showing menu choices. Select **Application Insights** from the Settings, then select **View Application Insights data**.

    此链接将转到在使用 Visual Studio Code 创建 Azure 函数时为你创建的单独的指标资源。

  3. 从“监视”部分,选择“日志”。 如果出现“查询”弹出窗口,请选择弹出窗口右上角的 X 将其关闭

  4. 在“新查询 1”窗格中的“表”选项卡上,双击“跟踪”表

    这会在查询窗口中输入 Kusto 查询traces

  5. 编辑查询来搜索自定义日志:

    traces 
    | where message startswith "***"
    
  6. 选择“运行”。

    如果日志未显示任何结果,可能是因为 HTTP 请求与 Kusto 中的日志可用性之间存在几分钟延迟。 等待几分钟,然后再次运行查询。

    你无需额外执行任何操作即可获取此日志记录信息:

    • 代码使用了 context.log 函数框架提供的函数。 通过使用 context日志记录, console可以将日志记录筛选为特定的单个函数。 如果函数应用具有许多函数,这非常有用。
    • 函数应用为你添加了 Application Insights
    • Kusto 查询工具包含在Azure 门户中。
    • 可以选择 traces ,而无需了解如何编写 Kusto 查询 ,以便从日志中获取最小信息。

20. 清理资源

由于使用了单个资源组,因此可以通过删除资源组来删除所有资源。

  1. 在 Visual Studio Code 中,通过选择主侧栏中的 Azure 图标或使用键盘快捷方式(Shift + Alt + A)打开 Azure 资源管理器。
  2. 搜索并选择 “Azure:按资源组分组”。
  3. 右键单击选择资源组,然后选择“ 删除资源组”。
  4. 输入资源组名称以确认删除。

可用的源代码

此 Azure 函数应用的完整源代码:

后续步骤