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

Мар 17, 2022 DEV

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

Бэ­кенд при­ло­же­ния бу­дет на­пи­сан на язы­ке 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 это чис­ло. Так мы бо­лее без­опас­но смо­жем пе­ре­дать за­прос к ба­зе дан­ных поз­же.

Мо­дель

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

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

// 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. До­ба­вим за­да­чу. В Postman пе­ре­клю­чи­те ме­тод на «PUT», за­тем от­крой­те вклад­ку «Body». Вы­бе­ри­те «raw», за­тем JSON (application/json) как тип. В тек­сто­вом по­ле вве­ди­те сле­ду­ю­щее:

{
    "name": "Foobar"
}

В от­вет долж­но прий­ти ‘created’. Об­ра­ти­те вни­ма­ние, id вер­нул­ся пот­му, что нам нуж­но про­те­сти­ро­вать ро­ут «DELETE /tasks». Сме­ни­те ме­тод на «DELETE» и ука­жи­те Postman «/tasks/:id», за­ме­нив «:id» на id, ко­то­рый вер­нул­ся в про­шлом пунк­те. В от­вет долж­но прий­ти со­об­ще­ние об успе­хе «deleted». Ес­ли всё хо­ро­шо, то за­про­сив «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. Ни­же по­след­не­го те­га «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 и фрон­тен­да на VueJS. На­де­ем­ся, она спо­двиг­нет вас на со­зда­ние по-на­сто­я­ще­му хо­ро­ших и слож­ных веб-при­ло­же­ний!

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

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

go get gopkg.in/labstack/echo.v2

Если у вас ошибки, необходимо убрать из todo.go:

"github.com/labstack/echo/engine/standard"

а вместо:

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

запускаем сервер так:

e.Logger.Fatal(e.Start(":8000"))

Добавить комментарий