教程:使用 Azure Functions 和 Azure Web PubSub 服务创建无服务器通知应用

借助 Azure Web PubSub 服务,你可以使用 WebSocket 生成实时消息传递 Web 应用程序。 Azure Functions 是一个无服务器平台,可让你在不管理任何基础结构的情况下运行代码。 本教程介绍如何使用 Azure Web PubSub 服务和 Azure Functions,在通知方案下生成具有实时消息传递功能的无服务器应用程序。

在本教程中,你将了解如何:

  • 生成无服务器通知应用
  • 使用 Web PubSub 函数输入和输出绑定
  • 在本地运行示例函数
  • 将函数部署到 Azure 函数应用

先决条件

如果没有 Azure 试用版订阅,请在开始前创建一个试用版订阅

登录 Azure

使用 Azure 帐户登录到 https://portal.azure.cn/ 的 Azure 门户。

创建 Azure Web PubSub 服务实例

你的应用程序将连接到 Azure 中的 Web PubSub 服务实例。

  1. 选择 Azure 门户左上角的“新建”按钮。 在“新建”屏幕中,在搜索框中键入“Web PubSub”,然后按 Enter。 (还可以从 Web 类别中搜索 Azure Web PubSub。)

    Screenshot of searching the Azure Web PubSub in portal.

  2. 在搜索结果中选择“Web PubSub”,然后选择“创建” 。

  3. 输入以下设置。

    设置 建议值 说明
    资源名称 全局唯一名称 标识新 Web PubSub 服务实例的全局唯一名称。 有效字符为 a-zA-Z0-9-
    订阅 你的订阅 在其下创建此新的 Web PubSub 服务实例的 Azure 订阅。
    资源组 myResourceGroup 要在其中创建 Web PubSub 服务实例的新资源组的名称。
    位置 中国北部 2 选择你附近的区域
    定价层 免费 可以先免费试用 Azure Web PubSub 服务。 了解有关 Azure Web PubSub 服务定价层的更多详细信息
    单位计数 - 单位计数指定 Web PubSub 服务实例可接受的连接数。 每个单位最多支持 1000 个并发连接。 它只能在标准层中配置。

    Screenshot of creating the Azure Web PubSub instance in portal.

  4. 选择“创建”,开始部署 Web PubSub 服务实例。

在本地创建和运行函数

  1. 确保已安装 Azure Functions Core Tools。 现在为项目创建一个空目录。 在此工作目录下运行命令。 使用以下给定选项之一。

    func init --worker-runtime javascript --model V4
    
  2. 按照步骤安装 Microsoft.Azure.WebJobs.Extensions.WebPubSub

    确认或更新 host.json 的 extensionBundle 到版本 4.* 或更高版本,以获取 Web PubSub 支持。 若要更新 host.json,请在编辑器中打开该文件,然后将现有的 extensionBundle 版本替换为 4.* 或更高版本。

    {
        "extensionBundle": {
            "id": "Microsoft.Azure.Functions.ExtensionBundle",
            "version": "[4.*, 5.0.0)"
        }
    }
    
  3. 创建 index 函数,为客户端读取和托管静态网页。

    func new -n index -t HttpTrigger
    
    • 更新 src/functions/index.js 并复制以下代码。
      const { app } = require('@azure/functions');
      const { readFile } = require('fs/promises');
      
      app.http('index', {
          methods: ['GET', 'POST'],
          authLevel: 'anonymous',
          handler: async (context) => {
              const content = await readFile('index.html', 'utf8', (err, data) => {
                  if (err) {
                      context.err(err)
                      return
                  }
              });
      
              return { 
                  status: 200,
                  headers: { 
                      'Content-Type': 'text/html'
                  }, 
                  body: content, 
              };
          }
      });
      
  4. 创建 negotiate 函数,以帮助客户端使用访问令牌获取服务连接 URL。

    func new -n negotiate -t HttpTrigger
    
    • 更新 src/functions/negotiate.js 并复制以下代码。
      const { app, input } = require('@azure/functions');
      
      const connection = input.generic({
          type: 'webPubSubConnection',
          name: 'connection',
          hub: 'notification'
      });
      
      app.http('negotiate', {
          methods: ['GET', 'POST'],
          authLevel: 'anonymous',
          extraInputs: [connection],
          handler: async (request, context) => {
              return { body: JSON.stringify(context.extraInputs.get('connection')) };
          },
      });
      
  5. 创建 notification 函数,以使用 TimerTrigger 生成通知。

    func new -n notification -t TimerTrigger
    
    • 更新 src/functions/notification.js 并复制以下代码。
      const { app, output } = require('@azure/functions');
      
      const wpsAction = output.generic({
          type: 'webPubSub',
          name: 'action',
          hub: 'notification'
      });
      
      app.timer('notification', {
          schedule: "*/10 * * * * *",
          extraOutputs: [wpsAction],
          handler: (myTimer, context) => {
              context.extraOutputs.set(wpsAction, {
                  actionName: 'sendToAll',
                  data: `[DateTime: ${new Date()}] Temperature: ${getValue(22, 1)}\xB0C, Humidity: ${getValue(40, 2)}%`,
                  dataType: 'text',
              });
          },
      });
      
      function getValue(baseNum, floatNum) {
          return (baseNum + 2 * floatNum * (Math.random() - 0.5)).toFixed(3);
      }
      
  6. 在项目根文件夹中添加客户端单页 index.html 并复制内容。

    <html>
        <body>
        <h1>Azure Web PubSub Notification</h1>
        <div id="messages"></div>
        <script>
            (async function () {
                let messages = document.querySelector('#messages');
                let res = await fetch(`${window.location.origin}/api/negotiate`);
                let url = await res.json();
                let ws = new WebSocket(url.url);
                ws.onopen = () => console.log('connected');
    
                ws.onmessage = event => {
                    let m = document.createElement('p');
                    m.innerText = event.data;
                    messages.appendChild(m);
                };
            })();
        </script>
        </body>
    </html>
    
  7. 配置和运行 Azure 函数应用

    • 在浏览器中,打开“Azure 门户”,确认已成功创建前面部署的 Web PubSub 服务实例。 导航到该实例。
    • 选择“密钥”并复制连接字符串。

    Screenshot of copying the Web PubSub connection string.

    在函数文件夹中运行命令设置来服务连接字符串。 根据需要将 <connection-string> 替换为你的值。

    func settings add WebPubSubConnectionString "<connection-string>"
    

    注意

    示例中使用的 TimerTrigger 依赖于 Azure 存储,但是在本地运行 Azure 函数应用时,可以使用本地存储模拟器。 如果收到一些类似 There was an error performing a read operation on the Blob Storage Secret Repository. Please ensure the 'AzureWebJobsStorage' connection string is valid. 的错误,则需要下载并启用存储模拟器

    现在,可通过命令运行本地函数。

    func start --port 7071
    

    若要检查正在运行的日志,可以通过访问 http://localhost:7071/api/index 来访问本地主机静态页面。

    注意

    某些浏览器会自动重定向到 https,这会导致 URL 错误。 如果未能成功呈现内容,建议使用 Edge 并仔细检查 URL。

