跳转至

第16章:制作一个简单的Web应用,第一部分

在前几章中,你学会了如何在网站中显示数据,以及如何用Bootstrap使页面看起来漂亮。在本章中,你将学习如何创建不同的模型以及如何编辑缩写。

类别

你已经创建了用于查看首字母缩写词和用户的页面。现在是时候为类别创建类似的页面了。打开WebsiteController.swift。在文件的底部,为"所有类别"页面添加一个上下文:

struct AllCategoriesContext: Encodable {
  // 1
  let title = "All Categories"
  // 2
  let categories: [Category]
}

下面是这个的作用:

  1. 为模板定义页面的标题。
  2. 定义一个要在页面中显示的类别数组。

接下来,在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)
  }
}

下面是这个路由处理程序的工作:

  1. 像以前一样从数据库中获取所有的类别。
  2. 创建一个AllCategoriesContext。注意,该上下文直接包括查询结果,因为Leaf可以处理期货。
  3. 用提供的上下文渲染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

这个模板就像所有缩略语的表格,但重要的一点是:

  1. 设置content变量,供base.leaf使用。
  2. 检查是否存在任何类别。
  3. 循环浏览每个类别,并在表格中添加一行名称,链接到一个类别页面。

现在,你需要一种方法来显示一个类别中的所有缩略语。打开,WebsiteController.swift,在文件的底部为新的类别页添加以下上下文:

struct CategoryContext: Encodable {
  // 1
  let title: String
  // 2
  let category: Category
  // 3
  let acronyms: [Acronym]
}

以下是上下文的内容:

  1. 一个页面的标题;你将把它设置为类别名称。
  2. 该页的类别。
  3. 类别的缩略语。

接下来,在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)
      }
  }
}

以下是路线处理程序的工作:

  1. 从请求的参数中获取类别,并解包返回的未来。
  2. 使用Fluent的帮助器进行查询,获得该类别的所有缩写。
  3. 为该页面创建一个上下文。
  4. 使用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)

下面是这个的作用:

  1. /categories注册一个路由,接受GET请求并调用allCategoriesHandler(_:)
  2. /categories/<CATEGORY ID>注册一个路由,接受GET请求并调用categoryHandler(_:)

建立并运行,然后在你的浏览器中进入http://localhost:8080/。点击菜单中新的所有类别链接,你将进入新的"所有类别"页面:

img

点击一个类别,你会看到类别信息页面,上面有该类别的所有缩写:

img

创建首字母缩写词

要在一个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)
  }
}

下面是这个的作用:

  1. 从数据库中获取所有的用户。
  2. 为模板创建一个上下文。
  3. 使用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)")
  }
}

下面是这个的作用:

  1. 声明一个返回EventLoopFuture<Response>的路由处理程序。
  2. 对请求中的数据进行解码,并使用它来创建一个首字母缩写。你在AcronymsController中做同样的事情。
  3. 保存首字母缩写并解决未来。注意这里使用了flatMapThrowing(_:),因为这个闭包不返回未来,但可以抛出。
  4. 确保ID被设置,否则抛出一个500 Internal Server Error
  5. 重定向到新创建的首字母缩写的页面。

接下来,为了注册这些路由,在boot(routes:)的底部添加以下内容:

// 1
routes.get("acronyms", "create", use: createAcronymHandler)
// 2
routes.post("acronyms", "create", use: createAcronymPostHandler)

以下是代码的作用:

  1. /acronyms/create注册一个路由,接受GET请求并调用createAcronymHandler(_:)
  2. /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

下面是它的作用:

  1. 扩展base.leaf并定义所需的content变量。
  2. 创建一个HTML表单。将方法设置为POST。这意味着当用户提交表单时,浏览器会使用POST请求将数据发送到同一个URL
  3. 为首字母缩写的短值创建一个组。使用 <input>元素来允许用户插入文本。name属性告诉浏览器在发送请求中的数据时,这个输入的键应该是什么。
  4. 使用HTML <input>元素为缩写的长值创建一个组。
  5. 为首字母缩写用户创建一个组。使用HTML <select>元素来显示不同用户的下拉菜单。
  6. 使用Leaf#for()循环遍历所提供的用户,并将每个用户作为一个选项添加到<select>上。
  7. 创建一个用户可以点击的提交按钮,将表单发送到你的网络应用。

