Сегодня мы создадим несложное приложение для ведения списка задач. В нём можно будет указывать наименование задачи, выводить созданные задачи на экран и удалять задачи.
Бэкенд приложения будет написан на языке 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 и задали несколько роутов. При создании роута ему передаётся шаблон запроса первым параметром и функция-обработчик вторым.
Наши роуты пока могут только выдавать статический текст, доработаем их позже. А в конце мы запускаем наше приложение по адресу http://localhost:8000 используя встроенный в Go HTTP-сервер.
Для тестирования работы роутов сначала скомпилируем приложение и запустим его, а потом воспользуемся расширением Chrome под названием Postman.
$ go build todo.go
$ ./todo
После запуска приложения откроем Postman и подключим его к адресу localhost:8000. Нужно протестировать роут «/tasks» с помощью запросов GET, PUT и DELETE, обработчики которых мы создали ранее.
Если всё работает правильно, мы увидим следующую картину:
Теперь перейдём к созданию базы данных. Назовём файл «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». Вы должны увидеть примерно следующее:
Чтобы выйти, введите «.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:
Модель
Нам осталось подключить приложение к базе данных. Вместо того, чтобы делать прямые вызовы из обработчиков, мы сохраним код простым, превратив логику базы данных в модель.
Но сначала включим ссылки на нашу модель в созданный файл обработчиков.
Включим файл моделей в блок импорта:
// 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»:
My Tasks
-
{{ task.name }}
Пересоберите приложение, запустите его и откройте в браузере http://localhost:8000.
Ниже последнего тега «div» мы разместим наш код VueJS в теге «script». Код этот не очень прост, но хорошо прокомментирован. Здесь у нас несколько методов для создания и удаления задач, а также метод инициализации, возвращающий список всех задач в базе.
Для общения с бэкендом нам понадобится HTTP клиент, мы будем использовать vue-resource так: «this.$http» и далее что нам нужно, типа (get, put, и т.п.).
Запуск
У нас всё готово, пересоберём приложение и запустим его!
$ go build todo.go
$ ./todo
Откроем в браузере http://localhost:8000
Мы рассмотрели несложную задачу по созданию бэкенда на Go с помощью фреймворка Echo и фронтенда на VueJS. Надеемся, она сподвигнет вас на создание по-настоящему хороших и сложных веб-приложений!
UPD: в новой версии Echo была поломана обратная совместимость и этот пример уже не работает. Устанавливайте предыдущую версию:
go get gopkg.in/labstack/echo.v2
Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.
Пишите: @ighar. Buy me a coffee, please :).