基于Node Express、MongoDB

244 阅读7分钟

项目结构

server-mate
├─ app.js --------- 配置文件
├─ bin ------------ 项目的入口文件 www
│  └─ www
├─ package.json --- 依赖配置文件
├─ README.md ------ 项目开发文档
├─ routes --------- API路由规划
│  ├─ index.js
│  └─ users.js
├─ utils ---------- 工具箱
├─ models --------- Schemay,数据模型
├─ config --------- 数据库...配置文件
└─ yarn.lock

数据结构

{
  "code": 200,
  "state": 1,
  "message": "操作成功",
  "data": {}
}

code: HTTP 状态码 stats: 状态码 0 or 1 message: 提示信息 data: 数据

请求参数处理

const cookieParser = require('cookie-parser');

解析 JSON 格式的请求体数据 `app.use(express.json());`
解析 URL-encoded form 格式的请求体数据 `app.use(express.urlencoded({ extended: false }));`
`app.use(cookieParser());`

处理私密内容

使用的公钥与私钥可自己生成,操作如下:

通过 openssl 工具生成RSA的公钥和私钥( 需要下载 )

打开 openssl 文件夹下的bin文件夹,执行openssl.exe文件

  1. 生成私钥:genrsa -out rsa_private_key.pem 2048
  2. 生成公钥: rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

Navicat for MongoDB 使用

浏览器输入 localhost:27017/ 出现,It looks like you are trying to access MongoDB over HTTP on the native driver port. 连接成功

连接数据库

config/connect.js

const mongoose = require('mongoose');
const dbInfo = {
    NAME: '',
    PASSWORD: '',
    DBNAME: 'mate-db',
};
/**
 * `mongodb://${dbInfo.NAME}:${dbInfo.PASSWORD}@localhost:27017/${dbInfo.DBNAME}`;
 * @param {*} dbInfo 数据库信息
 * @returns {*}
 */
const mongodbURI = `mongodb://localhost:27017/${dbInfo.DBNAME}`;
mongoose
    .connect(mongodbURI)
    .then(() => {
        console.log('MongoDB 数据库连接成功!');
    })
    .catch((err) => {
        console.log('MongoDB 数据库连接失败!' + err);
    });

日志记录

const logger = require('morgan');

app.use(logger(':method :url :status :res[content-length] - :response-time ms'));

监听文件修改自动重启服务

npm i nodemon -D

"start": "nodemon ./bin/www"

npm run start

创建 Schema 模型

/models/xxx.js

属性描述
type数据类型( 大写字母开头 )
default默认值
max最大值( Number 有效 )
min最小值( Number 有效 )
trim清除前后字符串空格( Boolean )
index定义对 idcard 的索引( Boolean )
required是否必填
validate自定义校验( Function Boolean )
match正则校验
select设置返回数据对象中是否显示该字段信息( 例如密码项 )
enum元组 [0,1] 只能是0 or 1
lowercase转化为小写

UserSchema

const mongoose = require('mongoose');
const { getCurrentTime } = require('../utils');
const UserSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        trim: true,
        validate: (val) => {
            return val.length < 12;
        },
    },
    email: {
        type: String,
        required: true,
        trim: true,
        match: /^[a-z0-9]+([._\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/i,
    },
    password: {
        type: String,
        select: false,
        required: true,
    },
    avatar: {
        type: String,
        default: '',
    },
    introduce: {
        type: String,
        trim: true,
        validate: (val) => {
            return val.length < 40;
        },
    },
    createTime: {
        type: String,
        default: getCurrentTime(),
    },
});
/**
 * mongoose.model('db集合', Schema模型);
 * @param {*}
 * @returns {*}
 */
module.exports = User = mongoose.model('mate-user', UserSchema);

随机生成头像

const crypto = require('crypto');

const Identicon = require('identicon.js');

/**
 * 随机生成哈希头像
 * @param {*} name 用户名
 * @returns {*} 根据一个hash值来随机生成类似 github 头像
 */
const generateAvatar = (name) => {
    const hash = crypto.createHash('md5');
    hash.update(name);
    const imgData = new Identicon(hash.digest('hex')).toString();
    return 'data:image/png;base64,' + imgData;
};

注册路由

router/users/index.js

const express = require('express');
const router = express.Router();
​
/* GET users listing. */
router.get('/', function (req, res, next) {
    res.send('user');
});module.exports = router;

router/index.js 路由主文件

/**
 * 注册所有路由
 * @param {*} app express实例
 * @param {*} fileName 当前目录所有文件名
 * @returns {*}
 */
const fs = require('fs');
module.exports = (app) => {
    fs.readdirSync(__dirname).forEach((fileName) => {
        if (fileName === 'index.js') return;
        const router = require(`./${fileName}`);
        app.use(`/api/${fileName}`, router);
    });
};

const Routrs = require('./routes/index');

Routrs(app);

接口路径为:

/ api / 该路由所处目录文件名 / xxx

module-alias介绍

