项目结构
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文件
- 生成私钥:genrsa -out rsa_private_key.pem 2048
- 生成公钥: 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-jwt 和 passport 中间件来验证 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 的选项。
| Key | Description |
|---|---|
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 })
有两个选项可用,destination 和 filename。他们都是用来确定文件存储位置的函数。
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('前端传值字段名') */