Создание одностраничного веб-приложения на Go, Echo и Vue

Разработка /
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue

Сегодня мы создадим несложное приложение для ведения списка задач. В нём можно будет указывать наименование задачи, выводить созданные задачи на экран и удалять задачи.

Бэкенд приложения будет написан на языке Go. Для лучшего понимания материала необходимы хотя бы минимальные знания синтаксиса и установленный Go.

Чтобы ускорить создание приложения, мы возьмём микро-фреймворк Echo. А задачи мы будем хранить в базе SQLite.

Фронтенд мы создадим на HTML5 с популярным Javascript фреймворком VueJS.

Роуты и база данных

Вначале установим недостающие библиотеки:

$ go get github.com/labstack/echo
$ go get github.com/mattn/go-sqlite3

И создадим директорию для нашего проекта:

$ cd $GOPATH/src
$ mkdir go-echo-vue && cd go-echo-vue

Начнём с создания роутов. Создайте файл todo.go в корне созданной папки с таким содержимым:

// todo.go
package main

import (
    "github.com/labstack/echo"
    "github.com/labstack/echo/engine/standard"
)

func main() {
    // Create a new instance of Echo
    e := echo.New()

    e.GET("/tasks", func(c echo.Context) error { return c.JSON(200, "GET Tasks") })
    e.PUT("/tasks", func(c echo.Context) error { return c.JSON(200, "PUT Tasks") })
    e.DELETE("/tasks/:id", func(c echo.Context) error { return c.JSON(200, "DELETE Task "+c.Param("id")) })

    // Start as a web server
    e.Run(standard.New(":8000"))
}

Здесь мы импортировали фреймворк Echo и создали обязательный метод main(). Затем в нём создали экземпляр Echo и задали несколько роутов. При создании роута ему передаётся шаблон запроса первым параметром и функция-обработчик вторым.
Наши роуты пока могут только выдавать статический текст, доработаем их позже. А в конце мы запускаем наше приложение по адресу localhost:8000 используя встроенный в Go HTTP-сервер.

Для тестирования работы роутов сначала скомпилируем приложение и запустим его, а потом воспользуемся расширением Chrome под названием Postman.

$ go build todo.go
$ ./todo

После запуска приложения откроем Postman и подключим его к адресу localhost:8000. Нужно протестировать роут "/tasks" с помощью запросов GET, PUT и DELETE, обработчики которых мы создали ранее.
Если всё работает правильно, мы увидим следующую картину:
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue

Теперь перейдём к созданию базы данных. Назовём файл «storage.db» и если его нет, драйвер создаст его для нас.
После создания базы данных необходимо будет запустить миграции.

Доработаем наш файл todo.go:

// todo.go
package main
import (
    "database/sql"

    "github.com/labstack/echo"
    "github.com/labstack/echo/engine/standard"
    _ "github.com/mattn/go-sqlite3"
)

И доработаем метод main():

// todo.go
func main() {

    db := initDB("storage.db")
    migrate(db)

Добавим метод работы с базой данных:

// todo.go
func initDB(filepath string) *sql.DB {
    //откроем файл или создадим его
    db, err := sql.Open("sqlite3", filepath)

    // проверяем ошибки и выходим при их наличии
    if err != nil {
        panic(err)
    }

    // если ошибок нет, но не можем подключиться к базе данных,
    // то так же выходим
    if db == nil {
        panic("db nil")
    }
    return db
}

func migrate(db *sql.DB) {
    sql := `
    CREATE TABLE IF NOT EXISTS tasks(
        id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        name VARCHAR NOT NULL
    );
    `

    _, err := db.Exec(sql)
    // выходим, если будут ошибки с SQL запросом выше
    if err != nil {
        panic(err)
    }
}

Здесь мы просто создаём таблицу для наших задач, если её ещё нет и завершаем приложение при любой ошибке.

Протестируем ещё раз:

$ go build todo.go
$ ./todo

Теперь откроем терминал, перейдём в папку проекта и запустим следующую команду для проверки созданного файла БД:

$ sqlite3 storage.db

Если у вас не получилось запустить команду sqlite, возможно требуется установить её с официального сайта или через менеджер пакетов вашей ОС.

Если команда запустилась, то введите команду ".tables". Вы должны увидеть примерно следующее:
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue
Чтобы выйти, введите ".quit".

Обработчики

Мы создали обработчики запросов, теперь нам нужно доработать их.
Откроем файл todo.go и вставим в блок импортов файл с обработчиками, который создадим позже:

package main
import (
    "database/sql"
    "go-echo-vue/handlers"

    "github.com/labstack/echo"
    "github.com/labstack/echo/engine/standard"
    _ "github.com/mattn/go-sqlite3"
)

Тут же доработаем вызовы обработчиков:

// todo.go
    e := echo.New()

    e.File("/", "public/index.html")
    e.GET("/tasks", handlers.GetTasks(db))
    e.PUT("/tasks", handlers.PutTask(db))
    e.DELETE("/tasks/:id", handlers.DeleteTask(db))

    e.Run(standard.New(":8000"))
}

