## 4.9.1 文章模型设计
我们只存储文章的作者 id、标题、正文和点击量这几个字段,对应修改 lib/mongo.js,添加如下代码:
**lib/mongo.js**
```
exports.Post = mongolass.model('Post', {
author: { type: Mongolass.Types.ObjectId },
title: { type: 'string' },
content: { type: 'string' },
pv: { type: 'number' }
});
exports.Post.index({ author: 1, _id: -1 }).exec();// 按创建时间降序查看用户的文章列表
```
## 4.9.2 发表文章
现在我们来实现发表文章的功能。首先创建发表文章页,新建 views/create.ejs,添加如下代码:
**views/create.ejs**
```
<%- include('header') %>
<div class="ui grid">
<div class="four wide column">
<a class="avatar"
href="/posts?author=<%= user._id %>"
data-title="<%= user.name %> | <%= ({m: '男', f: '女', x: '保密'})[user.gender] %>"
data-content="<%= user.bio %>">
<img class="avatar" src="/img/<%= user.avatar %>">
</a>
</div>
<div class="eight wide column">
<form class="ui form segment" method="post" action="/posts">
<div class="field required">
<label>标题</label>
<input type="text" name="title">
</div>
<div class="field required">
<label>内容</label>
<textarea name="content" rows="15"></textarea>
</div>
<input type="submit" class="ui button" value="发布">
</div>
</form>
</div>
<%- include('footer') %>
```
新建 models/posts.js 用来存放与文章操作相关的代码:
**models/posts.js**
```
var Post = require('../lib/mongo').Post;
module.exports = {
// 创建一篇文章
create: function create(post) {
return Post.create(post).exec();
}
};
```
修改 routes/posts.js,在文件上方引入 PostModel:
**routes/posts.js**
```
var PostModel = require('../models/posts');
```
将:
```
// GET /posts/create 发表文章页
router.get('/create', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// POST /posts 发表一篇文章
router.post('/', checkLogin, function(req, res, next) {
res.send(req.flash());
});
```
修改为:
```
// GET /posts/create 发表文章页
router.get('/create', checkLogin, function(req, res, next) {
res.render('create');
});
// POST /posts 发表一篇文章
router.post('/', checkLogin, function(req, res, next) {
var author = req.session.user._id;
var title = req.fields.title;
var content = req.fields.content;
// 校验参数
try {
if (!title.length) {
throw new Error('请填写标题');
}
if (!content.length) {
throw new Error('请填写内容');
}
} catch (e) {
req.flash('error', e.message);
return res.redirect('back');
}
var post = {
author: author,
title: title,
content: content,
pv: 0
};
PostModel.create(post)
.then(function (result) {
// 此 post 是插入 mongodb 后的值,包含 _id
post = result.ops[0];
req.flash('success', '发表成功');
// 发表成功后跳转到该文章页
res.redirect(`/posts/${post._id}`);
})
.catch(next);
});
```
现在访问 `localhost:3000/posts/create` 发表篇文章试试吧,发表成功后跳转到了文章页但并没有任何内容,下面我们就来实现文章页及主页。
## 4.9.3 主页与文章页
现在我们来实现主页及文章页。修改 models/posts.js 如下:
**models/posts.js**
```
var marked = require('marked');
var Post = require('../lib/mongo').Post;
// 将 post 的 content 从 markdown 转换成 html
Post.plugin('contentToHtml', {
afterFind: function (posts) {
return posts.map(function (post) {
post.content = marked(post.content);
return post;
});
},
afterFindOne: function (post) {
if (post) {
post.content = marked(post.content);
}
return post;
}
});
module.exports = {
// 创建一篇文章
create: function create(post) {
return Post.create(post).exec();
},
// 通过文章 id 获取一篇文章
getPostById: function getPostById(postId) {
return Post
.findOne({ _id: postId })
.populate({ path: 'author', model: 'User' })
.addCreatedAt()
.contentToHtml()
.exec();
},
// 按创建时间降序获取所有用户文章或者某个特定用户的所有文章
getPosts: function getPosts(author) {
var query = {};
if (author) {
query.author = author;
}
return Post
.find(query)
.populate({ path: 'author', model: 'User' })
.sort({ _id: -1 })
.addCreatedAt()
.contentToHtml()
.exec();
},
// 通过文章 id 给 pv 加 1
incPv: function incPv(postId) {
return Post
.update({ _id: postId }, { $inc: { pv: 1 } })
.exec();
}
};
```
需要讲解两点:
1. 我们使用了 markdown 解析文章的内容,所以在发表文章的时候可使用 markdown 语法(如插入链接、图片等等),关于 markdown 的使用请参考: [Markdown 语法说明](http://wowubuntu.com/markdown/)。
2. 我们在 PostModel 上注册了 `contentToHtml`,而 `addCreatedAt` 是在 lib/mongo.js 中 mongolass 上注册的。
接下来完成主页的模板,修改 views/posts.ejs 如下:
**views/posts.ejs**
```
<%- include('header') %>
<% posts.forEach(function (post) { %>
<%- include('components/post-content', { post: post }) %>
<% }) %>
<%- include('footer') %>
```
新建 views/components/post-content.ejs 用来存放单篇文章的模板片段:
**views/components/post-content.ejs**
```
<div class="post-content">
<div class="ui grid">
<div class="four wide column">
<a class="avatar"
href="/posts?author=<%= post.author._id %>"
data-title="<%= post.author.name %> | <%= ({m: '男', f: '女', x: '保密'})[post.author.gender] %>"
data-content="<%= post.author.bio %>">
<img class="avatar" src="/img/<%= post.author.avatar %>">
</a>
</div>
<div class="eight wide column">
<div class="ui segment">
<h3><a href="/posts/<%= post._id %>"><%= post.title %></a></h3>
<pre><%- post.content %></pre>
<div>
<span class="tag"><%= post.created_at %></span>
<span class="tag right">
<span>浏览(<%= post.pv %>)</span>
<span>留言(<%= post.commentsCount %>)</span>
<% if (user && post.author._id && user._id.toString() === post.author._id.toString()) { %>
<div class="ui inline dropdown">
<div class="text"></div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item"><a href="/posts/<%= post._id %>/edit">编辑</a></div>
<div class="item"><a href="/posts/<%= post._id %>/remove">删除</a></div>
</div>
</div>
<% } %>
</span>
</div>
</div>
</div>
</div>
</div>
```
> 注意:我们用了 `<%- post.content %>`,而不是 `<%= post.content %>`,因为 post.content 是 markdown 转换成的 html 字符串。
修改 routes/posts.js,将:
**routes/posts.js**
```
router.get('/', function(req, res, next) {
res.render('posts');
});
```
修改为:
```
router.get('/', function(req, res, next) {
var author = req.query.author;
PostModel.getPosts(author)
.then(function (posts) {
res.render('posts', {
posts: posts
});
})
.catch(next);
});
```
> 注意:主页与用户页通过 url 中的 author 区分。
现在完成了主页与用户页,访问 `http://localhost:3000/posts` 试试吧,尝试点击用户的头像看看效果。
接下来完成文章页。新建 views/post.ejs,添加如下代码:
**views/post.ejs**
```
<%- include('header') %>
<%- include('components/post-content') %>
<%- include('footer') %>
```
打开 routes/posts.js,将:
**routes/posts.js**
```
// GET /posts/:postId 单独一篇的文章页
router.get('/:postId', function(req, res, next) {
res.send(req.flash());
});
```
修改为:
```
// GET /posts/:postId 单独一篇的文章页
router.get('/:postId', function(req, res, next) {
var postId = req.params.postId;
Promise.all([
PostModel.getPostById(postId),// 获取文章信息
PostModel.incPv(postId)// pv 加 1
])
.then(function (result) {
var post = result[0];
if (!post) {
throw new Error('该文章不存在');
}
res.render('post', {
post: post
});
})
.catch(next);
});
```
现在刷新浏览器,点击文章的标题看看浏览器地址的变化吧。
## 4.9.4 编辑与删除文章
现在我们来完成编辑与删除文章的功能。修改 models/routes.js,在 module.exports 对象上添加如下 3 个方法:
**models/routes.js**
```
// 通过文章 id 获取一篇原生文章(编辑文章)
getRawPostById: function getRawPostById(postId) {
return Post
.findOne({ _id: postId })
.populate({ path: 'author', model: 'User' })
.exec();
},
// 通过用户 id 和文章 id 更新一篇文章
updatePostById: function updatePostById(postId, author, data) {
return Post.update({ author: author, _id: postId }, { $set: data }).exec();
},
// 通过用户 id 和文章 id 删除一篇文章
delPostById: function delPostById(postId, author) {
return Post.remove({ author: author, _id: postId}).exec();
}
```
> 注意:不要忘了在适当位置添加逗号,如 incPv 的结束大括号后。
> 注意:我们通过新函数 `getRawPostById` 用来获取文章原生的内容,而不是用 `getPostById` 返回将 markdown 转换成 html 后的内容。
新建编辑文章页 views/edit.ejs,添加如下代码:
**views/edit.ejs**
```
<%- include('header') %>
<div class="ui grid">
<div class="four wide column">
<a class="avatar"
href="/posts?author=<%= user._id %>"
data-title="<%= user.name %> | <%= ({m: '男', f: '女', x: '保密'})[user.gender] %>"
data-content="<%= user.bio %>">
<img class="avatar" src="/img/<%= user.avatar %>">
</a>
</div>
<div class="eight wide column">
<form class="ui form segment" method="post" action="/posts/<%= post._id %>/edit">
<div class="field required">
<label>标题</label>
<input type="text" name="title" value="<%= post.title %>">
</div>
<div class="field required">
<label>内容</label>
<textarea name="content" rows="15"><%= post.content %></textarea>
</div>
<input type="submit" class="ui button" value="发布">
</div>
</form>
</div>
<%- include('footer') %>
```
修改 routes/posts.js,将:
**routes/posts.js**
```
// GET /posts/:postId/edit 更新文章页
router.get('/:postId/edit', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// POST /posts/:postId/edit 更新一篇文章
router.post('/:postId/edit', checkLogin, function(req, res, next) {
res.send(req.flash());
});
// GET /posts/:postId/remove 删除一篇文章
router.get('/:postId/remove', checkLogin, function(req, res, next) {
res.send(req.flash());
});
```
修改为:
```
// GET /posts/:postId/edit 更新文章页
router.get('/:postId/edit', checkLogin, function(req, res, next) {
var postId = req.params.postId;
var author = req.session.user._id;
PostModel.getRawPostById(postId)
.then(function (post) {
if (!post) {
throw new Error('该文章不存在');
}
if (author.toString() !== post.author._id.toString()) {
throw new Error('权限不足');
}
res.render('edit', {
post: post
});
})
.catch(next);
});
// POST /posts/:postId/edit 更新一篇文章
router.post('/:postId/edit', checkLogin, function(req, res, next) {
var postId = req.params.postId;
var author = req.session.user._id;
var title = req.fields.title;
var content = req.fields.content;
PostModel.updatePostById(postId, author, { title: title, content: content })
.then(function () {
req.flash('success', '编辑文章成功');
// 编辑成功后跳转到上一页
res.redirect(`/posts/${postId}`);
})
.catch(next);
});
// GET /posts/:postId/remove 删除一篇文章
router.get('/:postId/remove', checkLogin, function(req, res, next) {
var postId = req.params.postId;
var author = req.session.user._id;
PostModel.delPostById(postId, author)
.then(function () {
req.flash('success', '删除文章成功');
// 删除成功后跳转到主页
res.redirect('/posts');
})
.catch(next);
});
```
现在刷新主页,点击文章右下角的小三角,编辑文章和删除文章试试吧。
- 使用 Express + MongoDB 搭建多人博客
- 1.1 Node.js 的安装与使用
- 1.2 MongoDB 的安装与使用
- 2.1 require
- 2.2 exports 和 module.exports
- 2.3 Promise
- 2.4 环境变量
- 2.5 package.json
- 2.6 npm 使用注意事项
- 3.1 初始化一个 Express 项目
- 3.2 路由
- 3.3 模板引擎
- 3.4 Express 浅析
- 4.1 开发环境
- 4.2 准备工作
- 4.3 配置文件
- 4.4 功能设计
- 4.5 页面设计
- 4.6 连接数据库
- 4.7 注册
- 4.8 登出与登录
- 4.9 文章
- 4.10 留言
- 4.11 404 页面
- 4.12 错误页面
- 4.13 日志
- 4.14 测试
- 4.15 部署