update the online visitors and vuejs +iris mvc todo app (this gave me some ideas to make the api a bit easier)

Former-commit-id: 8c84486a22505b7137669bde52383d2564a6b382
This commit is contained in:
Gerasimos (Makis) Maropoulos 2019-07-15 18:45:22 +03:00
parent 55bdb44e26
commit 1c2472c53f
9 changed files with 159 additions and 131 deletions

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"sync/atomic" "sync/atomic"
"github.com/kataras/iris" "github.com/kataras/iris"
@ -8,6 +9,13 @@ import (
"github.com/kataras/iris/websocket" "github.com/kataras/iris/websocket"
) )
var events = websocket.Namespaces{
"default": websocket.Events{
websocket.OnRoomJoined: onRoomJoined,
websocket.OnRoomLeft: onRoomLeft,
},
}
func main() { func main() {
// init the web application instance // init the web application instance
// app := iris.New() // app := iris.New()
@ -16,11 +24,9 @@ func main() {
// load templates // load templates
app.RegisterView(iris.HTML("./templates", ".html").Reload(true)) app.RegisterView(iris.HTML("./templates", ".html").Reload(true))
// setup the websocket server // setup the websocket server
ws := websocket.New(websocket.Config{}) ws := websocket.New(websocket.DefaultGorillaUpgrader, events)
ws.OnConnection(HandleWebsocketConnection)
app.Get("/my_endpoint", ws.Handler()) app.Get("/my_endpoint", websocket.Handler(ws))
app.Any("/iris-ws.js", websocket.ClientHandler())
// register static assets request path and system directory // register static assets request path and system directory
app.HandleDir("/js", "./static/assets/js") app.HandleDir("/js", "./static/assets/js")
@ -114,48 +120,54 @@ func (v *pageViews) Reset() {
var v pageViews var v pageViews
// HandleWebsocketConnection handles the online viewers per example(gist source) func viewsCountBytes(viewsCount uint64) []byte {
func HandleWebsocketConnection(c websocket.Connection) { // * there are other methods to convert uint64 to []byte
return []byte(fmt.Sprintf("%d", viewsCount))
c.On("watch", func(pageSource string) { }
v.Add(pageSource)
// join the socket to a room linked with the page source func onRoomJoined(ns *websocket.NSConn, msg websocket.Message) error {
c.Join(pageSource) // the roomName here is the source.
pageSource := string(msg.Room)
viewsCount := v.Get(pageSource).getCount()
if viewsCount == 0 { v.Add(pageSource)
viewsCount++ // count should be always > 0 here
} viewsCount := v.Get(pageSource).getCount()
c.To(pageSource).Emit("watch", viewsCount) if viewsCount == 0 {
}) viewsCount++ // count should be always > 0 here
}
c.OnLeave(func(roomName string) {
if roomName != c.ID() { // if the roomName it's not the connection iself // fire the "onNewVisit" client event
// the roomName here is the source, this is the only room(except the connection's ID room) which we join the users to. // on each connection joined to this room (source page)
pageV := v.Get(roomName) // and notify of the new visit,
if pageV == nil { // including this connection (see nil on first input arg).
return // for any case that this room is not a pageView source ns.Conn.Server().Broadcast(nil, websocket.Message{
} Namespace: msg.Namespace,
// decrement -1 the specific counter for this page source. Room: pageSource,
pageV.decrement() Event: "onNewVisit", // fire the "onNewVisit" client event.
// 1. open 30 tabs. Body: viewsCountBytes(viewsCount),
// 2. close the browser. })
// 3. re-open the browser
// 4. should be v.getCount() = 1 return nil
// in order to achieve the previous flow we should decrement exactly when the user disconnects }
// but emit the result a little after, on a goroutine
// getting all connections within this room and emit the online views one by one. func onRoomLeft(ns *websocket.NSConn, msg websocket.Message) error {
// note: // the roomName here is the source.
// we can also add a time.Sleep(2-3 seconds) inside the goroutine at the future if we don't need 'real-time' updates. pageV := v.Get(msg.Room)
go func(currentConnID string) { if pageV == nil {
for _, conn := range c.Server().GetConnectionsByRoom(roomName) { return nil // for any case that this room is not a pageView source
if conn.ID() != currentConnID { }
conn.Emit("watch", pageV.getCount()) // decrement -1 the specific counter for this page source.
} pageV.decrement()
} // fire the "onNewVisit" client event
}(c.ID()) // on each connection joined to this room (source page)
} // and notify of the new, decremented by one, visits count.
ns.Conn.Server().Broadcast(nil, websocket.Message{
}) Namespace: msg.Namespace,
Room: msg.Room,
Event: "onNewVisit",
Body: viewsCountBytes(pageV.getCount()),
})
return nil
} }

View File

@ -1,21 +1,24 @@
(function() { (function () {
var socket = new Ws("ws://localhost:8080/my_endpoint"); var events = {
default: {
socket.OnConnect(function () { _OnNamespaceConnected: function (ns, msg) {
socket.Emit("watch", PAGE_SOURCE); ns.joinRoom(PAGE_SOURCE);
}); },
_OnNamespaceDisconnect: function (ns, msg) {
document.getElementById("online_views").innerHTML = "you've been disconnected";
socket.On("watch", function (onlineViews) { },
var text = "1 online view"; onNewVisit: function (ns, msg) {
if (onlineViews > 1) { var text = "1 online view";
var onlineViews = Number(msg.Body);
if (onlineViews > 1) {
text = onlineViews + " online views"; text = onlineViews + " online views";
}
document.getElementById("online_views").innerHTML = text;
} }
document.getElementById("online_views").innerHTML = text; }
}); };
socket.OnDisconnect(function () { neffos.dial("ws://localhost:8080/my_endpoint", events).then(function (client) {
document.getElementById("online_views").innerHTML = "you've been disconnected"; client.connect("default");
}); });
})(); })();

View File

@ -30,14 +30,14 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
/* take the page source from our passed struct on .Render */ /* take the page source from our passed struct on .Render */
var PAGE_SOURCE = {{ .PageID }} var PAGE_SOURCE = {{ .PageID }}
</script> </script>
<script src="/iris-ws.js"></script> <script src="https://cdn.jsdelivr.net/npm/neffos.js@latest/dist/neffos.min.js"></script>
<script src="/js/visitors.js"></script> <script src="/js/visitors.js"></script>
</body> </body>
</html> </html>

View File

@ -16,14 +16,14 @@
<script type="text/javascript"> <script type="text/javascript">
/* take the page source from our passed struct on .Render */ /* take the page source from our passed struct on .Render */
var PAGE_SOURCE = {{ .PageID }} var PAGE_SOURCE = {{ .PageID }}
</script> </script>
<script src="/iris-ws.js"></script> <script src="https://cdn.jsdelivr.net/npm/neffos.js@latest/dist/neffos.min.js"></script>
<script src="/js/visitors.js"></script> <script src="/js/visitors.js"></script>
</body> </body>
</html> </html>

View File

@ -14,6 +14,8 @@ type TodoController struct {
Service todo.Service Service todo.Service
Session *sessions.Session Session *sessions.Session
NS *websocket.NSConn
} }
// BeforeActivation called once before the server ran, and before // BeforeActivation called once before the server ran, and before
@ -51,15 +53,14 @@ func (c *TodoController) Post(newItems []todo.Item) PostItemResponse {
return PostItemResponse{Success: true} return PostItemResponse{Success: true}
} }
func (c *TodoController) GetSync(conn websocket.Connection) { func (c *TodoController) Save(msg websocket.Message) error {
// join to the session in order to send "saved" id := c.Session.ID()
// events only to a single user, that means c.NS.Conn.Server().Broadcast(nil, websocket.Message{
// that if user has opened more than one browser window/tab Namespace: msg.Namespace,
// of the same session then the changes will be reflected to one another. Event: "saved",
conn.Join(c.Session.ID()) To: id,
conn.On("save", func() { // "save" event from client. Body: websocket.Marshal(c.Service.Get(id)),
conn.To(c.Session.ID()).Emit("saved", nil) // fire a "saved" event to the rest of the clients w.
}) })
conn.Wait() return nil
} }

View File

@ -1,14 +1,15 @@
package main package main
import ( import (
"strings"
"github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo"
"github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/web/controllers" "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/web/controllers"
"github.com/kataras/iris" "github.com/kataras/iris"
"github.com/kataras/iris/mvc"
"github.com/kataras/iris/sessions" "github.com/kataras/iris/sessions"
"github.com/kataras/iris/websocket" "github.com/kataras/iris/websocket"
"github.com/kataras/iris/mvc"
) )
func main() { func main() {
@ -26,16 +27,8 @@ func main() {
Cookie: "iris_session", Cookie: "iris_session",
}) })
// configure the websocket server. // create a sub router and register the http controllers.
ws := websocket.New(websocket.Config{})
// create a sub router and register the client-side library for the iris websockets,
// you could skip it but iris websockets supports socket.io-like API.
todosRouter := app.Party("/todos") todosRouter := app.Party("/todos")
// http://localhost:8080/todos/iris-ws.js
// serve the javascript client library to communicate with
// the iris high level websocket event system.
todosRouter.Any("/iris-ws.js", websocket.ClientHandler())
// create our mvc application targeted to /todos relative sub path. // create our mvc application targeted to /todos relative sub path.
todosApp := mvc.New(todosRouter) todosApp := mvc.New(todosRouter)
@ -44,11 +37,27 @@ func main() {
todosApp.Register( todosApp.Register(
todo.NewMemoryService(), todo.NewMemoryService(),
sess.Start, sess.Start,
ws.Upgrade,
) )
todosController := new(controllers.TodoController)
// controllers registration here... // controllers registration here...
todosApp.Handle(new(controllers.TodoController)) todosApp.Handle(todosController)
// Create a sub mvc app for websocket controller.
// Inherit the parent's dependencies.
todosWebsocketApp := todosApp.Party("/sync")
todosWebsocketApp.HandleWebsocket(todosController).
SetNamespace("todos").
SetEventMatcher(func(methodName string) (string, bool) {
return strings.ToLower(methodName), true
})
websocketServer := websocket.New(websocket.DefaultGorillaUpgrader, todosWebsocketApp)
idGenerator := func(ctx iris.Context) string {
id := sess.Start(ctx).ID()
return id
}
todosWebsocketApp.Router.Get("/", websocket.Handler(websocketServer, idGenerator))
// start the web server at http://localhost:8080 // start the web server at http://localhost:8080
app.Run(iris.Addr(":8080")) app.Run(iris.Addr(":8080"))

View File

@ -11,8 +11,7 @@
<script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- --> <!-- -->
<script src="https://unpkg.com/director@1.2.8/build/director.js"></script> <script src="https://unpkg.com/director@1.2.8/build/director.js"></script>
<!-- websocket sync between multiple tabs --> <script src="https://cdn.jsdelivr.net/npm/neffos.js@latest/dist/neffos.min.js"></script>
<script src="/todos/iris-ws.js"></script>
<!-- --> <!-- -->
<style> <style>
[v-cloak] { [v-cloak] {

View File

@ -1,15 +1,24 @@
// Full spec-compliant TodoMVC with Iris // Full spec-compliant TodoMVC with Iris
// and hash-based routing in ~200 effective lines of JavaScript. // and hash-based routing in ~200 effective lines of JavaScript.
var socket = new Ws("ws://localhost:8080/todos/sync"); var ws;
socket.On("saved", function () { ((async () => {
// console.log("receive: on saved"); const events = {
fetchTodos(function (items) { todos: {
app.todos = items saved: function (ns, msg) {
}); app.todos = msg.unmarshal()
}); // or make a new http fetch
// fetchTodos(function (items) {
// app.todos = msg.unmarshal()
// });
}
}
};
const conn = await neffos.dial("ws://localhost:8080/todos/sync", events);
ws = await conn.connect("todos");
})()).catch(console.error);
function fetchTodos(onComplete) { function fetchTodos(onComplete) {
axios.get("/todos").then(response => { axios.get("/todos").then(response => {
@ -38,7 +47,7 @@ var todoStorage = {
return; return;
} }
// console.log("send: save"); // console.log("send: save");
socket.Emit("save") ws.emit("save")
}); });
} }
} }
@ -202,4 +211,4 @@ window.addEventListener('hashchange', onHashChange)
onHashChange() onHashChange()
// mount // mount
app.$mount('.todoapp') app.$mount('.todoapp');

