nodejs学习笔记-10项目的开发

119 阅读9分钟

1.项目初始化

1.1 创建项目

  1. 新建21-api_server文件夹作为项目的根目录,并在项目根目录中运行如下命令,初始化包管理配置文件:
npm init -y

2.运行npm i express@4.17.1命令,安装特定版本的express 3.在项目根目录中新建app.js文件作为整个项目的入口文件,使用如下代码初始化一个服务器:

// 导入express
const express = require("express");

// 创建服务器的实例对象
const app = express();

// 启动服务器
app.listen(3007, () => {
  console.log("api_server running at 127.0.0.1:3007");
});

1.2配置cors跨域

  1. 运行npm install cors@2.8.5安装cors中间件
  2. app.js文件中导入并配置cors中间件
// 配置cors,解决跨域问题
const cors = require("cors");

// 将cors注册为全局中间件
app.use(cors());

1.3配置解析表单数据的中间件

通过如下代码,配置解析application/x-www-form-urlencoded格式的表单数据的中间件:

// 配置解析表单数据的中间件,
// 注意:这个中间件只能解析 application/x-www-form-urlencoded 格式的表单数据
app.use(express.urlencoded({ extended: false }));

1.4 初始化路由相关的文件夹

  1. 在项目根目录中,新建router文件夹,用来存放所有的路由模块

路由模块中只存放客户端与处理函数之间的映射关系

  1. 在项目根目录中,新建router_handler文件夹,用来存放所有的路由处理函数模块

路由处理函数模块中,专门负责存放每个路由对应的处理函数

1.5初始化用户路由模块

  1. 在router文件夹中,新建user.js文件,作为用户的路由模块,并初始化代码如下:
/*用户的路由模块*/
// 导入express
const express = require("express");

// 创建路由对象
const router = express.Router();

// 注册新用户
router.post("/reguser",(req,res)=>{
    res.send(reguser is ok!)
});
// 登录
router.post("/login", (req,res)=>{
    res.send(login is ok!)
});

// 将路由对象router共享出去
module.exports = router;
  1. app.js文件中,导入并使用用户路由模块
// 导入并使用用户路由模块
const userRouter = require("./router/user");
app.use("/api", userRouter);

1.6抽离用户路由模块中的处理函数

目的:为了保证路由模块的纯粹性,所有的路由处理函数,必须抽离到对应的路由处理函数模块之中

  1. /router_handler目录下,新建user.js文件,使用exports对象,分别向外共享如下两个路由处理函数:
/* 路由对应的路由处理函数模块 ,供/router/user.js调用*/
// 导入数据库的操作模块
const db = require("../db/index");

// 导入 密码加密模块 -bcryptjs
const bcrypt = require("bcryptjs");

// 注册新用户的处理函数
exports.regUser = (req, res) => {
  res.send('regUser is OK~')
};
// 登录的处理函数
exports.login = (req, res) => {
  res.send("login is OK~");
};

2.修改/router/user.js文件中的代码

/*用户的路由模块*/
const express = require("express");

// 创建路由对象
const router = express.Router();

// 导入用户路由对应的路由处理函数
const userHandler = require("../router_handler/user");

// 注册新用户
router.post("/reguser",userHandler.regUser);

// 登录
router.post("/login", userHandler.login);

// 共享出去router
module.exports = router;

2.登录注册

2.1新建ev_users表

my_db_01数据库中,新建ev_users表如下:

image.png

2.2安装并配置mysql模块

在API接口项目中,需要安装并配置mysql这个第三方模块,来连接和操作MySQL数据库

  1. 运行npm install mysql@2.18.1命令,安装mysql模块
  2. 在项目根目录中新建/db/index.js文件,在此自定义模块中创建数据库的连接对象
// 导入mysql模块
const mysql = require('mysql')

// 调用mysql提供的createPool方法
const db = mysql.createPool({
    host:'127.0.0.1', // 数据库的IP地址
    user:'root', // 登录数据库的账号
    password:'admin123', // 登录数据库的密码
    database:'my_db_01' // 指定要操作的哪个数据库
})

module.exports = db

2.3开发注册用户的接口

2.3.0分析实现步骤

  1. 检测客户端提交的表单数据是否合法
  2. 检测用户名是否被占用
  3. 对用户提交的密码进行加密处理
  4. 给ev_users表中插入新用户

