评论列表算是编程领域极具代表性的问题了,算法上使用树结构。现在我们一步一步来使用Javascript、Node、mongodDB来解决评论模型问题。
#Step 1 建立模型
User:
_id : OBJECTID 用户ID
username : STRING 用户名
Article:
_id : OBJECTID 文章ID
userID : OBJECTID 用户ID
username : STRING 用户名 反常规化
title : STRING 标题
text : STRING 内容
Comment:
_id : OBJECTID 评论ID
userID : OBJECTID 用户ID
username : STRING 用户名 反常规化
articleID : OBJECTID 文章ID
parentID : OBJECTID 上级评论ID,可以没有
text : STRING 内容
在这里,使用了反常规化,在Article、Comment中不但存储了用户ID,还存储了用户名。 这样会增加数据文件的容量,但是查询的时候可以不用去关联User,从而减少一次请求,提升速度。所谓的“以空间换时间”。
#Step 2 使用node-mongodb-native,编写MongoDB数据库服务器连接代码 dbm.js:
/// dbm.js ///
var mongodb = require("mongodb");
// 数据库连接缓存
var cache = {};
// 连接数据库
function connect (url, options) {
var fns = [];
var status = 0;
var _db = cache[url];
var args;
return function (f) {
args = arguments;
if (_db !== null && typeof _db === "object") {
f(_db);
return;
}
fns.push(f);
// 当有一个连接初始化请求时,挂起其他初始化请求
// 连接池建立完后,使用该连接处理挂起的请求
if (status === 0) {
status = 1;
mongodb.MongoClient.connect(url, options, function (err, db) {
if (err) { throw err; }
_db = cache[url] = db;
for (var i = 0, len = fns.length; i < len; i++) {
fns.shift().call(null, _db);
}
});
}
};
}
// 关闭数据库
function close (url) {
var db = cache[url];
if (db !== null && typeof db === "object") {
db.close();
delete cache[url];
}
}
exports.connect = connect;
exports.close = close;
这段代码封装了连接MongoDB的细节,只需要提供url和options(详情见MongoDb MongoClient连接配置)。 好处是可以并发访问MongoDB,在展示并发之前,还需要下面这个小工具:
/// dbm.js ///
function roll (count, f) {
return function () {
count--;
if (count === 0) {
f();
}
};
}
function rollData (count, f) {
var cache = {};
return function (name, data) {
count--;
if (typeof name === "string") {
cache[name] = data;
}
if (count === 0) {
f(cache);
}
};
}
exports.roll = roll;
exports.rollData = rollData;
roll
rollData
可以测试并发请求是否全部结束,并在结束后启动回调函数,我们来写个Example:
var dbm = require("./dbm.js");
var connect = dbm.connect("mongodb://127.0.0.1:27017/teste", {
"server" : {
"poolSize" : 10 // 10条连接数
}
});
var roll = dbm.roll(3, function () { // 3个并发请求
console.log("并发请求结束.");
});
console.log("并发请求开始.");
connect(function (db) { // db就是映射的数据库teste
db.collection("users").find({}, function (err, result) {
roll();
});
});
connect(function (db) { // db就是映射的数据库teste
db.collection("articles").find({}, function (err, result) {
roll();
});
});
connect(function (db) { // db就是映射的数据库teste
db.collection("comments").find({}, function (err, result) {
roll();
});
});
代码是并列写的,每个connect会向MongoDB服务器发送请求,并侦听结果。 每个返回会测试当前结束请求的个数,并在全部结束后,启动回调函数,输出console.log("并发请求结束.");
。
#Step 3 编写个查找评论的函数
/// dbm.js ///
var COLL_COMMENTS = "comments";
var contact = util.connect("mongodb://127.0.0.1:27017/teste", {
"server" : {
"poolSize" : 10 // 10条连接数
}
});
function checkId (id) {
return String(id).length === 24;
}
function findComment (id, f) {
// id : STRING, Comment ID
// f(err, result)
// err : OBJECT | NULL 服务器错误
// id格式错误
// result : OBJECT 评论文档
// NULL 没有该评论文档
if(!checkId(id)) { return f({ name:"IDError", err:"Comment id invalid" }); }
var selector = { _id: new mongodb.ObjectID(id) };
contact(function (db) {
db.collection(COLL_COMMENTS).findOne(selector, f);
});
}
exports.findComment = findComment;
#Step 4 第2、3步都不是本章要关注的,现在才是我们主要关心的: 树渲染
当我们取到评论列表的时候应该如何渲染?
取到的结果会是这样的:
comments:
[
{ _id:"1", text:"foo", ... },
{ _id:"2", text:"foo", parentID:"1", ... },
{ _id:"3", text:"foo", parentID:"1", ... },
{ _id:"4", text:"foo", ... },
{ _id:"5", text:"foo", parentID:"2", ... },
{ _id:"6", text:"foo", parentID:"5", ... },
...
]
如何解决嵌套评论?
A lalala
B lalala
C lalala
D lalala
E lalala
F lalala
这是个树递归的问题,每个项会有parentID来指定当前的判断KEY,如果没有parentID,那么这个KEY就是undefined.
=> 每当用KEY去comments列表查找,会找到一组结果。 => 我们再跳到结果第一个,保存上次的KEY,并设置KEY=当前的查找parentID。 => 又找到一组结果,然后保存上次的KEY,跳到新的结果,并设置KEY=当前的查找parentID。 => ... => 当树最左边的底层叶子完成的时候,向上回跳进行下一个叶子。 => ...
画成图形,可以是这样的过程:
[]
[] [] []
[ ][][]
[完成]
[]
[] [] []
[ ][][]
[完成][完成]
[]
[] [] []
[ 完成 ][ ][]
[完成][完成] [完成]
[]
[] [] []
[ 完成 ][ ][]
[完成][完成] [完成][完成]
...
[]
[] [] []
[ 完成 ][ 完成 ][]
[完成][完成] [完成][完成]
...
[]
[完成] [完成] [开始]
...
[]
[完成] [完成] [完成]
...
从树的最左一支,最底点完成,向上向右破动,依次完成。 每个点在自己的栈内存中,跳到上一级会提取当前栈保存的KEY。
代码很简单,如下:
/// dbm.js ///
function serilize (comments, format) {
var tree = { childs:[] };
var UNDEFINED;
function walk (key, value) {
var i = 0;
var comment = comments[i];
while (comment) {
var parentID = comment.parentID ? String(comment.parentID) : comment.parentID;
if (parentID === key) {
var child = {};
var childKey = String(comment._id);
var childValue = format(comment);
childValue.childs = [];
child[childKey] = childValue;
value.childs.push(child);
walk(childKey, childValue);
}
comment = comments[++i];
}
}
walk(UNDEFINED, tree);
return tree;
}
exports.serilize = serilize;
当然了,我们可以写一个测试,看看ta是不是能成功渲染一棵树:
var serilize = require("./dbm.js").serilize;
var assert = require("assert");
var samples = [
{
_id: "id1",
text: "foo 1"
},
{
_id: "id2",
parentID: "id1",
text: "foo 2"
},
{
_id: "id3",
text: "foo 3"
},
{
_id: "id4",
parentID: "id2",
text: "foo 4"
},
{
_id: "id5",
parentID: "id2",
text: "foo 5"
},
{
_id: "id6",
parentID: "id4",
text: "foo 6"
}
];
var result = serilize(samples, function (cmt) {
return {
text: cmt.text,
};
});
assert.strictEqual(result["childs"][0]["id1"]["childs"][0]["id2"]["text"], "foo 2");
assert.strictEqual(result["childs"][0]["id1"]["childs"][0]["id2"]["childs"][0]["id4"]["text"], "foo 4");
assert.strictEqual(result["childs"][1]["id3"]["text"], "foo 3");
#Step 5 完成的评论查找测试
var dbm = require("./dbm.js");
var assert = require("assert");
dbm.findComments("53fded391f6769586a5e7fe1", function (err, result) {
var tree = dbm.serilize(result, function (cmt) {
return {
text: cmt.text,
username: cmt.username
};
});
console.log("%j", tree);
assert.ok(result instanceof Array);
assert.ok(result.length > 0);
});
这里有个文章评论创建查找修改的代码,太长了,如果你感兴趣,可以来github: node-dbm看看。 当然,单元测试在这儿node-dbm-test。