View File

@ -41,10 +41,11 @@ var (
// //
// See `mvc#New` for more. // See `mvc#New` for more.
type Application struct { type Application struct {
Dependencies di.Values Dependencies di.Values
Router router.Party Router router.Party
Controllers []*ControllerActivator Controllers []*ControllerActivator
ErrorHandler hero.ErrorHandler websocketControllers []websocket.ConnHandler
ErrorHandler hero.ErrorHandler
} }
func newApp(subRouter router.Party, values di.Values) *Application { func newApp(subRouter router.Party, values di.Values) *Application {
@ -185,9 +186,23 @@ func (app *Application) Handle(controller interface{}) *Application {
// Note that a websocket controller is registered and ran under a specific connection connected to a namespace // Note that a websocket controller is registered and ran under a specific connection connected to a namespace
// and it cannot send HTTP responses on that state. // and it cannot send HTTP responses on that state.
// However all static and dynamic dependency injection features are working, as expected, like any regular MVC Controller. // However all static and dynamic dependency injection features are working, as expected, like any regular MVC Controller.
func (app *Application) HandleWebsocket(controller interface{}) { func (app *Application) HandleWebsocket(controller interface{}) *websocket.Struct {
c := app.handle(controller) c := app.handle(controller)
c.markAsWebsocket() c.markAsWebsocket()
websocketController := websocket.NewStruct(c.Value).SetInjector(makeInjector(c.injector))
app.websocketControllers = append(app.websocketControllers, websocketController)
return websocketController
}
func makeInjector(injector *di.StructInjector) websocket.StructInjector {
return func(_ reflect.Type, nsConn *websocket.NSConn) reflect.Value {
v := injector.Acquire()
if injector.CanInject {
injector.InjectElem(v.Elem(), reflect.ValueOf(websocket.GetContext(nsConn.Conn)))
}
return v
}
} }
var _ websocket.ConnHandler = (*Application)(nil) var _ websocket.ConnHandler = (*Application)(nil)
@ -200,27 +215,7 @@ func (app *Application) GetNamespaces() websocket.Namespaces {
websocket.EnableDebug(golog.Default) websocket.EnableDebug(golog.Default)
} }
makeInjector := func(injector *di.StructInjector) websocket.StructInjector { return websocket.JoinConnHandlers(app.websocketControllers...).GetNamespaces()
return func(_ reflect.Type, nsConn *websocket.NSConn) reflect.Value {
v := injector.Acquire()
if injector.CanInject {
injector.InjectElem(v.Elem(), reflect.ValueOf(websocket.GetContext(nsConn.Conn)))
}
return v
}
}
var websocketControllers []websocket.ConnHandler
for _, c := range app.Controllers {
if c.servesWebsocket {
wsInjector := makeInjector(c.injector)
s := websocket.NewStruct(c.Value).SetInjector(wsInjector)
websocketControllers = append(websocketControllers, s)
}
}
return websocket.JoinConnHandlers(websocketControllers...).GetNamespaces()
} }
func (app *Application) handle(controller interface{}) *ControllerActivator { func (app *Application) handle(controller interface{}) *ControllerActivator {