Здесь мы добавили к существующим роутам дополнительный. В этом html-файле мы будем хранить код VueJS.

Теперь создадим директорию 'handlers', а в ней файл «tasks.go»:

// handlers/tasks.go
package handlers

import (
    "database/sql"
    "net/http"
    "strconv"

    "github.com/labstack/echo"
)

А в строке ниже будет небольшой трюк, который позволит возвращать произвольный JSON. Это просто map со ключами типа string и любым типом значения.

// handlers/tasks.go
type H map[string]interface{}


Для того, чтобы просто проверить работу обработчиков, сделаем вывод левых данных, а не из базы данных:

// handlers/tasks.go

// конечная точка GetTasks
func GetTasks(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        return c.JSON(http.StatusOK, "tasks")
    }
}

// конечная точка PutTask
func PutTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        return c.JSON(http.StatusCreated, H{
            "created": 123,
    }
}

// конечная точка DeleteTask
func DeleteTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        id, _ := strconv.Atoi(c.Param("id"))
        return c.JSON(http.StatusOK, H{
            "deleted": id,
        })
    }
}


Библиотека http в Go позволяет работать со статусами HTTP и мы можем ответить http.StatusCreated для запроса PUT.
Функция «DeleteTask» принимает параметром id задачи, а мы с помощью пакета strconv и метода Atoi (alpha в число) проверяем, что id это число. Так мы более безопасно сможем передать запрос к базе данных позже.

Пересоберём приложение и заново запустим Postman:

Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue

Модель

Нам осталось подключить приложение к базе данных. Вместо того, чтобы делать прямые вызовы из обработчиков, мы сохраним код простым, превратив логику базы данных в модель.

Но сначала включим ссылки на нашу модель в созданный файл обработчиков.
Включим файл моделей в блок импорта:

// handlers/tasks.go
package handlers

import (
    "database/sql"
    "net/http"
    "strconv"

    "go-echo-vue/models"

    "github.com/labstack/echo"
)

Затем добавим вызовы к модели в метод обработчика:

// handlers/tasks.go

// конечная точка GetTasks
func GetTasks(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        // получаем задачи из модели
        return c.JSON(http.StatusOK, models.GetTasks(db))
    }
}

// конечная точка PutTask
func PutTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        // создаём новую задачу
        var task models.Task
        // привязываем пришедший JSON в новую задачу
        c.Bind(&task)
        // добавим задачу с помощью модели
        id, err := models.PutTask(db, task.Name)
        // вернём ответ JSON при успехе
        if err == nil {
            return c.JSON(http.StatusCreated, H{
                "created": id,
            })
        // обработка ошибок
        } else {
            return err
        }
    }
}

// конечная точка DeleteTask
func DeleteTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        id, _ := strconv.Atoi(c.Param("id"))
        // используем модель для удаления задачи
        _, err := models.DeleteTask(db, id)
        // вернём ответ JSON при успехе
        if err == nil {
            return c.JSON(http.StatusOK, H{
                "deleted": id,
            })
        // обработка ошибок
        } else {
            return err
        }
    }
}


А теперь создадим модель. Создайте каталог «models», а в нём файл «tasks.go».

// models/tasks.go
package models

import (
    "database/sql"

    _ "github.com/mattn/go-sqlite3"
)

// Task это структура с данными задачи
type Task struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

// TaskCollection это список задач
type TaskCollection struct {
    Tasks []Task `json:"items"`
}

