Image

在上週的 使用 Powershell + 工作排程器 + Line Notify 來定時提醒公車到站時間,我們利用工作排程器來定時觸發腳本,藉此用 Line 提醒我下班的公車還有多久才來。

做完之後靈機一動,對呀!最近上班挺常接觸到 Azure Functions 這個方便東東,不如就把這個小提醒給架設到 Azure Functions 上吧!

這樣就省卻了特定主機要開著掛工作排程器的困擾,又可以用香香的 Azure 工具來控制監聽的開關,豈不美哉。

如此如此這般這般,讓我們開始建立 Azure Functions 服務吧!

建立 Azure Functions 資源

Azure Functions 是 Azure 推出的一款無伺服器(Serverless)服務,簡單來說就是伺服器之類的麻煩事就交給 Azure 去處理,我們只要專心寫功能就好。對我這種愛寫小腳本的偷懶工程師來說,可以說是香到爆的服務。

小提醒:Azure Functions 是一款收費服務,使用前請務必確認定價

在這篇文章撰寫當下,Azure Functions 有提供每月免費執行一百萬次的授權,對我們每天一次的公車通知來說綽綽有餘了(我們應該不會搭這麼多趟吧…?)

首先讓我們到 Azure 建立一個函數應用程式(如果用英文,請找 Azure Functions):

Image

Image

接著進到建立 Functions 的頁面,讓我們先選好資源群組,並取個好名字

因為「MyFunctions」之類的都被取走了,這邊就直接取「林北ㄟ Functions」:

Image

執行階段堆疊請選擇自己開發用的語言,我這邊使用 .Net 6 進行開發,作業系統則按照建議的選擇。

這邊要稍微注意方案的選擇!如同前面提到的定價,也可以參照 Microsoft Docs 的預估 Azure Functions 中的取用方案成本說明,裡面會有使用量、進階等方案的說明。

這次我們要做的只是簡單的提醒通知,所以就選費用最低的使用量計價就好囉~

按下確認後就會開始部屬:

Image

部屬完成就可以前往我們建立的資源囉,可以在這裡確認記憶體、執行次數等資訊:

Image

Image

從左側的「函式」可以確認我們現在有哪些 Functions,當然目前還是空的:

Image

接著就讓我們來撰寫第一個 Function 吧!

使用 Azure Functions 開發公車到站提醒服務

因為這篇的功能完全是使用 Powershell + 工作排程器 + Line Notify 來定時提醒公車到站時間的完美復刻版,因此我們要做的事情還是一樣:

  • 每天下班前十分鐘(定時執行)
  • 告訴我(通知功能)
  • 下一班到達的公車時間(查詢資訊)

只是這次的功能使用 .Net 6 撰寫,並且使用 Visual Studio 為範例來記錄,定時功能則從臭臭又綁電腦的工作排程器改用香香 Azure Functions 的定時觸發功能。

建立 Azure Funtcions 專案

首先讓我們新增專案,內建已經有 Azure Functions 的範例可以使用:

Image

取個好名字,這邊沿用剛剛開資源的命名:

Image

接著就要選擇版本,這邊要注意 Azure Function 目前還有分出隔離版(Isolated)

Image

簡單來說,原本的 Azure Functions 和主機環境太耦合了,如果用到同一個套件不同版本就有可能翻車。因此推出了隔離式的 Azure Functions 讓我們可以乾乾淨淨地用。

想更了解隔離式版本的差異,可以參見:

由於 .NET on Azure Functions Roadmap 的示意圖:

明確指出將來的主軸會是隔離式(Isolated),因此我們這邊專案也選擇 .Net 6 已隔離的版本。這樣我往後抄起來比較方便

小提示:查詢 Azure Functions 相關資料時也要注意版本的差異!在 .Net 開發隔離式的 SDK 並不一樣,連最基本標示 Function 的語法,原本是 [FunctionName()],隔離式也改成了更簡潔的 [Function()]因此查資料或開發時要特別注意版本差異,避免被 IDE 畫了紅線卻搞不懂為什麼。

接著我們就可以選擇 Function 的觸發條件,因為我們要定時提醒,因此這邊選擇 Timer Trigger 就可以了:

Image

同樣常用的還有當成 API 打的 Http Trigger,以及和我們上一次介紹過的 ServiceBus 一起使用的 Service Bus Queue/Topic Trigger 等等。

關於提供的觸發方式和程式碼範例,可以參照 Microsoft Docs 的 Azure Functions 中的觸發程序和繫結