最后,在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>

以下是代码的作用:

  1. 在导航栏中添加一个新的导航项。如果你在"创建首字母缩写"页面上,标记该项目为活动项目。
  2. 添加一个指向创建页面的链接。

建立并运行,然后打开你的浏览器。导航到http://localhost:8080,你会在导航栏中看到一个新的选项,"创建首字母缩写"。点击链接,进入新的页面。填写表格并点击Submit

该应用程序会将你重定向到新首字母缩写的页面:

img

编辑缩略语

你现在知道如何通过网站创建缩略语了。但是,编辑缩略语呢?感谢Leaf,你可以重复使用许多相同的组件来让用户编辑缩写。打开WebsiteController.swift

在文件的末尾,添加以下用于编辑首字母缩写的上下文:

struct EditAcronymContext: Encodable {
  // 1
  let title = "Edit Acronym"
  // 2
  let acronym: Acronym
  // 3
  let users: [User]
  // 4
  let editing = true
}

以下是上下文的内容:

  1. 该页的标题:"编辑缩略语"。
  2. 要编辑的首字母缩写。
  3. 要在表格中显示的用户数组。
  4. 一个标志,告诉模板这个页面是用来编辑首字母缩写的。

接下来,在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)
  }
}

以下是这条路线的作用:

  1. 创建一个未来,从请求的参数中获得要编辑的首字母缩写。
  2. 创建一个未来,从数据库中获得所有的用户。
  3. 使用.and(_:)将期货链在一起,flatMap(_:)等待两个期货的完成。
  4. 创建一个上下文来编辑缩略语,传入所有的用户。
  5. 使用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)
  }
}

以下是该路线的作用:

  1. 将请求体解码为CreateAcronymData
  2. 从请求的参数中获取要编辑的首字母缩写,并解决future
  3. 用新的数据更新首字母缩写。
  4. 确保ID被设置,否则返回一个失败的未来,并出现500 Internal Server Error
  5. 保存更新后的首字母缩写,并将结果转化为重定向到更新后的首字母缩写的页面。

接下来,在boot(routes:)的底部添加以下内容来注册两个新的路由:

routes.get(
  "acronyms", ":acronymID", "edit",
   use: editAcronymHandler)
routes.post(
  "acronyms", ":acronymID", "edit", 
  use: editAcronymPostHandler)

这在/acronyms/<ACRONYM ID>/edit注册了一个路由,接受GET请求,调用editAcronymHandler(_:)。它还注册了一个路由来处理对同一URLPOST请求,调用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标签,根据页面的模式,将按钮的文本设置为UpdateSubmit

最后,打开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按钮:

img

点击Edit,进入编辑缩略语页面,所有信息都已预先填入。标题和按钮也是不同的:

img

改变首字母缩写,然后点击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>&nbsp;
  <!-- 3 -->
  <input class="btn btn-danger" type="submit" value="Delete" />
</form>

以下是新代码的作用:

  1. 声明一个发送POST请求的表单。将action属性设置为/acronyms/<ACRONYM ID>/delete。对于修改数据库的操作,如创建或删除,使用POST请求是一个好的做法。例如,这使你能够在将来用CSRF(跨网站请求伪造)令牌来保护它们。
  2. 纳入页面上已经存在的编辑按钮。这使得Bootstrap能够将它们对齐。使用Bootstrap的按钮样式,这样按钮看起来就一样了。
  3. 为删除表单创建一个提交按钮。

保存该文件,然后在浏览器中打开http://localhost:8080/。打开一个缩略语页面,你会看到删除按钮:

img

点击Delete来删除首字母缩写。该应用程序会将你重定向到主页,被删除的首字母缩写不再显示。

接下来去哪?

在这一章中,你学到了如何显示你的类别以及如何创建、编辑和删除首字母缩写。你仍然需要完成对类别的支持,允许你的用户将首字母缩写放入类别并删除它们。你将在下一章中学习如何做到这一点!