教學課程:使用 Azure Functions 和 Azure Web PubSub 服務建立無伺服器通知應用程式
在此文章
Azure Web PubSub 服務可協助您使用 WebSocket 建置即時傳訊 Web 應用程式。 Azure Functions 是無伺服器平台,可讓您在不需要管理任何基礎結構的情況下執行您的程式碼。 在本教學課程中,您會了解如何使用 Azure Web PubSub 服務和 Azure Functions,在通知案例下建置具有即時傳訊的無伺服器應用程式。
在本教學課程中,您會了解如何:
建置無伺服器通知應用程式
使用 Web PubSub 函式輸入和輸出繫結
在本機執行範例函式
將函式部署至 Azure 函數應用程式
必要條件
如果您沒有 Azure 訂用帳戶 ,請在開始之前先建立 Azure 免費帳戶 。
登入 Azure
請使用您的 Azure 帳戶登入 Azure 入口網站 (https://portal.azure.com/ )。
建立 Azure Web PubSub 服務執行個體
您的應用程式將會連線至 Azure 中的 Web PubSub 服務執行個體。
選取 Azure 入口網站左上角的 [新增] 按鈕。 在 [新增] 畫面的搜尋方塊中輸入 Web PubSub 並按 Enter。 (您也可以從 Web
類別搜尋 Azure Web PubSub。)
從搜尋結果中選取 [Web PubSub] ,然後選取 [建立] 。
輸入下列設定。
設定
建議的值
描述
資源名稱
全域唯一的名稱
識別新 Web PubSub 服務執行個體的全域唯一名稱。 有效字元:a-z
、A-Z
、0-9
和 -
。
訂用帳戶
您的訂用帳戶
將在其下建立這個新 Web PubSub 服務執行個體的 Azure 訂用帳戶。
資源群組
myResourceGroup
要在其中建立 Web PubSub 服務執行個體之新資源群組的名稱。
地點
美國西部
選擇您附近的區域 。
定價層
免費
您可以先免費試用 Azure Web PubSub 服務。 深入了解 Azure Web PubSub 服務定價層
單位計數
-
單位計數會指出您的 Web PubSub 服務執行個體可接受的連線數目。 每個單位支援最多 1,000 個同時連線。 它只能在標準層中設定。
選取 [建立] 以開始部署 Web PubSub 服務執行個體。
在本機建立及執行函式
請確定您已安裝 Azure Functions Core Tools 。 現在,請為專案建立空的目錄。 在此工作目錄下執行命令。 使用下列其中一個指定選項。
func init --worker-runtime javascript --model V4
func init --worker-runtime javascript --model V3
func init --worker-runtime dotnet
func init --worker-runtime dotnet-isolated
func init --worker-runtime python --model V1
請遵循步驟來安裝 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)"
}
}
確認或將 host.json
的 extensionBundle 更新為 3.3.0 版或更新版本,以取得 Web PubSub 支援。 若要更新 host.json
,請在編輯器中開啟檔案,然後將現有的版本 extensionBundle 取代為 3.3.0 版或更新版本。
{
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.3.*, 4.0.0)"
}
}
dotnet add package Microsoft.Azure.WebJobs.Extensions.WebPubSub
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.WebPubSub --prerelease
將 host.json
的 extensionBundle 更新為 3.3.0 版或更新版本,以取得 Web PubSub 支援。 若要更新 host.json
,請在編輯器中開啟檔案,然後將現有的版本 extensionBundle 取代為 3.3.0 版或更新版本。
{
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.3.*, 4.0.0)"
}
}
建立 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,
};
}
});
更新 index/function.json
並複製下列 JSON 程式碼。
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
更新 index/index.js
並複製下列程式碼。
var fs = require('fs');
var path = require('path');
module.exports = function (context, req) {
var index = context.executionContext.functionDirectory + '/../index.html';
context.log("index.html path: " + index);
fs.readFile(index, 'utf8', function (err, data) {
if (err) {
console.log(err);
context.done(err);
}
context.res = {
status: 200,
headers: {
'Content-Type': 'text/html'
},
body: data
};
context.done();
});
}
更新 index.cs
,並以下列程式碼取代 Run
函式。
[FunctionName("index")]
public static IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req, ExecutionContext context, ILogger log)
{
var indexFile = Path.Combine(context.FunctionAppDirectory, "index.html");
log.LogInformation($"index.html path: {indexFile}.");
return new ContentResult
{
Content = File.ReadAllText(indexFile),
ContentType = "text/html",
};
}
更新 index.cs
,並以下列程式碼取代 Run
函式。
[Function("index")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, FunctionContext context)
{
var path = Path.Combine(context.FunctionDefinition.PathToAssembly, "../index.html");
_logger.LogInformation($"index.html path: {path}.");
var response = req.CreateResponse();
response.WriteString(File.ReadAllText(path));
response.Headers.Add("Content-Type", "text/html");
return response;
}
更新 index/function.json
並複製下列 JSON 程式碼。
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
更新 index/__init__.py
並複製下列程式碼。
import os
import azure.functions as func
def main(req: func.HttpRequest) -> func.HttpResponse:
f = open(os.path.dirname(os.path.realpath(__file__)) + '/../index.html')
return func.HttpResponse(f.read(), mimetype='text/html')
建立 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')) };
},
});
更新 negotiate/function.json
並複製下列 JSON 程式碼。
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "res"
},
{
"type": "webPubSubConnection",
"name": "connection",
"hub": "notification",
"direction": "in"
}
]
}
建立資料夾交涉並更新 negotiate/index.js
並複製下列程式碼。
module.exports = function (context, req, connection) {
context.res = { body: connection };
context.done();
};
更新 negotiate.cs
,並以下列程式碼取代 Run
函式。
[FunctionName("negotiate")]
public static WebPubSubConnection Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
[WebPubSubConnection(Hub = "notification")] WebPubSubConnection connection,
ILogger log)
{
log.LogInformation("Connecting...");
return connection;
}
在標頭中新增 using
陳述式,以解析必要的相依性。
using Microsoft.Azure.WebJobs.Extensions.WebPubSub;
更新 negotiate.cs
,並以下列程式碼取代 Run
函式。
[Function("negotiate")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
[WebPubSubConnectionInput(Hub = "notification")] WebPubSubConnection connectionInfo)
{
var response = req.CreateResponse(HttpStatusCode.OK);
response.WriteAsJsonAsync(connectionInfo);
return response;
}
建立資料夾交涉並更新 negotiate/function.json
並複製下列 JSON 程式碼。
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "$return"
},
{
"type": "webPubSubConnection",
"name": "connection",
"hub": "notification",
"direction": "in"
}
]
}
更新 negotiate/__init__.py
並複製下列程式碼。
import logging
import azure.functions as func
def main(req: func.HttpRequest, connection) -> func.HttpResponse:
return func.HttpResponse(connection)
使用 TimerTrigger
建立 notification
函式來產生通知。
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);
}
更新 notification/function.json
並複製下列 JSON 程式碼。
{
"bindings": [
{
"name": "myTimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "*/10 * * * * *"
},
{
"type": "webPubSub",
"name": "actions",
"hub": "notification",
"direction": "out"
}
]
}
更新 notification/index.js
並複製下列程式碼。
module.exports = function (context, myTimer) {
context.bindings.actions = {
"actionName": "sendToAll",
"data": `[DateTime: ${new Date()}] Temperature: ${getValue(22, 1)}\xB0C, Humidity: ${getValue(40, 2)}%`,
"dataType": "text"
}
context.done();
};
function getValue(baseNum, floatNum) {
return (baseNum + 2 * floatNum * (Math.random() - 0.5)).toFixed(3);
}
更新 notification.cs
,並以下列程式碼取代 Run
函式。
[FunctionName("notification")]
public static async Task Run([TimerTrigger("*/10 * * * * *")]TimerInfo myTimer, ILogger log,
[WebPubSub(Hub = "notification")] IAsyncCollector<WebPubSubAction> actions)
{
await actions.AddAsync(new SendToAllAction
{
Data = BinaryData.FromString($"[DateTime: {DateTime.Now}] Temperature: {GetValue(23, 1)}{'\xB0'}C, Humidity: {GetValue(40, 2)}%"),
DataType = WebPubSubDataType.Text
});
}
private static string GetValue(double baseNum, double floatNum)
{
var rng = new Random();
var value = baseNum + floatNum * 2 * (rng.NextDouble() - 0.5);
return value.ToString("0.000");
}
在標頭中新增 using
陳述式,以解析必要的相依性。
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.WebPubSub;
using Microsoft.Azure.WebPubSub.Common;
更新 notification.cs
,並以下列程式碼取代 Run
函式。
[Function("notification")]
[WebPubSubOutput(Hub = "notification")]
public SendToAllAction Run([TimerTrigger("*/10 * * * * *")] MyInfo myTimer)
{
return new SendToAllAction
{
Data = BinaryData.FromString($"[DateTime: {DateTime.Now}] Temperature: {GetValue(23, 1)}{'\xB0'}C, Humidity: {GetValue(40, 2)}%"),
DataType = WebPubSubDataType.Text
};
}
private static string GetValue(double baseNum, double floatNum)
{
var rng = new Random();
var value = baseNum + floatNum * 2 * (rng.NextDouble() - 0.5);
return value.ToString("0.000");
}
建立資料夾通知並更新 notification/function.json
,並複製下列 JSON 程式碼。
{
"scriptFile": "__init__.py",
"bindings": [
{
"name": "myTimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "*/10 * * * * *"
},
{
"type": "webPubSub",
"name": "actions",
"hub": "notification",
"direction": "out"
}
]
}
更新 notification/__init__.py
並複製下列程式碼。
import datetime
import random
import json
import azure.functions as func
def main(myTimer: func.TimerRequest, actions: func.Out[str]) -> None:
time = datetime.datetime.now().strftime("%A %d-%b-%Y %H:%M:%S")
actions.set(json.dumps({
'actionName': 'sendToAll',
'data': '\x5B DateTime: {0} \x5D Temperature: {1:.3f} \xB0C, Humidity: {2:.3f} \x25'.format(time, 22 + 2 * (random.random() - 0.5), 44 + 4 * (random.random() - 0.5)),
'dataType': 'text'
}))
在專案根資料夾中新增用戶端單一頁面 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>
因為 C# 專案會將檔案編譯至不同的輸出資料夾,所以您必須更新您的 *.csproj
,讓內容頁面隨其一起執行。
<ItemGroup>
<None Update="index.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
因為 C# 專案會將檔案編譯至不同的輸出資料夾,所以您必須更新您的 *.csproj
,讓內容頁面隨其一起執行。
<ItemGroup>
<None Update="index.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
設定及執行 Azure Function 應用程式
在瀏覽器中,開啟 Azure 入口網站 ,並確認您稍早部署的 Web PubSub 服務執行個體已成功建立。 瀏覽至執行個體。
選取 [金鑰] 並複製連接字串。
在函式資料夾中執行命令,以設定服務連接字串。 視需要使用您的值取代 <connection-string>
。
func settings add WebPubSubConnectionString "<connection-string>"
注意
範例中使用的 TimerTrigger
函式相依於 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,您必須先建立三個資源:
資源群組,其為相關資源的邏輯容器。
儲存體帳戶,用來維護函式的狀態和其他資訊。
函數應用程式,其提供執行函式程式碼的環境。 函數應用程式可對應至您的本機函式專案,並可讓您將函式分組為邏輯單位,以便管理、部署和共用資源。
請使用下列命令來建立這些項目。
登入 Azure:
az login
建立資源群組,或者您可以重複使用其中一個 Azure Web PubSub 服務來略過:
az group create -n WebPubSubFunction -l <REGION>
在您的資源群組和區域中建立一般用途的儲存體帳戶:
az storage account create -n <STORAGE_NAME> -l <REGION> -g WebPubSubFunction
在 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>
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>
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime dotnet --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime dotnet-isolated --functions-version 4 --name <FUNCIONAPP_NAME> --storage-account <STORAGE_NAME>
az functionapp create --resource-group WebPubSubFunction --consumption-plan-location <REGION> --runtime python --runtime-version 3.9 --functions-version 4 --name <FUNCIONAPP_NAME> --os-type linux --storage-account <STORAGE_NAME>
將函式專案部署至 Azure
在 Azure 中建立函數應用程式之後,您就可以開始使用 func azure functionapp publish 命令來部署本機函式專案。
func azure functionapp publish <FUNCIONAPP_NAME> --publish-local-settings
注意
在這裡,我們會將本機設定 local.settings.json
與命令參數 --publish-local-settings
一起進行部署。 如果您使用 Microsoft 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 入口網站 -> [設定] -> [組態] 中更新函數應用程式設定。
現在,您可以透過瀏覽至 URL,從 Azure 函數應用程式檢查您的網站: https://<FUNCIONAPP_NAME>.azurewebsites.net/api/index
。
清除資源
如果您不會繼續使用此應用程式,請使用下列步驟來刪除本文件所建立的全部資源,這樣才不會產生任何費用:
在 Azure 入口網站中選取最左側的 [資源群組] ,然後選取您所建立的資源群組。 使用搜尋方塊,改為依資源群組的名稱尋找。
在開啟的視窗中選取資源群組,然後選取 [刪除資源群組] 。
在新視窗中輸入要刪除的資源群組名稱,然後選取 [刪除] 。
下一步
在本快速入門中,您已了解如何執行無伺服器聊天應用程式。 現在,您可以開始建置自己的應用程式。