loading...
Cover image for Nuxt-Socket.IO: How Namespaces Config May Make Your Life Insanely Easier

Nuxt-Socket.IO: How Namespaces Config May Make Your Life Insanely Easier

richardeschloss profile image Richard Schloss ・7 min read

TL; DR -- This Christmas present has arrived a little late, but I think users of the nuxt-socket-io will like it, once it's fully understood. Now, it is possible to configure socket.IO emitters, listeners, and "emitbacks" directly in nuxt.config for specified namespaces. This article describes how to take advantage of the new feature.

Disclaimer: I am the author of the nuxt-socket-io module. I present here a new kind of syntax which may take some getting used to, but I think it's a syntax that makes sense. Approach it with an open mind.


Prerequisites:

  1. Introduction to Nuxt Socket.IO - This describes the Nuxt Socket.IO and basic setup.

Introduction:

Socket.IO is the real-time engine for the web, and the Nuxt-Socket.IO module is the module that makes it clean and straightforward to use inside your Nuxt application. A lot of times, it is desired to instantiate socket.IO client inside of components, and then restrict the scope of communication to only what those components should and would care about.

For example, a chat room component might only want to talk to a chat room service, and a room's channel might only want to talk to that channel's service. The messages that get sent in a specific channel shouldn't leak out to the rest of the room. Whether or not we refer to them as "channels" or "rooms", the word "namespaces" seems to be most fitting for each case. Plus, whereas "rooms" and "channels" confine our thinking to that of "just a chat application", the term "namespaces", on the other hand, is universal and allows to think of scoped communication for all of our web applications.

In addition to scoping a component's IO to that of a given namespace, it is often desirable to also signal a disconnect (i.e., close the socket) when the component is destroyed. While it is good practice for developers to do their own cleanup steps, such practice can either be cumbersome or easy to forget for every component in a given application.

Therefore, with the aforementioned in mind, the nuxt-socket-io plugin and namespace config feature had the following goals:

  • The plugin had to allow namespaces to be configured in nuxt.config for each socket.
  • The plugin had to support the configuration of emitters, listeners, and "emitbacks" at the page- and component-level.
  • The configuration had to be as straightforward as that for vuex options (listeners and emitbacks), for users who already configured the module.
  • The new feature had to support a new and, arguably, a more intuitive arrow (-->) syntax. This makes IO config easier to version control, and share with stakeholders (non-devs), if needed.
  • The configuration had to allow support for hooks to be run before and after the specified IO events.
  • The plugin had to automatically disconnect the socket before the component is destroyed, by default (overwriting this is possible with the teardown: false option passed to this.$nuxtSocket).
  • The plugin had to make life easier for application developers by encouraging them to write less, but more consistent code.
  • The plugin still had to expose the socket.io client instance in case the developer needed to access the client API methods directly.

Today, it is now possible to configure namespaces in nuxt.config. Each socket set can have its own configuration of namespaces and each namespace can now have emitters, listeners, and emitbacks. The configuration supports an arrow syntax in each entry to help describe the flow (with pre/post hook designation support too).

Configuring Namespaces

This section describes how to configure emitters, listeners and emitbacks for each namespace. The general syntax is:

preHook] body [postHook

Sometimes the body includes a "+", "<--" or a "-->". While the use and placement of the characters "]", "[", "+", "<--", and "-->" is strict, the names you use for hooks, events, and props are entirely up to you.

The specific syntax is as follows:

  • Emitters:

preEmit hook] componentMethod + msg --> componentProp [postRx hook

** How to think about this: the IO event is triggered by componentMethod, and the event "componentMethod" gets sent with "msg" (defined in the component as this.msg). When the server responds, the response goes to component's prop componentProp. preEmit hook is run before the event is sent, and postRx hook is run after data is received. A nice thing here is the plugin creates the componentMethod for you so you don't have to. Just call it and it will work.

→ The preEmit and postRx hooks are optional, but if using them, the "]" and "[" characters are needed so the plugin can parse them. These hooks would be defined in the component's methods section like this.preEmit and this.postRx
→ The msg is optional, but if using, must use the '+' character
→ The componentMethod is auto-created by the plugin and sends the event with the same name. If the componentMethod is named "getMessage" it sends the event "getMessage"
→ The componentProp is optional, but if entered, will be the property that will get set with the response, if a response comes back. This is optional too, and needs to be initially defined on the component, otherwise it won't get set. Vuejs will also complain if you try to render undefined props. If componentProp is omitted from the entry, the arrow "-->" can also be omitted.

  • Listeners:

preHook] listenEvent --> componentProp [postRx hook

** How to think about this: when event "listenEvent" is received, the prop this.componentPropwill be set to that event's data. preHook will run when data is received, but before setting componentProp. postRx hook will run after setting the componentProp.

→ Both preHook and postRx hooks are optional. Here, preHook is called when data is received, but before setting componentProp. postRx hook is called after setting the prop. this.preHook and this.postRx would need to be defined in the component's methods section if planning to use either.
→ If using the arrow syntax, when listenEvent is received, componentProp will get set with that event's data. If only the listenEvent is entered, then the plugin will try to set a property on the component of the same name. I.e., if listenEvent is "progressRxd", then the plugin will try to set this.progressRxd on the component.
→ Important NOTE: This syntax can now also work on the Vuex options for mutations and actions, which are also set up as listeners.

  • Emitbacks:

preEmitHook] emitEvt <-- watchProp [postAck hook

** How to think about this: When a component's property watchProp changes, emit back the event "emitEvt". preEmitHook will run before the data is emitted, and postAck will run after the server acknowledges its event, if any.

preEmitHook and postAck hooks are optional. preEmitHook runs before emitting the event, postAck hook runs after receiving the acknolwedgement, if any. this.preEmitHook and this.postAck would need to be defined in the component's methods if planning to use.
watchProp is the property on the component to watch using "myObj.child.grandchild" syntax. Just like you would on the component.
emitEvt is the event name to emit back to the server when the watchProp changes. If watchProp and the arrow "<--" are omitted, then emitEvt will double as the watchProp.
→ Important NOTE: this syntax can now also work in the Vuex options for emitbacks, with ONE important difference. In Vuex (and Nuxt, specifically), the watch property path may require forward slashes "/". For example, if your stores folder has an "examples.js" file in it, with state properties "sample" and "sample2", watchProp would have to be specified as "examples/sample" and "examples/sample2". The exception to the rule is "index.js" which is treated as the stores root. I.e., "sample" in index.js would be referred to simply as "sample" and not "index/sample")


Example Configuration

Consider the following configuration as an example:
In nuxt.config.js:

namespaces: {
  '/index': {
    emitters: ['getMessage2 + testMsg --> message2Rxd'],
    listeners: ['chatMessage2', 'chatMessage3 --> message3Rxd']
  },
  '/examples': {
    emitBacks: ['sample3', 'sample4 <-- myObj.sample4'],
    emitters: [
      'reset] getProgress + refreshInfo --> progress [handleDone'
    ],
    listeners: ['progress']
  }
}

1) First, let's analyze the /index config.

  • Emitters:
    When getMessage() is called, the event "getMessage" will be sent with component's data this.testMsg. this.testMsg should be defined on the component, but if it isn't no message will get sent (the plugin will warn when the component data is not defined). When a response is received, this.messageRxd on the component will get set to that response.

  • Listeners:
    When chatMessage2 is received, this.chatMessage2 on the component will be set. When chatMessage3 is received, the mapped property this.message3Rxd will be set.

2) Let's analyze the /examples config.

  • Emitbacks:
    When this.sample3 changes in the component, the event sample3 will be emitted back to the server. When this.myObj.sample4 changes in the component, the mapped event sample4 will be emitted back.

  • Emitters:
    When this.getProgress() is called, first this.reset() will be called (if it's defined) and then the event "getProgress" will be emitted with the message this.refreshInfo. When the response is received, this.progress will get set to the response, and then this.handleDone() will be called (if it's defined)

  • Listeners:
    When event "progress" is received, this.progress will get set to that data.

Chat rooms: a more exciting example

Want to see a slightly more exciting example? Check out my [very basic] chat rooms example!

  1. Clone my git repo: https://github.com/richardeschloss/nuxt-socket-io
  2. Run the server with npm run dev:server
  3. Go to the chat rooms page at: https://localhost:3000/rooms and have fun! (open two browser windows because...you need at least two clients to have a conversation; you can also chat with yourself, if you want :))

My next article in this series should help explain that example.

Conclusion

This was a rather lengthy, but important, discussion of nuxt-socket-io's new namespace configuration feature. While the time spent reading this article may have been longer than desired, it is very possible that the time spent on your future web applications will be significantly reduced by taking advantage of what you learned here. It may be a bumpy road at first, but I think over time, you'll get used to the syntax. If you hate it, no worries...the plugin will still expose the socket.io client API directly, so your fingertips will have that if needed.

Credits / Acknowledgement

Thanks to Ofoe Apronti @OfoeApronti for opening the issue and asking about it. At first, I didn't think there was a clean way to create this feature, and gave up on it early. But, with more thought put into it, I think the solution may end up satisfying most users (fingers crossed).

Posted on by:

richardeschloss profile

Richard Schloss

@richardeschloss

My goal is to be efficient and effective (#EnE) by writing less code that accomplishes more. Wannabe minimalist.

Discussion

markdown guide
 

How would I register a new websocket to a namespace if I don't know the name of the namespace when I build the app? The namespace is variable and I would get it from the server (I control the server).

For instance, I connect via websocket to server, then later the server tells me what namespace would be suitable for the client.

 

I've been wondering the same thing. I have a "dynamic" and "evolving" API feature I've been thinking about and wanting to implement! Currently the namespace config works for namespaces known ahead of time, but I would like to get dynamic namespaces working somehow. I hope I can help in the future! (I'm sorry the feature isn't there yet :/ )

 

It's okay. This seems like a great tool. And frankly, it turned out that it would be better to source handling of namespaces and rooms on the server side, so your current implementation is arranged perfectly for me. Thank you for writing this! It does make it simpler to manage reactivity by a lot, in my opinion.

I fully agree. I was thinking that maybe the server would already have an API it supports, perhaps like:

const api = Object.freeze({
  version: 1.14,
  addUser() { ... },
  sendMsg() { ... }
})

And then maybe a specific set of events can be reserved for the dynamic api use, so that the client listen for "serverApi" event, and the server may handle it like:

io.on('connection', (socket) => {
  socket.emit('serverApi', { api: Object.keys(api), version: api.version })
})

And then maybe when the client receives that api, dynamically configure the namespace emitters. And that might be pretty cool, because that could be a lot less manual config for the developer to do.