本笔记是根据 node+mongodb 建站攻略 教程所记录的笔记。如有需要,可以参考原视频。
项目前期准备
后端使用的是 node.js + express。模板引擎是 jade,对时间和日期的格式化使用的是 moment.js
前端使用的是 jQuery 和 Bootstrap。
本地环境使用的是 less + cssmin + jsHint + UglifyJS + mocha + grunt
开发的流程为:
项目前后端流程的打通
项目结构的初始化
1 | npm install express --save |
入口文件编码
app.js:
1 | let express = require('express'); |
index.jade:
1 | <!DOCTYPE html> |
项目的结构为:
创建四个 jade 视图以及入口文件处理
入口文件如下:
app.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34let express = require('express');
let port = process.env.PORT || 3000;
let app = express();
app.set('views', './views'); // 应用的视图目录
app.set('view engine', 'jade'); // 应用的视图引擎
console.log('listen to port', port);
app.listen(port);
app.get('/', function(req, res) {
res.render('index', {
title: 'shiningdan 首页'
});
});
app.get('/admin/list', function(req, res) { //访问 /admin/list 返回 list.jade 渲染后的效果
res.render('list', { // list 是从 views 里面找到的 list.jade
title: 'shiningdan 列表页'
})
})
app.get('/admin/movie', function(req, res) {
res.render('admin', {
title: 'shiningdan 后台录入页'
})
})
app.get('/admin/:id', function(req, res) { // 访问 /admin/3 返回 detail.jade 渲染后的效果
res.render('detail', {
title: 'shiningdan 详情页'
})
})
模板文件如下:
1 | <!DOCTYPE html> |
当访问 http://127.0.0.1:3000/admin/list 等 url 的时候,会找到 list.jade,渲染模板并且返回 html 文件。
伪造模板数据跑通前后端交互流程
在 app.js 中需要获取静态资源,新版express4中,要独立安装static
,npm install serve-static --save
在app.js中,
1 | var serveStatic = require('serve-static') |
bodyParser 已经不再与Express捆绑,需要独立安装。
命令行执行:npm install body-parser --save
程序中修改:
1 | var bodyParser = require('body-parser') |
最终的项目目录如下:
其中,每一部分的内容为:
app.js
1 | let express = require('express'); |
head.jade
1 | link(href='/bootstrap/dist/css/bootstrap.min.css', rel='stylesheet', type='text/css') |
header.jade
1 | .container |
layout.jade
1 | <!DOCTYPE html> |
admin.jade
1 | extends ../layout |
detail.jade
1 | extends ../layout |
index.jade
1 | extends ../layout |
list.jade
1 | extends ../layout |
项目数据库的实现
mongodb 模式模型设计及编码
使用 mongoose 对 mongodb 进行操作
- Schema 模式定义:进行数据库每一个字段的定义
- Model 编译模型,生成构造函数
- Document:文档实例化
编写数据库 Schema 定义
model.js
1 | let mongoose = require('mongoose'); |
编写数据库编译模型,生成构造函数
1 | let mongoose = require('mongoose'); |
编写数据库交互代码
主要的数据库交互逻辑都在 app.js 里面:
1 | let express = require('express'); |
同时 list.jade 也修改了一些地方
1 | extends ../layout |
在定义 movie 结构的 model.js 里也添加了一些字段:
1 | let mongoose = require('mongoose'); |
删除功能以及项目生成配置文件
删除的时候,在点击删除按钮时,用 jQuery 提交一个 AJAX 请求来删除数据库中相关的信息。
如果在根目录下还有一个 public 目录中也有静态资源,则应该把 bower_components
中的内容放在 public 下面。我们可以通过生成一个 .bowerrc
配置文件来指定 bower 的安装路径。
1 | { |
创建 admin.js,用来在前端接卸完成后对 .del
元素添加点击删除事件。admin.js 中的逻辑就是,当删除点击事件发生以后,发送一个 DELETE
的 AJAX 请求来请求后端从数据库中删除对应的电影信息。
1 | $(function() { |
然后在 list.jade
中引入 admin.js
1 | extends ../layout |
最后在 app.js
中添加对 DELETE
AJAX 请求的响应:
1 | app.delete('/admin/list', function(req, res) { |
可以使用 bower init
和 npm init
来生成项目的配置文件,这样就可以方便打包在其他地方部署。
整个项目的架构如下:
Grunt 集成自动重启
Grunt 可以定义项目的自动处理流程,并且监听项目文件的变动自动热重启项目方便调试。
首先需要安装 Grunt:
1 | npm install grunt --save-dev |
然后在项目的根目录下生成 gruntfile.js
文件,内容如下:
1 | module.exports = function(grunt) { |
运行 grunt
命令即可。
开发用户模型及密码处理
使用 bcrypt 来为用户的密码进行加盐和 hash 求值。
1 | npm install --save bcrypt |
创建 schemas/user.js
,将 schemas/movie.js
中的内容拷贝进来,添加 bcrypt,并且在 pre('save')
中为用户密码加盐:
1 | let bcrypt = require('bcrypt'); |
登录注册前端视图
登录的视图主要是在 header.jade 中添加 .navbar-fixed-bottom,实现在页面最下部添加注册与登录视图。点击注册与登录按钮,就会通过 bootstrap 的 modal 弹出遮罩层。
1 | .container |
注册用户后台存储
前端给后端传输 userid 的方法有很多种:
- 在 URL 中包含 userid
1 | app.post('/user/signup/:userid', function() { |
这个样子就需要在 req.params.userid
中获取
- 在 queryString 中包含 userid:
1 | app.post('/user/signup/:userid', function() { |
遇到这种情况需要去 req.query
中去获取 userid
- 通过 POST 中传 userid,可以去
req.body
中获取:
1 | app.post('/user/signup/:userid', function() { |
- 统一使用
req.param
来获取 userid 的方法,如果传入的参数如下,有在 URL 中有 userid,在 queryString 中也有 userid,异步提交的数据中也有 userid (在 body) 里:
1 | app.post('/user/signup/:userid', function() { |
在这种情况下,express 中获得 userid 的优先级是:
1 | URL > body > queryString |
对 User 生成构造函数
在 /models
中添加 user.js
1 | let mongoose = require('mongoose'); |
并且导入到 app.js
中:
1 | let User = require('./models/user'); |
在 app.js
中完成对 POST
请求的处理,如果该用户存在,则定向到 /
,如果该用户不存在,子啊 mongodb 中创建该用户,并且重定向到 /admin/userlist
:
1 | app.post('/user/signup', function(req, res) { |
然后在 /views/pages
中添加 userlist.jade
的模板,但是此时还没有处理查看/修改/删除功能。:
1 | extends ../layout |
实现登录逻辑
为了实现登录的逻辑,我们需要对 /user/login
的 POST 请求路由进行处理。
1 | app.post('/user/signin', function(req, res) { |
然后在 schemas/user.js
中创建实例方法 comparePassword
,来进行密码比对:
1 | //实例方法,只有在实例中才能调用 |
保持用户登录状态
可以在 session 中存储用户的登录信息。首先导入 express-session 模块:
1 | let session = require('express-session'); |
然后在 express 启动后使用该中间件:
1 | app.use(session({ |
当用户登录以后,把用户的登录验证信息放在 req.session.user
中:
1 | user.comparePassword(password, function(err, isMatch) { |
然后在 /
路由的处理中可以打印得到 req.session.user
:
1 | app.get('/', function(req, res) { |
但是,此时 session 并没有做持久化存储,当页面刷新以后,该 session 就会消失,所以要在 mongodb 中存储该用户的登录。首先导入 connect-mongo 模块:
1 | let mongoStore = require('connect-mongo')(session); |
当对 session 进行存储的时候,可以使用 connect-mongo 模块对 session 数据进行自动化存储:
1 | app.use(session({ |
此时,每当我们在 req.session 中存储一个新的数据后,就会在 mongodb 中的 sessions 表内创建一个新的内容,用来存储该会话,即使是页面重新刷新也存在。
注销用户,用户退出功能实现
为了实现用户注销的功能,需要在 header.jade 中修改页面结构,判断用户是否登陆过:
1 | .navbar.navbar-default.navbar-fixed-bottom |
这里 if user
判断的是在 app.locals.user
中是否已经有值,如果有值,则渲染登出部分模板。如果没有值,就渲染登录部分模板。
创建 /logout
路由的处理方法:
1 | // logout |
并且在 /
中添加判断 req.sesison.user
中是否有值得判断,如果有值,则将 user 的值赋值给 app.locals.user
1 | app.get('/', function(req, res) { |
会话持久的预处理
目前路由在添加 req.locals.user
的部分是在 /
下面,但是在其他的页面登录就不会进行保存。所以要在所有的页面都添加 req.locals.user
,要在 app.use
中进行预处理:
1 | app.use(function(req, res, next) { |
调整路由结构,独立路由处理层
将所有路由相关的逻辑都放在了 /config/route.js
逻辑下,然后通过 module.exports
导出。在 app.js
中导入 route.js
模块,并且通过 require
引入 route.js
在 app.js
中
1 | require('./config/route')(app); |
在 /config/route.js
中:
1 | let Movie = require('../models/movie'); |
配置入口文件
在本地开发环境中,可以添加一些配置,方便本地调试。在 app.js
中添加如下的代码:
1 | let morgan = require('morgan'); |
调整目录结构,彻底分离 MVC 层
现在所有的路由的处理都是在 route.js
中执行,我们可以把 route.js
中所有的处理逻辑分成 user.js
、movie.js
、index.js
,然后把对应的路由处理逻辑放在其中,都放在 /app/controllers
里面,把 、view
文件夹放在 /app/view
这里,把 /models
和 /schemas
文件夹都放在 /app
文件夹下。。最后修改 app.js
中视图的目录,调整为:
1 | app.set('views', './app/views/pages/'); // 应用的视图目录 |
最后项目的结构和部分文件的内容如下:
route.js
1 | let Index = require('../app/controllers/index'); |
index.js
:
1 |
|
movie.js
1 | let Movie = require('../models/movie'); |
user.js
1 | let User = require('../models/user'); |
增加注册和登录跳转页面
添加注册模板 signup.jade
和登录模板 signin.jade
:
signup.jade
1 | extends ../layout |
signin.jade
1 | extends ../layout |
然后在 route.js
中添加对该模板的路由解析:
1 | app.get('/signin', User.showSignin); |
在 user.js
中添加 showSignin
和 showSignup
方法来渲染模板:
1 | exports.showSignin = function(req, res) { |
修改原来的 signin
方法和 signup
方法,重定向到这两个新的页面:
1 | exports.signin = function(req, res) { |
用户权限管理
首先,在 /schemas/user.js
中给用户添加一个 role 字段,用来代表不同等级的用户:
1 | // 0: normal user |
然后,在访问 /admin/
开头的 url 中,都使用中间件对用户的 url 进行判断,在 /app/controllers/user.js
中定义验证用户登录的中间件和验证用户权限的中间件:
1 | //middleware for user |
对所有的 /admin
url 进行修改,整理对外访问的 url 的格式:
1 | app.get('/admin/user/list', User.signinRequired, User.adminRequired, User.list); |
并且修改 /public/js/admin.js
中删除的路径为新的路径:
1 | $.ajax({ |
开发评论功能
设计评论的数据模型
在页面中显示的评论需要被存储到数据库中,所以需要为评论定义一个合适的模型,模型中需要包含的字段有:
- 这条评论是在哪个 movie 下面的
- 这条评论是谁发送的 from
- 这条评论是评论给谁的 to
- 这条评论的内容是什么
所以在 /schemas/
中创建 comment.js
,用来定义评论的模型:
1 | let mongoose = require('mongoose'); |
评论存储、查询与展现
在 /app/models
中创建 comment.js
,内容为创建 Comment 的构造函数:
1 | let mongoose = require('mongoose'); |
在 route.js
中创建对 /admin/comment
URL 的处理,用来处理对 comment 的 POST 请求:
1 | app.post('/user/comment', User.signinRequired, Comment.save); |
然后在 /app/controllers/
中创建 comment.js
来处理 post 请求:
1 | let Comment = require('../models/comment'); |
在 detail.jade
模板中添加评论的部分,包括评论的显示部分和一个发表评论的部分:
1 | extends ../layout |
然后在 detail.jade
被渲染的时候,也就是在 /app/controllers/movie.js
中的 detail
函数中,通过获得的 movie,找到对应的评论,然后通过评论找到用户,传入用户的名字,最后交给 detail.jade
渲染出来。
1 | exports.detail = function(req, res) { // 访问 /admin/3 返回 detail.jade 渲染后的效果 |
用户之间的相互回复功能
当需要显示用户之间的评论和在评论下发表的子评论时,需要修改评论的数据库模型,在 Comment 中存储子评论:
1 | let CommentSchema = new Schema({ |
然后修改 detail.jade
,添加子评论的部分。在原来的用户头像 img
标签外扩展一个 a
标签,当点击这个 a
标签的时候,会跳转到评论输入区域。
1 | extends ../layout |
然后创建一个新的 js 文件叫 /public/js/detail.js
在 .comment
上面绑定一个事件,事件的内容是,如果点击了该用户的头像,就会在评论区添加隐藏的 input
标签,表示这是对某个用户的评论:
1 | $(function() { |
然后在处理 comment
的路由里面,添加对 tId
和 cId
的处理:
1 | let Comment = require('../models/comment'); |
在处理 detail.jade
的 Movie.detail
函数中,添加对子评论的查找以及显示的功能:
1 | exports.detail = function(req, res) { // 访问 /admin/3 返回 detail.jade 渲染后的效果 |
对评论做登录限制
当用户没有登录的时候,不能显示评论按钮,而是显示登陆后评论,在 detail.jade
中进行修改:
1 | #comment |
显示效果如下,点击登录后评论,会弹出登录对话窗:
实现电影的分类功能
设计分类的数据模型
单独建立一个分类的表,管理分类的名字、分类的添加和删除,将电影的表和分类的表建立关系,在 /schemas
中创建 category.js
文件:
1 | let mongoose = require('mongoose'); |
分类后台录入及分类存储
创建 /models/category.js
来创建 category
的模型:
1 | let mongoose = require('mongoose'); |
首先创建 admin 的分类录入模板 category_admin.jade
,在访问 /admin/category/new
的时候返回该模板的渲染效果,在 route.js
中创建对分类的路由处理:
category_admin.jade
1 | extends ../layout |
然后创建对 category_admin.jade
渲染的路由处理,在 /controllers
下面创建 category.js
,创建 new
函数来处理 category_admin
的渲染:
category.js
1 | exports.new = function(req, res) { |
然后在 route.js
中将 /admin/category/new
的 get 请求对应 new
函数进行处理:
1 | let Category = require('../app/controllers/category'); |
然后对 /admin/category/new
的 POST 请求进行处理,将得到添加分类的请求传输到数据库中:
category.js
1 | let Category = require('../models/category'); |
在新添加完分类以后,路由会被从定向到 /admin/category/list
,所以要在 route.js
中创建对 /admin/category/list
的 GET 请求的处理:
route.js
1 | app.post('/admin/category/list', User.signinRequired, User.adminRequired, Category.save); |
category.js
1 |
list
函数需要渲染一个新的模板来显示分类页面,所以在 /app/views/pages/
下面创建一个 categorylist/jade
来处理所有的 Category 输出的渲染:
categorylist.jade
1 | extends ../layout |
电影录入增加分类选择
在电影录入的模板 admin.jade
中添加关于电影分类的选取部分:
1 | form.form-horizontal(method='post', action='/admin/movie/new') |
在 /app/schemas/movie.js
中添加 category 字段:
1 | category: { |
在 movie.new
方法中,需要在对 admin.jade
模板进行初始化的时候传入 categories
值:
1 | exports.new = function(req, res) { |
然后在 admin.jade
中处理所有的 value,将 value='#{movie.language}'
替换为 value=movie.language
,不然在页面显示中会有 undefined
出现。
然后,在 movie.save
方法中,需要处理对分类的 post 请求,在存储 movie 的数据的时候,也要在 category 中将该 movie 添加进去:
/app/controllers/movie.js
1 | exports.save = function(req, res) { |
这个时候,在 db.movies
中创建了含有 category
字段的一条数据,在 db.categories
中对应分类的 movies
数组中也添加了对该电影的引用。
我们可以修改 /
对应的页面,将电影按照分类来进行显示:
index.jade
1 | extends ../layout |
然后修改 /
对应的路由处理 /app/controller/index.js
:
1 | let Movie = require('../models/movie'); |
当点击 /admin/movie/list/
页面中的更新按钮时,跳转的对象是不正确的,所以需要修改跳转的对象:
list.jade
1 | td: a(target='_blank', href='/movie/#{item._id}') 查看 |
然后要修改 /admin/movie/update/#{item._id}
中的请求处理方法,在 /app/controllers/movie.js
中修改 update
方法:
1 | exports.update = function(req, res) { |
jsonp 同步豆瓣数据
使用 Jsonp 同步豆瓣数据,可以在 /admin/movie
中添加 input
,获得影片的 id ,然后发送 Ajax 来获取数据。豆瓣有提供 Movie API。
首先在 admin.js
中创建请求豆瓣 API 的 blur 事件:
1 | $('#douban').blur(function(event) { |
然后在 admin.jade
中添加一个 input,输入需要同步的影片 id:
1 | form.form-horizontal(method='post', action='/admin/movie/new') |
并且在 admin.jade
的最后引入 admin.js
:
1 | script(src='/js/admin.js') |
电影录入增加分类自定义
电影的分类不一定在 categories 里面都可以找到,所以在添加电影数据的时候,如果是不存在的电影分类,就创建一个新的分类:
1 | exports.save = function(req, res) { |
增加分类列表及分页
在首页,原有的展示页面是显示不同的分类,在每个分类下显示其中的电影。现在可以添加用户针对分类的查询,当点击分类的时候,可以导航到针对单个分类的查询结果上,所以,首先修改 index.jade
添加对分类的跳转:
1 | each cat in categories |
在 route.js
中添加对 /result
URL 的跳转:
1 | app.get('/results', Index.search); |
创建一个新的 reuslts.jade
,作为分类查找显示页面的模板:
1 | extends ../layout |
然后在 /app/controllers/index.js
中创建 search
方法来处理对 /results?cat=#{cat._id}&p=0'
的路由请求:
1 | exports.search = function(req, res) { |
增加搜索、公用列表及分页
在 header.jade
里面加入搜索框:
1 | .container |
然后该查询的目的地址是 /result
,所以要在 index.search
函数中添加对应路径的处理:
1 | exports.search = function(req, res) { |
此时搜索 欢乐
就可以返回欢乐的搜索结果: