node+koa+nedb 实现登录、注册、修改密码、用户列表删除、查询、分页

224 阅读6分钟

一. 项目的初始化

1. npm 初始化

npm init -y
//生成package.json文件:

2. git 初始化

git init
//生成'.git'隐藏文件夹, git 的本地仓库

3 . 创建 ReadMe.MD 文件

二. 搭建项目

1 安装 Koa 框架

npm install koa

2 编写最基本的 app

创建src/main.js

const Koa = require('koa')

const app = new Koa()

app.use((ctx, next) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('server is running on http://localhost:3000')
})

3 测试

在终端, 使用node src/main.js

image-20210521142016066

三. 项目的基本优化

1 自动重启服务

安装 nodemon 工具

npm i nodemon -D

编写package.json脚本

"scripts": {
  "dev": "nodemon ./src/main.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},

执行npm run dev启动服务

image-20210521142807478

2 读取配置文件

安装dotenv ,读取根目录中的.env文件, 将配置写到process.env

创建 .env 文件

APP_PORT=8000

创建src/config/config.default.js

onst dotenv = require('dotenv')

dotenv.config()

// console.log(process.env.APP_PORT)

module.exports = process.env

改写main.js

const Koa = require('koa')

const { APP_PORT } = require('./config/config.default')

const app = new Koa()

app.use((ctx, next) => {
  ctx.body = 'hello api'
})

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`)
})

四. 添加路由

路由: 根据不同的 URL, 调用对应处理函数

1 安装 koa-router

npm i koa-router

步骤:

  1. 导入包

  2. 实例化对象

  3. 编写路由

  4. 注册中间件

2 编写路由

创建src/router目录, 编写user.route.js

const Router = require('koa-router')

const router = new Router({ prefix: '/users' })

// GET /users/
router.get('/', (ctx, next) => {
  ctx.body = 'hello users'
})

module.exports = router

3 改写 main.js

const Koa = require('koa')

const { APP_PORT } = require('./config/config.default')

const userRouter = require('./router/user.route')

const app = new Koa()

app.use(userRouter.routes())

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`)
})

五. 目录结构优化

1 将 http 服务和 app 业务拆分

创建src/app/index.js

const Koa = require('koa')

const userRouter = require('../router/user.route')

const app = new Koa()

app.use(userRouter.routes())

module.exports = app

改写main.js

const { APP_PORT } = require('./config/config.default')

