前言
我们通过引入会话(session)机制来记录用户登录状态,来判断用户是否已经登陆,针对已登录和未登录的用户显示不同的内容。
但是我们还有文章数据保存在哪里呢,我们的数据量假如很小可以考虑使用json文件来保存,但是我们打算做一个blog系统,会使用到增查删改,还是直接上数据库吧,Express配合MongoDB更加湿滑哦。
MongoDB简介
MongoDB 是一个基于分布式文件存储的 NoSQL(非关系型数据库)的一种由 C++ 语言编写,旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 支持的数据结构非常松散,是类似 json 的 bjson 格式,因此可以存储比较复杂的数据类型。MongoDB 最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
MongoDB 没有关系型数据库中行和表的概念,不过有类似的文档和集合的概念。文档是 MongoDB 最基本的单位,每个文档都会以唯一的 _id 标识,文档的属性为 key/value 的键值对形式,文档内可以嵌套另一个文档,因此可以存储比较复杂的数据类型。集合是许多文档的总和,一个数据库可以有多个集合,一个集合可以有多个文档。
下面是一个 MongoDB 文档的示例:
{
"_id" : ObjectId( "4f7fe8432b4a1077a7c551e8" ),
"name" : "nswbmw",
"age" : 22,
"email" : [ "xxx@126.com", "xxx@gmail.com" ],
"family" : {
"mother" : { ... },
"father" : { ... },
"sister : {
"name" : "miaomiao",
"age" : 27,
"email" : "xxx@163.com",
"family" : {
"mother" : { ... },
"father" : { ... },
"brother : { ... },
"husband" : { ... },
"son" : { ... }
}
}
}
}
更多有关 MongoDB 的知识请参阅 《mongodb权威指南》
或者http://www.runoob.com/mongodb/mongodb-tutorial.html
连接MongoDB
数据库自行安装启动,简单介绍一下,如何连接,在 Node.js 中使用 MongoDB ,我们一般需要一个驱动模块来连接数据库,
官方的 mongodb(也叫 node-mongodb-native)库,它简单。参照文档即可上手,但是不支持不支持文档校验,而且写出的代码很沉余。
大多数人会选择第三方比较优秀的Mongoose,Mongoose 通过 Schema 支持文档校验,虽说 mongodb 是 no schema 的,但在生产环境中使用 Schema 有两点好处。一是对文档做校验,防止非正常情况下写入错误的数据到数据库,二是可以简化一些代码,如类型为 ObjectId 的字段查询或更新时可通过对应的字符串操作,不用每次包装成 ObjectId 对象。
在express的骨架中在新建一个models文件夹,这个文件夹用来存放我们的模型,我们使用Mongoose这个模块操作 mongodb 进行增删改查。在 myblog 下新建 lib 目录,在该目录下新建 mongo.js,添加如下代码:
lib/mongo.js
const config = require('config-lite')(__dirname)
var mongoose = require('mongoose');
mongoose.connect(config.mongodb)
思考
其实我们现在的设计就有一点MVC的影子,每一个人对MVC的思考都不一样,我才疏学浅,是这样认为的,models代表M,存放可以对数据库增删查改的模型,routes代表 C,存放我们控制返回的信息,views代表着V,就是用户可以观察的视图。
现在我们来设计用户模型
我们只存储用户的名称、密码(加密后的)、头像、性别和个人简介这几个字段,对应修改 lib/mongo.js,添加如下代码:
lib/mongo.js
我们定义了用户表的 schema,生成并导出了 User 这个 model,同时设置了 name 的唯一索引,保证用户名是不重复的。
当然你也可以自己设计自己的用户模型。
exports.User = mongoose.model('User', {
name: { type: 'string' },
password: { type: 'string' },
avatar: { type: 'string' },
gender: { type: 'string', enum: ['m', 'f', 'x'] },
bio: { type: 'string' }
})
小提示:mongoose 的操作就不仔细介绍,可以自行查找资料。
用户模型有了,我们需要对模型进行操作,所有我们新建 models/users.js,添加如下代码:
var User = require('../lib/mongo.js').User;
module.exports = {
// 注册一个用户
create: function create(user) {
return User.create(user).exec();
},
// 通过用户名获取用户信息
getUserByName: function getUserByName (name) {
return User
.findOne( {name: name} )
.exec();
},
};
然后修改完善处理用户注册的路由,最终修改 routes/signup.js 如下:
const fs = require('fs')
const path = require('path')
const sha1 = require('sha1')
const express = require('express')
const router = express.Router()
const UserModel = require('../models/users')
const checkNotLogin = require('../middlewares/check').checkNotLogin
// GET /signup 注册页
router.get('/', checkNotLogin, function (req, res, next) {
res.render('signup')
})
// POST /signup 用户注册
router.post('/', checkNotLogin, function (req, res, next) {
const name = req.fields.name
const gender = req.fields.gender
const bio = req.fields.bio
const avatar = req.files.avatar.path.split(path.sep).pop()
let password = req.fields.password
const repassword = req.fields.repassword
// 校验参数
try {
if (!(name.length >= 1 && name.length <= 10)) {
throw new Error('名字请限制在 1-10 个字符')
}
if (['m', 'f', 'x'].indexOf(gender) === -1) {
throw new Error('性别只能是 m、f 或 x')
}
if (!(bio.length >= 1 && bio.length <= 30)) {
throw new Error('个人简介请限制在 1-30 个字符')
}
if (!req.files.avatar.name) {
throw new Error('缺少头像')
}
if (password.length < 6) {
throw new Error('密码至少 6 个字符')
}
if (password !== repassword) {
throw new Error('两次输入密码不一致')
}
} catch (e) {
// 注册失败,异步删除上传的头像
fs.unlink(req.files.avatar.path)
req.flash('error', e.message)
return res.redirect('/signup')
}
// 明文密码加密
password = sha1(password)
// 待写入数据库的用户信息
let user = {
name: name,
password: password,
gender: gender,
bio: bio,
avatar: avatar
}
// 用户信息写入数据库
UserModel.create(user)
.then(function (result) {
// 此 user 是插入 mongodb 后的值,包含 _id
user = result.ops[0]
// 删除密码这种敏感信息,将用户信息存入 session
delete user.password
req.session.user = user
// 写入 flash
req.flash('success', '注册成功')
// 跳转到首页
res.redirect('/posts')
})
.catch(function (e) {
// 注册失败,异步删除上传的头像
fs.unlink(req.files.avatar.path)
// 用户名被占用则跳回注册页,而不是错误页
if (e.message.match('duplicate key')) {
req.flash('error', '用户名已被占用')
return res.redirect('/signup')
}
next(e)
})
})
module.exports = router
然后就是视图咯,我们来完成注册页面。新建 views/signup.ejs,添加如下代码:
<%- include('header') %>
<script src="http://static.runoob.com/assets/jquery-validation-1.14.0/lib/jquery.js"></script>
<script src="http://static.runoob.com/assets/jquery-validation-1.14.0/dist/jquery.validate.min.js"></script>
<script src="http://static.runoob.com/assets/jquery-validation-1.14.0/dist/localization/messages_zh.js"></script>
<script>
// validate signup form on keyup and submit
var validator = $("#signupform").validate({
rules: {
name: {
required: true,
rangelength: [2,10],
remote: {
url: "/signup/signupcheck",
type: "post",
dataType: "json",
data: {
name: function() {
return $("#name").val();
}
}
}
},
password: {
required: true,
minlength: 6
},
repassword: {
required: true,
minlength: 6,
equalTo: "#password"
},
//avatar: required,
avatar: {
required: true
},
bio: {
required: true,
rangelength: [1,30]
}
},
messages: {
name: {
required: "请输入用户名",
rangelength: jQuery.validator.format("请输入长度在 {0} 到 {1} 之间的字符串"),
remote: jQuery.validator.format("{0} 已经被使用")
},
password: {
required: "请输入密码",
minlength: jQuery.validator.format("密码至少为 {0} 个字符")
},
repassword: {
required: "请输入密码",
minlength: jQuery.validator.format("密码至少为 {0} 个字符"),
equalTo: "两次输入的密码不一样"
},
avatar: {
required: "请上传头像"
},
bio: {
required: "请输入个人简介",
rangelength: jQuery.validator.format("个人简介请限制在 {0} - {1} 个字符")
// the errorPlacement has to take the table layout into account
errorPlacement: function (error, element) {
error.appendTo(element.next());
},
// specifying a submitHandler prevents the default submit,good for the demo
submitHandler: function () {
alert("submitted!");
},
// set this class to error-labels to indicate valid fields
success: function (label) {
// set as text for IE
label.html(" ").addClass("checked");
}
}
});
</script>
<style>
.status{
color: red;
}
</style>
<div class="ui grid">
<div class="four wide column"></div>
<div class="eight wide column">
<form class="ui form segment" id="signupform" method="post" enctype="multipart/form-data">
<div class="field required">
<label for="name">用户名</label>
<input placeholder="用户名" type="text" name="name" id="name">
<p class="status"></p>
</div>
<div class="field required">
<label for="password">密码</label>
<input placeholder="密码" type="password" name="password" id="password">
<p class="status"></p>
</div>
<div class="field required">
<label for="repassword">重复密码</label>
<input placeholder="重复密码" type="password" name="repassword" id="repassword">
<p class="status"></p>
</div>
<div class="field required">
<label for="gender">性别</label>
<select class="ui compact selection dropdown" name="gender" id="gender">
<option value="m">男</option>
<option value="f">女</option>
<option value="x">保密</option>
</select>
<p class="status"></p>
</div>
<div class="field required">
<label for="avatar">头像</label>
<input type="file" name="avatar" id="avatar" accept="image/*">
<p class="status"></p>
</div>
<div class="field required">
<label for="bio">个人简介</label>
<textarea id="bio" name="bio" rows="5" v-model="user.bio"></textarea>
<p class="status"></p>
</div>
<input type="submit" class="ui button fluid" value="注册">
</form>
</div>
</div>
<%- include('footer') %>
在视图中,我们使用了jQuery validate来进行客户端页面的表单验证,这样可以加深我们的使用体验。文档请参见jQuery Validate或者jQuery Validate 官网
文档里面讲得很清楚明白,有些这些不需要和后端有交互,前端直接验证就可以搞定,比较简单。但是用户名name需要和后端交互,通过在后端检测用户名是否存在,然后返回给前端。jquery validate中提供了remote:URL方法来进行异步验证,使用 ajax 方式进行验证,默认会提交当前验证的值到远程地址,如果需要提交其他的值,可以使用 data 选项。如下:
rules: {
name: {
remote: {
url: "/signup/signupcheck", // 后台处理程序地址
type: "post", // 数据发送方式
dataType: "json", // 接受数据格式
data: { // 要传递的数据
name: function() {
return $("#name").val();
}
}
}
}
}
这里我们简单的使用使用jQuery mockjax来模拟Ajax,具体资料自行百度。
$.mockjax({
url:"users.action",
response: function (settings) {
var user = settings.data.name,
users = ["phr","asdf","zhangshan"];
this.responseText = "true";
if($.inArray(user,users) !== -1) {
this.responseText = "false";
}
},
responseTime: 500
});
我们使用了表单,肯定是要来出来表单的,我们使用express-formidable处理 form 表单(包括文件上传)。修改 index.js ,在app.use(flash())
下一行添加如下代码:
index.js
// 处理表单及文件上传的中间件
app.use(require('express-formidable')({
uploadDir: path.join(__dirname, 'public/img'), // 上传文件目录
keepExtensions: true// 保留后缀
}))
这样一个注册功能基本上是实现了.
待续。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。