博客十

前言

我们通过引入会话(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 &nbsp; as text for IE
                label.html("&nbsp;").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// 保留后缀
}))

这样一个注册功能基本上是实现了.

待续。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容