【译】使用 Go,Echo 和 Vue 创建单页 TODO 应用

本教程中我们将会创建一个 “todo” 应用。完成后可以实现创建任务,展示新创建的任务和删除它们。

此程序后端使用 Go 语言。Go 由 Google 开发。虽然不是最流行的语言,但是正在逐步得到认可。Go 非常轻量级,易于学习,运行快。此教程假设你已经对于这门语言有了一些了解,并且已经安装和配置好了开发环境。

我们将会使用 Echo 框架,Echo 框架相当于 PHP 语言的 Slim PHP 或 Lumen 框架,你应该有点熟悉使用微框架和使用路由处理 http 请求的概念。

任务数据将会储存在 SQLite 数据库中,SQLite 是一个轻量级的可替代 MySQL 或 PostgreSQL 的数据库。数据会存在一个独立的文件中,与应用在同一目录而不是存在服务器上。

最后,前端使用 HTML5 和流行的 VueJS JavaScript 框架,需要对 VueJS 有一定的了解。

我们的应用程序将分解成四个基本部分。我们将拥有我们的主包,用来设置路由和数据库。接下来,我们将有几个处理程序用来处理不同的路由。 当然,我们也会有一个 Task 模型,它将使用 SQLite 进行持久化。最后,应用程序将有一个简单的 index.html 文件,其中包含我们的 HTML5 和 VueJS 客户端代码。 让我们深入挖掘!

路由和数据库

在入口文件中会引入几个包。”database/sql” 是Go标准包,但是 Echo 和 SQLite 需要从 Github 下载。

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

然后创建应用的目录。

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

现在开始写路由,在 go-echo-vue 目录创建一个文件并命名为 “todo.go” ,然后引入 Echo 框架。

// todo.go
package main

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

下一步,创建go程序必需的 “main” 方法。

// todo.go
func main() { }

为了使前端的 VueJS 可以和后端通信,创建任务,需要设置一些基本的路由。第一件事就是实例化一个 Echo 。然后使用內建方法定义几个路由。如果使用过其他框架,应该会熟悉这个概念。

路由使用一个正则作为第一个参数,然后使用一个处理方法作为第二个参数。在此教程中必须使用 Echo.HandlerFunc 接口。

现在可以在 “main” 方法中创建几个给前端通信使用的路由了。

// todo.go
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"))
}

以上路由只输出了固定的文本内容,将会在接下来改进。

可以使用 Postman 测试以上接口。

$ go build todo.go
$ ./todo

运行后,打开 Postman,输入 localhost:8000,选择 GET 来测试 “/tasks” 路由,正常可以看到 “GET Tasks”。

然后是配置数据库,指定存储文件为 “storage.db” ,如果不存在程序会自动创建。数据库创建后需要运行数据迁移。

// todo.go

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)

然后需要定义 initDB 和 migrate 方法。

// todo.go
func initDB(filepath string) *sql.DB {
	db, err := sql.Open("sqlite3", filepath)

	//检查错误
	if err != nil {
		panic(err)
	}

	//如果open没有报错,但是仍然没有数据库连接,一样要退出
	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)

	//出错退出
	if err != nil {
		panic(err)
	}
}

这两个方法用于连接数据库,建表。”initDB” 会打开一个 db 文件或者创建它。如果失败程序会退出。

“migrate” 方法运行创建表的 SQL 。如果失败程序退出。

然后

$ go build todo.go
$ ./todo

查看效果。

如果打开另一个终端,列出当前目录内容时会发现已经创建了 “storage.db” ,执行以下命令来确认它确实是个 SQLite 文件。

$ 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"
)

然后修改路由,使用刚刚创建的 handlers 包去处理。

// 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"))
}

查看这段代码,你可能会注意到列出的处理程序实际上并不遵循 Echo 所要求的函数签名。相反,这些函数返回一个满足该接口的函数。这是我用过的一个技巧,所以我们可以将 db 实例从 handler 传递到 handler ,而不必在每次我们要使用数据库时创建一个新实例。稍后会更清楚。

我们还增加了一条额外的路由。这是一个包含我们的 VueJS 客户端代码的静态 html 文件。我们可以使用 “File” 功能提供静态文件。在这种情况下,将在访问 “/“ 的时候输出我们的客户端代码。

然后创建一个名为 “handlers” 的目录,并在该目录中创建一个名为 “tasks.go” 的文件。接下来,我们需要导入一些我们需要的软件包。

// handlers/tasks.go
package handlers

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

    "github.com/labstack/echo"
)

接下来的这一行代码,它允许我们在响应中返回任意的 JSON ,就像你稍后会看到的一样。这是一个以字符串作为 key ,任意类型作为值的 map 结构。 在Go中,”interface” 关键字表示从原始数据类型到用户定义类型或结构的任何内容。

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

这个文件主要是处理函数。它们都以 db 连接作为参数,但要记住, Echo 路由的正确处理程序,需要实现 Echo.HandlerFunc 接口。 我们通过返回与接口签名匹配的匿名函数来实现此目标。该函数现在可以使用数据库连接并将其传递给我们的模型。

为了能正常工作,暂时我们不会处理数据库。只会返回一些假数据。

// handlers/tasks.go

// GetTasks endpoint
func GetTasks(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        return c.JSON(http.StatusOK, "tasks")
    }
}

// PutTask endpoint
func PutTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        return c.JSON(http.StatusCreated, H{
            "created": 123,
    }
}

// DeleteTask endpoint
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,
        })
    }
}

Go http 软件包为我们提供了一些便利的常量来表示 HTTP 状态代码。例如,我们使用 http.StatusCreated 作为我们的 PUT 响应。 这些处理程序中的每一个现在都会返回 JSON 格式的响应。最后一个函数 “DeleteTask” 需要一个 id 参数。我们使用 strconv 包和 Atoi(alpha to integer)函数来确保 id 被转换为整数。 保证在通过数据库中的 id 查询任务时正确使用它。

要测试这些处理程序,要重新编译并运行应用程序。我们可以使用 Postman 再次测试。

MODEL

现在我们已经有了一部分处理程序,我们的应用程序需要使用数据库。但是我们不是直接从处理程序进行数据库调用,而是通过将数据库逻辑抽象为模型来保持代码的整洁。

首先让我们在新创建的处理程序文件中引用我们的新模型。

导入我们即将创建的模型包。

// handlers/tasks.go
package handlers

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

    "go-echo-vue/models"

    "github.com/labstack/echo"
)

然后将调用添加到我们的处理函数中。

// handlers/tasks.go

// GetTasks endpoint
func GetTasks(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        // Fetch tasks using our new model
        return c.JSON(http.StatusOK, models.GetTasks(db))
    }
}

// PutTask endpoint
func PutTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        // Instantiate a new task
        var task models.Task
        // Map imcoming JSON body to the new Task
        c.Bind(&task)
        // Add a task using our new model
        id, err := models.PutTask(db, task.Name)
        // Return a JSON response if successful
        if err == nil {
            return c.JSON(http.StatusCreated, H{
                "created": id,
            })
        // Handle any errors
        } else {
            return err
        }
    }
}

// DeleteTask endpoint
func DeleteTask(db *sql.DB) echo.HandlerFunc {
    return func(c echo.Context) error {
        id, _ := strconv.Atoi(c.Param("id"))
        // Use our new model to delete a task
        _, err := models.DeleteTask(db, id)
        // Return a JSON response on success
        if err == nil {
            return c.JSON(http.StatusOK, H{
                "deleted": id,
            })
        // Handle errors
        } else {
            return err
        }
    }
}

现在在 “PutTask” 函数中,你会看到 “c.Bind”。 将会在 PUT 请求中发送 JSON 格式的响应内容,并将其映射到 Task 结构。 Task 结构将在我们的模型包中定义。

这里还需要注意一些错误检查。 Tasks Model 具有根据操作是否成功返回数据或错误的函数。我们的处理程序需要做出相应的处理。

现在我们可以创建我们的模型。 这是实际与数据库进行交互的。 创建一个名为 “models” 的目录,并在该目录中创建一个名为 “tasks.go” 的文件。

然后引入需要的包。

// models/tasks.go
package models

import (
    "database/sql"

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

接下来,我们需要创建一个 Task 类型,包含两个字段 ID 和 Name。 Go 允许使用反引号将元数据添加到变量。在这种情况下,我们只是定义了每个字段在转换为 JSON 后的样子。 “c.Bind” 函数在填充新 Task 时知道在如何映射 JSON 数据。

另外还需要一个表示 Task 的集合的模型。

// models/tasks.go

// Task is a struct containing Task data
type Task struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

// TaskCollection is collection of Tasks
type TaskCollection struct {
    Tasks []Task `json:"items"`
}

获取任务只需从数据库中查询所有 Task,将其放到 TaskCollection 并返回。

// models/tasks.go

func GetTasks(db *sql.DB) TaskCollection {
    sql := "SELECT * FROM tasks"
    rows, err := db.Query(sql)
    // Exit if the SQL doesn't work for some reason
    if err != nil {
        panic(err)
    }
    // make sure to cleanup when the program exits
    defer rows.Close()

    result := TaskCollection{}
    for rows.Next() {
        task := Task{}
        err2 := rows.Scan(&task.ID, &task.Name)
        // Exit if we get an error
        if err2 != nil {
            panic(err2)
        }
        result.Tasks = append(result.Tasks, task)
    }
    return result
}

PutTask 将新 Task 插入数据库,并在成功时返回新的ID,并在失败时 panic。

// models/tasks.go

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

    // Create a prepared SQL statement
    stmt, err := db.Prepare(sql)
    // Exit if we get an error
    if err != nil {
        panic(err)
    }
    // Make sure to cleanup after the program exits
    defer stmt.Close()

    // Replace the '?' in our prepared statement with 'name'
    result, err2 := stmt.Exec(name)
    // Exit if we get an error
    if err2 != nil {
        panic(err2)
    }

    return result.LastInsertId()
}

DeleteTask 用来删除 Task。

// models/tasks.go

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

    // Create a prepared SQL statement
    stmt, err := db.Prepare(sql)
    // Exit if we get an error
    if err != nil {
        panic(err)
    }

    // Replace the '?' in our prepared statement with 'id'
    result, err2 := stmt.Exec(id)
    // Exit if we get an error
    if err2 != nil {
        panic(err2)
    }

    return result.RowsAffected()
}

请注意,我们通过 “db.Prepare” 在我们的模型函数中使用准备好的 SQL 语句。有两个原因。首先,一个准备好的语句可以被编译和缓存,所以执行多次更快。 其次,最重要的是准备好的语句可以防止 SQL 注入攻击。

现在再次使用 Postman 。 首先,我们将检查 “GET /tasks “ 。正常应该看到 Tasks 为空的 JSON。

现在来添加一个 Task 。 在 Postman 中,将 HTTP请求方式切换到 “PUT”,然后单击 “Body” 选项卡。 选中 “raw” 并选择 JSON(application/json) 作为类型。 在文本框中输入以下内容。

{
    "name": "Foobar"
}

提交后应该收到 “created” 的响应。

记下返回的 id ,因为我们需要它来测试 “DELETE /tasks” 。 就像在前面的例子中一样,将请求方式设置为 “DELETE” 并将 URL 改为 “/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>

现在重新运行我们的应用程序,然后打开 “http://localhost:8000“。

在最后的 “div” 标签下,将我们的 VueJS 代码放在一个 “script” 标签中。 VueJS 代码稍微复杂一点,但也非常明显。 我们有几个创建和删除任务的方法以及一个在初始化时运行的方法。初始化时获取当前存储在数据库中的所有任务。

为了与后端进行通信,我们需要使用 HTTP 客户端。在这种情况下,我们将使用 vue-resource。 可以通过引用 “this.$http” 然后使用任何 HTTP 请求方式(get,put等)来使用它。

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

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

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

         // This is run whenever the page is loaded to make sure we have a current task list
               created: function() {
       // Use the vue-resource $http client to fetch data from the /tasks route
                   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
                       }

            // Post the new task to the /tasks route using the $http client
                       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) {
            // Use the $http client to delete a task by its 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

然后打开 “http://localhost:8000“。

总结

在本教程中,我们学习了如何使用 Echo 框架和 VueJS 创建前端页面和简单的 Go 后端应用。 希望这会激起你对 Go 语言的好奇心,并激励你建立更复杂的 Web 应用。

原文链接

Create a Single Page App With Go, Echo and Vue