Tutorials

使用 Go 模板构建博客

Read this post in other languages:

本文由外部贡献者撰写。

Aniket Bhattacharyea

热爱计算机和软件的数学研究生。

网站

 

Go 模板功能强大,可以根据 Go 程序中的数据生成文本或 HTML 输出。 您可以将对象传递给模板,自定义数据的显示方式。 模板通常用于生成网页、电子邮件和其他基于文本的输出。 Go 模板非常流行的现实用法在 kubectl 命令行工具中,您可以将模板传递到 --template 标志,根据需要自定义输出。

模板总览

在 Go 中,有两个软件包提供模板功能:text/templatehtml/template 软件包。 两者具有完全相同的接口集,唯一的区别是后者自动保护 HTML 输出免受各种攻击。 html/template 因此是生成 HTML 输出的更好选择,这也是本文使用 html/template 软件包的原因。

模板是字符串,包含使用双花括号括起来的操作特殊命令。 这些操作用于访问或评估数据,或控制模板的结构。

以下是一个示例模板:

 

tmpl := "Hello {{.Name}}!"

 

以上模板只有一个操作,输出传递到模板的数据对象的 Name 字段的值。 操作中的 . 字符指的是传递到模板的数据对象,.Name 访问对象的 Name 字段。

要呈现模板,必须通过 Parse 函数解析模板,并使用 Execute 函数将写入器和数据对象作为实参并将输出写入写入器。

 

// Defining the data to pass
type User struct {
    Name string
}

user := User{"James"}
t, err := template.New("test").Parse(tmpl)
if err != nil {
    panic(err)
}
err = t.Execute(os.Stdout, user)
if err != nil {
    panic(err)
}

 

上方代码会将 Hello James! 输出到控制台。

除了访问数据之外,您还可以使用 if 等操作有条件地呈现内容,使用 range 迭代集合。 您还可以定义自己的函数并在模板中使用。 这里是模板的完整概述。

使用模板构建博客

在按照教程操作前,您需要在系统上安装并设置 Go。 您还需要安装 GoLand

您可以在 GitHub 上找到此教程的代码。 您可以随意克隆和研究代码,也可以按照教程从头开始创建应用程序。

创建新项目

[返回页首]

启动 GoLand,点击 New project(新建项目)按钮。 选择 Go 选项并提供一个名称,例如“GoBlog”。

点击 Create(创建)创建项目。 在项目的根目录下创建文件 main.go,内容如下:

 

package main

import (
    "database/sql"
    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    _ "github.com/mattn/go-sqlite3"
)

 

模块将以红色高亮显示。 这是因为这些模块尚未下载。

GoLand 可以自动为您下载。 将鼠标悬停在高亮显示的条目上,然后在出现的弹出窗口中点击 Sync dependencies of GoBlog(同步 GoBlog 的依赖项)。

几秒钟后,模块开始下载,红色高亮显示消失。

请注意,如果您启用 Auto-format on save(保存时自动格式化),一旦文件被保存(例如,窗口失去焦点时),GoLand 将格式化代码并移除未使用的依赖项。 在 Preference / Settings | Tools | Actions on Save | Configure autosave options… | Autosave(偏好设置/设置 | 工具 | 保存时的操作 | 配置自动保存选项… | 自动保存)中切换到不同的应用程序或内置终端时,可以禁用自动保存。

您也可以手动添加导入。 Sync Dependencies(同步依赖项)操作将软件包下载到缓存内存,手动添加导入时,您会获得自动补全建议。

作为编写服务器代码的第一步,在 main.go 中声明以下两个全局变量:

 

var router *chi.Mux
var db *sql.DB

 

虽然可以使用标准库设置 REST 服务器,但本文将为此使用 chi router – chi 是易于使用、轻量级且速度极快的 router,捆绑了许多功能。

router 将存储 chi router 实例,db 将存储数据库对象。

定义 Article 结构,它将具有标题、内容和 ID:

 

type Article struct {
    ID      int           `json:"id"`
    Title   string        `json:"title"`
    Content template.HTML `json:"content"`
}

 

