使用 Go 模板构建博客
本文由外部贡献者撰写。
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
软件包创建一个简单的博客应用程序。
本博文英文原作者: