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:
- 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 tothis.$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.componentProp
will 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:
WhengetMessage()
is called, the event "getMessage" will be sent with component's datathis.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:
WhenchatMessage2
is received,this.chatMessage2
on the component will be set. WhenchatMessage3
is received, the mapped propertythis.message3Rxd
will be set.
2) Let's analyze the /examples
config.
Emitbacks:
Whenthis.sample3
changes in the component, the eventsample3
will be emitted back to the server. Whenthis.myObj.sample4
changes in the component, the mapped eventsample4
will be emitted back.Emitters:
Whenthis.getProgress()
is called, firstthis.reset()
will be called (if it's defined) and then the event "getProgress" will be emitted with the messagethis.refreshInfo
. When the response is received,this.progress
will get set to the response, and thenthis.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!
- Clone my git repo: https://github.com/richardeschloss/nuxt-socket-io
- Run the server with
npm run dev:server
- 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).
Top comments (4)
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:
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:
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.