最後因為我們選擇了 Timer Trigger,這邊提供我們直接設定時間。使用的是 NCrontab 格式,可以參考 NCRONTAB expressions 的說明,Visuat Studio 上也有簡短地介紹:

Image

按照需求,我們希望下班前,也就是每天的 17:45 左右能提醒公車預估到站的時間。

NCrontab 和常見的 Crontab 差在多了第一個欄位來控制秒,因此這邊果斷直接使用 Cronitor 查一下,再往前加上一欄當作秒數即可。

補充:另一個香香工具 DevToys 也能迅速組裝和確認 Cron 語法呦!

每天的 17:45 在 Crontab 表示為「45 17 * * *」:

Image

我們希望在 0 秒的時候觸發,因此轉 NCrontab 時需要在秒的位置指定 0,也就是「0 45 17 * * *」

但在填到 Azure Functions 要注意,在伺服器的時間會是 UTC+0。為了在台灣,也就是 UTC+8 的 17:45 觸發,因此將時間更改為「0 45 9 * * *」,否則 Line 就會在半夜通知你起來搭公車

Image

這樣其他資訊的頁面就填寫完了,可以按下建立囉!

Image

建立後會看到 Visual Studio 已經使用我們剛剛的設置建立了一個 Function 及 Timer Trigger,時間也填好了(如果後續還要調整時間,就修改 TimerTrigger 的值就好):

public class Function1
{
    private readonly ILogger _logger;

    public Function1(
        ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<Function1>();
    }

    [Function("Function1")]
    public void Run([TimerTrigger("0 45 9 * * *")] MyInfo myTimer)
    {
        _logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
        _logger.LogInformation($"Next timer schedule at: {myTimer.ScheduleStatus.Next}");
    }
}

為了後續管理方便,我們先改個名。這邊就叫做 BusReminderFunctions 吧:

public class BusReminderFunctions
{
    private readonly ILogger _logger;

    public BusReminderFunctions(
        ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<BusReminderFunctions>();
    }

    [Function("BusReminder-TimerTrigger")]
    public void RunTimerTrigger([TimerTrigger("0 45 9 * * *")] MyInfo myTimer)
    {
        _logger.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
        _logger.LogInformation($"Next timer schedule at: {myTimer.ScheduleStatus.Next}");
    }
}

並且為了測試方便,我們再增加一個 HttpTrigger,這時候會需要先去 Nuget 安裝相關的套件

搜尋 Microsoft.Azure.Functions.Worker.Extensions 就會看到各種 Trigger,這邊就安裝一下 Http 需要的套件吧:

Image

現在讓我們回到 Functions,增加一個 HttpTrigger,調整一下非同步,並建立一個私有方法 TrackBusAsync,讓 HttpTriggerTimerTrigger 都去呼叫這個方法,這樣我們就可以定時觸發也可以手動觸發它,後續測試起來也比較方便:

[Function("BusReminder-TimerTrigger")]
public async Task RunTimerTrigger([TimerTrigger("0 45 9 * * *")] MyInfo myTimer)
{
    await TrackBusAsync();
}

[Function("BusReminder")]
public async Task<HttpResponseData> HttpTrigger(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    FunctionContext executionContext)
{
    await TrackBusAsync();

    var response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync<object>(new { result = true });

    return response;
}

private async Task TrackBusAsync()
{
    throw new NotImplementedException();
}

補充:HttpTrigger 的參數有三個部份,可以拆分成:

  • 授權層級:Anonymous, Admin 之類的,可參照授權層級
  • 方法:Get, Post 之類的,沒指定就會是不限方法
  • 路由:用來定義 API 路由,預設會拿 FunctionName,在這例子就是 BusReminder

都有指定的樣子會像這樣:
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]

這樣事前準備就差不多了,讓我們開始撰寫邏輯部分吧!

呼叫 MOTC Api 查詢公車到站預估時間

首先我們需要取得公車到站預估時間,這部分跟上一期一樣就直接從公共運輸動態服務 MOTC Transport API 的「市區公車之預估到站資料」(EstimatedTimeOfArrival/City/{City}/{RouteName}) 這支 API 取得就行了。

2024.11.25 更新:發現 MOTC 的 API 已經下架了,現在公車資訊已經被整合到運輸資料服務 TDX (Transport Data eXchange),有要動手串公車資訊的朋友可能得觀察新版 API 再進行調整了 QQ

