教程:使用 Go 模板构建博客 | GoLand
Go 模板功能强大,可以根据 Go 程序中的数据生成文本或 HTML 输出。您可以将对象传递给模板,自定义数据的显示方式。模板通常用于生成网页、电子邮件和其他基于文本的输出。Go 模板非常流行的现实用法在 kubectl 命令行工具中,您可以将模板传递到 --template
标志,根据需要自定义输出。
模板总览
在 Go 中,有两个软件包提供模板功能:text/template 和 html/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 数据库,例如 MySQL 或 PostgreSQL。有关 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
结构的 Title
和 Content
字段的值。您会注意到,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
被设置为PUT
或DELETE
,则此中间件会将请求方法更改为PUT
或DELETE
。此项为必需,因为 HTML 表单仅支持GET
和POST
方法。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()
函数。它将从表单提取 Title
和 Content
字段,并使用 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()
函数。它将从表单中提取 Title
和 Content
字段,并使用 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>
此模板使用 if
和 range
模板函数。 if
函数将检查文章数是否等于零。如果是,它将显示 Nothing to see here
消息,否则,它将显示文章列表。 range
函数将遍历并显示文章列表。 .
变量指的是传递到模板的所有文章的切片。在 range
函数体内,.
变量指的是一篇文章。文章的 ID
和 Title
字段使用点表示法访问。
更新 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>
此模板使用其他三个模板:title
、scripts
和 body
。您将定义这些模板的不同版本,它们将在 base.html
中被替换。这些模板将用于显示页面的标题、脚本和正文。另外,请注意,title
和 body
模板使用 .
变量传递当前对象,这使嵌套模板能够在需要时访问它。
将 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_urls
、remove_script_host
和 convert_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
软件包创建一个简单的博客应用程序。
本文由外部贡献者撰写
Aniket Bhattacharyea
热爱计算机和软件的数学研究生。
GoLand
相关阅读
关于 GoLand
GoLand 是对 JavaScript、TypeScript 和数据库提供扩展支持的跨平台 Go IDE。GoLand 不仅可以让你快速读写和修改 Go 代码,它还能与 Web、前端、后端和数据库共同运作。
GoLand 提供了一系列智能功能,如智能代码补全、一步撤销的安全重构、强大的内置调试器以及具有快速修复的实时错误检测。借助 GoLand,无论是新手还是经验丰富的专业人士,所有 Go 开发者都可以创建快速、高效且可靠的代码。
进一步了解 GoLand
⏬ 戳「阅读原文」了解更多