const app = require('./app')

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}` )
})

 npm i koa-body

六.跨域

安装跨域

npm install koa-cors --save

在 main.js 引入和注册

const cors = require('koa-cors');   // 解决跨域
app.use(cors());

七. 连接数据库 nedb

1.安装中间键

npm install --save koa-bodyparser

改写main.js

const bodyParser = require('koa-bodyparser');
app.use(bodyParser());//(格式数据)

2.安装数据库模块

npm install nedb --save

八、使用 nedb( 增、删、改、查)

前端页面

企业微信截图_16854377216662.png

增、删、改、查

接口规整

const Router = require('koa-router');

const {
	register,
	login,
	changepassword,
	unsubscribe,
} = require('../controller/user.controller');
const { userlist, deletedata } = require('../controller/userlist');
const router = new Router({ prefix: '/users' });
// 登录注册页
router.post('/register', register); //注册接口
router.post('/login', login); //登录接口
router.post('/changepassword', changepassword); //忘记密码接口
router.post('/unsubscribe', unsubscribe); //注销信息接口

module.exports = router;

var Datastore = require('nedb');
db = new Datastore({
	filename: './src/dbdata/userlist.db', //文件从根目录出发,如果想要存在src文件里,那么就要./src/...,在这里读取文件时从外到里引入的文件,更加细致,也可以在这里直接写一个./xxx,依然从根目录出发,生成一个未创建的文件夹
	autoload: true,
});
class userController {
    //注册
	async register(ctx, next) {
		let registerData = await parseDataFun(ctx, 'register');
		ctx.body = registerData;
	}
    //登录
	async login(ctx, next) {
		let loginData = await parseDataFun(ctx, 'login');
		ctx.body = loginData;
	}
    
    //修改密码
	async changepassword(ctx, next) {
		let changepassword = await changeDataFun(ctx, 'changepassword');
		ctx.body = changepassword;
	}
    //注销账户
	async unsubscribe(ctx, next) {
		let unsubscribedata = await removeDataFun(ctx, 'unsubscribeuser');
		ctx.body = unsubscribedata;
	}
}
//初始化传输给前端的数据
const returnData = {
	code: 200,
	data: {},
	msg: '注册成功',
};
const phoneNumberreg =
	/^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/;
// 解析上下文里node原生请求的POST参数(注册登录)
function parseDataFun(ctx, type) {
	return new Promise((resolve, reject) => {
		try {
			let postdata = '';
			ctx.req.addListener('data', (data) => {
				postdata += data;
                //格式化获取的数据
				const jsdata = JSON.parse(postdata);
				db.find({ phoneNumber: jsdata.phoneNumber }, function (err, doc) {
					//如果数据库有
					if (doc.length > 0) {
						if (type == 'login') {
							console.log(doc[0].password, jsdata.password, '登录成功');
							if (doc[0].password == jsdata.password) {
								returnData.code = 200;
								returnData.msg = '登录成功';
								resolve(returnData);
							} else {
								returnData.code = 500;
								returnData.msg = '密码错误';
								resolve(returnData);
							}
						} else if (type == 'register') {
							returnData.code = 500;
							returnData.msg = '手机号已存在';
							resolve(returnData);
						}
						//如果数据库没有
					} else if (doc.length == 0) {
						if (type == 'login') {
							returnData.code = 500;
							returnData.msg = '手机号不存在,请注册';
							resolve(returnData);
						} else if (type == 'register') {
                            //判断确认密码和密码两个是否一致
                            //一致
							if (
								jsdata.checkpassword == jsdata.password &&
								phoneNumberreg.test(jsdata.phoneNumber)
							) {
								// 提前将json文件转换成对象(因为只是用json的话,第二次或获取的数据将会是undefine),生成的数据先在后面生成_id=随机xxx,后转为json格式呈现,
								db.insert(jsdata, function (inserterr, newDoc) {
									console.log('插入成功:', newDoc);
								});
								returnData.code = 200;
								returnData.msg = '注册成功';
								resolve(returnData);
                                //不一致
							} else if (jsdata.checkpassword != jsdata.password) {
								returnData.code = 500;
								returnData.msg = '两个密码不一致';
								resolve(returnData);
                                //这么好则校验手机号(顺序有误)
							} else if (!phoneNumberreg.test(jsdata.phoneNumber)) {
								returnData.code = 500;
								returnData.msg = '手机号错误';
								resolve(returnData);
							}
						}
					}
				});
			});
			ctx.req.addListener('end', function () {
				// resolve(returnData);
			});
		} catch (err) {
			reject(err);
		}
	});
}
// 修改数据
//问题一:需要根据手机号,捕捉到对应原密码,然后修改原密码,但是目前没办法捕获到对应密码的手机号
// 解决1:在登录时,先直接保存对应的手机号
// 解决2:提示四个输入框:手机号,验证码,旧密码,新密码
function changeDataFun(ctx, type) {
	return new Promise((resolve, reject) => {
		try {
			let postdata = '';
			ctx.req.addListener('data', (data) => {
				postdata += data;
                // 1.格式化传输过来的数据
				const JSONdata = JSON.parse(postdata);
				console.log(JSONdata, 'postdata');
                //检查数据库是否有该手机号
				db.find({ phoneNumber: JSONdata.phoneNumber }, function (err, doc) {
					console.log(doc, '');]
                    //如果没有
					if (doc.length == 0) {
						returnData.code = 500;
						returnData.msg = '手机号错误';
						resolve(returnData);
					} else {
                        //如果有
                        //检查数据库的密码是否和拿到的密码相匹配
						if (JSONdata.oldpassword != doc[0].password) {
							returnData.code = 500;
							returnData.msg = '旧密码不匹配';
							resolve(returnData);
						} else {
							// 匹配 旧手机号和旧密码
							const query = {
								phoneNumber: doc[0].phoneNumber,
								password: doc[0].password,
							};
							// 将匹配到的数据替换
							const update = {
								phoneNumber: doc[0].phoneNumber,
								password: JSONdata.newpassword,
							};
							const options = { multi: true, returnUpdatedDocs: true };
							db.update(
								query,
								update,
								options,
								(err, numAffected, affectedDocuments, upsert) => {
									returnData.code = 200;
									returnData.msg = '密码修改成功';
									resolve(returnData);
									console.log(
										`修改了${numAffected}个文档,修改后的文档为:`,
										affectedDocuments
									);
								}
							);
						}
					}
				});
			});
			ctx.req.addListener('end', function () {
				// resolve(returnData);
			});
		} catch (err) {
			reject(err);
		}
	});
}

// 注销账户
// 问题一:是否需要用户手机号,密码

function removeDataFun(ctx, type) {
	return new Promise((resolve, reject) => {
		try {
			let removeData = '';
			ctx.req.addListener('data', (data) => {
				removeData += data;
                // 1.格式化传输过来的数据
				const jsremoveData = JSON.parse(removeData);
                // 2.正则手机号判断
				if (!phoneNumberreg.test(jsremoveData.phoneNumber)) {
                    //如果手机号格式错误
					returnData.code = 500;
					returnData.msg = '手机号错误';
					resolve(returnData);
				} else {
                    //如果手机号正确
                    // 3. 检查数据库是否有该手机号
					db.find(
						{ phoneNumber: jsremoveData.phoneNumber },
						function (err, doc) {
							console.log(doc, '');
                            //如果数据库没有
							if (doc.length == 0) {
								returnData.code = 500;
								returnData.msg = '手机号不存在,请确认输入内容';
								resolve(returnData);
							} else {
								// 如果有,删除一条数据库
								db.remove(
									{ phoneNumber: doc[0].phoneNumber },
									{},
									function (err, numRemoved) {
										returnData.code = 200;
										returnData.msg = '注销账户成功,请登录';
										resolve(returnData);
									}
								);
							}
						}
					);
				}
			});
			ctx.req.addListener('end', function () {
				resolve(returnData);
			});
		} catch (err) {
			reject(err);
		}
	});
}
module.exports = new userController();

九.模块化 nedb 文 件

创建 src/dbconfig/index.js 文件

const Datastore = require('nedb');
userdb = new Datastore({
	filename: './src/dbdata/userlist.db', //文件从根目录出发,如果想要存在src文件里,那么就要./src/...,在这里读取文件时从外到里引入的文件,更加细致,也可以在这里直接写一个./xxx,依然从根目录出发,生成一个未创建的文件夹
	autoload: true,
    noSort: true,//不排序
});
module.exports = { userdb };

使用

user.controller.js

// 登录注册接口
const { userdb } = require('../dbConfig/index');

userlist.js

// 用户数据列表接口
const { userdb } = require('../dbConfig/index');

十、用户列表

1.用户列表 get 接口

企业微信截图_16854377471366.png

规整路由出口

const Router = require('koa-router');

const {
	register,
	login,
	changepassword,
	unsubscribe,
} = require('../controller/user.controller');
const { userlist, deletedata } = require('../controller/userlist');
const router = new Router({ prefix: '/users' });
// 登录注册页
router.post('/register', register); //注册接口
router.post('/login', login); //登录接口
router.post('/changepassword', changepassword); //忘记密码接口
router.post('/unsubscribe', unsubscribe); //注销信息接口
// 用户管理列表页
router.get('/userlist', userlist); //用户列表接口++++
router.post('/deletedata', deletedata); //删除列表接口+++

module.exports = router;

2.userlist.js

// 用户数据列表接口
// 引入数据库
const { userdb } = require('../dbConfig/index');
class userController {
	async userlist(ctx, next) {
		let userlistData = await getUserListFun(ctx, 'userlist');
		ctx.body = userlistData;
	}
	async deletedata(ctx, next) {
		let userdeleteData = await postdeleteFun(ctx, 'deletedata');
		ctx.body = userdeleteData;
	}
}

const getreturndata = {
	code: 200,
	data: {
		list: [],
		pageNum: 1,
		pageSize: 10,
		total: 0,
	},
	msg: 'OK',
};
function getUserListFun(ctx, type) {
	return new Promise((resolve, reject) => {
		try {
			//1.初始化数据库总数
			userdb.count({}, function (err, count) {
				getreturndata.data.total = count;
			});
			//初始化传过来的值,如果没传,就使用默认的
			getreturndata.data.pageNum = parseInt(ctx.request.query.pageNum) || 1; // 当前页码
			getreturndata.data.pageSize = parseInt(ctx.request.query.pageSize) || 10; // 每页文档数量
			const skipNum =
				(getreturndata.data.pageNum - 1) * getreturndata.data.pageSize; // 要跳过的文档数量
			const queryphoneNumber = parseInt(ctx.request.query.phoneNumber) || '';
			console.log(queryphoneNumber, '');
			//正则模糊查询
			userdb
				.find({ phoneNumber: new RegExp(queryphoneNumber, 'i') })
				.skip(skipNum)
				.limit(getreturndata.data.pageSize)
				.exec(function (err, docs) {
					getreturndata.data.list = docs;
					//如果有在查询,就使用查询到的数据长度
					if (queryphoneNumber) {
						getreturndata.data.total = docs.length;
					} else {
						//否则使用数据库的总数据
						userdb.count({}, function (err, count) {
							getreturndata.data.total = count;
						});
					}
					//将数据回馈给用户
					resolve(getreturndata);
				});
		} catch (error) {
			reject(error);
		}
	});
}
// 删除数据
function postdeleteFun(ctx, type) {
	return new Promise((resolve, reject) => {
		try {
			let removeData = '';
			ctx.req.addListener('data', (data) => {
				removeData += data;
				const jsremoveData = JSON.parse(removeData);
				if (!jsremoveData.id) {
					getreturndata.code = 500;
					getreturndata.msg = 'id为必选参数';
					getreturndata.data = {};
				} else {
					userdb.remove(
						{ _id: jsremoveData.id },
						{},
						function (err, numRemoved) {
							getreturndata.code = 200;
							getreturndata.msg = '删除成功';
							getreturndata.data = {};
							resolve(getreturndata);
						}
					);
				}
			});
			ctx.req.addListener('end', function () {
				resolve(getreturndata);
			});
		} catch (err) {
			reject(err);
		}
	});
}
module.exports = new userController();

nedb 坑:

1.一个 .db 不能引用两次

在 NeDB 中,同一个 db 文件只能被同一个实例引用,如果尝试在两个不同的文件或模块中引入同一个 db 文件,会出现数据混乱或文件被锁定的情况。

原因在于,NeDB 的读写操作需要对 db 文件进行独占式访问,因此当多个实例同时引用同一个 db 文件时,会出现数据互相覆盖或者文件被锁定的情况,导致应用程序无法正常运行。

为了避免这种情况发生,可以在一个模块中创建一个单例数据库实例并导出,其他模块只需要引用该实例即可。例如:

// db.js
const Datastore = require('nedb');
const db = new Datastore({ filename: 'db.db', autoload: true });

module.exports = db;
// module1.js
const db = require('./db');
// 使用 db 进行数据库操作
// module2.js
const db = require('./db');
// 使用 db 进行数据库操作