註:為了避免被查水表,以下就用台北的 307 號公車為例,並假設目標站點是「台北車站(忠孝)」

嘗試把縣市和公車路線名稱代入後,可以取得公車行經的站牌資訊,其中就有我們最想要的估計到站時間(秒):

Image

在這個步驟我們還會需要調整 $top 參數的筆數,順便取得我們要監聽公車到站的站牌 ID(StopId)以及行進方向(Direction) 在這個例子中「台北車站(忠孝)」的站牌 ID 會是 15250

Image

確保了資料來源之後,讓我們回到專案裡。

這次為了之後方便擴展,決定將公車名稱、站牌 ID 等查詢資訊放到組態裡。

註:接下來會使用到 依賴注入 以及讀取 Config 的 IOptions;對這兩項不太熟悉的朋友可以先看過去,並且在後續 BusReminderFunctions 的建構式裡把公車資訊寫死就好,並不會影響功能。

一般的 .Net 6 API 專案我們會把組態設定放到 appsettings.json 中,而在開發 Azure Functions 的時候,則會需要用到 host.jsonlocal.settings.json

Image

其中 host.json 用來設定站台相關的組態,例如在先前調整 ServiceBus Trigger 的時候,我們就是在 host.json 修改重新傳遞訊息到 Azure Functions 的時間。

local.settings.json 則是讓我們在本機開發時使用,對應到線上的組態設定:

Image

可以看到 Azure 上的組態有「應用程式設定」和「連接字串」兩個區塊,而在本機開發時會對應到「Values」以及「ConnectionStrings」

Image

有個大概的認識之後,現在讓我們填入一些值吧。

現在我希望能從組態中,使用 IOptions 取得查詢公車估計時間相關的設定,因此我先建立了一個 Class BusTrackerOption

/// <summary>
/// 公車到站監聽設定
/// </summary>
public class BusTrackerOption
{
    /// <summary>
    /// 公車路線名稱
    /// </summary>
    public string RouteName { get; set; }

    /// <summary>
    /// 要預估到站的站牌 ID
    /// </summary>
    public int StopId { get; set; }

    /// <summary>
    /// 公車路線行進方向
    /// </summary>
    public int Direction { get; set; } = 0;
}

並且到 local.settings.json 加上組態。這邊要特別提到的是:原本我們在 appsettings.json 加上組態,會用這種階層式的寫法:

{
    "BusTracker": {
      "RouteName": "307",
      "StopId" : 15250,
      "Direction": 0 
    }
}

但我們前面已經看到了,在 Azure 上的組態實際上是一個單層的列表。因此我們會使用 __ 當作分隔符號來將設定攤平,並放到 local.settings.jsonValues(如果是連線字串就放到 ConnectionStrings 裡),也就是像這樣:

{
  "Values": {
    // ...這裡會有其他組態設定

    // 加上 BusTracker 的 參數,用 __ 來表示階層
    "BusTracker__RouteName": 307,
    "BusTracker__StopId": 15250,
    "BusTracker__Direction": 0
  }
}

接著讓我們到 Program.cs 來將 json 的設定值繫結到 BusTrackerOption,Section 名稱要記得和 json 中的階層名稱對上

// 注意 Azure Functions 的 settings.json 的階層要用 __ 來區隔
services.AddOptions();
services.Configure<BusTrackerOption>(hostContext.Configuration.GetSection("BusTracker"));

註:如果像我一樣開 .Net 6,只看到 HostBuilder 的朋友,可以自行加入 ConfigureServices((hostContext, services) => {}) 的部份來註冊,如下:

Image

接著就可以回到我們的 BusReminderFunctions 來把 IOptions<BusTrackerOption> 注入進來:

public class BusReminderFunctions
{
    private readonly ILogger _logger;
    private readonly BusTrackerOption _busTrackerOption;

    public BusReminderFunctions(
        ILoggerFactory loggerFactory,
        IOptions<BusTrackerOption> busTrackerOption)
    {
        _logger = loggerFactory.CreateLogger<BusReminderFunctions>();
        _busTrackerOption = busTrackerOption.Value;
    }

    // ...略
}

對依賴注入還不太熟悉的朋友,可以在這邊的建構式裡宣告出 BusTrackerOption 並賦值即可。但還是推薦閱讀依賴注入的筆記來了解一下呦

現在公車路線等組態都準備得差不多了,只剩下回傳時候要用來接收資料的 Model 還沒開。這時候就直接偷懶,拿 MOTC Transport API 的 Swagger 打回來的 Json,直接到 Json2Cshrp 之類的轉換網站直接產生 C# Class 就好了:

Image

Image

記得轉換結果的 Class Root 要改個名稱,這邊就直接取名叫做 BusEstimateInfo 吧:

Image

現在我們有傳過去的參數,也有接回來的 Model 了,讓我們來開一個私有方法,從 MOTC API 取回資料吧:

/// <summary>
/// 查詢公車估計到站時間
/// </summary>
/// <param name="routeName">路線名稱</param>
/// <param name="stopId">站牌 ID</param>
/// <param name="direction">去返程</param>
private async Task<BusEstimateInfo> FetchBusEstimateTime(
    string routeName,
    int stopId,
    int direction = 0)
{
    var api = $"https://ptx.transportdata.tw/MOTC/v2/Bus/EstimatedTimeOfArrival/City/Taipei/{routeName}";

    // 將站牌 ID 及路線方向 加入到查詢參數中
    var filter = $"stopId eq '{stopId}'";
    if (direction != 0)
    {
        filter += $" and direction eq {direction}";
    }

    var query = new Dictionary<string, string>()
    {
        ["$filter"] = filter,
        ["$top"] = 1.ToString(),
        ["$format"] = "JSON"
    };

    // 使用套件 Microsoft.AspNetCore.WebUtilities 提供的 QueryHelpers 
    // 直接組出帶 QueryString 的 Url
    var url = QueryHelpers.AddQueryString(api, query);

    // 建立 request;注意直接呼叫 API 會要求驗證
    // 必須將 Header 掛成瀏覽器才能吃到每日 50 次的呼叫額度
    var request = new HttpRequestMessage
    {
        Method = HttpMethod.Get,
        RequestUri = new Uri(url)
    };
    request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");

    using var client = new HttpClient();

    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode is false)
    {
        throw new Exception(response.StatusCode.ToString());
    }

    var body = await response.Content.ReadAsStringAsync();
    var result = JsonSerializer.Deserialize<IEnumerable<BusEstimateInfo>>(body)?.FirstOrDefault();
    return result ?? throw new Exception("呼叫 API 失敗,無法取得估計到站資訊");
}

這支呼叫 API 的 Method 步驟相當簡單:組出參數,呼叫 API,取回資料。

這邊有偷懶不想組字串,直接使用 Microsoft.AspNetCore.WebUtilitiesQueryHelpers.AddQueryString 來把 GET 的 QueryString 參數組到 Url 裡,不想多安裝一個套件的朋友也可以自己手動組。

註:MOTC 的 文件 有提到非會員一天有 50 次的免費 Swagger 呼叫次數,實測如果是從程式呼叫時會需要吃驗證,要掛 Header 假裝成瀏覽器才能取得資料,不確定是不是還有其他限制。如果是要給自己開發的工具或產品使用的話,還是可以考慮了解一下 MOTC 的會員制度。

註:這邊的 HttpClient 也可以直接改用注入 HttpClientFactory 的方式來製作。可以參照 Yowko’s Notes 的這篇:在 .NET Core 與 .NET Framework 上使用 HttpClientFactory

現在取得公車資料的 FetchBusEstimateTime 已經建好了,讓我們調整一下外面的 Trigger 和共通的私有方法,來把 BusTrackerOption 的資訊帶到參數裡吧:

public BusReminderFunctions(
    IOptions<BusTrackerOption> busTrackerOption,
    ILoggerFactory loggerFactory)
{
    _busTrackerOption = busTrackerOption.Value;
    _logger = loggerFactory.CreateLogger<BusReminderFunctions>();
}

[Function("BusReminder-TimerTrigger")]
public async Task RunTimerTrigger([TimerTrigger("0 45 9 * * *")] MyInfo myTimer)
{
    var routeName = this._busTrackerOption.RouteName;
    var stopId = this._busTrackerOption.StopId;
    var direction = this._busTrackerOption.Direction;

    await TrackBusAsync(routeName, stopId, direction);
}

[Function("BusReminder")]
public async Task<HttpResponseData> HttpTrigger(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
    FunctionContext executionContext)
{
    var routeName = this._busTrackerOption.RouteName;
    var stopId = this._busTrackerOption.StopId;
    var direction = this._busTrackerOption.Direction;

    await TrackBusAsync(routeName, stopId, direction);

    var response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync<object>(new { result = true });

    return response;
}

private async Task TrackBusAsync(
    string routeName,
    int stopId,
    int direction = 0)
{
    var busEstimateInfo = await FetchBusEstimateTime(routeName, stopId, direction);

    throw new NotImplementedException();
}

就像前面提到的,我們在這邊讓 TimerTriggerHttpTrigger 取出 BusTrackerOption 的公車相關參數,例如公車路線名稱後,再傳遞到共用的 TrackBusAsync,現在第一部份的查詢公車資訊已經完工,就讓我們先添加上去。

註:這邊之所以要讓查詢參數交由外面決定,就是以後如果我想把 HttpTrigger 改成可以從外部呼叫 API 時可以指定公車路線之類的參數,又或者是將來要註冊多組公車路線到資料庫之類的改動時,可以不用再動「查詢公車到站資訊」之類的邏輯。畢竟懶惰的訣竅在於提早準備

發送 Line Notify

2024.10 更新: Line Notify 將於 2025 年 3 月停止服務(LINE Notify 結束服務公告),有看到這篇的朋朋請選擇一組新的通知服務來串吧 QQ

現在搞定取得公車資訊的段落了,接著就讓我們來撰寫發送 Line Notify 的部份吧。

這邊為了過程完整一點,就重播一下上次衝刺去申請 Line Notify Token 的過程吧:

當下一個直衝 Line Notify 高速申請權杖:

Image

Image

Image

好的回想完畢,現在讓我們把拿到的 Line Notify Token 也丟到 local.settings.jsonValues 裡方便管理:

{
  "Values": {
    // ...這裡會有其他組態設定

    // 加上 LineNotify 的 Token,用 __ 來表示階層
    "LineNotify__Token": "YOUR LINE NOTIFY TOKEN"
  }
}

一樣建立一個 Class,方便後續用 IOption 從組態中取得值:

public class LineNotifyOptions
{
    public string Token { get; set; }
}

接著到 Program.cs 來將 json 的設定值繫結到 LineNotifyOptions,Section 名稱要記得和 json 中的名稱對上:

services.Configure<LineNotifyOption>(hostContext.Configuration.GetSection("LineNotify"));

Image

接著就可以回到我們的 BusReminderFunctions 來把 IOptions<LineNotifyOptions> 注入進來:

Image

這樣從 Config 取得 Token 的準備就完成了,現在讓我們來撰寫傳送訊息的方法本體吧

發送 Line Notify 實際上就是傳送一個 Post 請求到 /api/notify,並將 Token 放到 Header 的 Authorization、訊息丟到 Body 就行了,因此我們可以直接把這方法包裝成接收到訊息就傳遞給 API:

/// <summary>
/// 發送 Line Notify 通知
/// </summary>
/// <param name="message">The message.</param>
/// <exception cref="System.Exception">發送失敗</exception>
private async Task SendLineNotify(string message)
{
    if (string.IsNullOrEmpty(_lineNotifyOptions.Token))
    {
        throw new Exception("取得 Line Notify Token 失敗");
    }

    const string lineNotifyApi = "https://notify-api.line.me/api/notify";
    var requestBody = new Dictionary<string, string>
    {
        ["message"] = message
    };

    var request = new HttpRequestMessage
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri(lineNotifyApi),
        Content = new FormUrlEncodedContent(requestBody)
    };
    request.Headers.Add("Authorization", $"Bearer {_lineNotifyOptions.Token}");

    using var client = new HttpClient();
    var response = await client.SendAsync(request);

    if (response.IsSuccessStatusCode is false)
    {
        throw new Exception("Line Notify 發送失敗");
    }
}

現在我們也已經有了傳送訊息的方法,是時候把它們倆串起來了!

組合查詢公車資訊與發送到站通知

先把鏡頭回到我們讓 Trigger 們共用的 TrackBusAsync 方法:

private async Task TrackBusAsync(
    string routeName,
    int stopId,
    int direction = 0)
{
    var busEstimateInfo = await FetchBusEstimateTime(routeName, stopId, direction);

    throw new NotImplementedException();
}

可以看到我們已經嘗試取得公車資訊,現在我們要組裝訊息,然後傳遞到剛剛撰寫的 Line Notify 通知方法裡。

而我想要的訊息是這樣的:「您追蹤的公車 307 將在 10 分鐘後(18:00)抵達 台北車站(忠孝)」

因此我會需要計算出剩餘的分數,以及抵達時間,再從前面拿到的公車資訊來組成訊息並傳遞到方法中:

private async Task TrackBusAsync(
    string routeName,
    int stopId,
    int direction = 0)
{
    var busEstimateInfo = await FetchBusEstimateTime(routeName, stopId, direction);

    // 將預估幾秒後抵達 轉換成 預估幾分鐘後抵達
    var estimateMin = new TimeSpan(0, 0, busEstimateInfo.EstimateTime).Minutes;
    
    // 取得台北時區的目前時間,並和預估秒數計算出預估抵達時間
    var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Taipei Standard Time");
    var convertedTime = TimeZoneInfo.ConvertTime(DateTime.UtcNow, timeZone);
    var estimateTime = convertedTime.AddSeconds(busEstimateInfo.EstimateTime).ToString("HH:mm");

    var message = $"您追蹤的公車 {busEstimateInfo.RouteName.Zh_tw} 將在 {estimateMin} 分鐘後({estimateTime})抵達 {busEstimateInfo.StopName.Zh_tw}";
    await SendLineNotify(message);
}

這邊要特別注意取得時間的部份,因為在伺服器的時間會是 UTC+0,因此我們這邊使用 TimeZoneInfo.ConvertTime 轉換成台北時區的時間。

註:像這種丟到雲端平台的服務,會時常遇到時區時間的轉換。單純的時間轉換可以參照之前筆記的 C#: 時區轉換、民國西元、國曆農曆、中文月份週期

或是可以嘗試更優雅的 DateTimeOffset,請參考 還在用 DateTime 嗎?試試 DateTimeOffset 吧 - demo小鋪

現在我們已經將前面撰寫的兩個小方法組裝起來,是時候來測試看看了!

本機測試 Functions

總之,啟動鍵先用力給它按下去:

Image

接著就會看到小黑窗跑起來,告訴你已經啟動了哪些 Functions:

Image

可以看到我們的 Http Trigger 以及 Timer Trigger

因為我們的 Http Trigger 有支援 GET,所以這邊直接複製 Api 網址丟到瀏覽器打看看就可以試囉:

Image

Image

這時候小黑窗也會記錄到這次呼叫:

Image

看起來運行良好,該放上 Azure 上啦!

補充:在 Azure 上要看小黑窗的話,可以從左邊選單找到「監視 > 紀錄資料流」: Image

使用 Visual Studio 發佈到 Azure Functions

現在讓我們把寫好的 Functions 佈到前面申請的 Azure Functions 服務上運行。為了方便這邊就使用 Visual Studio 來示範,首先讓我們對專案點選發佈:

Image

接著選取發佈目標,我們想要發到 Azure 雲端上:

Image

接著會讓我們選擇目標,因為我們前面建立時的作業系統是選用建議的 Windows,因此選擇 Azure Functions (Windows):

Image

接著就可以選取符合條件的 Azure Functions,這邊當然是選擇我們前面建立好的「林北ㄟ Functions」

Image

註:如果先前沒有建立過 Azure Functions 服務,發佈的時候也可以從 Visual Studio 中建立,可以說是相當貼心:

Image

按下完成之後,我們的發佈檔就建立完囉!

Image

這邊要特別注意的是,我們先前有在 local.settings.json 加入一些設定值,例如 BusTracker 的公車路線等設定。因此我們這邊可以順手同步到目標服務上,在裝載的右上角點開「管理 Azure App Service 設定」:

Image

就可以看到我們目前在 Local 的組態設定值,以及 Azure 服務裡的組態設定值。這邊為了將 Local 的設定值同步上去,就直接按下「插入本機的值」:

Image

註:按下「插入本機的值」並確定後,設定值就會同步到 Azure Functions 服務的組態中:

Image

反過來說,如果沒有從本機同步上去,而是在 Azure 上設定組態也是完全 OK 的

現在一切就緒,讓我們按下發佈吧!

Image

接著就可以看到我們撰寫的 Functions 被發佈上去,並且幫我們重啟了 Azure Functions 服務:

Image

現在讓我們回到 Azure Functions 服務的函式看看:

Image

可以確認我們撰寫的 Functions 已經出現囉!

回到概觀來複製一下我們 Azure Functions 的 Url:

Image

並且加上我們的 HttpTrigger 路由 api/BusReminder 呼叫看看:

Image

Image

大功告成!

小結

終於把這個小提醒給上雲啦!唉呀不得不說 Azure Functions 真是香。

接下來只要下班前收到公車到站通知,然後在公車來之前下班就可以啦!

……只要在公車來之前下班,就可以……吧?

參考資料