注意,对于 Content,使用的是 template.HTML 而不是 string。 因为内容将是富文本并呈现为 HTML,需要使用 template.HTML 防止 HTML 在呈现模板时被转义。

设置数据库

[返回页首]

创建文件 db.go,用于存放数据库相关功能。 首先,导入必要模块:

 

package main

import (
    "database/sql"
)

 

在本文中,您将使用 database/sql 软件包与数据库交互。 但是,模板系统不受数据库软件包选择的影响,您可以将其与任何其他数据库软件包一起使用。

本教程将使用 SQLite 数据库存储数据,但您可以使用任何其他 SQL 数据库,例如 MySQLPostgreSQL。 有关 database/sql 支持的数据库的完整列表,请参见此处

定义 connect 函数,它将创建与数据库的初始连接。 您将使用本地文件 data.sqlite 存储数据。 如果文件不存在,它将被自动创建。

 

func connect() (*sql.DB, error) {
    var err error
    db, err = sql.Open("sqlite3", "./data.sqlite")
    if err != nil {
        return nil, err
    }

    sqlStmt := `
    create table if not exists articles (id integer not null primary key autoincrement, title text, content text);
    `

    _, err = db.Exec(sqlStmt)
    if err != nil {
        return nil, err
    }

    return db, nil
}

 

SQL 语句将在 GoLand 中高亮显示。

GoLand 具有一个内置数据库插件,可以连接到不同的数据库,用于在 IDE 内查询、创建和管理表。 使用插件前,您需要配置一个数据源。 点击高亮显示的 SQL 语句,然后在上下文操作菜单(黄色灯泡图标)中点击 Configure data source(配置数据源)。 或者,您也可以点击高亮显示区域并按 Alt+Enter (⌥ ↩) 查看可用的意图操作。

在出现的对话框中点击 + 添加新数据源,选择 SQLite 作为数据库类型。 为数据库提供一个名称,例如“Database”,并使用 data.sqlite 作为文件名。 如果没有安装适当的数据库驱动程序,系统会提示安装。 准备就绪后,点击 OK(确定)保存数据源。

如果文件不存在,GoLand 将创建该文件并连接到它,然后打开数据库控制台。 您可以在这里编写 SQL 查询。 您也可以在右侧的 Database(数据库)工具窗口中看到新创建的数据库。

返回代码,将光标放在 create table SQL 语句上,然后按 Ctrl+Enter (⌘↩)。 从弹出窗口菜单选择控制台,GoLand 将在控制台中运行查询。

您会在日志中看到表成功创建;Database(数据库)中也将有所体现。

我们来编写其余的数据库函数。 dbCreateArticle() 函数将从 Article 结构在数据库中创建新文章:

 

func dbCreateArticle(article *Article) error {
    query, err := db.Prepare("insert into articles(title,content) values (?,?)")
    defer query.Close()

    if err != nil {
        return err
    }
    _, err = query.Exec(article.Title, article.Content)

    if err != nil {
        return err
    }

    return nil
}

 

这里有一条准备好的语句,用于将文章插入数据库。 ? 占位符将被替换为 Article 结构的 TitleContent 字段的值。 您会注意到,GoLand 将正确识别嵌入式 SQL 语句并将其高亮显示。

和先前一样,您可以将光标置于 SQL 语句上并按 Ctrl+Enter (⌘↩) 运行查询。 这次,系统会提示您为占位符提供值。 输入值,点击 Execute(执行)。

请注意,需要为值添加引号,因为这些值将被逐字替换。

在 Database(数据库)工具窗口中双击表名即可查看数据库中的所有行。 新创建的文章也应该在这里显示。

dbGetAllArticles() 函数会将数据库中的所有文章作为 Article 结构的切片返回:

 

func dbGetAllArticles() ([]*Article, error) {
    query, err := db.Prepare("select id, title, content from articles")
    defer query.Close()

    if err != nil {
        return nil, err
    }
    result, err := query.Query()

    if err != nil {
        return nil, err
    }
    articles := make([]*Article, 0)
    for result.Next() {
        data := new(Article)
        err := result.Scan(
            &data.ID,
            &data.Title,
            &data.Content,
        )
        if err != nil {
            return nil, err
        }
        articles = append(articles, data)
    }

    return articles, nil
}

 