Мы создали тип «Task» — структуру с двумя полями, ID и Name. Go позволяет добавлять метаданные с помощью обратных кавычек. Мы указали, что хотим видеть поля в виде JSON. И это позволяет функции «c.Bind» (из обработчиков) знать, какие привязки данных нужны при создании новой задачи.
Тип «TaskCollection» это просто коллекция наших задач. Мы используем её при возврате списка всех задач из базы данных.

// models/tasks.go

func GetTasks(db *sql.DB) TaskCollection {
    sql := "SELECT * FROM tasks"
    rows, err := db.Query(sql)
    // выходим, если SQL не сработал по каким-то причинам
    if err != nil {
        panic(err)
    }
    // убедимся, что всё закроется при выходе из программы
    defer rows.Close()

    result := TaskCollection{}
    for rows.Next() {
        task := Task{}
        err2 := rows.Scan(&task.ID, &task.Name)
        // выход при ошибке
        if err2 != nil {
            panic(err2)
        }
        result.Tasks = append(result.Tasks, task)
    }
    return result
}

Метод GetTasks выбирает все задачи из базы данных, добавляет их в новую коллекцию задач, и возвращает их.

// models/tasks.go

func PutTask(db *sql.DB, name string) (int64, error) {
    sql := "INSERT INTO tasks(name) VALUES(?)"

    // выполним SQL запрос
    stmt, err := db.Prepare(sql)
    // выход при ошибке
    if err != nil {
        panic(err)
    }
    // убедимся, что всё закроется при выходе из программы
    defer stmt.Close()

    // заменим символ '?' в запросе на 'name'
    result, err2 := stmt.Exec(name)
    // выход при ошибке
    if err2 != nil {
        panic(err2)
    }

    return result.LastInsertId()
}

Метод PutTask вставляет новую задачу в базу данных и возвращает её id при успехе или panic при неудаче.

// models/tasks.go

func DeleteTask(db *sql.DB, id int) (int64, error) {
    sql := "DELETE FROM tasks WHERE id = ?"

    // выполним SQL запрос
    stmt, err := db.Prepare(sql)
    // выход при ошибке
    if err != nil {
        panic(err)
    }

    // заменим символ '?' в запросе на 'id'
    result, err2 := stmt.Exec(id)
    // выход при ошибке
    if err2 != nil {
        panic(err2)
    }

    return result.RowsAffected()
}

Можно ещё раз протестировать приложение с Postman. Проверим роут «GET /tasks» — в ответ должен прийти JSON с «tasks» равным null:
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue
Добавим задачу. В Postman переключите метод на «PUT», затем откройте вкладку «Body». Выберите «raw», затем JSON (application/json) как тип. В текстовом поле введите следующее:

{
    "name": "Foobar"
}

В ответ должно прийти 'created', типа такого:
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue
Обратите внимание, id вернулся потму, что нам нужно протестировать роут «DELETE /tasks». Смените метод на «DELETE» и укажите Postman "/tasks/:id", заменив ":id" на id, который вернулся в прошлом пункте. В ответ должно прийти сообщение об успехе «deleted».
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue
Если всё хорошо, то запросив «GET /tasks», вы получите в ответ null.

Фронтенд

Для простоты изложения, мы включим код Javascript в файл разметки HTML. Включим несколько библиотек: Bootstrap, JQuery и, конечно, VueJS. В интерфейсе у нас будет только поле ввода, кнопки и неупорядоченный список для наших задач.
Создайте директорию 'public' и в ней файл «index.html»:

<!-- public/index.html -->

<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">

        <title>TODO App</title>

        <!-- Latest compiled and minified CSS -->
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">

        <!-- Font Awesome -->
        <link rel="stylesheet"  href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">

        <!-- JQuery -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

        <!-- Latest compiled and minified JavaScript -->
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>

        <!-- Vue.js -->
        <script src="http://cdnjs.cloudflare.com/ajax/libs/vue/1.0.24/vue.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue-resource/0.7.0/vue-resource.min.js"></script>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-md-4">
                    <h2>My Tasks</h2>
                    <ul class="list-group">
                        <li class="list-group-item" v-for="task in tasks">
                            {{ task.name }}
                            <span class="pull-right">
                                <button class="btn btn-xs btn-danger" v-on:click="deleteTask($index)">
                                    <i class="fa fa-trash-o" aria-hidden="true"></i>
                                </button>
                            </span>
                        </li>
                    </ul>
                    <div class="input-group">
                        <input type="text" 
                            class="form-control" 
                            placeholder="New Task" 
                            v-on:keyup.enter="createTask"
                            v-model="newTask.name">
                        <span class="input-group-btn">
                            <button class="btn btn-primary" type="button" v-on:click="createTask">Create</button>
                        </span>
                    </div><!-- /input-group -->
                </div>
            </div>
        </div>
    </body>
