SignalR 在Vue 中的使用 @latelier/vue-signalr

July 28, 2020

Adding real-time communication to your web applications such online games, dashboards, and chat systems can greatly enhance the usability and interactivity of the app. Using WebSockets, your users see the most up-to-date information without having to manually refresh their browsers, or your application having to poll constantly for changes.

ASP.NET Core SignalR provides an API to allow server-to-client communication. By default, SignalR will use WebSockets but will gracefully fallback if the browser doesn’t support it. SignalR allows you to easily send messages to all clients at once, or send messages to individual users or groups.

In this post, we’ll look at how to get SignalR set up, and then how you can add it to your Vue.js application.

Setting up SignalR

For purposes of this post, I’m going to assume you have an API project already set up. Adding SignalR in an ASP.NET Core project is fairly straight forward. If you are targeting netcoreapp3.1, SignalR is included in the “meta” package and there is nothing else to install.

You’ll need to set up a “Hub” which is responsible for handling the socket connections with the clients. These can include both sending and receiving messages. One drawback to the traditional hubs is that they often have a lot of “magic strings.” These are often fragile and can lead to runtime errors. However, Hubs can be strongly-typed to help mitigate that risk. For purposes of this post, we’ll use a strongly-typed hub, but it is not required. Read more about strongly-type hubs.

When using a strongly-typed hub, you’ll need to create an interface that represents the client. Like any other interface in C#, it represents that contract of the messages and their parameters that can be sent. Below is an example of my IGameClient:

public interface IGameClient
{
    Task GameStarted(string gameCode);
    Task PlayerJoined(string playerName);
}

Now that we have our client interface, we need to create a Hub. Here’s what that looks like:

public class GameHub : Hub<IGameClient>
{
    private readonly IRepository<Game> _gameRepository;
    private readonly IRepository<Player> _playerRepository;

    public GameHub(IRepository<Game> gameRepository,
        IRepository<Player> playerRepository)
    {
        _gameRepository = gameRepository;
        _playerRepository = playerRepository;
    }

    public async Task GameCreated(string gameCode)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, gameCode);
    }

    public async Task PlayerJoined(string gameCode, string playerName)
    {
        var game = await GetGameByCode(gameCode);
        var player = new Player
        {
            GameId = game.Id,
            Name = playerName,
            JoinedOn = DateTime.UtcNow,
            IsHost = false
        };

        await _playerRepository.CreateAsync(player);

        await Groups.AddToGroupAsync(Context.ConnectionId, gameCode);
        await Clients.OthersInGroup(gameCode).PlayerJoined(playerName);
    }
}

You’ll notice that the GameHub class inherits from Hub<IGameClient> - this is what’s strongly typing our hub to the interface we defined earlier. If we were not making a strongly-typed hub, we would just inherit from Hub instead.

You’ll also see that we have a constructor, with a few repository dependencies. A hub can have dependencies injected into it, just like any other controller or service. This is helpful if you need to persist data to a database or other store.

Following, we have two methods, which correlate to our IGameClient interface. We’ll look at the PlayerJoined method. First, we’re getting the game info by it’s code (method redacted for brevity) and then we are creating a Player object and persisting it to our data store (in this case that’s CosmosDB). SignalR has the concept of groups, so we are adding this player to the group for this game and finally notifying all of the other players that the player has joined.

The last thing we need to do for now is to wire up our Hub in Startup.cs. Add the following line to the ConfigureServices method. I typically like to do this just after services.AddControllers() (or MVC):

services.AddSignalR();

Finally, we need to add the SignalR endpoint, by modifying UseEndpoints to look like this:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
    endpoints.MapHub<GameHub>("/gamehub"); // The URL passed can be named whatever you'd like
});

Adding SignalR to Vue

Adding SignalR to Vue was a little trickier than the server-side, at least for me. I wasn’t sure how to get started, such as “where do I start the connection?” And “where do I listen for these messages?” After some brief Google searches and reading a few blog posts, I found a package on NPM, vue-signalr. The documentation makes it a little hard to get started so it look a little playing around to get things figured out. (Ps once I figure things out more clearly, I plan on submitting a PR to update the docs.)

Once you’ve installed the package, open up main.js and add the following:

import VueSignalR from '@latelier/vue-signalr'

Vue.use(VueSignalR, 'https://localhost:5001/gamehub')

The URL shown above is the URL to your API (or backend project) with the URL you defined as the endpoint for the hub.

Next, you’ll need to start the socket connection. For now, I chose to do this in my App.vue component, but I may move it as I think it is better suited in a more “scoped” component (App.vue is used as the “base” for all Vue URL routing).

export default {
  created () {
    this.$socket.start({
      log: true // Logging is optional but very helpful during development
    })
  },
  sockets: {
    PlayerJoined (data) {
      // this has to be here even though it's not being used?
      // otherwise other events don't actually fire
    }
  }
}

The Vue library adds an additional option, sockets that you can add to each component to get access to the socket events that can be triggered. The alternative to this would be:

this.$socket.on('PlayerJoined', (data) => { });
// or
Vue.prototype.$socket.on('PlayerJoined' (data) => { });

One caveat I learned here (a very important one) is that you’ll need to define whatever socket messages you’ll be listening for in this file. They can be blank as mine is above, but if they aren’t there, none of the components seem to get the messages. The names of these messages match the names of the methods defined in your Hub.

Now that the setup is out of the way, we can start listening to socket messages for the events we care about and update our UI accordingly. This is fairly straightforward. You can use either syntax I noted above to do this; I’ll show the one I’m primarily using in my app:

// additional component code redacted for brevity
created () {
    const that = this

    this.$socket.on('PlayerJoined', function (data) {
        that.$store.dispatch('game/playerJoined', { playerName: data })
    })
},

Here, we are listening for the PlayerJoined message and then dispatch an action to our Vuex store with the player’s name. We could similarly directly update our UI instead of dispatching an action.

Sending messages (or “invoking”) is similar to receiving, just a slightly different syntax. In the following example, I am redirecting the user to a new page after the invocation is successful. You could easily submit a follow-up request or prompt the user for more information. Ideally, if you are using Vuex, you might want to invoke the request as an action on your store, instead of directly from the component.

// additional component code redacted for brevity
methods: {
    joinGame () { // triggered by a form submit
        this.$socket.invoke('PlayerJoined', data.gameCode, data.playerName).then(() => {
            this.$router.push(`/game/${this.gameCode}`)
        })
    }
}

Summary

And there you have it; you can now enhance your Vue app’s connectivity by adding SignalR WebSockets. I am currently trying to figure out how I can better integrate the sockets with my Vuex store, so that I don’t have to listen for messages in components just to call an action on a store. Do you use SignalR or other sockets in your Vue application? Have any tips for things we could improve here or better integrate with Vuex? Leave a comment below!

Header photo by Carlos Muza on Unsplash

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358

推荐阅读更多精彩内容