1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-20 03:17:04 +00:00

reorganization of _examples and add some new examples such as iris+groupcache+mysql+docker

Former-commit-id: ed635ee95de7160cde11eaabc0c1dcb0e460a620
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-06-07 15:26:06 +03:00
parent 9fdcb4c7fb
commit ed45c77be5
328 changed files with 4262 additions and 41621 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,56 @@
package main
import (
"time"
"github.com/kataras/iris/v12/_examples/mvc/login-mvc-single-responsibility/user"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"github.com/kataras/iris/v12/sessions"
)
func main() {
app := iris.New()
// You got full debug messages, useful when using MVC and you want to make
// sure that your code is aligned with the Iris' MVC Architecture.
app.Logger().SetLevel("debug")
app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html"))
app.HandleDir("/public", "./public")
userRouter := app.Party("/user")
{
manager := sessions.New(sessions.Config{
Cookie: "sessioncookiename",
Expires: 24 * time.Hour,
})
userRouter.Use(manager.Handler())
mvc.Configure(userRouter, configureUserMVC)
}
// http://localhost:8080/user/register
// http://localhost:8080/user/login
// http://localhost:8080/user/me
// http://localhost:8080/user/logout
// http://localhost:8080/user/1
app.Listen(":8080", configure)
}
func configureUserMVC(userApp *mvc.Application) {
userApp.Register(
user.NewDataSource(),
)
userApp.Handle(new(user.Controller))
}
func configure(app *iris.Application) {
app.Configure(
iris.WithOptimizations,
iris.WithFireMethodNotAllowed,
iris.WithLowercaseRouting,
iris.WithPathIntelligence,
iris.WithTunneling,
)
}

View File

@@ -0,0 +1,61 @@
/* Bordered form */
form {
border: 3px solid #f1f1f1;
}
/* Full-width inputs */
input[type=text], input[type=password] {
width: 100%;
padding: 12px 20px;
margin: 8px 0;
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
}
/* Set a style for all buttons */
button {
background-color: #4CAF50;
color: white;
padding: 14px 20px;
margin: 8px 0;
border: none;
cursor: pointer;
width: 100%;
}
/* Add a hover effect for buttons */
button:hover {
opacity: 0.8;
}
/* Extra style for the cancel button (red) */
.cancelbtn {
width: auto;
padding: 10px 18px;
background-color: #f44336;
}
/* Center the container */
/* Add padding to containers */
.container {
padding: 16px;
}
/* The "Forgot password" text */
span.psw {
float: right;
padding-top: 16px;
}
/* Change styles for span and cancel button on extra small screens */
@media screen and (max-width: 300px) {
span.psw {
display: block;
float: none;
}
.cancelbtn {
width: 100%;
}
}

View File

@@ -0,0 +1,110 @@
package user
import (
"errors"
"strconv"
"strings"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"github.com/kataras/iris/v12/sessions"
)
const sessionIDKey = "UserID"
// paths
var (
PathLogin = mvc.Response{Path: "/user/login"}
PathLogout = mvc.Response{Path: "/user/logout"}
)
// AuthController is the user authentication controller, a custom shared controller.
type AuthController struct {
// context is auto-binded if struct depends on this,
// in this controller we don't we do everything with mvc-style,
// and that's neither the 30% of its features.
// Ctx iris.Context
Source *DataSource
Session *sessions.Session
// the whole controller is request-scoped because we already depend on Session, so
// this will be new for each new incoming request, BeginRequest sets that based on the session.
UserID int64
}
// BeginRequest saves login state to the context, the user id.
func (c *AuthController) BeginRequest(ctx iris.Context) {
c.UserID, _ = c.Session.GetInt64(sessionIDKey)
}
// EndRequest is here just to complete the BaseController
// in order to be tell iris to call the `BeginRequest` before the main method.
func (c *AuthController) EndRequest(ctx iris.Context) {}
func (c *AuthController) fireError(err error) mvc.View {
return mvc.View{
Code: iris.StatusBadRequest,
Name: "shared/error.html",
Data: iris.Map{"Title": "User Error", "Message": strings.ToUpper(err.Error())},
}
}
func (c *AuthController) redirectTo(id int64) mvc.Response {
return mvc.Response{Path: "/user/" + strconv.Itoa(int(id))}
}
func (c *AuthController) createOrUpdate(firstname, username, password string) (user Model, err error) {
username = strings.Trim(username, " ")
if username == "" || password == "" || firstname == "" {
return user, errors.New("empty firstname, username or/and password")
}
userToInsert := Model{
Firstname: firstname,
Username: username,
password: password,
} // password is hashed by the Source.
newUser, err := c.Source.InsertOrUpdate(userToInsert)
if err != nil {
return user, err
}
return newUser, nil
}
func (c *AuthController) isLoggedIn() bool {
// we don't search by session, we have the user id
// already by the `BeginRequest` middleware.
return c.UserID > 0
}
func (c *AuthController) verify(username, password string) (user Model, err error) {
if username == "" || password == "" {
return user, errors.New("please fill both username and password fields")
}
u, found := c.Source.GetByUsername(username)
if !found {
// if user found with that username not found at all.
return user, errors.New("user with that username does not exist")
}
if ok, err := ValidatePassword(password, u.HashedPassword); err != nil || !ok {
// if user found but an error occurred or the password is not valid.
return user, errors.New("please try to login with valid credentials")
}
return u, nil
}
// if logged in then destroy the session
// and redirect to the login page
// otherwise redirect to the registration page.
func (c *AuthController) logout() mvc.Response {
if c.isLoggedIn() {
c.Session.Destroy()
}
return PathLogin
}