</html>

Пересоберите приложение, запустите его и откройте в браузере localhost:8000.
Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue

Ниже последнего тега «div» мы разместим наш код VueJS в теге «script». Код этот не очень прост, но хорошо прокомментирован. Здесь у нас несколько методов для создания и удаления задач, а также метод инициализации, возвращающий список всех задач в базе.
Для общения с бэкендом нам понадобится HTTP клиент, мы будем использовать vue-resource так: «this.$http» и далее что нам нужно, типа (get, put, и т.п.).

<!-- public/index.html -->

        <script>
            new Vue({
                el: 'body',

                data: {
                    tasks: [],
                    newTask: {}
                },

          // запускаем при загрузке страницы, чтобы у нас был актуальный список всех задач
                created: function() {
        // используем $http-клиента из vue-resource для получения данных от роута /tasks
                    this.$http.get('/tasks').then(function(response) {
                        this.tasks = response.data.items ? response.data.items : []
                    })
                },

                methods: {
                    createTask: function() {
                        if (!$.trim(this.newTask.name)) {
                            this.newTask = {}
                            return
                        }

             // передаём новую задачу роуту /tasks с помощью $http клиента
                        this.$http.put('/tasks', this.newTask).success(function(response) {
                            this.newTask.id = response.created
                            this.tasks.push(this.newTask)
                            console.log("Task created!")
                            console.log(this.newTask)
                            this.newTask = {}
                        }).error(function(error) {
                            console.log(error)
                        });
                    },

                    deleteTask: function(index) {
             // используем $http клиента для удаления задачи по её id
                        this.$http.delete('/tasks/' + this.tasks[index].id).success(function(response) {
                            this.tasks.splice(index, 1)
                            console.log("Task deleted!")
                        }).error(function(error) {
                            console.log(error)
                        })
                    }
                }
            })
        </script>   


Запуск

У нас всё готово, пересоберём приложение и запустим его!

$ go build todo.go
$ ./todo

Откроем в браузере localhost:8000

Разработка: Создание одностраничного веб-приложения на Go, Echo и Vue

Мы рассмотрели несложную задачу по созданию бэкенда на Go с помощью фреймворка Echo и фронтенда на VueJS. Надеемся, она сподвигнет вас на создание по-настоящему хороших и сложных веб-приложений!

Исходный код примера.

UPD: в новой версии Echo была поломана обратная совместимость и этот пример уже не работает. Устанавливайте предыдущую версию:

go get gopkg.in/labstack/echo.v2
5 комментариев
VadimSuharnikov
Привет! А что за софтину ты используешь для отладки http запросов?
fokusov
Привет! Вообще тестирую запросы от api в Postman, но можно использовать отдельную тулзу, типа такой parnurzeal.github.io/gorequest/
JamesAlexander
Привет, пытаюсь запустить, выдает ошибку:
todo.go:9:2: cannot find package «github.com/labstack/echo/engine/standard» in any of:
/usr/local/Cellar/go/1.7.3/libexec/src/github.com/labstack/echo/engine/standard (from $GOROOT)
/Users/Alexander/go/src/github.com/labstack/echo/engine/standard (from $GOPATH)
todo.go:6:2: cannot find package «go-echo-vue/handlers» in any of:
/usr/local/Cellar/go/1.7.3/libexec/src/go-echo-vue/handlers (from $GOROOT)
/Users/Alexander/go/src/go-echo-vue/handlers (from $GOPATH)
Последний раз редактировалось
fokusov
Привет. Похоже, что в Echo что-то поломали и пример уже не работает :(
shvetsov
такая же проблема. Необходимо убрать из todo.go:
"github.com/labstack/echo/engine/standard"

а вместо:
e.Run(standard.New(":8000"))

запускаем сервер так:
e.Logger.Fatal(e.Start(":8000"))
Последний раз редактировалось
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.