在 “改变 Grails 应用程序的外观” 一文中,我们看到了如何使用层叠样式表(CSS)对一个 Grails 应用程序 — Blogito blog 站点 — 进行外观更改。这次,我将向您展示如何影响 Web 应用程序的命脉: 用于导航的 URI。这对于像 Blogito 这样的 weblog 极其重要。指向单个条目的那些永久链接(permalink)被像名片一样在 Internet 上传递;描述性越好,就越有效。
要获得描述性更好的 URI,需要定制控制器代码以支持个性化的 URI。还需要处理 UrlMappings.groovy 文件来创建新的路径。最后,您将创建一个定制的 codec 来更为轻松地生成定制 URI。
URI 中的 U 在正式的场合下代表的是 Uniform,但是也可以表示 Unique(参见 参考资料)。如果 URI http://www.ibm.com/developerworks 不能确切标识您目前所处于的 Web 站点,它就没什么用处了。它还能使资源 的标识符 更容易让人记住。通过键入 http://129.42.56.216 虽然可以进入该站点,但是很少有人愿意去记忆这个 Web 站点的用圆点分隔的数字形式的 IP 地址。
所以,URI 至少必须是惟一的。理想情况下,它还应该容易被人记住。(参见 围绕模糊 URI 的争论 侧栏获得对容易被人记住的 URI 的不同看法)。Grails 绝对能够满足第一个要求。它综合使用了控制器名称、闭包名称以及数据库记录的主键以确保 URI 的惟一性。比如,如果想要向用户显示数据库内的第一个 Entry,就让他们在其浏览器内键入 http://localhost:9090/blogito/entry/show/1。
虽然在 URI 内包含主键的默认设置十分合理,但我认为它还是在两个方面违背了美学标准。首先,实现牵涉的内容较多。这个附带的数据库工件贯穿了整个 Web 站点。Google、Amazon 和 eBay 都在后台使用了数据库,但是很难在它们的 URI 内找到任何数据库的迹象。其次,从 URI 删除主键是出于语义的要求。Jane Smith 的 blog 的读者更愿意用 jsmith 作为她的标识,而不是一个数字 12。同样地,按标题而不是主键列出 blog 条目更能满足可记忆 URI 的要求。
Blogito 虽然已经支持条目,但它尚不支持用户。因此,必须先创建一个新的 User 类。
|
首先,在命令行提示符键入 grails create-domain-class User。接下来,将清单 1 内的代码添加到 grails-app/domain/User.groovy:
login 和 password 字段的作用不言自明;它们用来处理身份验证。name 字段用于显示的目的。比如,如果用 jsmith 登录,将会显示 “Jane Smith”。正如您所见,User 和 Entry 之间存在着一对多的关系。
将 static belongsTo 字段添加到 grails-app/domain/Entry.groovy,以完成一对多的关系,如清单 2 所示:
class Entry {
static belongsTo = [author:User]
//snip
}
|
我们注意到,在定义关系时,可以很容易地重命名此字段。User 类具有一个名为 entries 的字段。Entry 类现在具有一个名为 author 的字段。
通常,在此时,都会创建一个相关的 UserController 以提供一个完整的 UI 来管理 Users。我却没有打算这么做。我只是想用几个无存根的 Users 作为占位符。在下一篇 精通 Grails 的文章中,您将更为全面地了解用户身份验证和授权的相关内容。因此,我们走 “刚刚好” 的路线,通过使用 grails-app/conf/BootStrap.groovy 添加几个新用户,如清单 3 所示:
清单 3. 在 BootStrap.groovy 中使用无存根 Users
请注意,我是如何将条目分配给一个 User 的。您无需担心处理主键或外键的麻烦。Grails Object Relational Mapping (GORM) API 让您能从对象的角度而不是关系数据库理论来进行思考。
接下来,对在 上一篇 文章中所创建的 grails-app/views/entry/_entry.gsp 局部模板稍作处理。在 Entry.lastUpdated 字段的旁边显示作者,如清单 4 所示:
${entryInstance.author} 在 User 类上调用 toString() 方法。也可以使用 ${entryInstance.author.name} 来显示您所选择的字段。还可以使用此语法随心所欲地遍历这些类的嵌套结构。
现在,我们就可以看看所做的这些变更的实际效果了。键入 grails run-app 并在 Web 浏览器内访问 http://localhost:9090/blogito/。屏幕应该类似于图 1:

现在 Blogito 可以支持多个用户,下一步是让读者能按作者来查看这些条目。
我们的最终目的就是支持像 http://localhost:9090/blogito/entry/list/jdoe 这样的 URI。注意到,User.login 出现在此 URI 内,而不是主键。在这个过程中,还需要对分页(pagination)做稍许调整。
EntryController.list 的搭建(scaffolded)行为不允许按 User 过滤。清单 5 显示了 list 闭包的默认实现:
def list = {
if(!params.max) params.max = 10
[ entryInstanceList: Entry.list( params ) ]
}
|
若要支持在路径的末尾允许出现一个可选的用户名,还需要对之进行扩展。编辑 grails-app/controllers/EntryController.groovy 并添加一个新的 list 闭包,如清单 6 所示:
您应该注意到的第一件事情是,若终端用户没有提供 params.max 和 params.id 二者的值,就用默认值填充。现在,先不要担心 flash.id — 我稍后在探讨有关分页问题的时候还会对之进行详细讨论。
params.id 值通常是一个整型 — 确切的说是主键。我们一般习惯于 /entry/show/1 和 entry/edit/2 这样的 URI。我本可以在 grails-app/conf/UrlMappings.groovy 内设置一个映射以便返回一个描述性更好的名称,比如 params.name 或 params.login,但现有的映射已经获取了操作名称后的路径元素并将其存储在 params.id 内。我只是充分利用了现有的行为。在 URLMapper.groovy 内,如清单 7 所示,可以看到返回 params.id 的默认映射:
清单 7. UrlMappings.groovy 内的默认映射
由于这不是 User 的主键,所以不能像往常那样使用 User.get(params.id)。相反,必须使用 User.findByLogin(params.id)。
如果找到了一个匹配的 User,就需要创建一个查询块。这就需要用到 Hibernate Criteria Builder(参见 参考资料)。在本例中,我们限制了列表只包含匹配某特定作者的那些条目。同样地,我们注意到 GORM 也允许您从对象而不是主键或外键的角度来思考。
如果没有匹配 params.id 的作者,就会返回全部条目的完整列表: entryList = Entry.list( params )。
注意,entryCount 值是被显式计算出来的。Scaffolded GroovyServer Pages (GSP) 代码通常会在 <g:paginate> 标记内调用 Entry.count()。由于会传递回一个过滤了的列表,所以需要在此控制器的一个变量内处理这一点。
在 flash.id 内存储 params.id 值将允许应用程序将此查询条件传递回 <g:paginate> 标记。调整 grails-app/views/entry/list.gsp 内的 <g:paginate> 以便利用新的 entryCount 变量以及存储在 flash 范围内的参数,如清单 8 所示:
<div class="paginateButtons">
<g:paginate total="${entryCount}" params="${flash}"/>
</div>
|
重启 Grails 并在 Web 浏览器内访问 http://localhost:9090/blogito/entry/list/jsmith。屏幕应该类似图 2:

为了确保分页仍能工作,键入 http://localhost:9090/blogito/entry/list/jsmith?max=1。单击 Previous 和 Next 按钮以确保只有 Jane 的 blog 条目才会出现,如图 3 所示:

按作者过滤的功能就绪后,就可以更进一步,创建一个更为友好的定制 URI。
UrlMappings.groovy 文件为创建新的 URI 提供了额外的灵活性。虽然 http://localhost:9090/blogito/entry/list/jsmith 已经可以发挥作用,但是假设,最新出现的用户请求要求支持 http://localhost:9090/blogito/blog/jsmith 这样的 URI,又该如何呢?没问题!如清单 9 所示那样向 UrlMappings.groovy 添加一个新的映射:
清单 9. 向 UrlMappings.groovy 添加一个新的定制映射
现在,以 /blog 开头的那些 URI 都将会被重新定向到条目控制器和列表动作。虽然 $user 或 $login 的描述性可能更好,但是让 $id 与 Grails 约定保持一致就意味着 "/$controller/$action?/$id?" 和 "/blog/$id"(controller:"entry", action="list") 二者能够指向同一个端点。
在 Web 浏览器内键入 http://localhost:9090/blogito/blog/jsmith 以验证此映射能够工作。
处理好 Users 之后,就可以集中精力为 Entries 创建更友好的 URI。
在使用 User.login 而非 User.id 时,URI 很简单,因为它不包含空白。不错,目前尚没有任何的验证规则强制这种 “无空白” 的要求,但我们可以很轻松地添加一个这样的规则来强制 URI 遵从这一要求(参见 参考资料)。
但是,若在 URI 内用 Entry.title 代替 Entry.id 又如何呢?标题几乎都要包含空白。一种解决方法是向 Entry 类内添加另一个字段并让终端用户重新输入没有空白的标题。这种做法不是很理想,因为它要求用户做更多的工作,而且还要求必须要编写另一个验证规则来确保用户能正确输入。更好的方法是让 Grails 根据使用 Entry.title 的位置自动将空白转变为下划线。要实现此目的,需要创建一个定制 codec(即 编码-解码器 的简写)。
创建 grails-app/utils/UnderscoreCodec 并添加清单 10 所示代码:
Grails 提供了几个开箱即用的内置 codec:HtmlCodec、UrlCodec、Base64Codec 和 JavaScriptCodec(参见 参考资料)。HtmlCodec 是所生成的 GSP 文件内的 encodeAsHtml() 和 decodeHtml() 方法的源代码。
您也可以向其中添加您自己的 codec。Grails 使用 grails-app/utils 目录内任何一个具有 Codec 后缀的类来将 encodeAs() 和 decode() 方法添加到 String。在本例中,Blogito 内的所有 String 都魔法般地具有了两个新方法:encodeAsUnderscore() 和 decodeUnderscore()。
通过在 test/integration 内创建 UnderscoreCodecTests.groovy 可以验证这一点,如清单 11 所示:
在命令行提示符键入 grails test-app 运行测试。所看到的结果应该类似清单 12:
UnderscoreCodec 也就绪后,您就可以支持在 URI 中包括用户和条目标题 — 比如,http://localhost:9090/blogito/blog/jsmith/this_is_my_latest_entry。
首先,调整 UrlMappings.groovy 内的 /blog 映射以支持一个可选的 $title,如清单 13 所示。还记得么,在 Groovy 内,尾部加个问号代表这是可选的。
接下来,调整 EntryController.list 来说明新的 params.title 值,如清单 14 所示:
我已经在此查询内使用了 like 以让此 URI 更为灵活。例如,用户可以键入 /blog/jsmith/mastering_grails 来返回所有以 mastering_grails 开头的标题。如果您愿意更为严格一些,可以使用此查询内的 eq 方法来要求一个确切的匹配。
在 Web 浏览器内键入 http://localhost:9090/blogito/jsmith/Codecs_in_Grails 来观察运行中的这个新的 codec。您的屏幕应该类似图 4:

URI 是一个 Web 应用程序的命脉。Grails 的默认设置是一个很好的开端,但是您也应习惯于定制这些 URI 以最好地满足您的 Web 站点的要求。得益于您的艰苦工作,Blogito 现在具有了 Users 和 Entries。但更为重要的是,您对 URI 使用其他内容而不是主键来查看它们。您了解了如何通过调整控制器代码创建更为友好的 URI、向 UrlMappings.groovy 添加映射以及创建一个定制 codec。
下一次,您将创建一个登录表单以便能对 Blogito Users 进行身份验证。一旦用户登录,他们就能上传一个文件用作 blog 条目的主体 — HTML、一个图像或是一个 MP3 文件。到那时,就可以享受精通 Grails 带来的乐趣了。
本文介绍如何在Grails应用中定制URI以增强用户体验,并创建自定义Codec来处理URI中的特殊字符,实现更友好的路径映射。
252

被折叠的 条评论
为什么被折叠?