View File

@@ -0,0 +1,189 @@
package user
import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
)
var (
// About Code: iris.StatusSeeOther ->
// When redirecting from POST to GET request you -should- use this HTTP status code,
// however there're some (complicated) alternatives if you
// search online or even the HTTP RFC.
// "See Other" RFC 7231
pathMyProfile = mvc.Response{Path: "/user/me", Code: iris.StatusSeeOther}
pathRegister = mvc.Response{Path: "/user/register"}
)
// Controller is responsible to handle the following requests:
// GET /user/register
// POST /user/register
// GET /user/login
// POST /user/login
// GET /user/me
// GET /user/{id:int64}
// All HTTP Methods /user/logout
type Controller struct {
AuthController
}
type formValue func(string) string
// BeforeActivation called once before the server start
// and before the controller's registration, here you can add
// dependencies, to this controller and only, that the main caller may skip.
func (c *Controller) BeforeActivation(b mvc.BeforeActivation) {
// bind the context's `FormValue` as well in order to be
// acceptable on the controller or its methods' input arguments (NEW feature as well).
b.Dependencies().Register(func(ctx iris.Context) formValue { return ctx.FormValue })
}
type page struct {
Title string
}
// GetRegister handles GET:/user/register.
// mvc.Result can accept any struct which contains a `Dispatch(ctx iris.Context)` method.
// Both mvc.Response and mvc.View are mvc.Result.
func (c *Controller) GetRegister() mvc.Result {
if c.isLoggedIn() {
return c.logout()
}
// You could just use it as a variable to win some time in serve-time,
// this is an exersise for you :)
return mvc.View{
Name: pathRegister.Path + ".html",
Data: page{"User Registration"},
}
}
// PostRegister handles POST:/user/register.
func (c *Controller) PostRegister(form formValue) mvc.Result {
// we can either use the `c.Ctx.ReadForm` or read values one by one.
var (
firstname = form("firstname")
username = form("username")
password = form("password")
)
user, err := c.createOrUpdate(firstname, username, password)
if err != nil {
return c.fireError(err)
}
// setting a session value was never easier.
c.Session.Set(sessionIDKey, user.ID)
// succeed, nothing more to do here, just redirect to the /user/me.
return pathMyProfile
}
// with these static views,
// you can use variables-- that are initialized before server start
// so you can win some time on serving.
// You can do it else where as well but I let them as pracise for you,
// essentially you can understand by just looking below.
var userLoginView = mvc.View{
Name: PathLogin.Path + ".html",
Data: page{"User Login"},
}
// GetLogin handles GET:/user/login.
func (c *Controller) GetLogin() mvc.Result {
if c.isLoggedIn() {
return c.logout()
}
return userLoginView
}
// PostLogin handles POST:/user/login.
func (c *Controller) PostLogin(form formValue) mvc.Result {
var (
username = form("username")
password = form("password")
)
user, err := c.verify(username, password)
if err != nil {
return c.fireError(err)
}
c.Session.Set(sessionIDKey, user.ID)
return pathMyProfile
}
// AnyLogout handles any method on path /user/logout.
func (c *Controller) AnyLogout() {
c.logout()
}
// GetMe handles GET:/user/me.
func (c *Controller) GetMe() mvc.Result {
id, err := c.Session.GetInt64(sessionIDKey)
if err != nil || id <= 0 {
// when not already logged in, redirect to login.
return PathLogin
}
u, found := c.Source.GetByID(id)
if !found {
// if the session exists but for some reason the user doesn't exist in the "database"
// then logout him and redirect to the register page.
return c.logout()
}
// set the model and render the view template.
return mvc.View{
Name: pathMyProfile.Path + ".html",
Data: iris.Map{
"Title": "Profile of " + u.Username,
"User": u,
},
}
}
func (c *Controller) renderNotFound(id int64) mvc.View {
return mvc.View{
Code: iris.StatusNotFound,
Name: "user/notfound.html",
Data: iris.Map{
"Title": "User Not Found",
"ID": id,
},
}
}
// Dispatch completes the `mvc.Result` interface
// in order to be able to return a type of `Model`
// as mvc.Result.
// If this function didn't exist then
// we should explicit set the output result to that Model or to an interface{}.
func (u Model) Dispatch(ctx iris.Context) {
ctx.JSON(u)
}
// GetBy handles GET:/user/{id:int64},
// i.e http://localhost:8080/user/1
func (c *Controller) GetBy(userID int64) mvc.Result {
// we have /user/{id}
// fetch and render user json.
user, found := c.Source.GetByID(userID)
if !found {
// not user found with that ID.
return c.renderNotFound(userID)
}
// Q: how the hell Model can be return as mvc.Result?
// A: I told you before on some comments and the docs,
// any struct that has a `Dispatch(ctx iris.Context)`
// can be returned as an mvc.Result(see ~20 lines above),
// therefore we are able to combine many type of results in the same method.
// For example, here, we return either an mvc.View to render a not found custom template
// either a user which returns the Model as JSON via its Dispatch.
//
// We could also return just a struct value that is not an mvc.Result,
// if the output result of the `GetBy` was that struct's type or an interface{}
// and iris would render that with JSON as well, but here we can't do that without complete the `Dispatch`
// function, because we may return an mvc.View which is an mvc.Result.
return user
}

View File

@@ -0,0 +1,114 @@
package user
import (
"errors"
"sync"
"time"
)
// IDGenerator would be our user ID generator
// but here we keep the order of users by their IDs
// so we will use numbers that can be easly written
// to the browser to get results back from the REST API.
// var IDGenerator = func() string {
// return uuid.NewV4().String()
// }
// DataSource is our data store example.
type DataSource struct {
Users map[int64]Model
mu sync.RWMutex
}
// NewDataSource returns a new user data source.
func NewDataSource() *DataSource {
return &DataSource{
Users: make(map[int64]Model),
}
}
// GetBy receives a query function
// which is fired for every single user model inside
// our imaginary database.
// When that function returns true then it stops the iteration.
//
// It returns the query's return last known boolean value
// and the last known user model
// to help callers to reduce the loc.
//
// But be carefully, the caller should always check for the "found"
// because it may be false but the user model has actually real data inside it.
//
// It's actually a simple but very clever prototype function
// I'm think of and using everywhere since then,
// hope you find it very useful too.
func (d *DataSource) GetBy(query func(Model) bool) (user Model, found bool) {
d.mu.RLock()
for _, user = range d.Users {
found = query(user)
if found {
break
}
}
d.mu.RUnlock()
return
}
// GetByID returns a user model based on its ID.
func (d *DataSource) GetByID(id int64) (Model, bool) {
return d.GetBy(func(u Model) bool {
return u.ID == id
})
}
// GetByUsername returns a user model based on the Username.
func (d *DataSource) GetByUsername(username string) (Model, bool) {
return d.GetBy(func(u Model) bool {
return u.Username == username
})
}
func (d *DataSource) getLastID() (lastID int64) {
d.mu.RLock()
for id := range d.Users {
if id > lastID {
lastID = id
}
}
d.mu.RUnlock()
return lastID
}
// InsertOrUpdate adds or updates a user to the (memory) storage.
func (d *DataSource) InsertOrUpdate(user Model) (Model, error) {
// no matter what we will update the password hash
// for both update and insert actions.
hashedPassword, err := GeneratePassword(user.password)
if err != nil {
return user, err
}
user.HashedPassword = hashedPassword
// update
if id := user.ID; id > 0 {
_, found := d.GetByID(id)
if !found {
return user, errors.New("ID should be zero or a valid one that maps to an existing User")
}
d.mu.Lock()
d.Users[id] = user
d.mu.Unlock()
return user, nil
}
// insert
id := d.getLastID() + 1
user.ID = id
d.mu.Lock()
user.CreatedAt = time.Now()
d.Users[id] = user
d.mu.Unlock()
return user, nil
}

View File

@@ -0,0 +1,36 @@
package user
import (
"time"
"golang.org/x/crypto/bcrypt"
)
// Model is our User example model.
type Model struct {
ID int64 `json:"id"`
Firstname string `json:"firstname"`
Username string `json:"username"`
// password is the client-given password
// which will not be stored anywhere in the server.
// It's here only for actions like registration and update password,
// because we caccept a Model instance
// inside the `DataSource#InsertOrUpdate` function.
password string
HashedPassword []byte `json:"-"`
CreatedAt time.Time `json:"created_at"`
}
// GeneratePassword will generate a hashed password for us based on the
// user's input.
func GeneratePassword(userPassword string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
}
// ValidatePassword will check if passwords are matched.
func ValidatePassword(userPassword string, hashed []byte) (bool, error) {
if err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword)); err != nil {
return false, err
}
return true, nil
}

View File

@@ -0,0 +1,4 @@
<h1>Error.</h1>
<h2>An error occurred while processing your request.</h2>
<h3>{{.Message}}</h3>

View File

@@ -0,0 +1,12 @@
<html>
<head>
<title>{{.Title}}</title>
<link rel="stylesheet" type="text/css" href="/public/css/site.css" />
</head>
<body>
{{ yield }}
</body>
</html>

View File

@@ -0,0 +1,11 @@
<form action="/user/login" method="POST">
<div class="container">
<label><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required>
<label><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Login</button>
</div>
</form>

View File

@@ -0,0 +1,3 @@
<p>
Welcome back <strong>{{.User.Firstname}}</strong>!
</p>

View File

@@ -0,0 +1,3 @@
<p>
User with ID <strong>{{.ID}}</strong> does not exist.
</p>

View File

@@ -0,0 +1,14 @@
<form action="/user/register" method="POST">
<div class="container">
<label><b>Firstname</b></label>
<input type="text" placeholder="Enter Firstname" name="firstname" required>
<label><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required>
<label><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Register</button>
</div>
</form>

View File

@@ -9,9 +9,9 @@ import (
// UsersController is our /users API controller.
// GET /users | get all
// GET /users/{id:long} | get by id
// PUT /users/{id:long} | update by id
// DELETE /users/{id:long} | delete by id
// GET /users/{id:int64} | get by id
// PUT /users/{id:int64} | update by id
// DELETE /users/{id:int64} | delete by id
// Requires basic authentication.
type UsersController struct {
// Optionally: context is auto-binded by Iris on each request,

View File

@@ -0,0 +1,593 @@
# A Todo MVC Application using Iris and Vue.js
## Hackernoon Article: https://medium.com/hackernoon/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064
Vue.js is a front-end framework for building web applications using javascript. It has a blazing fast Virtual DOM renderer.
Iris is a back-end framework for building web applications using The Go Programming Language (disclaimer: author here). It's one of the fastest and featured web frameworks out there. We wanna use this to serve our "todo service".
## The Tools
Programming Languages are just tools for us, but we need a safe, fast and “cross-platform” programming language to power our service.
[Go](https://golang.org) is a [rapidly growing](https://www.tiobe.com/tiobe-index/) open source programming language designed for building simple, fast, and reliable software. Take a look [here](https://github.com/golang/go/wiki/GoUsers) which great companies use Go to power their services.
### Install the Go Programming Language
Extensive information about downloading & installing Go can be found [here](https://golang.org/dl/).
[![](https://i3.ytimg.com/vi/9x-pG3lvLi0/hqdefault.jpg)](https://youtu.be/9x-pG3lvLi0)
> Maybe [Windows](https://www.youtube.com/watch?v=WT5mTznJBS0) or [Mac OS X](https://www.youtube.com/watch?v=5qI8z_lB5Lw) user?
> The article does not contain an introduction to the language itself, if youre a newcomer I recommend you to bookmark this article, [learn](https://github.com/golang/go/wiki/Learn) the languages fundamentals and come back later on.
## The Dependencies
Many articles have been written, in the past, that lead developers not to use a web framework because they are useless and "bad". I have to tell you that there is no such thing, it always depends on the (web) framework that youre going to use. At production environment, we dont have the time or the experience to code everything that we wanna use in the applications, and if we could are we sure that we can do better and safely than others? In short term: **Good frameworks are helpful tools for any developer, company or startup and "bad" frameworks are waste of time, crystal clear.**
Youll need two dependencies:
1. Vue.js, for our client-side requirements. Download it from [here](https://vuejs.org/), latest v2.
2. The Iris Web Framework, for our server-side requirements. Can be found [here](https://github.com/kataras/iris), latest v12.
> If you have Go already installed then just execute `go get github.com/kataras/iris/v12@latest` to install the Iris Web Framework.
## Start
If we are all in the same page, its time to learn how we can create a live todo application that will be easy to deploy and extend even more!
We're going to use a vue.js todo application which uses browser'
s local storage and doesn't have any user-specified features like live sync between browser's tabs, you can find the original version inside the vue's [docs](https://vuejs.org/v2/examples/todomvc.html).
Assuming that you know how %GOPATH% works, create an empty folder, i.e "vuejs-todo-mvc" in the %GOPATH%/src directory, there you will create those files:
- web/public/js/app.js
- web/public/index.html
- todo/item.go
- todo/service.go
- web/controllers/todo_controller.go
- web/main.go
_Read the comments in the source code, they may be very helpful_
### The client-side (vue.js)
```js
/* file: vuejs-todo-mvc/web/public/js/app.js */
// Full spec-compliant TodoMVC with Iris
// and hash-based routing in ~200 effective lines of JavaScript.
var ws;
((async () => {
const events = {
todos: {
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) {
axios.get("/todos").then(response => {
if (response.data === null) {
return;
}
onComplete(response.data);
});
}
var todoStorage = {
fetch: function () {
var todos = [];
fetchTodos(function (items) {
for (var i = 0; i < items.length; i++) {
todos.push(items[i]);
}
});
return todos;
},
save: function (todos) {
axios.post("/todos", JSON.stringify(todos)).then(response => {
if (!response.data.success) {
window.alert("saving had a failure");
return;
}
// console.log("send: save");
ws.emit("save")
});
}
}
// visibility filters
var filters = {
all: function (todos) {
return todos
},
active: function (todos) {
return todos.filter(function (todo) {
return !todo.completed
})
},
completed: function (todos) {
return todos.filter(function (todo) {
return todo.completed
})
}
}
// app Vue instance
var app = new Vue({
// app initial state
data: {
todos: todoStorage.fetch(),
newTodo: '',
editedTodo: null,
visibility: 'all'
},
// we will not use the "watch" as it works with the fields like "hasChanges"
// and callbacks to make it true but let's keep things very simple as it's just a small getting started.
// // watch todos change for persistence
// watch: {
// todos: {
// handler: function (todos) {
// if (app.hasChanges) {
// todoStorage.save(todos);
// app.hasChanges = false;
// }
// },
// deep: true
// }
// },
// computed properties
// http://vuejs.org/guide/computed.html
computed: {
filteredTodos: function () {
return filters[this.visibility](this.todos)
},
remaining: function () {
return filters.active(this.todos).length
},
allDone: {
get: function () {
return this.remaining === 0
},
set: function (value) {
this.todos.forEach(function (todo) {
todo.completed = value
})
this.notifyChange();
}
}
},
filters: {
pluralize: function (n) {
return n === 1 ? 'item' : 'items'
}
},
// methods that implement data logic.
// note there's no DOM manipulation here at all.
methods: {
notifyChange: function () {
todoStorage.save(this.todos)
},
addTodo: function () {
var value = this.newTodo && this.newTodo.trim()
if (!value) {
return
}
this.todos.push({
id: this.todos.length + 1, // just for the client-side.
title: value,
completed: false
})
this.newTodo = ''
this.notifyChange();
},
completeTodo: function (todo) {
if (todo.completed) {
todo.completed = false;
} else {
todo.completed = true;
}
this.notifyChange();
},
removeTodo: function (todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
this.notifyChange();
},
editTodo: function (todo) {
this.beforeEditCache = todo.title
this.editedTodo = todo
},
doneEdit: function (todo) {
if (!this.editedTodo) {
return
}
this.editedTodo = null
todo.title = todo.title.trim();
if (!todo.title) {
this.removeTodo(todo);
}
this.notifyChange();
},
cancelEdit: function (todo) {
this.editedTodo = null
todo.title = this.beforeEditCache
},
removeCompleted: function () {
this.todos = filters.active(this.todos);
this.notifyChange();
}
},
// a custom directive to wait for the DOM to be updated
// before focusing on the input field.
// http://vuejs.org/guide/custom-directive.html
directives: {
'todo-focus': function (el, binding) {
if (binding.value) {
el.focus()
}
}
}
})
// handle routing
function onHashChange() {
var visibility = window.location.hash.replace(/#\/?/, '')
if (filters[visibility]) {
app.visibility = visibility
} else {
window.location.hash = ''
app.visibility = 'all'
}
}
window.addEventListener('hashchange', onHashChange)
onHashChange()
// mount
app.$mount('.todoapp');
```
Let's add our view, the static html.
```html
<!-- file: vuejs-todo-mvc/web/public/index.html -->
<!doctype html>
<html data-framework="vue">
<head>
<meta charset="utf-8">
<title>Iris + Vue.js • TodoMVC</title>
<link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@2.0.4/index.css">
<!-- this needs to be loaded before guide's inline scripts -->
<script src="https://vuejs.org/js/vue.js"></script>
<!-- $http -->
<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://cdn.jsdelivr.net/npm/neffos.js@latest/dist/neffos.min.js"></script>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus autocomplete="off" placeholder="What needs to be done?" v-model="newTodo"
@keyup.enter="addTodo">
</header>
<section class="main" v-show="todos.length" v-cloak>
<input class="toggle-all" type="checkbox" v-model="allDone">
<ul class="todo-list">
<li v-for="todo in filteredTodos" class="todo" :key="todo.id"
:class="{ completed: todo.completed, editing: todo == editedTodo }">
<div class="view">
<!-- v-model="todo.completed" -->
<input class="toggle" type="checkbox" @click="completeTodo(todo)">
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input class="edit" type="text" v-model="todo.title" v-todo-focus="todo == editedTodo" @blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">
</li>
</ul>
</section>
<footer class="footer" v-show="todos.length" v-cloak>
<span class="todo-count">
<strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
</span>
<ul class="filters">
<li>
<a href="#/all" :class="{ selected: visibility == 'all' }">All</a>
</li>
<li>
<a href="#/active" :class="{ selected: visibility == 'active' }">Active</a>
</li>
<li>
<a href="#/completed" :class="{ selected: visibility == 'completed' }">Completed</a>
</li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
</footer>
<script src="/js/app.js"></script>
</body>
</html>
```
### The server-side (iris)
Our view model.
```go
// file: vuejs-todo-mvc/todo/item.go
package todo
type Item struct {
SessionID string `json:"-"`
ID int64 `json:"id,omitempty"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
```
Our service.
```go
// file: vuejs-todo-mvc/todo/service.go
package todo
import (
"sync"
)
type Service interface {
Get(owner string) []Item
Save(owner string, newItems []Item) error
}
type MemoryService struct {
// key = session id, value the list of todo items that this session id has.
items map[string][]Item
// protected by locker for concurrent access.
mu sync.RWMutex
}
func NewMemoryService() *MemoryService {
return &MemoryService{
items: make(map[string][]Item, 0),
}
}
func (s *MemoryService) Get(sessionOwner string) []Item {
s.mu.RLock()
items := s.items[sessionOwner]
s.mu.RUnlock()
return items
}
func (s *MemoryService) Save(sessionOwner string, newItems []Item) error {
var prevID int64
for i := range newItems {
if newItems[i].ID == 0 {
newItems[i].ID = prevID
prevID++
}
}
s.mu.Lock()
s.items[sessionOwner] = newItems
s.mu.Unlock()
return nil
}
```
We are going to use some of the MVC functionalities of the iris web framework here but you can do the same with the standard API as well.
```go
// file: vuejs-todo-mvc/web/controllers/todo_controller.go
package controllers
import (
"vuejs-todo-mvc/todo"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"github.com/kataras/iris/v12/sessions"
"github.com/kataras/iris/v12/websocket"
)
// TodoController is our TODO app's web controller.
type TodoController struct {
Service todo.Service
Session *sessions.Session
NS *websocket.NSConn
}
// BeforeActivation called once before the server ran, and before
// the routes and dependencies binded.
// You can bind custom things to the controller, add new methods, add middleware,
// add dependencies to the struct or the method(s) and more.
func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) {
// this could be binded to a controller's function input argument
// if any, or struct field if any:
b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) {
ctx.ReadJSON(&items)
return
})
}
// Get handles the GET: /todos route.
func (c *TodoController) Get() []todo.Item {
return c.Service.Get(c.Session.ID())
}
// PostItemResponse the response data that will be returned as json
// after a post save action of all todo items.
type PostItemResponse struct {
Success bool `json:"success"`
}
var emptyResponse = PostItemResponse{Success: false}
// Post handles the POST: /todos route.
func (c *TodoController) Post(newItems []todo.Item) PostItemResponse {
if err := c.Service.Save(c.Session.ID(), newItems); err != nil {
return emptyResponse
}
return PostItemResponse{Success: true}
}
func (c *TodoController) Save(msg websocket.Message) error {
id := c.Session.ID()
c.NS.Conn.Server().Broadcast(nil, websocket.Message{
Namespace: msg.Namespace,
Event: "saved",
To: id,
Body: websocket.Marshal(c.Service.Get(id)),
})
return nil
}
```
And finally our main application's endpoint.
```go
// file: web/main.go
package main
import (
"strings"
"github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/todo"
"github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/web/controllers"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"github.com/kataras/iris/v12/sessions"
"github.com/kataras/iris/v12/websocket"
)
func main() {
app := iris.New()
// serve our app in public, public folder
// contains the client-side vue.js application,
// no need for any server-side template here,
// actually if you're going to just use vue without any
// back-end services, you can just stop afer this line and start the server.
app.HandleDir("/", "./public")
// configure the http sessions.
sess := sessions.New(sessions.Config{
Cookie: "iris_session",
})
// create a sub router and register the http controllers.
todosRouter := app.Party("/todos")
// create our mvc application targeted to /todos relative sub path.
todosApp := mvc.New(todosRouter)
// any dependencies bindings here...
todosApp.Register(
todo.NewMemoryService(),
)
todosController := new(controllers.TodoController)
// controllers registration here...
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
app.Listen(":8080")
}
```
Run the Iris web server you've just created by executing `go run main.go` from your current path (%GOPATH%/src/%your_folder%/web/).
```sh
$ go run main.go
Now listening on: http://localhost:8080
Application Started. Press CTRL+C to shut down.
_
```
Open one or more browser tabs at: http://localhost:8080 and have fun!
![](screen.png)
### Download the Source Code
The whole project, all the files you saw in this article are located at: https://github.com/kataras/iris/tree/master/_examples/mvc/vuejs-todo-mvc
## References
https://vuejs.org/v2/examples/todomvc.html (using browser's local storage)
https://github.com/kataras/iris/tree/master/_examples/mvc (mvc examples and features overview repository)
## Thank you, once again
Happy new year and thank you for your pattience, once again:) Don't hesitate to post any questions and provide feedback(I'm very active dev therefore you will be heard here!)
Don't forget to check out my medium profile and twitter as well, I'm posting some (useful) stuff there too:)
- https://medium.com/@kataras
- https://twitter.com/MakisMaropoulos

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -0,0 +1,8 @@
package todo
type Item struct {
SessionID string `json:"-"`
ID int64 `json:"id,omitempty"`
Title string `json:"title"`
Completed bool `json:"completed"`
}

View File

@@ -0,0 +1,46 @@
package todo
import (
"sync"
)
type Service interface {
Get(owner string) []Item
Save(owner string, newItems []Item) error
}
type MemoryService struct {
// key = session id, value the list of todo items that this session id has.
items map[string][]Item
// protected by locker for concurrent access.
mu sync.RWMutex
}
func NewMemoryService() *MemoryService {
return &MemoryService{
items: make(map[string][]Item, 0),
}
}
func (s *MemoryService) Get(sessionOwner string) []Item {
s.mu.RLock()
items := s.items[sessionOwner]
s.mu.RUnlock()
return items
}
func (s *MemoryService) Save(sessionOwner string, newItems []Item) error {
var prevID int64
for i := range newItems {
if newItems[i].ID == 0 {
newItems[i].ID = prevID
prevID++
}
}
s.mu.Lock()
s.items[sessionOwner] = newItems
s.mu.Unlock()
return nil
}

View File

@@ -0,0 +1,66 @@
package controllers
import (
"github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/todo"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"github.com/kataras/iris/v12/sessions"
"github.com/kataras/iris/v12/websocket"
)
// TodoController is our TODO app's web controller.
type TodoController struct {
Service todo.Service
Session *sessions.Session
NS *websocket.NSConn
}
// BeforeActivation called once before the server ran, and before
// the routes and dependencies binded.
// You can bind custom things to the controller, add new methods, add middleware,
// add dependencies to the struct or the method(s) and more.
func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) {
// this could be binded to a controller's function input argument
// if any, or struct field if any:
b.Dependencies().Register(func(ctx iris.Context) (items []todo.Item) {
ctx.ReadJSON(&items)
return
}) // Note: from Iris v12.2 these type of dependencies are automatically resolved.
}
// Get handles the GET: /todos route.
func (c *TodoController) Get() []todo.Item {
return c.Service.Get(c.Session.ID())
}
// PostItemResponse the response data that will be returned as json
// after a post save action of all todo items.
type PostItemResponse struct {
Success bool `json:"success"`
}
var emptyResponse = PostItemResponse{Success: false}
// Post handles the POST: /todos route.
func (c *TodoController) Post(newItems []todo.Item) PostItemResponse {
if err := c.Service.Save(c.Session.ID(), newItems); err != nil {
return emptyResponse
}
return PostItemResponse{Success: true}
}
func (c *TodoController) Save(msg websocket.Message) error {
id := c.Session.ID()
c.NS.Conn.Server().Broadcast(nil, websocket.Message{
Namespace: msg.Namespace,
Event: "saved",
To: id,
Body: websocket.Marshal(c.Service.Get(id)),
})
return nil
}

View File

@@ -0,0 +1,63 @@
package main
import (
"strings"
"github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/todo"
"github.com/kataras/iris/v12/_examples/mvc/vuejs-todo-mvc/src/web/controllers"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"github.com/kataras/iris/v12/sessions"
"github.com/kataras/iris/v12/websocket"
)
func main() {
app := iris.New()
// serve our app in public, public folder
// contains the client-side vue.js application,
// no need for any server-side template here,
// actually if you're going to just use vue without any
// back-end services, you can just stop afer this line and start the server.
app.HandleDir("/", "./public")
// configure the http sessions.
sess := sessions.New(sessions.Config{
Cookie: "iris_session",
})
// create a sub router and register the http controllers.
todosRouter := app.Party("/todos")
// create our mvc application targeted to /todos relative sub path.
todosApp := mvc.New(todosRouter)
// any dependencies bindings here...
todosApp.Register(
todo.NewMemoryService(),
)
todosController := new(controllers.TodoController)
// controllers registration here...
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
app.Listen(":8080")
}

View File

@@ -0,0 +1,2 @@
index.css is not here to reduce the disk space for the examples.
https://unpkg.com/todomvc-app-css@2.0.4/index.css is used instead.

View File

@@ -0,0 +1,73 @@
<!doctype html>
<html data-framework="vue">
<head>
<meta charset="utf-8">
<title>Iris + Vue.js • TodoMVC</title>
<link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@2.0.4/index.css">
<!-- this needs to be loaded before guide's inline scripts -->
<script src="https://vuejs.org/js/vue.js"></script>
<!-- $http -->
<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://cdn.jsdelivr.net/npm/neffos.js@latest/dist/neffos.min.js"></script>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" autofocus autocomplete="off" placeholder="What needs to be done?" v-model="newTodo"
@keyup.enter="addTodo">
</header>
<section class="main" v-show="todos.length" v-cloak>
<input class="toggle-all" type="checkbox" v-model="allDone">
<ul class="todo-list">
<li v-for="todo in filteredTodos" class="todo" :key="todo.id"
:class="{ completed: todo.completed, editing: todo == editedTodo }">
<div class="view">
<!-- v-model="todo.completed" -->
<input class="toggle" type="checkbox" @click="completeTodo(todo)">
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input class="edit" type="text" v-model="todo.title" v-todo-focus="todo == editedTodo" @blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">
</li>
</ul>
</section>
<footer class="footer" v-show="todos.length" v-cloak>
<span class="todo-count">
<strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
</span>
<ul class="filters">
<li>
<a href="#/all" :class="{ selected: visibility == 'all' }">All</a>
</li>
<li>
<a href="#/active" :class="{ selected: visibility == 'active' }">Active</a>
</li>
<li>
<a href="#/completed" :class="{ selected: visibility == 'completed' }">Completed</a>
</li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
</footer>
<script src="/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,214 @@
// Full spec-compliant TodoMVC with Iris
// and hash-based routing in ~200 effective lines of JavaScript.
var ws;
((async () => {
const events = {
todos: {
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) {
axios.get("/todos").then(response => {
if (response.data === null) {
return;
}
onComplete(response.data);
});
}
var todoStorage = {
fetch: function () {
var todos = [];
fetchTodos(function (items) {
for (var i = 0; i < items.length; i++) {
todos.push(items[i]);
}
});
return todos;
},
save: function (todos) {
axios.post("/todos", JSON.stringify(todos)).then(response => {
if (!response.data.success) {
window.alert("saving had a failure");
return;
}
// console.log("send: save");
ws.emit("save")
});
}
}
// visibility filters
var filters = {
all: function (todos) {
return todos
},
active: function (todos) {
return todos.filter(function (todo) {
return !todo.completed
})
},
completed: function (todos) {
return todos.filter(function (todo) {
return todo.completed
})
}
}
// app Vue instance
var app = new Vue({
// app initial state
data: {
todos: todoStorage.fetch(),
newTodo: '',
editedTodo: null,
visibility: 'all'
},
// we will not use the "watch" as it works with the fields like "hasChanges"
// and callbacks to make it true but let's keep things very simple as it's just a small getting started.
// // watch todos change for persistence
// watch: {
// todos: {
// handler: function (todos) {
// if (app.hasChanges) {
// todoStorage.save(todos);
// app.hasChanges = false;
// }
// },
// deep: true
// }
// },
// computed properties
// http://vuejs.org/guide/computed.html
computed: {
filteredTodos: function () {
return filters[this.visibility](this.todos)
},
remaining: function () {
return filters.active(this.todos).length
},
allDone: {
get: function () {
return this.remaining === 0
},
set: function (value) {
this.todos.forEach(function (todo) {
todo.completed = value
})
this.notifyChange();
}
}
},
filters: {
pluralize: function (n) {
return n === 1 ? 'item' : 'items'
}
},
// methods that implement data logic.
// note there's no DOM manipulation here at all.
methods: {
notifyChange: function () {
todoStorage.save(this.todos)
},
addTodo: function () {
var value = this.newTodo && this.newTodo.trim()
if (!value) {
return
}
this.todos.push({
id: this.todos.length + 1, // just for the client-side.
title: value,
completed: false
})
this.newTodo = ''
this.notifyChange();
},
completeTodo: function (todo) {
if (todo.completed) {
todo.completed = false;
} else {
todo.completed = true;
}
this.notifyChange();
},
removeTodo: function (todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
this.notifyChange();
},
editTodo: function (todo) {
this.beforeEditCache = todo.title
this.editedTodo = todo
},
doneEdit: function (todo) {
if (!this.editedTodo) {
return
}
this.editedTodo = null
todo.title = todo.title.trim();
if (!todo.title) {
this.removeTodo(todo);
}
this.notifyChange();
},
cancelEdit: function (todo) {
this.editedTodo = null
todo.title = this.beforeEditCache
},
removeCompleted: function () {
this.todos = filters.active(this.todos);
this.notifyChange();
}
},
// a custom directive to wait for the DOM to be updated
// before focusing on the input field.
// http://vuejs.org/guide/custom-directive.html
directives: {
'todo-focus': function (el, binding) {
if (binding.value) {
el.focus()
}
}
}
})
// handle routing
function onHashChange() {
var visibility = window.location.hash.replace(/#\/?/, '')
if (filters[visibility]) {
app.visibility = visibility
} else {
window.location.hash = ''
app.visibility = 'all'
}
}
window.addEventListener('hashchange', onHashChange)
onHashChange()
// mount
app.$mount('.todoapp');

View File

@@ -0,0 +1,2 @@
vue.js is not here to reduce the disk space for the examples.
Instead https://vuejs.org/js/vue.js is used instead.