Senparc.Weixin.MP SDK 微信公眾平臺開發教程(二十一):在小程式中使用 WebSocket (.NET Core)
本文將介紹如何在 .NET Core 環境下,藉助 SignalR 在小程式內使用 WebSocket。關於 WebSocket 和 SignalR 的基礎理論知識不在這裡展開,已經有足夠的參考資料,例如參考 SignalR 的官方教程:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/introduction?view=aspnetcore-2.1
我們先看一下完成本教程內容後,在小程式內實現的 WebSocket 效果:
私有及群發訊息
小程式模板訊息
上圖中包含了只有傳送者本人可以收到的訊息,以及群發的訊息,還有一條傳送者本人收到的模板訊息。
所有程式碼都已開源,可以在 https://github.com/JeffreySu/WeiXinMPSDK 中找到,其中伺服器端程式碼位於 /Samples/Senparc.Weixin.MP.Sample.vs2017/Senparc.Weixin.MP.Sample.vs2017.sln 解決方案中,小程式客戶端程式碼位於 /src/Senparc.Weixin.WxOpen/src/Senparc.Weixin.WxOpen.AppDemo 中。
下面讓我們開始吧!
伺服器端
首先,我們需要通過 nuget 安裝 Separc.WebSocket 包:https://www.nuget.org/packages/Senparc.WebSocket。Senparc.WebSocket 包提供了可供小程式使用的 WebScoket 的基礎功能封裝(當然,它也可以被很好地使用在小程式以外的場景,只不過在其他場景中我們通常有更多的選擇,以及更多的複雜的業務需求)。
注意:Senparc.WebSocket 提供了針對 .NET Framework 4.5 和 .NET Standard 2.0(支援 .NET Core)的兩個版本。其中,前者使用了相對比較底層的 WebSocket 構建方式,而後者我們直接使用了 SignalR,因為 .NET Core 下的 SignalR 官方擺脫了對 jQuery 的依賴,而 jQuery 在微信小程式中是不被支援的,這也是為什麼我們沒有在 .NET Framework 4.5 中沒有使用 SignalR 這個優秀元件的原因。
安裝完畢 Senparc.WebSocket 之後,我們還需要在 startup.cs 中對其進行啟用,如在 ConfigureServices() 方法中新增程式碼:
services.AddSenparcWebSocket<CustomNetCoreWebSocketMessageHandler>();//Senparc.WebSocket 註冊(按需)
注意:上述過程會附帶執行 services.AddSignalR(); 指令,因此如果你在系統的其他地方也需要使用 SIgnalR,那麼可以忽略對其註冊過程。
關於上述方法中的泛型 CustomNetCoreWebSocketMessageHandler,其邏輯類似於公眾號的 CustomMessageHandler,用於對訊息進行同一的處理。讓我們來看一下 CustomNetCoreWebSocketMessageHandler.cs的樣子:
1 using System; 2 using System.Linq; 3 using System.Threading.Tasks; 4 using Senparc.WebSocket; 5 using Senparc.Weixin.MP.AdvancedAPIs.TemplateMessage; 6 using Senparc.Weixin.WxOpen.Containers; 7 8 namespace Senparc.Weixin.MP.Sample.CommonService.MessageHandlers.WebSocket 9 { 10 /// <summary> 11 /// .NET Core 自定義 WebSocket 處理類 12 /// </summary> 13 public class CustomNetCoreWebSocketMessageHandler : WebSocketMessageHandler 14 { 15 public override Task OnConnecting(WebSocketHelper webSocketHandler) 16 { 17 //TODO:處理連線時的邏輯 18 return base.OnConnecting(webSocketHandler); 19 } 20 21 public override Task OnDisConnected(WebSocketHelper webSocketHandler) 22 { 23 //TODO:處理斷開連線時的邏輯 24 return base.OnDisConnected(webSocketHandler); 25 } 26 27 28 public override async Task OnMessageReceiced(WebSocketHelper webSocketHandler, ReceivedMessage receivedMessage, string originalData) 29 { 30 if (receivedMessage == null || string.IsNullOrEmpty(receivedMessage.Message)) 31 { 32 return; 33 } 34 35 var message = receivedMessage.Message; 36 37 await webSocketHandler.SendMessage("originalData:" + originalData, webSocketHandler.WebSocket.Clients.Caller); 38 await webSocketHandler.SendMessage("您傳送了文字:" + message, webSocketHandler.WebSocket.Clients.Caller); 39 await webSocketHandler.SendMessage("正在處理中(反轉文字)...", webSocketHandler.WebSocket.Clients.Caller); 40 41 await Task.Delay(1000); 42 43 //處理文字 44 var result = string.Concat(message.Reverse()); 45 await webSocketHandler.SendMessage(result, webSocketHandler.WebSocket.Clients.Caller); 46 47 var appId = Config.SenparcWeixinSetting.WxOpenAppId;//與微信小程式賬號後臺的AppId設定保持一致,區分大小寫。 48 49 try 50 { 51 52 var sessionBag = SessionContainer.GetSession(receivedMessage.SessionId); 53 54 //臨時演示使用固定openId 55 var openId = sessionBag != null ? sessionBag.OpenId : "onh7q0DGM1dctSDbdByIHvX4imxA";// "使用者未正確登陸"; 56 57 //await webSocketHandler.SendMessage("OpenId:" + openId, webSocketHandler.WebSocket.Clients.Caller); 58 //await webSocketHandler.SendMessage("FormId:" + formId); 59 60 //群發 61 await webSocketHandler.SendMessage($"[群發訊息] [來自 OpenId:***{openId.Substring(openId.Length - 10, 10)},暱稱:{sessionBag.DecodedUserInfo?.nickName}]:{message}", webSocketHandler.WebSocket.Clients.All); 62 63 //傳送模板訊息 64 65 //var data = new WxOpenTemplateMessage_PaySuccessNotice( 66 // "線上購買", SystemTime.Now, "圖書眾籌", "1234567890", 67 // 100, "400-9939-858", "http://sdk.senparc.weixin.com"); 68 69 var formId = receivedMessage.FormId;//傳送模板訊息使用,需要在wxml中設定<form report-submit="true"> 70 71 var data = new 72 { 73 keyword1 = new TemplateDataItem("來自小程式WebSocket的模板訊息(測試資料)"), 74 keyword2 = new TemplateDataItem(SystemTime.Now.LocalDateTime.ToString()), 75 keyword3 = new TemplateDataItem($"來自 Senparc.Weixin SDK 小程式 .Net Core WebSocket 觸發\r\n您剛才傳送了文字:{message}"), 76 keyword4 = new TemplateDataItem(SystemTime.NowTicks.ToString()), 77 keyword5 = new TemplateDataItem(100.ToString("C")), 78 keyword6 = new TemplateDataItem("400-031-8816"), 79 }; 80 81 var tmResult = Senparc.Weixin.WxOpen.AdvancedAPIs.Template.TemplateApi.SendTemplateMessage(appId, openId, "Ap1S3tRvsB8BXsWkiILLz93nhe7S8IgAipZDfygy9Bg", data, receivedMessage.FormId, "pages/websocket/websocket", "websocket", 82 null); 83 } 84 catch (Exception ex) 85 { 86 var msg = ex.Message + "\r\n\r\n" + originalData + "\r\n\r\nAPPID:" + appId; 87 88 await webSocketHandler.SendMessage(msg, webSocketHandler.WebSocket.Clients.Caller); //VS2017以下如果編譯不通過,可以註釋掉這一行 89 90 WeixinTrace.SendCustomLog("WebSocket OnMessageReceiced()過程出錯", msg); 91 } 92 } 93 } 94 }View Code
從上述程式碼啊可以看到,CustomNetCoreWebSocketMessageHandler 繼承了來自 Senparc.WebSocket 的 WebSocketMessageHandler 抽象類,實現了 3 個重要的方法:
- OnConnecting()
- OnDisConnected()
- OnMessageReceiced()
其中最重要的就是 OnMessageReceiced() 方法,用於處理來自於小程式的 WebSocket 訊息(注意:這條訊息在小程式中經過了特殊約定的處理,才能到達此處,並被正確讀取,在本文後面將會介紹)。
上述方法中,執行了如下邏輯:
- 讀取訊息
- 向用戶的客戶端即時傳送三條訊息(originalData……、您傳送了文字……、正在處理中(反轉文字)...),本條訊息只有傳送者可以收到(注意使用了webSocketHandler.WebSocket.Clients.Caller)
- 對使用者傳送的訊息字串進行處理,使其反轉
- 將處理結果傳送給操作人的客戶端(Caller)
- 執行緒休眠 1 秒鐘(模擬長時間的系統執行)
- 向所有已經連線的客戶端群發訊息(包括髮送者本人在內):[群發訊息] [來自 OpenId:***…… (注意使用了webSocketHandler.WebSocket.Clients.All)
- 獲取 formId,並使用獲取到的 formId 傳送模板訊息
上述過程中,一共傳送了 5 條 WebSocket 訊息,其中 4 條只有傳送者本人可以收到,1 條所有人(包括髮送者本人)可以收到,除此之外,還發送了一條模板訊息。
我們可以使用 SignalR 的優秀特性,有針對性地進行定向傳送或分組傳送,相關知識點可以參考官方教程,這裡不再展開。
接下來,我們需要在 Startup.cs 對 SignalR 新增一個 Hub 的路由規則,在 Configure() 方法中新增:
//使用 SignalR app.UseSignalR(routes => { routes.MapHub<SenparcHub>("/senparcHub"); });
根據 SignalR 的定義,"/senparcHub" 是我們自定義的 Hub 的連線地址,和類的命名沒有關聯。泛型 SenparcHub 是我們自定義的 SignalR 類,程式碼如下:
using Senparc.WebSocket.SignalR; namespace Senparc.Weixin.MP.CoreSample.WebSocket.Hubs { public class SenparcHub : SenparcWebSocketHubBase { } }
注意:和常規的 SignalR 的 Hub 自定義類直接繼承 Microsoft.AspNetCore.SignalR.Hub 類不同,這裡繼承了 SenparcWebSocketHubBase,由 SenparcWebSocketHubBase 繼承了 Microsoft.AspNetCore.SignalR.Hub 類,SenparcWebSocketHubBase 為自動處理小程式訊息進行了一系列的邏輯處理。因此,SenparcHub 也具備了所有 Microsoft.AspNetCore.SignalR.Hub 的能力,我們也可以用於常規的網頁 WebSocket 通訊。
下面我們來豐富一下這個類:
using Microsoft.AspNetCore.SignalR; using Senparc.WebSocket.SignalR; using System.Threading.Tasks; namespace Senparc.Weixin.MP.CoreSample.WebSocket.Hubs { public class SenparcHub : SenparcWebSocketHubBase { /// <summary> /// 給普通網頁用的自定義擴充套件方法 /WebScoket /// </summary> /// <param name="user"></param> /// <param name="message"></param> /// <returns></returns> public async Task SendCustomMessage(string user, string message) { await Clients.All.SendAsync("ReceiveCustomMessage", user, message); } } }
這裡所新增的 SendCustomMessage 就是給普通網頁用的,可以參考官方教程:https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/signalr?view=aspnetcore-2.1&tabs=visual-studio,關於頁面上的程式碼以及js檔案的開發不再贅述。
可以開啟網頁 https://sdk.weixin.senparc.com/WebSocket 進行測試,效果如下:
關於 Web 端的 WebSocket 不是本文的重點,這裡不再展開,下面看小程式的客戶端中如何處理。
小程式客戶端
第一步,建立或開啟小程式客戶端專案,如圖:
第二步:在 utils 資料夾中加入加入兩個檔案:
- signalr.1.0.js 檔案(https://github.com/JeffreySu/WeiXinMPSDK/blob/Developer/src/Senparc.Weixin.WxOpen/src/Senparc.Weixin.WxOpen.AppDemo/utils/signalr.1.0.js)
- senparc.websocket.js 檔案(https://github.com/JeffreySu/WeiXinMPSDK/blob/Developer/src/Senparc.Weixin.WxOpen/src/Senparc.Weixin.WxOpen.AppDemo/utils/senparc.websocket.js)
注意:signalr.js 檔案還在測試中,目前請勿使用!雖然這裡的 signalr.1.0.js 使用的並非最新的版本的 js 客戶端檔案,不過至今為止仍然支援最新的 SignalR nuget 包,可以放心使用。
特別感謝 LcFireRabbit 提供的修改後的 signalr.js 檔案!如果對於新版本 js 您有更好的解決方案,也歡迎留言交流!
第三步:建立測試頁面,見 websocket_signalr 資料夾:
由於原始碼都已經提供,這裡不再展開,介紹一下和 WebSocket 通訊相關的核心程式碼,在 websocket_signalr.js 中:
1 var signalR = require("../../utils/signalr.1.0.js") 2 var senparcWebsocket = require("../../utils/senparc.websocket.js") 3 4 var connection;// Signalr 連線 5 var app = getApp() 6 var socketOpen = false;//WebSocket 開啟狀態 7 Page({ 8 data: { 9 messageTip: '正在連線中,請等待...', 10 messageTextArr: [], 11 messageContent: 'TEST', 12 userinfo: {} 13 }, 14 //sendMessage 15 formSubmit: function (e) { 16 var that = this; 17 console.log('formSubmit', e); 18 if (socketOpen) { 19 var text = e.detail.value.messageContent;//必填,獲得輸入文字 20 var sessionId = wx.getStorageSync("sessionId");//選填,不需要可輸入'' 21 var formId = e.detail.formId//選填formId用於傳送模板訊息,不需要可輸入'' 22 senparcWebsocket.sendMessage(text,sessionId,formId);//傳送 websocket 請求 23 24 that.setData({ 25 messageContent: '' 26 }) 27 } else { 28 that.setData({ 29 messageTip: 'WebSocket 連結失敗,請重新連線!' 30 }) 31 } 32 }, 33 onLoad: function () { 34 console.log('onLoad') 35 }, 36 onShow:function(){ 37 console.log('onShow'); 38 39 var that = this; 40 var hubUrl = wx.getStorageSync('wssDomainName') + "/SenparcHub";//Hub Url 41 var onStart = function () { 42 console.log('ws started'); 43 socketOpen = true; 44 that.setData({ 45 messageTip: 'WebSocket 連線成功!' 46 }) 47 }; 48 connection = senparcWebsocket.buildConnectionAndStart(hubUrl, signalR, onStart); 49 50 //定義收到訊息後觸發的事件 51 var onReceive = function (res) { 52 console.log('收到伺服器內容:' + res) 53 var jsonResult = JSON.parse(res); 54 var currentIndex = that.data.messageTextArr.length + 1; 55 var newArr = that.data.messageTextArr; 56 newArr.unshift( 57 { 58 index: currentIndex, 59 content: jsonResult.content, 60 time: jsonResult.time 61 }); 62 console.log(that); 63 that.setData({ 64 messageTextArr: newArr 65 }); 66 }; 67 senparcWebsocket.onReceiveMessage(onReceive); 68 69 //WebSocket 連線成功 70 wx.onSocketOpen(function (res) { 71 console.log('WebSocket 連線成功!') 72 73 }) 74 //WebSocket 已關閉 75 wx.onSocketClose(function (res) { 76 console.log('WebSocket 已關閉!') 77 socketOpen = false; 78 }) 79 //WebSocket 開啟失敗 80 wx.onSocketError(function (res) { 81 console.log('WebSocket連線開啟失敗,請檢查!') 82 }) 83 } 84 })View Code
其中:
- 第 1 行引入之前建立的 signalr.1.0.js 檔案。
- 第 2 行引入之前建立的 senparc.websocket.js 檔案。
- 第 15 行由點選了介面上的【傳送】按鈕之後,觸發提交form表單的事件(不使用form也可以,只是無法獲取到formId,並用於傳送模板訊息)。
- 第 19-21行準備了 3 個引數:text、sessionId、formId
- 第 22 行使用封裝好的 senparcWebsocket 進行 WebSocket 訊息傳送, Senparc.WebScoket 的方法內部能夠自動處理的格式,必須嚴格按照此格式設定,可以通過修改 senparc.websocket.js 增加屬性,但是目前已經提供的屬性不能減少(FormId 如果沒有可以留空)。
- 第 36 行開始,我們把 websocket 的連線和設定程式碼放在了 onShow() 事件中,而不是 onLoad(),這樣可以確保使用者每次回到此頁面都能夠正確獲得連線。當然為了節省開銷,我們也可以在 onLoad() 中啟動連線,並在 onShow() 中對連線狀態進行判斷,這裡是簡化的做法。
- 第 40 行,設定 SenparcHub 的連線地址。
- 第 41-47 行,設定連線成功後的事件程式碼。
- 第 48 行,正式啟動 WebSocket 連線。
- 第 51-67 行,設定收到 WebSocket 訊息後的觸發事件程式碼。
- 第 69-82 行,使用微信官方的介面進行連線狀態的監控。
以上效果可以掃描下方二維碼,授權登入後,點選【WebSocket】按鈕進行體驗(可開多個手機微信進行測試,但建議不要在一臺上多開,微信的 websocket 限制比較多)。
微信小程式開發QQ群:108830388
列教程索引
地址:http://www.cnblogs.com/szw/archive/2013/05/14/weixin-course-index.html
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(一):微信公眾平臺註冊
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(二):成為開發者
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(三):微信公眾平臺開發驗證
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(四):Hello World
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(五):使用Senparc.Weixin.MP SDK
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(六):瞭解MessageHandler
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(七):解決使用者上下文(Session)問題
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(八):通用介面說明
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(九):自定義選單介面說明
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十):多客服介面說明
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十一):高階介面說明
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十二):OAuth2.0說明
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十三):地圖相關介面說明
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十四):請求訊息去重
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十五):訊息加密
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十六):AccessToken自動管理機制
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十七):個性化選單介面說明
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十八):Web代理功能
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(十九):MessageHandler 的未知型別訊息處理
- Senparc.Weixin.MP SDK 微信公眾平臺開發教程(二十):使用選單訊息功能
&n