解决 CommonJS 的路径别名插件,ES Module 可直接在 package.json 配置

npm i module-alias

app.js最前,require('module-alias/register')

package.json中增加_moduleAliases属性进行配置

"_moduleAliases": {
		"models": "./models",
		"utils": "./utils"
	}

引入 bcryptjs 对密码进行加密

npm i bcryptjs

const bcrypt = require('bcryptjs');

nodemailer 实现邮件发送

npm install nodemailer

准备工作:

登录 QQ邮箱 获取授权码

const nodemailer = require('nodemailer');

getSubject(code)为主题HTML代码,code 为验证码

/**
 * 邮件发送
 * @param {*}
 * @returns {*}
 */
function sendEmail(email, code) {
	const transporter = nodemailer.createTransport({
		host: 'smtp.qq.com',
		port: 587 /* SMTP 端口 */,
		auth: {
			/* 发送者的账户和授权码 */
			user: '2683030687@qq.com',
			pass: 'tcyaiogjcpmjechb',
		},
	});
	const mailOptions = {
		from: '"🦫 Li" <2683030687@qq.com>',
		to: email /* 接收者的邮箱地址 */,
		subject: '请查收验证码' /* 邮件主题标题 */,
		html: getSubject(code),
	};
	//发送邮件
	return new Promise((resolve, reject) => {
		transporter.sendMail(mailOptions, (error, info) => {
			if (error) {
				return reject();
			}
			console.log('邮件发送成功 ID:', info.messageId);
			return resolve();
		});
	});
}

如果需要实现定时发送邮件,可以使用node-schedule这个第三方库来完成

注册路由

/**
 * 注册
 * @param {POST} /api/users/register
 * @param {name, password, email, code, introduce} 用户名,密码,邮箱,验证码,简介
 * @returns {*}
 */
router.post(
	'/register',
	({ body: { name, password, email, code, introduce } }, res) => {
		if (codes[email] != code) {
			res.json({
				code: 406,
				state: 1,
				message: '验证码错误!',
				data: {},
			});
			return;
		}
		User.findOne({ email: email }).then((user) => {
			if (user) {
				return res.json({
					code: 406,
					state: 1,
					message: '邮箱已被注册!',
					data: {},
				});
			}
			avatar = generateAvatar(name);
			const newUser = new User({
				name,
				email,
				avatar,
				password,
				introduce,
			});
			bcrypt.genSalt(10, function (err, salt) {
				/* 将哈希存储在密码数据库中 */
				bcrypt.hash(newUser.password, salt, function (err, hash) {
					if (err) return err;
					newUser.password = hash;
					newUser
						.save()
						.then((data) => {
							res.json({
								code: 200,
								state: 1,
								message: '注册成功!',
								data,
							});
						})
						.catch((err) => {
							res.json({
								code: 500,
								state: 0,
								message: '注册失败!',
								data: err,
							});
						});
				});
			});
		});
	}
);

获取验证码路由

/**
 * 获取验证码
 * @param {GET} /api/users/generateCode
 * @returns {*}
 */
router.get('/generateCode', ({ body: { email } }, res) => {
	const CODE = parseInt(Math.random() * (9999 - 100 * 10) + 100 * 10);
	sendEmail(email, CODE)
		.then(() => {
			codes[email] = CODE;
			res.json({
				code: 200,
				state: 1,
				message: '验证码发送成功!',
				data: {},
			});
		})
		.catch(() => {
			res.json({
				code: 500,
				state: 0,
				message: '验证码发送失败!',
				data: {},
			});
		});
});

登录路由

/**
 * 登录
 * @param {POST} /api/user/login
 * @returns {*}
 */
router.post('/login', ({ body }, res) => {
	const { email, password } = body;
	User.findOne({ email }).then((user) => {
		if (!user) {
			return res.json({
				code: 406,
				state: 0,
				message: '不存在该邮箱账号!',
				data: {},
			});
		}
		/* 用户存在 -> 密码匹配 */
		bcrypt.compare(password, user.password).then(async (isMatch) => {
			if (isMatch) {
				/* JWT 赋予一个 token */
				const { password, ...rule } = user;
				const token = await generateToken(rule);
				return res.json({
					code: 200,
					state: 1,
					message: '登录成功!',
					data: token,
				});
			}
			res.json({
				code: 406,
				state: 0,
				message: '密码错误!',
				data: {},
			});
		});
	});
});

生成 ToKen

npm i jsonwebtoken

const jwt = require('jsonwebtoken');

/**
 * 生成 Token
 * @param {*} 规则用户信息
 * @returns {*}
 */
function generateToken(rule) {
	return new Promise((resolve, reject) => {
		jwt.sign(rule, SECRET_KEY, { expiresIn: 3600 * 12 }, (err, token) => {
			if (err) return reject(err);
			resolve('Bearer ' + token);
		});
	});
} /* jwt.sign("规则信息", "加密值", "过期时间", "箭头函数"); */

Token 有效验证

使用 passport-jwtpassport 中间件来验证 token

passport 是 express 框架的一个针对密码的中间件

passport 提供了一个 authenticate 函数,用作路由中间件对请求进行身份验证

passport-jwt 是一个针对 jsonwebtoken 的插件

npm install passport

对 passport-jwt passport 进行配置

config/passport.js

const { Strategy, ExtractJwt } = require('passport-jwt');
const User = require('models/User');
const { SECRET_KEY } = require('./jwt');
const option = {
	jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
	secretOrKey: 'SECRET_KEY',
};

/**
 * 验证 token
 * @param {*} passport
 * @returns {*}
 */
function verifyToken(passport) {
	passport.use(
        /** jwt_payload为用户信息 **/
		new Strategy(option, (jwt_payload, done) => {
			User.findById(jwt_payload.id)
				.then((user) => {
					if (user) {
						return done(null, user);
					}
					return done(null, false);
				})
				.catch((err) => {
					console.log('未授权', err);
				});
		})
	);
}
module.exports = verifyToken;

使用步骤:

1、在入口文件app.js 引入 passport ,初始化 passport

const passport = require("passport");

2、在入口文件app.js 引入 passport 配置文件( 身份验证方法 )

require('config/connect');
app.use(passport.initialize());
require('config/passport')(passport);
Routrs(app);
/** 连接数据库之后、处理路由之前(引入) **/

3、在需要验证的路由接口处理函数中,配置 passport

const passport = require('passport');

router.get('路径',passport.authenticate('jwt', { session: false }),(req, res) => {});

当身份验证失败时,返回Unauthorized401 响应状态码

res.status(401).json(info);

另外:使用express-jwtye 可以校验 token 的有效

const expressJwt=require('express-jwt')
/** 验证token是否过期并规定那些路由不需要验证 **/
app.use(expressJwt({
  secret:SECRET_KEY,
  algorithms:['HS256']
}).unless({/** 白名单 **/
  path:['/login']
}))

更多方法参考

multer or formidable 实现文件上传

Multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。它是写在 busboy 之上非常高效。

注意: Multer 不会处理任何非 multipart/form-data 类型的表单数据。

npm install multer

multer(options)

Multer 接受一个 options 对象,其中最基本的是 dest 属性,这将告诉 Multer 将上传文件保存在哪。如果你省略 options 对象,这些文件将保存在内存中,永远不会写入磁盘。

为了避免命名冲突,Multer 会修改上传的文件名。这个重命名功能可以根据您的需要定制。

以下是可以传递给 Multer 的选项。

KeyDescription
dest or storage在哪里存储文件 ,这里的路径必须是相对路径,否则解析失败。使用 path.join 得到相对路径
fileFilter文件过滤器,控制哪些文件可以被接受
limits限制上传的数据
preservePath保存包含文件名的完整文件路径

storage

磁盘存储引擎可以让你控制文件的存储。

const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, '/tmp/my-uploads')/** 路径必须是相对路径 **/
  },
  filename: function (req, file, cb) {
    cb(null, file.fieldname + '-' + Date.now())
  }
})

const upload = multer({ storage: storage })

有两个选项可用,destinationfilename。他们都是用来确定文件存储位置的函数。

destination 是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个 string (例如 '/tmp/uploads')

Multer 在解析完请求体后,会向 Request 对象中添加一个 body 对象和一个 file 或 files 对象(上传多个文件时使用files对象 )。其中,body 对象中包含所提交表单中的文本字段(如果有),而 file(或files) 对象中包含通过表单上传的文件。

multer 解析完上传文件后,会被保存为一个包含以下字段的对象:

  • fieldname - 表单提交的文件名(input控件的name属性)
  • originalname - 文件在用户设备中的原始名称
  • encoding - 文件的编码类型
  • mimetype - 文件的Mime类型
  • size - 文件的大小
  • destination - 文件的保存目录(DiskStorage)
  • filename - 文件在destination中的名称(DiskStorage)
  • path - 上传文件的全路径(DiskStorage)
  • buffer - 文件对象的Buffer(MemoryStorage)

详情参考

使用mkdirp创建文件保存目录

npm i mkdirp

上传核心代码

const { getFormatTime } = require('utils');
const multer = require('multer');
const mkdirp = require('mkdirp');
const path = require('path');
const storage = multer.diskStorage({
	destination: async (req, file, cb) => {
		let dir = path.join('public/', file.mimetype);
		await mkdirp(dir);
		cb(null, dir);
	},
	filename: function (req, file, cb) {
		const nameArr = file['originalname'].split('.');
		const type = nameArr[nameArr.length - 1];
		const name = getFormatTime('YYYY-MM-DD HH_mm_ss');
		const filename = name + '.' + type;
		console.log(filename);
		cb(null, filename);
	},
});

const upload = multer({ storage: storage });
router.post('/upload', upload.single('avatar'), (req, res) => {
	console.log(req.file);
	console.log('---');
	res.send({
		file: req.file,
	});
}); /* upload.single('前端传值字段名') */