ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
## 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); }); ``` 现在刷新主页,点击文章右下角的小三角,编辑文章和删除文章试试吧。