dbGetArticle() 函数将根据 ID 从数据库返回一篇文章:

 

func dbGetArticle(articleID string) (*Article, error) {
    query, err := db.Prepare("select id, title, content from articles where id = ?")
    defer query.Close()

    if err != nil {
        return nil, err
    }
    result := query.QueryRow(articleID)
    data := new(Article)
    err = result.Scan(&data.ID, &data.Title, &data.Content)

    if err != nil {
        return nil, err
    }

    return data, nil
}

 

最后一部分是 dbUpdateArticle()dbDeleteArticle() 函数,它们分别从数据库更新和删除文章:

 

func dbUpdateArticle(id string, article *Article) error {
    query, err := db.Prepare("update articles set (title, content) = (?,?) where id=?")
    defer query.Close()

    if err != nil {
        return err
    }
    _, err = query.Exec(article.Title, article.Content, id)

    if err != nil {
        return err
    }

    return nil
}

func dbDeleteArticle(id string) error {
    query, err := db.Prepare("delete from articles where id=?")
    defer query.Close()

    if err != nil {
        return err
    }
    _, err = query.Exec(id)

    if err != nil {
        return err
    }

    return nil
}

 

创建路由

[返回页首]

数据库函数完成后,返回 main.go 编写服务器的其余部分。 从编写 catch() 函数开始,它会在出现错误时引发宕机:

 

func catch(err error) {
    if err != nil {
        fmt.Println(err)
        panic(err)
    }
}

 

main() 函数是设置路由和中间件的地方。 在 main.go 中添加如下代码:

 

func main() {
    router = chi.NewRouter()
    router.Use(middleware.Recoverer)

    var err error
    db, err = connect()
    catch(err)

    router.Use(ChangeMethod)
    router.Get("/", GetAllArticles)
    router.Route("/articles", func(r chi.Router) {
        r.Get("/", NewArticle)
        r.Post("/", CreateArticle)
        r.Route("/{articleID}", func(r chi.Router) {
            r.Use(ArticleCtx)
            r.Get("/", GetArticle) // GET /articles/1234
            r.Put("/", UpdateArticle)    // PUT /articles/1234
            r.Delete("/", DeleteArticle) // DELETE /articles/1234
            r.Get("/edit", EditArticle) // GET /articles/1234/edit
        })
    })

    err = http.ListenAndServe(":8005", router)
    catch(err)
}

 

注意 Recoverer 中间件的使用。 当 catch() 函数引发宕机时,此中间件将恢复服务器,使用堆栈跟踪记录错误,并向客户端发送 500 Internal Server Error(500 内部服务器错误)响应。

上方代码会设置以下路由:

  • GET /:显示数据库中的所有文章。
  • GET /articles:显示用于创建新文章的表单。
  • POST /articles:在数据库中创建新文章。
  • GET /articles/{articleID}:显示一篇文章。
  • PUT /articles/{articleID}:更新数据库中的文章。
  • DELETE /articles/{articleID}:从数据库删除文章。
  • GET /articles/{articleID}/edit:显示用于编辑文章的表单。

除了路由之外,代码还设置两个中间件:

  • ChangeMethod:如果请求方法是 POST 并且表单字段 _method 被设置为 PUTDELETE,则此中间件会将请求方法更改为 PUTDELETE。 此项为必需,因为 HTML 表单仅支持 GETPOST 方法。
  • ArticleCtx:此中间件将从数据库获取文章并将其存储在请求上下文中。 它将被 /articles/{articleID} 路径下的路由使用。

我们先来编写 ChangeMethod 函数。 如前所述,它将查找 _method 表单元素并相应地更改请求方法:

 

func ChangeMethod(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodPost {
            switch method := r.PostFormValue("_method"); method {
            case http.MethodPut:
                fallthrough
            case http.MethodPatch:
                fallthrough
            case http.MethodDelete:
                r.Method = method
            default:
            }
        }
        next.ServeHTTP(w, r)
    })
}

 

