4.7.1 用户模型设计

我们只存储用户的名称、密码(加密后的)、头像、性别和个人简介这几个字段,对应修改 lib/mongo.js,添加如下代码:

lib/mongo.js

  1. exports.User = mongolass.model('User', {
  2. name: { type: 'string', required: true },
  3. password: { type: 'string', required: true },
  4. avatar: { type: 'string', required: true },
  5. gender: { type: 'string', enum: ['m', 'f', 'x'], default: 'x' },
  6. bio: { type: 'string', required: true }
  7. })
  8. exports.User.index({ name: 1 }, { unique: true }).exec()// 根据用户名找到用户,用户名全局唯一

我们定义了用户表的 schema,生成并导出了 User 这个 model,同时设置了 name 的唯一索引,保证用户名是不重复的。

小提示:required: true 表示该字段是必需的,default: xxx 用于创建文档时设置默认值。更多关于 Mongolass 的 schema 的用法,请查阅 another-json-schema

小提示:Mongolass 中的 model 你可以认为相当于 mongodb 中的 collection,只不过添加了插件的功能。

4.7.2 注册页

首先,我们来完成注册。新建 views/signup.ejs,添加如下代码:

views/signup.ejs

  1. <%- include('header') %>
  2. <div class="ui grid">
  3. <div class="four wide column"></div>
  4. <div class="eight wide column">
  5. <form class="ui form segment" method="post" enctype="multipart/form-data">
  6. <div class="field required">
  7. <label>用户名</label>
  8. <input placeholder="用户名" type="text" name="name">
  9. </div>
  10. <div class="field required">
  11. <label>密码</label>
  12. <input placeholder="密码" type="password" name="password">
  13. </div>
  14. <div class="field required">
  15. <label>重复密码</label>
  16. <input placeholder="重复密码" type="password" name="repassword">
  17. </div>
  18. <div class="field required">
  19. <label>性别</label>
  20. <select class="ui compact selection dropdown" name="gender">
  21. <option value="m"></option>
  22. <option value="f"></option>
  23. <option value="x">保密</option>
  24. </select>
  25. </div>
  26. <div class="field required">
  27. <label>头像</label>
  28. <input type="file" name="avatar">
  29. </div>
  30. <div class="field required">
  31. <label>个人简介</label>
  32. <textarea name="bio" rows="5"></textarea>
  33. </div>
  34. <input type="submit" class="ui button fluid" value="注册">
  35. </form>
  36. </div>
  37. </div>
  38. <%- include('footer') %>

注意:form 表单要添加 enctype="multipart/form-data" 属性才能上传文件。

修改 routes/signup.js 中获取注册页的路由如下:

routes/signup.js

  1. // GET /signup 注册页
  2. router.get('/', checkNotLogin, function (req, res, next) {
  3. res.render('signup')
  4. })

现在访问 localhost:3000/signup 看看效果吧。

4.7.3 注册与文件上传

我们使用 express-formidable 处理 form 表单(包括文件上传)。修改 index.js ,在 app.use(flash()) 下一行添加如下代码:

index.js

  1. // 处理表单及文件上传的中间件
  2. app.use(require('express-formidable')({
  3. uploadDir: path.join(__dirname, 'public/img'), // 上传文件目录
  4. keepExtensions: true// 保留后缀
  5. }))

新建 models/users.js,添加如下代码:

models/users.js

  1. const User = require('../lib/mongo').User
  2. module.exports = {
  3. // 注册一个用户
  4. create: function create (user) {
  5. return User.create(user).exec()
  6. }
  7. }

完善处理用户注册的路由,最终修改 routes/signup.js 如下:

routes/signup.js

  1. const fs = require('fs')
  2. const path = require('path')
  3. const sha1 = require('sha1')
  4. const express = require('express')
  5. const router = express.Router()
  6. const UserModel = require('../models/users')
  7. const checkNotLogin = require('../middlewares/check').checkNotLogin
  8. // GET /signup 注册页
  9. router.get('/', checkNotLogin, function (req, res, next) {
  10. res.render('signup')
  11. })
  12. // POST /signup 用户注册
  13. router.post('/', checkNotLogin, function (req, res, next) {
  14. const name = req.fields.name
  15. const gender = req.fields.gender
  16. const bio = req.fields.bio
  17. const avatar = req.files.avatar.path.split(path.sep).pop()
  18. let password = req.fields.password
  19. const repassword = req.fields.repassword
  20. // 校验参数
  21. try {
  22. if (!(name.length >= 1 && name.length <= 10)) {
  23. throw new Error('名字请限制在 1-10 个字符')
  24. }
  25. if (['m', 'f', 'x'].indexOf(gender) === -1) {
  26. throw new Error('性别只能是 m、f 或 x')
  27. }
  28. if (!(bio.length >= 1 && bio.length <= 30)) {
  29. throw new Error('个人简介请限制在 1-30 个字符')
  30. }
  31. if (!req.files.avatar.name) {
  32. throw new Error('缺少头像')
  33. }
  34. if (password.length < 6) {
  35. throw new Error('密码至少 6 个字符')
  36. }
  37. if (password !== repassword) {
  38. throw new Error('两次输入密码不一致')
  39. }
  40. } catch (e) {
  41. // 注册失败,异步删除上传的头像
  42. fs.unlink(req.files.avatar.path)
  43. req.flash('error', e.message)
  44. return res.redirect('/signup')
  45. }
  46. // 明文密码加密
  47. password = sha1(password)
  48. // 待写入数据库的用户信息
  49. let user = {
  50. name: name,
  51. password: password,
  52. gender: gender,
  53. bio: bio,
  54. avatar: avatar
  55. }
  56. // 用户信息写入数据库
  57. UserModel.create(user)
  58. .then(function (result) {
  59. // 此 user 是插入 mongodb 后的值,包含 _id
  60. user = result.ops[0]
  61. // 删除密码这种敏感信息,将用户信息存入 session
  62. delete user.password
  63. req.session.user = user
  64. // 写入 flash
  65. req.flash('success', '注册成功')
  66. // 跳转到首页
  67. res.redirect('/posts')
  68. })
  69. .catch(function (e) {
  70. // 注册失败,异步删除上传的头像
  71. fs.unlink(req.files.avatar.path)
  72. // 用户名被占用则跳回注册页,而不是错误页
  73. if (e.message.match('duplicate key')) {
  74. req.flash('error', '用户名已被占用')
  75. return res.redirect('/signup')
  76. }
  77. next(e)
  78. })
  79. })
  80. module.exports = router

我们使用 express-formidable 处理表单的上传,表单普通字段挂载到 req.fields 上,表单上传后的文件挂载到 req.files 上,文件存储在 public/img 目录下。然后校验了参数,校验通过后将用户信息插入到 MongoDB 中,成功则跳转到主页并显示『注册成功』的通知,失败(如用户名被占用)则跳转回注册页面并显示『用户名已被占用』的通知。

注意:我们使用 sha1 加密用户的密码,sha1 并不是一种十分安全的加密方式,实际开发中可以使用更安全的 bcryptscrypt 加密。
注意:注册失败时(参数校验失败或者存数据库时出错)删除已经上传到 public/img 目录下的头像。

为了方便观察效果,我们先创建主页的模板。修改 routes/posts.js 中对应代码如下:

routes/posts.js

  1. router.get('/', function (req, res, next) {
  2. res.render('posts')
  3. })

新建 views/posts.ejs,添加如下代码:

views/posts.ejs

  1. <%- include('header') %>
  2. 这是主页
  3. <%- include('footer') %>

访问 localhost:3000/signup,注册成功后如下所示:

注册 - 图1