第16章:制作一个简单的Web应用,第一部分¶
在前几章中,你学会了如何在网站中显示数据,以及如何用Bootstrap
使页面看起来漂亮。在本章中,你将学习如何创建不同的模型以及如何编辑缩写。
类别¶
你已经创建了用于查看首字母缩写词和用户的页面。现在是时候为类别创建类似的页面了。打开WebsiteController.swift
。在文件的底部,为"所有类别"页面添加一个上下文:
struct AllCategoriesContext: Encodable {
// 1
let title = "All Categories"
// 2
let categories: [Category]
}
下面是这个的作用:
- 为模板定义页面的标题。
- 定义一个要在页面中显示的类别数组。
接下来,在allUsersHandler(_:)
下添加以下内容,为"所有类别"页面创建一个新的路由处理器:
func allCategoriesHandler(_ req: Request)
-> EventLoopFuture<View> {
// 1
Category.query(on: req.db).all().flatMap { categories in
// 2
let context = AllCategoriesContext(categories: categories)
// 3
return req.view.render("allCategories", context)
}
}
下面是这个路由处理程序的工作:
- 像以前一样从数据库中获取所有的类别。
- 创建一个
AllCategoriesContext
。注意,该上下文直接包括查询结果,因为Leaf
可以处理期货。 - 用提供的上下文渲染
allCategories.leaf
模板。
在Resources/Views
中为All Categories
页面创建一个名为allCategories.leaf
的新文件。打开新文件并添加以下内容:
#extend("base"):
<!-- 1 -->
#export("content"):
<h1>All Categories</h1>
<!-- 2 -->
#if(count(categories) > 0):
<table class="table table-bordered table-hover">
<thead class="thead-light">
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<!-- 3 -->
#for(category in categories):
<tr>
<td>
<a href="/categories/#(category.id)">
#(category.name)
</a>
</td>
</tr>
#endfor
</tbody>
</table>
#else:
<h2>There aren’t any categories yet!</h2>
#endif
#endexport
#endextend
这个模板就像所有缩略语的表格,但重要的一点是:
- 设置
content
变量,供base.leaf
使用。 - 检查是否存在任何类别。
- 循环浏览每个类别,并在表格中添加一行名称,链接到一个类别页面。
现在,你需要一种方法来显示一个类别中的所有缩略语。打开,WebsiteController.swift
,在文件的底部为新的类别页添加以下上下文:
struct CategoryContext: Encodable {
// 1
let title: String
// 2
let category: Category
// 3
let acronyms: [Acronym]
}
以下是上下文的内容:
- 一个页面的标题;你将把它设置为类别名称。
- 该页的类别。
- 类别的缩略语。
接下来,在allCategoriesHandler(_:)
下添加以下内容,为该页创建一个路由处理程序:
func categoryHandler(_ req: Request)
-> EventLoopFuture<View> {
// 1
Category.find(req.parameters.get("categoryID"), on: req.db)
.unwrap(or: Abort(.notFound)).flatMap { category in
// 2
category.$acronyms.get(on: req.db).flatMap { acronyms in
// 3
let context = CategoryContext(
title: category.name,
category: category,
acronyms: acronyms)
// 4
return req.view.render("category", context)
}
}
}
以下是路线处理程序的工作:
- 从请求的参数中获取类别,并解包返回的未来。
- 使用
Fluent
的帮助器进行查询,获得该类别的所有缩写。 - 为该页面创建一个上下文。
- 使用
category.leaf
模板返回一个渲染的视图。
在Resources/Views
中创建新的模板文件,category.leaf
。打开新文件并添加以下内容:
#extend("base"):
#export("content"):
<h1>#(category.name)</h1>
#extend("acronymsTable")
#endexport
#endextend
这与用户的页面几乎一样,只是标题用了类别名称。注意,你使用了acronymsTable.leaf
模板来显示缩写表。这避免了重复另一个表,并再次显示了模板的力量。打开base.leaf
,在所有用户页面的链接后添加以下内容:
<li class="nav-item
#if(title == "All Categories"): active #endif">
<a href="/categories" class="nav-link">All Categories</a>
</li>
这就在网站的导航中为所有类别页面添加了一个新链接。最后,打开WebsiteController.swift
,在boot(routes:)
的末尾,添加以下内容来注册新的路由:
// 1
routes.get("categories", use: allCategoriesHandler)
// 2
routes.get("categories", ":categoryID", use: categoryHandler)
下面是这个的作用:
- 在
/categories
注册一个路由,接受GET
请求并调用allCategoriesHandler(_:)
。 - 在
/categories/<CATEGORY ID>
注册一个路由,接受GET
请求并调用categoryHandler(_:)
。
建立并运行,然后在你的浏览器中进入http://localhost:8080/。点击菜单中新的所有类别链接,你将进入新的"所有类别"页面:
点击一个类别,你会看到类别信息页面,上面有该类别的所有缩写:
创建首字母缩写词¶
要在一个Web
应用程序中创建缩写,你实际上必须实现两个路线。你要处理一个GET
请求来显示要填写的表单。然后,你处理一个POST
请求,接受表单发送的数据。
创建首字母缩写的页面需要一个所有用户的列表,以允许选择哪个用户拥有这个首字母缩写。在WebsiteController.swift
的底部创建一个上下文来表示这个:
struct CreateAcronymContext: Encodable {
let title = "Create An Acronym"
let users: [User]
}
接下来,在categoryHandler(_:)
下创建一个路由处理程序来呈现"创建首字母缩写"页面:
func createAcronymHandler(_ req: Request)
-> EventLoopFuture<View> {
// 1
User.query(on: req.db).all().flatMap { users in
// 2
let context = CreateAcronymContext(users: users)
// 3
return req.view.render("createAcronym", context)
}
}
下面是这个的作用:
- 从数据库中获取所有的用户。
- 为模板创建一个上下文。
- 使用
createAcronym.leaf
模板来渲染页面。
接下来,在下面添加createAcronymHandler(_:)
来为POST
请求创建一个路由处理程序:
// 1
func createAcronymPostHandler(_ req: Request) throws
-> EventLoopFuture<Response> {
// 2
let data = try req.content.decode(CreateAcronymData.self)
let acronym = Acronym(
short: data.short,
long: data.long,
userID: data.userID)
// 3
return acronym.save(on: req.db).flatMapThrowing {
// 4
guard let id = acronym.id else {
throw Abort(.internalServerError)
}
// 5
return req.redirect(to: "/acronyms/\(id)")
}
}
下面是这个的作用:
- 声明一个返回
EventLoopFuture<Response>
的路由处理程序。 - 对请求中的数据进行解码,并使用它来创建一个首字母缩写。你在
AcronymsController
中做同样的事情。 - 保存首字母缩写并解决未来。注意这里使用了
flatMapThrowing(_:)
,因为这个闭包不返回未来,但可以抛出。 - 确保ID被设置,否则抛出一个
500 Internal Server Error
。 - 重定向到新创建的首字母缩写的页面。
接下来,为了注册这些路由,在boot(routes:)
的底部添加以下内容:
// 1
routes.get("acronyms", "create", use: createAcronymHandler)
// 2
routes.post("acronyms", "create", use: createAcronymPostHandler)
以下是代码的作用:
- 在
/acronyms/create
注册一个路由,接受GET
请求并调用createAcronymHandler(_:)
。 - 在
/acronyms/create
注册一个路由,接受POST
请求并调用createAcronymPostHandler(_:)
。
现在你需要一个模板来显示创建首字母缩写表。在Resources/Views
中创建一个新文件,称为createAcronym.leaf
。打开该文件并添加以下内容:
<!-- 1 -->
#extend("base"):
#export("content"):
<h1>#(title)</h1>
<!-- 2 -->
<form method="post">
<!-- 3 -->
<div class="form-group">
<label for="short">Acronym</label>
<input type="text" name="short" class="form-control"
id="short"/>
</div>
<!-- 4 -->
<div class="form-group">
<label for="long">Meaning</label>
<input type="text" name="long" class="form-control"
id="long"/>
</div>
<div class="form-group">
<label for="userID">User</label>
<!-- 5 -->
<select name="userID" class="form-control" id="userID">
<!-- 6 -->
#for(user in users):
<option value="#(user.id)">
#(user.name)
</option>
#endfor
</select>
</div>
<!-- 7 -->
<button type="submit" class="btn btn-primary">
Submit
</button>
</form>
#endexport
#endextend
下面是它的作用:
- 扩展
base.leaf
并定义所需的content
变量。 - 创建一个
HTML
表单。将方法设置为POST
。这意味着当用户提交表单时,浏览器会使用POST
请求将数据发送到同一个URL
。 - 为首字母缩写的短值创建一个组。使用
<input>
元素来允许用户插入文本。name
属性告诉浏览器在发送请求中的数据时,这个输入的键应该是什么。 - 使用
HTML <input>
元素为缩写的长值创建一个组。 - 为首字母缩写用户创建一个组。使用
HTML <select>
元素来显示不同用户的下拉菜单。 - 使用
Leaf
的#for()
循环遍历所提供的用户,并将每个用户作为一个选项添加到<select>
上。 - 创建一个用户可以点击的提交按钮,将表单发送到你的网络应用。
最后,在base.leaf
中的</ul>
标签之前添加一个指向新页面的链接:
<!-- 1 -->
<li class="nav-item
#if(title == "Create An Acronym"): active #endif">
<!-- 2 -->
<a href="/acronyms/create" class="nav-link">
Create An Acronym
</a>
</li>
以下是代码的作用:
- 在导航栏中添加一个新的导航项。如果你在"创建首字母缩写"页面上,标记该项目为活动项目。
- 添加一个指向创建页面的链接。
建立并运行,然后打开你的浏览器。导航到http://localhost:8080,你会在导航栏中看到一个新的选项,"创建首字母缩写"。点击链接,进入新的页面。填写表格并点击Submit
。
该应用程序会将你重定向到新首字母缩写的页面:
编辑缩略语¶
你现在知道如何通过网站创建缩略语了。但是,编辑缩略语呢?感谢Leaf
,你可以重复使用许多相同的组件来让用户编辑缩写。打开WebsiteController.swift
。
在文件的末尾,添加以下用于编辑首字母缩写的上下文:
struct EditAcronymContext: Encodable {
// 1
let title = "Edit Acronym"
// 2
let acronym: Acronym
// 3
let users: [User]
// 4
let editing = true
}
以下是上下文的内容:
- 该页的标题:"编辑缩略语"。
- 要编辑的首字母缩写。
- 要在表格中显示的用户数组。
- 一个标志,告诉模板这个页面是用来编辑首字母缩写的。
接下来,在createAcronymPostHandler(_:)
下面添加以下路由处理程序,以显示编辑缩略语的表单:
func editAcronymHandler(_ req: Request)
-> EventLoopFuture<View> {
// 1
let acronymFuture = Acronym
.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
// 2
let userQuery = User.query(on: req.db).all()
// 3
return acronymFuture.and(userQuery)
.flatMap { acronym, users in
// 4
let context = EditAcronymContext(
acronym: acronym,
users: users)
// 5
return req.view.render("createAcronym", context)
}
}
以下是这条路线的作用:
- 创建一个未来,从请求的参数中获得要编辑的首字母缩写。
- 创建一个未来,从数据库中获得所有的用户。
- 使用
.and(_:)
将期货链在一起,flatMap(_:)
等待两个期货的完成。 - 创建一个上下文来编辑缩略语,传入所有的用户。
- 使用
createAcronym.leaf
模板渲染该页面,与创建页面使用的模板相同。
接下来,为编辑首字母缩写页面的POST
请求添加以下路由处理程序editAcronymHandler(_:)
:
func editAcronymPostHandler(_ req: Request) throws
-> EventLoopFuture<Response> {
// 1
let updateData =
try req.content.decode(CreateAcronymData.self)
// 2
return Acronym
.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound)).flatMap { acronym in
// 3
acronym.short = updateData.short
acronym.long = updateData.long
acronym.$user.id = updateData.userID
// 4
guard let id = acronym.id else {
let error = Abort(.internalServerError)
return req.eventLoop.future(error: error)
}
// 5
let redirect = req.redirect(to: "/acronyms/\(id)")
return acronym.save(on: req.db).transform(to: redirect)
}
}
以下是该路线的作用:
- 将请求体解码为
CreateAcronymData
。 - 从请求的参数中获取要编辑的首字母缩写,并解决
future
。 - 用新的数据更新首字母缩写。
- 确保
ID
被设置,否则返回一个失败的未来,并出现500 Internal Server Error
。 - 保存更新后的首字母缩写,并将结果转化为重定向到更新后的首字母缩写的页面。
接下来,在boot(routes:)
的底部添加以下内容来注册两个新的路由:
routes.get(
"acronyms", ":acronymID", "edit",
use: editAcronymHandler)
routes.post(
"acronyms", ":acronymID", "edit",
use: editAcronymPostHandler)
这在/acronyms/<ACRONYM ID>/edit
注册了一个路由,接受GET请求,调用editAcronymHandler(_:)
。它还注册了一个路由来处理对同一URL
的POST
请求,调用editAcronymPostHandler(_:)
。
打开createAcronym.leaf
,修改模板以适应编辑首字母缩写。首先,替换首字母缩写的输入,以适应编辑:
<input type="text" name="short" class="form-control"
id="short" #if(editing): value="#(acronym.short)" #endif/>
如果editing
标志被设置,这将把<input>
的value
属性设置为首字母缩写的short
属性。这就是你为编辑而预先填充表格的方法。对首字母缩写的长输入做同样的处理:
<input type="text" name="long" class="form-control"
id="long" #if(editing): value="#(acronym.long)" #endif/>
替换用户的<select>
选项进行编辑:
<option value="#(user.id)"
#if(editing): #if(acronym.user.id == user.id):
selected #endif #endif>
#(user.name)
</option>
如果用户的ID
与首字母缩写的userID
相符,这将设置<option>
的selected
属性。这使得下拉菜单中的那个选项显示为选中的选项。接下来,替换提交表单的按钮:
<button type="submit" class="btn btn-primary">
#if(editing): Update #else: Submit #endif
</button>
这使用Leaf
的#if()/else
标签,根据页面的模式,将按钮的文本设置为Update
或Submit
。
最后,打开acronym.leaf,在#export("content"):
的底部添加一个编辑该缩写的按钮:
<a class="btn btn-primary" href="/acronyms/#(acronym.id)/edit"
role="button">Edit</a>
这将创建一个HTML链接到/acronyms/<ACRONYM ID>/edit
,并使用Bootstrap
将该链接设计成一个按钮。保存文件并在Xcode
中建立和运行该应用程序。在你的浏览器中打开http://localhost:8080/。
打开一个缩写页面,现在底部有一个Edit
按钮:
点击Edit
,进入编辑缩略语页面,所有信息都已预先填入。标题和按钮也是不同的:
改变首字母缩写,然后点击Update
。该应用程序会将你重定向到缩写的页面,你会看到更新的信息。
删除首字母缩写¶
与创建和编辑缩略语不同,删除缩略语只需要一条途径。然而,对于网络浏览器,没有简单的方法来发送DELETE
请求。
浏览器只能发送请求页面的GET
请求和发送数据的POST
请求与表单。
Note
用JavaScript
发送DELETE
请求是可能的,但这不属于本章的范围。
为了解决这个问题,你将发送一个POST
请求到一个删除路由。
打开WebsiteController.swift
,在editAcronymPostHandler(_:)
下面添加以下路由处理程序,以删除一个首字母缩写词:
func deleteAcronymHandler(_ req: Request)
-> EventLoopFuture<Response> {
Acronym
.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound)).flatMap { acronym in
acronym.delete(on: req.db)
.transform(to: req.redirect(to: "/"))
}
}
这个路由从请求的参数中提取首字母,解开未来,并对首字母调用delete(on:)
。然后该路由对结果进行转换,将页面重定向到主屏幕。在boot(route:)
的底部注册该路由:
routes.post(
"acronyms", ":acronymID", "delete",
use: deleteAcronymHandler)
这在/acronyms/<ACRONYM ID>/delete
注册了一个路由,接受POST
请求并调用deleteAcronymHandler(_:)
。建立并运行。打开acronym.leaf
,用下面的内容替换编辑按钮:
<!-- 1 -->
<form method="post" action="/acronyms/#(acronym.id)/delete">
<!-- 2 -->
<a class="btn btn-primary" href="/acronyms/#(acronym.id)/edit"
role="button">Edit</a>
<!-- 3 -->
<input class="btn btn-danger" type="submit" value="Delete" />
</form>
以下是新代码的作用:
- 声明一个发送
POST
请求的表单。将action
属性设置为/acronyms/<ACRONYM ID>/delete
。对于修改数据库的操作,如创建或删除,使用POST
请求是一个好的做法。例如,这使你能够在将来用CSRF
(跨网站请求伪造)令牌来保护它们。 - 纳入页面上已经存在的编辑按钮。这使得
Bootstrap
能够将它们对齐。使用Bootstrap
的按钮样式,这样按钮看起来就一样了。 - 为删除表单创建一个提交按钮。
保存该文件,然后在浏览器中打开http://localhost:8080/。打开一个缩略语页面,你会看到删除按钮:
点击Delete
来删除首字母缩写。该应用程序会将你重定向到主页,被删除的首字母缩写不再显示。
接下来去哪?¶
在这一章中,你学到了如何显示你的类别以及如何创建、编辑和删除首字母缩写。你仍然需要完成对类别的支持,允许你的用户将首字母缩写放入类别并删除它们。你将在下一章中学习如何做到这一点!