ArticleCtx 中间件将从 URL 形参访问文章 ID,并从数据库提取文章。 如果找到文章,它将被存储在请求上下文中。 如果找不到文章,中间件会返回 404 状态代码:

 

func ArticleCtx(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        articleID := chi.URLParam(r, "articleID")
        article, err := dbGetArticle(articleID)
        if err != nil {
            fmt.Println(err)
            http.Error(w, http.StatusText(404), 404)
            return
        }
        ctx := context.WithValue(r.Context(), "article", article)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

 

GetAllArticles() 函数将使用 dbGetAllArticles() 函数从数据库提取所有文章,并通过呈现模板将其显示在网页上。 现在,将呈现部分留空:

 

func GetAllArticles(w http.ResponseWriter, r *http.Request) {
    articles, err := dbGetAllArticles()
    catch(err)
    fmt.Println(articles)

    //TODO: Render template
}

 

NewArticle() 函数将显示用于创建新文章的表单。 它不需要与数据库交互,因此非常基础:

 

func NewArticle(w http.ResponseWriter, r *http.Request) {
    //TODO: Render template
}

 

提交用于创建新文章的表单后,将调用 CreateArticle() 函数。 它将从表单提取 TitleContent 字段,并使用 dbCreateArticle() 函数在数据库中创建新文章。 然后,它会将用户重定向到 / 页面:

 

func CreateArticle(w http.ResponseWriter, r *http.Request) {
    title := r.FormValue("title")
    content := r.FormValue("content")
    article := &Article{
        Title:   title,
        Content: template.HTML(content),
    }

    err := dbCreateArticle(article)
    catch(err)
    http.Redirect(w, r, "/", http.StatusFound)
}

 

GetArticle() 函数将显示一篇文章。 得益于 ArticleCtx 中间件,您不需要在这里提取文章 – 您可以简单地从请求上下文中获得它:

 

func GetArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)
    fmt.Println(article)
    //TODO: Render template
}

 

EditArticle 函数将显示用于编辑文章的表单:

 

func EditArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)
    fmt.Println(article)
    // TODO: Render template
}

 

提交用于编辑文章的表单后,将调用 UpdateArticle() 函数。 它将从表单中提取 TitleContent 字段,并使用 dbUpdateArticle() 函数在数据库中更新文章。 然后,它会将用户重定向到 /articles/{articleID} 页面:

 

func UpdateArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)

    title := r.FormValue("title")
    content := r.FormValue("content")
    newArticle := &Article{
        Title:   title,
        Content: template.HTML(content),
    }

    err := dbUpdateArticle(strconv.Itoa(article.ID), newArticle)
    catch(err)
    http.Redirect(w, r, fmt.Sprintf("/articles/%d", article.ID), http.StatusFound)
}

 

最后,DeleteArticle() 函数将从数据库删除文章并将用户重定向到 / 页面:

 

func DeleteArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)
    err := dbDeleteArticle(strconv.Itoa(article.ID))
    catch(err)

    http.Redirect(w, r, "/", http.StatusFound)
}

 

呈现模板

[返回页首]

路由现已准备就绪,下一步是呈现模板。 创建 templates 目录,其中包含一个 index.html 文件,文件将在 GetAllArticles() 函数中呈现。 将以下代码添加到文件中:

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>All articles</title>
</head>
<body>
{{if eq (len .) 0}}
Nothing to see here
{{end}}
{{range .}}
<div>
    <a href="/articles/{{.ID}}">{{.Title}}</a>
</div>
{{end}}
<p>
  <a href="/articles">Create new article</a>

</p>
</body>
</html>

 

此模板使用 ifrange 模板函数。 if 函数将检查文章数是否等于零。 如果是,它将显示 Nothing to see here 消息,否则,它将显示文章列表。 range 函数将遍历并显示文章列表。 . 变量指的是传递到模板的所有文章的切片。 在 range 函数体内,. 变量指的是一篇文章。 文章的 IDTitle 字段使用点表示法访问。

更新 GetAllArticles() 函数以呈现模板:

 

func GetAllArticles(w http.ResponseWriter, r *http.Request) {
    articles, err := dbGetAllArticles()
    catch(err)

    t, _ := template.ParseFiles("templates/index.html")
    err = t.Execute(w, articles)
    catch(err)
}

 

来看一下应用目前的样子。 右键点击 Project(项目)边栏中的项目名称,然后转到 Run(运行)菜单项,点击 go build GoBlog。 这将构建并运行项目。

打开浏览器,导航到 http://localhost:8005。 这将显示先前创建的文章。

接下来,我们将创建用于创建新文章的表单。 由于 Content 字段是富文本,需要富文本编辑器。 本文将使用 TinyMCE 编辑器。 您需要注册一个免费帐户来获得 API 密钥。 获得 API 密钥后,在 templates 目录中创建一个 new.html 文件,向其中添加以下代码:

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Create a new article</title>
    <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
    <script>
        tinymce.init({
            selector: '#mytextarea',
        });
    </script>
</head>
<body>
<h1>Create a new article</h1>
<form method="post" action="/articles">
    <input id="title" type="text" name="title" placeholder="Enter the title">
    <textarea id="mytextarea" name="content"></textarea>
    <button id="submit" type="submit">Create</button>
</form>
</body>
</html>

 

您需要将上述代码中的 no-api-key 换为 TinyMCE API 密钥。

注意,此文件仅是显示一个表单。 它不是严格意义上的模板,而是一个简单的 HTML 文件,因此无需使用 template.ParseFiles() 函数进行解析。 使用 http.ServeFile() 函数即可。 将以下行添加到 NewArticle() 函数中:

 

http.ServeFile(w, r, "templates/new.html")

 

您可以访问 http://localhost:8005/articles 查看表单的实际运作。

如果服务器正在运行,请将其重启。 为此,请使用配置菜单旁边的重启按钮。

每次更改 Go 文件时,您都需要重新启动服务器。 教程中没有明确提到这一点。

嵌套模板

[返回页首]

您可能已经注意到,到目前为止创建的两个模板共享了大量代码。 HTML 页面的基本结构在两个模板中是相同的 – 唯一的区别是页面的标题、内容和脚本。 使用嵌套模板可以避免在多个模板中重复相同的代码。 在 templates 目录中创建一个 base.html 文件,向其中添加以下代码:

 

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{ template "title" . }}</title>
  {{ template "scripts" }}
</head>
<body>
{{ template "body" . }}
</body>
</html>

 

此模板使用其他三个模板:titlescriptsbody。 您将定义这些模板的不同版本,它们将在 base.html 中被替换。 这些模板将用于显示页面的标题、脚本和正文。 另外,请注意,titlebody 模板使用 . 变量传递当前对象,这使嵌套模板能够在需要时访问它。

index.html 的内容替换为以下代码:

 

{{define "title"}}All articles{{end}}
{{define "scripts"}}{{end}}
{{define "body"}}
{{if eq (len .) 0}}
Nothing to see here
{{end}}
{{range .}}
<div>
    <a href="/articles/{{.ID}}">{{.Title}}</a>
</div>
{{end}}
<p>
    <a href="/articles">Create new article</a>

</p>
{{end}}

 

使用 define 函数,您可以定义嵌套模板。 在这里将标题、正文和脚本(为空,因为此页面未加载任何脚本)提取到它们自己的模板中。

为此,您需要修改 GetAllArticles() 函数来加载 base.html 模板以及 index.html

 

t, _ := template.ParseFiles("templates/base.html", "templates/index.html")

 

注意模板的顺序。 base.html 必须在 index.html 之前。

new.html 替换为以下代码:

 

{{define "title"}}Create new article{{end}}
{{define "scripts"}}
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
    tinymce.init({
        selector: '#mytextarea',
    });
</script>
{{end}}
{{define "body"}}
<form method="post" action="/articles">
    <input type="text" name="title" placeholder="Enter the title">
    <textarea id="mytextarea" name="content"></textarea>
    <button id="submit" type="submit">Create</button>
</form>
{{end}}

 

由于 new.html 现在是模板,http.ServeFile 将不再起作用。 您需要解析并执行模板:

 

func NewArticle(w http.ResponseWriter, r *http.Request) {
    t, _ := template.ParseFiles("templates/base.html", "templates/new.html")
    err := t.Execute(w, nil)
    catch(err)
}

 

此时,您可以使用表单创建新文章。

点击 Create(创建)后,您将被重定向到根 URL,新创建的文章将在列表中出现。

您会注意到富文本作为 HTML 存储在数据库中。

创建文件 article.html,它将显示一篇文章:

 

{{define "title"}}{{.Title}}{{end}}
{{define "scripts"}}{{end}}
{{define "body"}}
<h1>{{.Title}} </h1>
<div>
  {{.Content}}
</div>
<div>
  <a href="/articles/{{.ID}}/edit">Edit</a>
  <form action="/articles/{{.ID}}" method="post">
    <input type="hidden" name="_method" value="DELETE">
    <button type="submit">Delete</button>
  </form>
</div>
{{end}}

 

此页面有一个指向编辑页面的链接和一个删除文章的表单。 注意隐藏的 _method 元素。 如前文所述,此元素会将请求转换为 ChangeMethod 中间件提供的 DELETE 请求。

修改 GetArticle 函数以呈现模板:

 

func GetArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)
    t, _ := template.ParseFiles("templates/base.html", "templates/article.html")
    err := t.Execute(w, article)
    catch(err)
}

 

点击首页上文章的标题将显示该文章。

图像上传

[返回页首]

TinyMCE 默认支持图像上传。 您可以将图像拖放到编辑器中,它们将被转换为 base64 字符串并存储为 Content 字段的一部分。

图像也将显示在文章页面上。

但是,将图像存储为 base64 字符串并不是好的做法,因此,我们来添加对图像上传的支持。

向 router 添加两条新路由:

 

func main() {
    ...
    router.Use(ChangeMethod)
    router.Get("/", GetAllArticles)
    router.Post("/upload", UploadHandler) // Add this
    router.Get("/images/*", ServeImages) // Add this
    router.Route("/articles", func(r chi.Router) {
        ...
    })

    ...
}

 

/upload 路由将处理图像上传并将其作为文件存储在 images 目录中,/images/* 路由将提供图像。

我们来编写 UploadHandler 函数:

 

func UploadHandler(w http.ResponseWriter, r *http.Request) {
    const MAX_UPLOAD_SIZE = 10 << 20 // Set the max upload size to 10 MB
    r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE)
    if err := r.ParseMultipartForm(MAX_UPLOAD_SIZE); err != nil {
        http.Error(w, "The uploaded file is too big. Please choose a file that's less than 10MB in size", http.StatusBadRequest)
        return
    }

    file, fileHeader, err := r.FormFile("file")
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    defer file.Close()

    // Create the uploads folder if it doesn't already exist
    err = os.MkdirAll("./images", os.ModePerm)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Create a new file in the uploads directory
    filename := fmt.Sprintf("/images/%d%s", time.Now().UnixNano(), filepath.Ext(fileHeader.Filename))
    dst, err := os.Create("." + filename)
    if err != nil {
        fmt.Println(err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    defer dst.Close()

    // Copy the uploaded file to  the specified destination
    _, err = io.Copy(dst, file)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    fmt.Println(filename)
    response, _ := json.Marshal(map[string]string{"location": filename})
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    w.Write(response)
}

 

此函数会在 images 目录中创建一个新文件,并将传入文件复制到这个新文件中。 然后,它将文件的位置作为 JSON 响应返回,供 TinyMCE 关联图像。

ServeImages 函数非常直观:

 

func ServeImages(w http.ResponseWriter, r *http.Request) {
    fmt.Println(r.URL)
    fs := http.StripPrefix("/images/", http.FileServer(http.Dir("./images")))
    fs.ServeHTTP(w, r)
}

 

它将 images 目录中的文件作为静态文件提供。

最后一步是让 TinyMCE 知道 /upload 路由。 在 new.html 中,使用以下代码修改 tinymce.init 调用:

 

tinymce.init({
    selector: '#mytextarea',
    plugins: 'image',
    toolbar: 'undo redo | blocks | image | ' +
        'bold italic backcolor | alignleft aligncenter ' +
        'alignright alignjustify | bullist numlist outdent indent | ' +
        'removeformat | help',
    images_upload_url: "/upload",
    relative_urls : false,
    remove_script_host : false,
    convert_urls : true,
});

 

plugins 选项可以加载图像上传插件,toolbar 选项可以将图像上传按钮添加到工具栏。 images_upload_url 选项可以指定图像将上传到的路由。 relative_urlsremove_script_hostconvert_urls 选项用于将 /upload 路由返回的相对 URL 转换为绝对 URL。

新文章页面将在工具栏中显示图像上传按钮。

点击 Upload(上传)选项卡,上传需要的任何图像。

它将被上传并链接到文章。

最后,使用以下代码创建模板 edit.html

 

{{define "title"}}Create new article{{end}}
{{define "scripts"}}
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
    tinymce.init({
        selector: '#mytextarea',
        plugins: 'image',
        toolbar: 'undo redo | blocks | image | ' +
            'bold italic backcolor | alignleft aligncenter ' +
            'alignright alignjustify | bullist numlist outdent indent | ' +
            'removeformat | help',
        images_upload_url: "/upload",
        relative_urls : false,
        remove_script_host : false,
        convert_urls : true,
    });
</script>
{{end}}
{{define "body"}}
<form method="post" action="/articles/{{.ID}}">
  <input type="text" name="title" value="{{.Title}}">
  <textarea id="mytextarea" name="content">{{.Content}}</textarea>
  <input type="hidden" name="_method" value="PUT">
  <button id="submit" type="submit" onclick="submitForm()">Edit</button>
</form>
{{end}}

 

它与新文章表单非常相似,只是使用文章的标题和内容填充表单。 表单的 action 特性被设置为 /articles/{id}_method 字段被设置为 PUT,表明表单用于编辑文章。

EditArticle 函数中呈现此模板:

 

func EditArticle(w http.ResponseWriter, r *http.Request) {
    article := r.Context().Value("article").(*Article)

    t, _ := template.ParseFiles("templates/base.html", "templates/edit.html")
    err := t.Execute(w, article)
    catch(err)
}

 

现在,点击文章页面上的 Edit(编辑)按钮即可编辑文章。

编辑后,您将被重定向到同一页面,显示更新后的文章。

您也可以点击 Delete(删除)按钮删除文章。

总结

[返回页首]

如果您想在一个地方查看此项目的所有代码,可以前往此处Go 中的模板提供了与自定义输出格式相关的强大功能。 由于能够输出安全的 HTML,html/template 软件包在 Web 开发中得到广泛应用。 本文介绍了如何使用 html/template 软件包创建一个简单的博客应用程序。

 

本博文英文原作者:

Sue

Sergey Kozlovskiy

Discover more

Go 测试综合指南

本文由外部贡献者撰写。 Alexandre Couëdelo Alexandre 是一位复杂系统工程和管理专家。 他在职业生涯开始时就拥抱了 DevOps 文化,为加拿大一家领先金融机构的数字化转型做出贡献。 他热衷于 DevOps 革命和工业工程。 GitHub Twitter 测试是开发过程的重要部分,也是软件开发生命周期的关键部分。 它可以确保应用程序正常运行和满足客户需求。 本文将涵盖关于 Go 测试的所有须知事项。 我们将从一个简单的测试函数开始,通过更多工具和策略帮助您掌握 Go 中的测试。 您将详细了解许多测试模式,例如用于更好地组织测试用例的表驱动测试、用于验证性能的基准测试,以及用于探索边缘用例并发现错误的模糊测试。 您还将了解来自标准测试软件包及其辅助函数的工具,以及代码覆盖率如何显示正在测试的代码量。 您也将了解 Testify,这是一个可以提高测试可读性的断言和模拟库。 您可以在此 GitHub 仓库中找到所有代码示例。 编写简单的单元测试 单元测试是一种测试函数和方法等小段代码的方法。 它的用途在于让您及早发现错误。 单元测试会让您的测试策略更高效,因为它们小且独立,易于维护。 我们来创建一个示例,练习一下测试。 创建函数 Fooer,它将 int 作为输入并返回 string。 如果输入的整数能被三整除,则返回 "Foo",否则,将数