由于 CouchApp 可以把目录结构转换到 CouchDB 的设计文档中,因此创建show方法的时候,只需要在 Web 应用的根目录下面创建一个shows目录,并在其中创建 JavaScript 文件即可。如该目录下的book-edit.js文件会被转换成名为book-edit的方法。创建文档的show方法需要返回 HTML 文档,有两种方法可以实现。
- 直接在show方法构造 HTML 文档内容的字符串。这种方式比较直接,不过字符串拼接比较繁琐,而且容易出错,同时带来的维护成本也比较高。
- 使用简单的模板技术来实现。 CouchApp 自带了一个基于 JavaScript 的简单模板实现,定义在vendor/couchapp/template.js中。该模板实现可以把模板中的<%= title %>这样的声明替换成传入的 JSON 对象中title属性的值。
本文的示例应用中使用的是模板来实现的,代码清单 10给出了book-edit.js文件的内容。
清单 10. 创建和更新图书的show方法
function(doc, req) { // !json templates.book.edit // !code vendor/couchapp/path.js // !code vendor/couchapp/template.js return template(templates.book.edit, { doc : doc, docid : toJSON((doc && doc._id) || null), assets : assetPath(), index : listPath('index','recent-posts',{descending:true,limit:8}) }); } |
在代码清单 10中,!json和!code都是由 CouchApp 提供的宏声明,用来包含外部文件。!json用来包含设计文件中的 JSON 对象,后面接着的是 JSON 对象在设计文档中的路径。如!json templates.book.edit会把设计文档中templates字段的book字段的edit字段的内容包含到当前的show方法中,并作为变量templates.book.edit的值。!code用来包含一个 JavaScript 文件,后面接着的是 JavaScript 文件相对于根目录的路径。如!code vendor/couchapp/template.js会把template.js文件包含进来。template是包含在template.js文件中的一个方法,用来完成 HTML 模板内容的替换,它的第一个参数是 HTML 模板字符串,第二个参数是包含模板中<%= %>占位符实际值的 JSON 对象。变量templates.book.edit的值是templates目录下子目录book中edit.html文件的内容。该文件的主体内容如代码清单 11所示,完整代码见下载。assetPath和listPath是由 CouchApp 提供的帮助方法,用来生成所需的路径,可以在vendor/couchapp/path.js文件找到这些方法的定义。
清单 11. edit.html 文件的内容
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> ...... </head> <body> <div id="content"> <form id="new-book" action="new.html" method="POST"> <h1> 添加新的图书 </h1> <fieldset> <p> <label for="title"> 标题 </label> <input type="text" size="40" name="title" id="title"> </p> <p> <label for="author"> 作者 </label> <input type="text" size="20" name="author" id="author"> </p> ...... </fieldset> <p> <input type="submit" value=" 保存 " id="save"> <span id="message" style="display:none"></span> </p> </form> </div> </body> <script src="/_utils/script/json2.js"></script> <script src="/_utils/script/jquery.js?1.2.6"></script> <script src="/_utils/script/jquery.couch.js?0.8.0"></script> <script src="/_utils/script/jquery.cookies.js"></script> <script src="<%= assets %>/dianping.js"></script> <script type="text/javascript"> $(function() { var dbname = document.location.href.split('/')[3]; var dname = unescape(document.location.href).split('/')[5]; var db = $.couch.db(dbname); var localDoc = {}; var bookFields = ["title", "author", "press", "publish_date", "price", "tags", "thumbnail", "summary"]; $("form#new-book").submit(function(e) { e.preventDefault(); $.dianping.bookFormToDoc("form#new-book", bookFields, localDoc); if (localDoc.tags) { localDoc.tags = localDoc.tags.split(","); for(var idx in localDoc.tags) { localDoc.tags[idx] = $.trim(localDoc.tags[idx]); } } db.saveDoc(localDoc, { success : function(resp) { $("#message").text(" 保存成功! ").fadeIn(500).fadeOut(3000); }, error : function(status, error, reason) { $("#message").text(" 保存失败,原因是:" + reason); } }); }); var docId = <%= docid %>; if (docId) { db.openDoc(docId, { success : function(doc) { $("h1").html(" 编辑图书信息 - " + doc.title); localDoc = doc; $.dianping.docToBookForm("form#new-book", doc, bookFields); } }); } }); </script> </html> |
在代码清单 11中,edit.html的主体是一个 HTML 表单,用来输入图书的相关信息。如果在调用此show方法的时候传入了文档 ID 作为参数的话,会通过db.openDoc方法获取文档的内容,并填充表单。在表单提交的时候,首先把表单中用户输入的值变成 JSON 对象,再通过db.saveDoc方法保存文档。
修改文档结构
熟悉关系数据库的开发者可能都有过类似的经历,那就是要修改一个关系数据库的表结构是一件比较困难的事情,尤其当应用中已经有一定量的数据的时候。而 CouchDB 中保存的文档是没有结构的,因此当需要根据应用的需求做修改的时候,比关系数据库要简单。在本文的示例应用中,一开始并没有考虑为图书添加封面的缩略图。如果要增加这样的功能,只需要在创建文档的表单中添加一项,用来让用户输入缩略图的链接即可。之后再创建的文档就会自动添加该字段。
删除文档
删除文档只需要调用表 1中列出的$.couch.db(dbname).removeDoc(doc, options)方法即可。
视图
视图是 CouchDB 中用来查询和呈现文档的。完成视图的定义之后,视图的运行由专门的视图服务器来完成。 CouchDB 中默认的视图定义语言是 JavaScript 。 CouchDB 中的视图运行使用的是 MapReduce 编程模型(见参考资料)。每个视图的定义中至少需要提供 Map 方法,Reduce 方法是可选的。
视图的 Map 与 Reduce
Map 方法的参数只有一个,就是当前的文档对象。 Map 方法的实现需要根据文档对象的内容,确定是否要输出结果。如果需要输出的话,可以通过emit来完成。emit方法有两个参数,分别是key和value,分别表示输出结果的键和值。使用什么样的键和值应该根据视图的实际需要来确定。当希望对文档的某个字段进行排序和过滤操作的时候,应该把该字段作为键(key)或是键的一部分;value的值可以提供给 Reduce 方法使用,也可能会出现在最终的结果中。可以作为键的不仅是简单数据类型,也可以是任意的 JSON 对象。比如emit([doc.title, doc.price], doc)中,使用数组作为键。
通过 Map 方法输出的结果称为中间结果。中间结果可以通过 Reduce 方法来进一步做聚集操作。聚集操作是对结果中键(key)相同的数据集合来进行的。 Reduce 方法的输入不仅是 Map 方法输出的中间结果,也可以是上一次 Reduce 方法的结果,后面这种情况称为 rereduce 。 Reduce 方法的参数有三个:key、values和rereduce,分别表示键、值和是否是 rereduce 。由于 rereduce 情况的存在,Reduce 方法一般需要处理两种情况:
- 传入的参数rereduce的值为false:这表明 Reduce 方法的输入是 Map 方法输出的中间结果。参数key的值是一个数组,对应于中间结果中的每条记录。该数组的每个元素都是一个包含两个元素的数组,第一个元素是在 Map 方法中通过emit输出的键(key),第二个元素是记录所在的文档 ID 。参数values的值是一个数组,对应于 Map 方法中通过emit输出的值(value)。
- 传入的参数rereduce的值为true:这表明 Reduce 方法的输入是上次 Reduce 方法的输出。参数key的值为null。参数values的值是一个数组,对应于上次 Reduce 方法的输出结果。
下面通过一个实例来说明视图 Map 和 Reduce 的用法。该视图要解决的问题是对图书简介中出现的字符进行计数,这也是一个经典的 MapReduce 编程模型的实例。代码清单 12中给出了该视图的定义。
清单 12. 对图书简介中的字符计数的视图定义
//Map 方法 function(doc) { if(doc.type == 'book' && doc.summary) { var words = Array.prototype.slice.apply(doc.summary); for (var i = 0; i < words.length; i++) { emit(words[i], 1); } } } //Reduce 方法 function(key, values) { return sum(values); } |
该视图定义的基本思路是对于每本图书的简介,把其中包含的每个字符都作为键输出,而对应的值是 1,表明是一次计数。在介绍视图 REST API的时候提过,只需要发送 HTTP GET 请求就可以获得视图的运行结果。代码清单 12中视图的名字是word-count,因此只需要发送 GET 请求到http://127.0.0.1:5984/dianping/_design/dianping/_view/word-count就可以获得如代码清单 13所示的运行结果。