【译】使用 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 下载。

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

然后创建应用的目录。

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

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

1
2
3
4
5
6
7
// todo.go
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/engine/standard"
)

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

1
2
// todo.go
func main() { }

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
// 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 测试以上接口。

1
2
$ go build todo.go
$ ./todo

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

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

1
2
3
4
5
6
7
8
9
// todo.go
import (
"database/sql"
"github.com/labstack/echo"
"github.com/labstack/echo/engine/standard"
_ "github.com/mattn/go-sqlite3"
)

在 main 方法里增加

1
2
3
4
// todo.go
func main() {
db := initDB("storage.db")
migrate(db)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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 。如果失败程序退出。

然后

1
2
$ go build todo.go
$ ./todo

查看效果。

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

1
$ sqlite3 storage.db

需要安装 SQLite 才可以执行此命令。

此命令会给出提示,输入 “.tables” ,可以列出所有的表,输入 “.quit” 退出。

处理请求

之前已经创建了与前端交互的接口,现在需要创建或删除任务时给客户端真实的结果。这需要几个方法去完成。

在 “todo.go” 中需要引入新的包。

1
2
3
4
5
6
7
8
9
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 包去处理。

1
2
3
4
5
6
7
8
9
10
// 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” 的文件。接下来,我们需要导入一些我们需要的软件包。

1
2
3
4
5
6
7
8
9
10
// handlers/tasks.go
package handlers
import (
"database/sql"
"net/http"
"strconv"
"github.com/labstack/echo"
)

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
// handlers/tasks.go
package handlers
import (
"database/sql"
"net/http"
"strconv"
"go-echo-vue/models"
"github.com/labstack/echo"
)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 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” 的文件。

然后引入需要的包。

1
2
3
4
5
6
7
8
// models/tasks.go
package models
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)

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

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

1
2
3
4
5
6
7
8
9
10
11
12
// 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 并返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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) 作为类型。 在文本框中输入以下内容。

1
2
3
{
"name": "Foobar"
}

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

记下返回的 id ,因为我们需要它来测试 “DELETE /tasks” 。 就像在前面的例子中一样,将请求方式设置为 “DELETE” 并将 URL 改为 “/tasks/:id” 。 在我们以前的测试中用 “id” 替换 “:id”。 你应该得到一个成功的 “deleted” 消息。

现在可以再次请求 “GET /tasks”,正常应该返回 “null”。

前端

现在来处理我们的前端页面。为了简单起见,将我们的 Javascript 代码写在 HTML 中。标记很简单。 我们需要使用一些库,如 Bootstrap,JQuery,当然还有 VueJS。 用户界面只是一个输入框,一些按钮和任务的列表。 创建一个名为 ‘public’ 的目录,并在该目录内创建一个名为 “index.html” 的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- 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等)来使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- 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>

运行

现在我们的应用程序已经完成了。我们需要编译然后运行它。

1
2
$ go build todo.go
$ ./todo

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

总结

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

原文链接

Create a Single Page App With Go, Echo and Vue