2.3.1检测表单数据是否合法

在注册新用户的路由处理函数中判断用户名或者密码是否为空

// 注册新用户的处理函数
exports.regUser = (req, res) => {
  // 获取用户提交到服务器的注册信息
  const userInfo = req.body;
  //   console.log('userInfo`',userInfo)
  // 对表单中的数据进行合法性的校验
  if (!userInfo.username || !userInfo.password) {
    return res.send({ status: 1, message: "用户名或密码不能为空!" });
  }
}

2.3.2 检测用户名是否被占用

导入mysql模块 -----> 定义SQL语句 ----->执行SQL语句并根据结果判断用户名是否被占用

  1. 导入数据库模块const db = require('../db/index')
  2. 定义SQL语句 const sql = 'select * from ev_users where username=?'
  3. 执行SQL语句并根据结果判断用户名是否被占用
  db.query(sqlStr, userInfo.username, (err, results) => {
    // 执行SQL语句失败
    if (err) {
      return res.send({ status: 1, message: err.message });
    }
    // results是一个数组,如果长度大于0,就代表查找到了当前username,说明用户名已被占用
    if (results.length > 0) {
      return res.send({ status: 1, message: "用户名被占用,请更换其他用户名!" });
    }
    // TODO: 用户名可以使用,继续后续的流程...

2.3.3对密码进行加密处理

为了保证用户密码的安全性,不建议在数据库以明文的形式保存用户密码,推荐对密码进行加密存储

当前项目中,使用bcryptjs对用户密码进行加密,优点:

  • 加密之后的密码,无法被逆向破解
  • 同一明文密码多次加密,得到的加密结果各不相同,保证了安全性

  1. 运行npm install bcryptjs@2.4.3命令,安装依赖包
  2. router_handler/user.js文件中,导入bcryptjs
  3. 当查询用户名未被占用可以继续使用之后,调用bcrypt.hashSync(明文密码,随机盐的长度)方法,对用户密码加密处理
  // 加密密码:调用bcrypt.hashSync()方法对密码加密,返回值是加密之后的密码字符串
   userInfo.password = bcrypt.hashSync(userInfo.password, 10);

盐(Salt) 在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。

2.3.4给ev_users表中插入新用户

  // 定义插入新用户的SQL语句
    const sql = "insert into ev_users set ?";
    db.query(sql,{ username: userInfo.username, password: userInfo.password },(err, results)=>{
        //执行SQL语句失败
        if (err) {
          return res.send({ status: 1, message: err.message });
        }
        // 执行SQL语句成功,但影响行数不为1
        if (results.affectedRows !== 1) {
          return res.send({ status: 1, message: "注册用户失败,请稍后再试 !" });
        }
        res.send({ status: 0, messsage: "用户注册成功" });
    });

2.4 优化res.send()代码

我们看到,在路由处理函数中,需要多次调用res.send()方法向客户端响应不同的失败处理结果,为了简化代码,可以手动封装一个res.cc()函数 在app.js文件中,所有路由之前声明一个全局中间件,为res对象挂载一个res.cc()函数

// 在所有路由之前,声明一个全局中间件,为res对象挂载一个res.cc()函数
app.use(function (req, res, next) {
  //status = 1,表示失败的情况,
  //err的值可能是一个错误对象也可能是一个错误的描述字符串
  res.cc = function (err, status = 1) {
    res.send({
      status,
      message: err instanceof Error ? err.message : err,
    });
  };
  next();
});

// TODO:替换路由处理函数中的res.send为res.cc函数
//例如:res.send({ status: 1, message: "用户名或密码不合法!" })替换为 res.cc("用户名或密码不合法!");
// res.send({ status: 1, message: err.message })----->res.cc(err)

2.5优化表单数据验证

在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且后端作为数据合法性验证的最后一个关口,在拦截非法数据方面,尤为重要

单纯的使用if...else...的形式对数据合法性进行验证,效率低下,出错率高,维护性差,因此推荐使用第三方数据验证模块,来降低出错率.提高验证的效率与可维护性

  1. 安装express-joi中间件来实现自动对表单数据进行验证的功能npm install @escook/express-joi
  2. 安装依赖包joi为表单中携带的每个数据项定义验证规则 npm install joi@17.4.0
  3. 在项目根目录中新建/schems/user.js用户信息验证规则模块,并初始化代码如下
// 导入 Joi 来定义验证规则
const joi = require("joi");
/** 
* string() 值必须是字符串 
* alphanum() 值只能是包含 a-zA-Z0-9 的字符串 
* min(length) 最小长度 * max(length) 最大长度 
* required() 值是必填项,不能为 undefined 
* pattern(正则表达式) 值必须符合正则表达式的规则 
*/
//定义用户名和密码的验证规则
const username = joi.string().alphanum().min(1).max(10).required();
const password = joi
  .string()
  .pattern(/^[\s]{6,12}$/)
  .required();

// 定义验证注册和登录表单数据的规则对象
exports.reg_login_schema = {
  body: { username, password },
};
  1. 修改/router/user.js代码如下
const express = require("express");
// 创建路由对象
const router = express.Router();
// 导入用户路由对应的路由处理函数
const userHandler = require("../router_handler/user");

// 导入验证数据的中间件
const expressJoi = require("@escook/express-joi");
// 导入需要的验证规则对象
const { reg_login_schema } = require("../schema/user");

// 注册新用户
router.post("/reguser",expressJoi(reg_login_schema), userHandler.regUser);
// 登录
router.post("/login", userHandler.login);

// 共享出去router
module.exports = router;

5.在app.js路由后面定义全局错误级别中间件,捕获验证失败的错误,并把验证失败的结果响应个客户端

// 定义错误级别的中间件(捕获全局错误)
app.use((err, req, res, next) => {
  if (err instanceof joi.ValidationError) {
    return res.cc(err);
  }
  // 未知的错误
  res.cc(err);
});

2.6开发登录用户的接口

2.6.0分析实现步骤

  1. 检测用户提交的表单数据是否合法
  2. 根据用户名查询用户的数据
  3. 判断用户输入的密码是否正确
  4. 生成JWTToken字符串

2.6.1检测登录的表单数据是否合法

// 登录接口
router.post("/login", expressJoi(reg_login_schema), userHandler.login);

2.6.2根据用户名查询用户数据

// 登录的处理函数
exports.login = (req, res) => {
  // 接收表单数据
  const userInfo = req.body;
  // 定义sql语句(根据用户名去查询)
  const sql = `select * from ev_users where username=?`;
  // 执行sql语句
  db.query(sql, userInfo.username, (err, results) => {
    // 执行失败
    if (err) {
      return res.cc(err);
    }
    // 执行语句成功,但获取到的数据条数不为1,没查到信息
    if (results.length !== 1) {
      return res.cc("登录失败!");
    }
    // TODO:判断密码是否正确
    // res.send("登录成功!");
});

2.6.3判断用户输入的密码是否正确

核心实现思路:调用bcrypt.compareSync()方法比较密码是否一致,返回值是布尔值(true一致,false不一致)

// 拿到客户端提交的密码和数据库中的密码作比较
// bcrypt.compareSync(用户提交密码,数据库存储密码)
  const compareResult = bcrypt.compareSync(
    userInfo.password,
    results[0].password
  );
  if (!compareResult) {
    return res.cc("登录失败!");
  }
  // TODO:在服务器端生成Token的字符串

2.6.4 生成JWT的Token字符串

核心注意点:在生成Token字符串的时候,一定要剔除密码头像的值

  1. 通过ES6的高级语法,快速提出密码头像的值
// 在服务器端生成Token字符串
   const user = { ...results[0], user_pic: "", password: "" };
  1. 运行npm installjsonwebtoken@8.5.1命令安装生成Token字符串的依赖包
  2. /router_handler/user.js模块中,导入jsonwebtoken包: const jwt = require('jsonwebtoken')
  3. 创建config.js全局配置文件
module.exports = {
    // 加密和解密token的密钥
    jwtSecrteKey:'Estella',
    // token的有效期
    expiresIn:'10h'
}

5.将用户信息对象加密成Token字符串:

// 导入生成token的包
const jwt = require("jsonwebtoken");
// 导入全局的配置文件
const config = require("../config");
// 对用户信息进行加密,生成Token字符串
const tokenStr = jwt.sign(user, config.jwtSecrteKey, {
    expiresIn: config.expiresIn,
});

6.将生成的Token字符串响应给客户端

// 调用res.send()将token响应给客户端
    res.send({
      status: 0,
      message: "登录成功",
      token: 'Bearer ' + tokenStr,
    });

2.7配置解析Token的中间件

  1. 运行npm i express-jwt@5.3.3安装解析Token的中间件
  2. app.js中注册路由之前,配置解析Token的中间件
// 一定要在路由之前 配置解析token的中间件
// 导入配置文件
const expressJwt = require('express-jwt')
// 解析token的中间件
const config = require('./config')
// 使用unless({path:[/^\/api/]})指定哪些接口不需要进行Token的身份认证
app.use(expressJwt({secret:config.jwtSecrteKey}).unless({path:[/^\/api/]}))
  1. app.js中的错误级别中间件中,捕获并处理token认证失败后的错误
// 定义错误级别的中间件(捕获全局错误)
app.use((err, req, res, next) => {
  if (err instanceof joi.ValidationError) {
    return res.cc(err);
  }
  // 捕获token认证失败的错误
  if (err.name === 'UnauthorizedError') return res.cc('身份认证失败')
  // 未知的错误
  res.cc(err);
});

3.个人中心

3.1获取用户的基本信息

3.1.0分析实现步骤

  1. 初始化路由模块
  2. 初始化路由处理函数模块
  3. 获取用户的基本信息

3.1.1 初始化路由模块和路由处理函数模块

// 个人中心模块路由  /router/userinfo.js

const express = require("express");
// 生成路由实例
const router = express.Router();

// 挂载路由
// 导入获取用户信息的路由处理函数
const { getUserInfo } = require("../router_handler/userinfo");
// 获取用户基本信息的接口
router.get("/userinfo", getUserInfo);

module.exports = router;
/* 个人中心模块 路由处理函数目录:  /router_handler/userinfo.js  供 /router/userinfo.js调用*/
exports.getUserInfo = (req, res) => {
  res.send('userinfo is ok!')
};

3.1.2 获取用户的基本信息

const db = require("../db/index");

exports.getUserInfo = (req, res) => {
  // 定义查询用户信息的sql语句
  // 注意:为了防止用户的密码被泄露,需要排除掉password字段
  const sql =
    "select id,username,nickname,email,user_pic from ev_users where id=?";
  //注意:只要身份认证成功,中间件express会在req上就会被挂载一个user属性(是一个对象),存放用户id
  db.query(sql, req.user.id, (err, results) => {
    if (err) return res.cc(err);
    if (results.length !== 1) {
      return res.cc("获取用户信息失败");
    }
    // 将用户信息响应给客户端
    res.send({
      status: 0,
      message: "获取用户信息成功!~",
      data: results[0],
    });
  });
};

3.2 更新用户的基本信息

3.2.0分析实现步骤

  1. 定义路由路由处理函数
  2. 验证提交的表单数据
  3. 实现更新用户基本信息的功能

3.2.1定义路由处理函数

/router/userinfo.js文件中,新增更新用户信息的路由

const { getUserInfo ,updateUserInfo } = require("../router_handler/userinfo");
// 更新用户的基本信息的路由
router.post("/userinfo",updateUserInfo);

router_handler/userinfo.js文件中,定义并向外共享更新用户信息的路由处理函数

// 更新用户基本信息的路由处理函数
exports.updateUserInfo = (req, res) => {
  res.send('updateUserInfo is ok!~')
};

3.2.2 定义表单验证规则

/schema/user.js文件中,新增更新用户基本信息的验证规则对象并暴露出去

// 定义id,nickname,email的验证规则
const id = joi.number().integer().min(1).required(); // 数字 整数 最小值1 必传
const nickname = joi.string().required(); // 字符串 必传
const email = joi.string().email().required(); // 字符串 邮箱验证格式 必传

// 定义修改用户信息的验证规则对象
exports.update_userinfo_schema = {
  // 指需要对req.dody里的数据进行验证
  body: { id, nickname, email },
};

router/userinfo.js文件中导入数据合法性验证的中间件,并导入验证规则对象

// 导入验证数据的中间件
const expressJoi = require("@escook/express-joi");

// 导入验证数据的规则对象
const { update_userinfo_schema } = require("../schema/user");

// 修改更新用户的基本信息
router.post("/userinfo", expressJoi(update_userinfo_schema), updateUserInfo);

3.2.3 实现更新用户信息的功能

/router_handler/userinfo.js定义更新信息的sql语句,调用db.query()方法执行sql语句

// 更新用户基本信息
exports.updateUserInfo = (req, res) => {
  // 定义sql语句
  const sql = "update ev_users set ? where id=?";
  db.query(sql, [req.body, req.body.id], (err, results) => {
    if (err) return res.cc(err);
    if (results.affectedRows !== 1) {
      return res.cc("修改用户基本信息失败");
    }
    return res.cc("修改用户基本信息成功", 0);
  });
};

3.3开发重置密码的接口

3.3.0 分析实现步骤

  1. 定义路由路由处理函数
  2. 验证表单数据
  3. 实现重置密码的功能

3.3.1定义重置密码接口的路由路由处理函数

/router/userinfo.js文件中,新增更新用户密码的路由

const { getUserInfo ,updateUserInfo,updatePassword } = require("../router_handler/userinfo");
// 重置密码
router.post("/updatepwd", updatePassword);

/router_handler/userinfo.js文件中,定义并向外共享更新用户密码的路由处理函数

//重置密码路由处理函数
exports.updatePassword = (req, res) => {
  res.send('updateUserInfo is ok!~')
};

3.3.2 定义表单验证规则

注意:修改密码核心验证思路: 旧密码与新密码不能一致,且必须符合密码的验证规则

const password = joi.string().pattern(/^[\S]{6,12}$/).required();
// 定义修改密码的验证规则对象
exports.update_password_schema = {
  body: {
    oldPwd: password,
    // 符合密码验证规则且不和旧密码一致
    newPwd: joi.not(joi.ref("oldPwd")).concat(password),
  },
};

修改在router/userinfo.js定义的重置密码路由

// 导入验证数据的规则对象
const {getUserInfo, update_userinfo_schema, update_password_schema } = require("../schema/user");
// 重置密码
router.post("/updatepwd", expressJoi(update_password_schema), updatePassword);

3.3.3实现重置密码的功能

// 导入处理密码的模块
const bcrypt = require("bcryptjs");
const sql = "select * from ev_users where id=?";
  db.query(sql, req.user.id, (err, results) => {
    if (err) return res.cc(err);
    if (results.length !== 1) return res.cc("用户不存在");
    // TODO:判断用户输入的旧密码是否正确
    const compareResult = bcrypt.compareSync(
      req.body.oldPwd,
      results[0].password
    );
    if (!compareResult) return res.cc("原密码错误");
    // TODO:更新数据库的密码
    const sql = "update ev_users set password=? where id=?";
    // 对新密码进行处理
    const newPwd = bcrypt.hashSync(req.body.newPwd, 10);
    db.query(sql, [newPwd, req.user.id], (err, results) => {
      if (err) return res.cc(err);
      if (results.affectedRows !== 1) {
        return res.cc("修改密码失败,请稍后再试");
      }
      res.cc("更新密码成功 ok!~", 0);
    });
  });

3.4开发更换头像的接口

3.4.0 分析实现步骤

  1. 定义路由路由处理函数
  2. 验证表单数据
  3. 实现更换头像的功能

3.4.1定义路由路由处理函数

/router/userinfo.js文件中,新增更新用户密码的路由

const { getUserInfo, updateUserInfo, updatePassword, updateAvatar} =require("../router_handler/userinfo");
// 更换头像
router.post("/update/avatar", updateAvatar);

/router/userinfo.js文件中,新增更新用户密码的路由处理函数

// 更新用户基本信息
exports.updateUserInfo = (req, res) => {
  res.send('updateUserInfo is ok!~')
};

3.4.2定义表单验证规则

// 定义avatar的验证规则
// dataUri()指的是base64形式的图片,比如如下格式的字符串:
// data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = joi.string().dataUri().required();
// 定义更新头像的验证规则对象
exports.update_avatar_schema = {
  body: {
    avatar,
  },
};

修改在router/userinfo.js定义的更换头像路由

// 更换头像
router.post("/update/avatar", expressJoi(update_avatar_schema), updateAvatar);

3.4.3实现更换头像的功能

// 更新用户基本信息
exports.updateUserInfo = (req, res) => {
  // 定义sql语句
  const sql = "update ev_users set ? where id=?";
  db.query(sql, [req.body, req.body.id], (err, results) => {
    if (err) return res.cc(err);
    if (results.affectedRows !== 1) {
      return res.cc("修改用户基本信息失败");
    }
    return res.cc("修改用户基本信息成功", 0);
  });
};