将函数应用部署到 Azure

在将函数代码部署到 Azure 之前,需要创建三个资源:

  • 一个资源组:相关资源的逻辑容器。
  • 一个存储帐户:用于维护有关函数的状态和其他信息。
  • 一个函数应用:提供用于执行函数代码的环境。 函数应用映射到本地函数项目,并允许你将函数分组为一个逻辑单元,以便更轻松地管理、部署和共享资源。

使用以下命令创建这些项。

  1. 登录 Azure:

    az cloud set -n AzureChinaCloud
    az login
    # az cloud set -n AzureCloud   //means return to Public Azure.
    
  2. 创建资源组,或者可重用某个 Azure Web PubSub 服务来跳过此步骤:

    az group create -n WebPubSubFunction -l <REGION>
    
  3. 在资源组和区域中创建常规用途存储帐户:

    az storage account create -n <STORAGE_NAME> -l <REGION> -g WebPubSubFunction
    
  4. 在 Azure 中创建函数应用:

    az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime node --runtime-version 18 --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
    

    注意

    检查 Azure Functions 运行时版本文档,将 --runtime-version 参数设置为支持的值。

  5. 将函数项目部署到 Azure:

    在 Azure 中创建函数应用后,便可以使用 func azure functionapp publish 命令部署本地函数项目。

    func azure functionapp publish <FUNCIONAPP_NAME> --publish-local-settings
    

    注意

    在这里,我们将本地设置 local.settings.json 与命令参数 --publish-local-settings 一起部署。 如果你使用的是 Azure 存储仿真器,可以在出现以下提示消息后键入 no,跳过在 Azure 上覆盖此值的步骤:App setting AzureWebJobsStorage is different between azure and local.settings.json, Would you like to overwrite value in azure? [yes/no/show]。 除此之外,你还可以在“Azure 门户”->“设置”->“配置”中更新函数应用设置。

  6. 现在,你可以通过导航到 URL (https://<FUNCIONAPP_NAME>.chinacloudsites.cn/api/index),从 Azure 函数应用检查你的站点。

清理资源

如果不打算继续使用此应用,请按照以下步骤删除本文档中创建的所有资源,以免产生任何费用:

  1. 在 Azure 门户的最左侧选择“资源组”,,然后选择创建的资源组。 改用搜索框按名称查找资源组。

  2. 在打开的窗口中选择资源组,然后选择“删除资源组”。

  3. 在新窗口中键入要删除的资源组的名称,然后选择“删除”。

后续步骤

本快速入门介绍了如何运行无服务器聊天应用程序。 现在,可以开始构建自己的应用程序。