记录创建一个node项目的全过程

截止今天,终于要把在b站学习的《黑马程序员》的nodejs全套视频看完了,学习总是需要有输出的吧。所以今天记录创建一个node项目的全过程。废话不多说,下面进入正题。

项目初始化

新建项目

  1. 创建一个api_server的文件夹

    1
    mkdir api_server
  2. 通过npm初始化一个express工程

    1
    2
    3
    4
    5
    # 进入到api_server根目录下
    npm init -y

    # 在api_server根目录安装express
    npm i express
  3. 编写app.js配置web服务器

    api_server根目录下创建app.js文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 第一步 导入express模块
    let express = require('express');
    // 第二步 创建express的服务器实例
    let app = express();
    // 第三步 调用app.listen方法 指定端口号并启动web服务器
    let port = 3008
    app.listen(port, () => {
    console.log("api server is runnning at http://localhost:" + port)
    })

配置cors跨域

为了支持项目可以跨域访问,我们需要再项目里安装并配置cors跨域中间件

  1. 安装cors

    1
    npm i cors
  2. app.js配置全局中间件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 第一步 导入express模块
    let express = require('express');
    // 第二步 创建express的服务器实例
    let app = express();

    // 配置cors跨域中间件
    let cors = require('cors');
    app.use(cors())

    // 第三步 调用app.listen方法 指定端口号并启动web服务器
    let port = 3008
    app.listen(port, () => {
    console.log("api server is runnning at http://localhost:" + port)
    })

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

express提供了一个只解析application/x-www-form-unlencoded的中间件

  1. app.js配置解析表单数据的中间件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 第一步 导入express模块
    let express = require('express');
    // 第二步 创建express的服务器实例
    let app = express();

    // 配置cors跨域中间件
    let cors = require('cors');
    app.use(cors())

    // 配置解析表单数据的中间件
    app.use(express.urlencoded({extended: false}))

    // 第三步 调用app.listen方法 指定端口号并启动web服务器
    let port = 3008
    app.listen(port, () => {
    console.log("api server is runnning at http://localhost:" + port)
    })

初始化路由相关的文件夹

  1. api_server根目录下,新建**router文件夹,用来存放所有的路由模块**

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

  2. api_server根目录下,新建router_handler文件夹,用来存放所有的路由处理函数模块

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

初始化用户路由模块

  1. router文件夹中,新建user.js文件,作为用户的路由模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let express = require('express');
    // 创建路由对象
    let router = express.Router();

    // 注册新用户
    router.post('/register', (req, res) => {
    res.send('register ok')
    })

    // 登录
    router.post('/login', (req, res) => {
    res.send('login success')
    })

    // 将路由共享出去
    module.exports = router
  2. app.js中,导入并使用用户路由模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // ...省略以上代码

    // 配置解析表单数据的中间件
    app.use(express.urlencoded({extended: false}))

    // 导入并使用用户路由模块
    let userRouter = require("./router/user");
    app.use('/api', userRouter)

    // 第三步 调用app.listen方法 指定端口号并启动web服务器
    let port = 3008
    app.listen(port, () => {
    console.log("api server is runnning at http://localhost:" + port)
    })

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

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

  1. router_handler目录下创建user.js,并且将登录和注册的路由处理函数共享出去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 注册用户的处理函数
    exports.register = (req, res) => {
    res.send('register ok')
    }

    // 用户登录的处理函数
    exports.login = (req, res) => {
    res.send('login ok')
    }
  2. 改造router/user.js的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let express = require('express');
    // 创建路由对象
    let router = express.Router();

    let userHandler = require("../router_handler/user");
    // 注册新用户
    router.post('/register', userHandler.register)

    // 登录
    router.post('/login', userHandler.login)

    // 将路由共享出去
    module.exports = router

注册登录

创建用户表

以下为用户信息表ev_users的DDL

1
2
3
4
5
6
7
8
9
10
11
12
13
create table if not exists ev_users
(
id int auto_increment comment '主键'
primary key,
username varchar(255) not null comment '用户名',
password varchar(255) not null comment '用户密码',
nickname varchar(255) null comment '昵称',
email varchar(255) null comment '用户邮箱',
user_pic text null comment '用户头像',
constraint ev_users_pk2
unique (id, username)
)
comment '用户信息表';

安装并配置mysql模块

  1. 安装mysql模块

    1
    2
    # 在api_server根目录下
    npm i mysql
  2. 配置mysql链接对象

    在api_server根目录下新建一个db目录,创建一个index.js文件,用来创建一个mysql的链接对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 导入mysql模块
    let mysql = require('mysql');

    // 创建
    let db = mysql.createPool({
    host: '127.0.0.1',
    port: 3306,
    user: 'root',
    password: '123456',
    database: 'api_server'
    });
    // 向外共享db数据库连接对象
    module.exports = db

注册功能

  1. 检测表单数据是否合法

    判断前端传过来的用户名和密码是否为空

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 注册用户的处理函数
    exports.register = (req, res) => {
    // 获取表单数据
    let userinfo = req.body;
    // 判断数据是否合法
    if (!userinfo.username || !userinfo.password) {
    return res.send({
    status: 1,
    message: "用户名或密码不能为空!"
    })
    }
    // res.send('register ok')
    }

    // ...省略
  2. 检测用户名是否被占用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 引入数据库操作模块
    let db = require("../db/index");
    // 注册用户的处理函数
    exports.register = (req, res) => {
    // 获取表单数据
    let userinfo = req.body;
    // 判断数据是否合法
    if (!userinfo.username || !userinfo.password) {
    return res.send({
    status: 1,
    message: "用户名或密码不能为空!"
    })
    }
    // 定义sql语句
    let sql = "select * from ev_users where username = ?"

    // 判断用户名是否可用
    db.query(sql, [userinfo.username], function (err, results) {
    // 查询失败的话
    if (err) {
    return res.send({
    status: 1,
    message: err.message
    })
    }
    if (results.length > 0) {
    return res.send({
    status: 1,
    message: "用户名被占用,请更换其他用户名!"
    })
    }
    })
    res.send('register ok')
    }

    // ...省略
  3. 对密码进行加密处理

    众所皆知,在我们的系统中,为了保证安全性,我们不会将明文的密码存入数据库,正常我们都会选择加密后存储

    • 安装bcryptjs组件对用户密码进行加密

      bcryptjs加密后的密码无法被逆向破解

      bcryptjs对同一明文密码进行加密,所得结果各不相同

      1
      npm i bcryptjs
    • /router_handler/user.js导入bcryptjs

      1
      let bcrypt = require('bcryptjs');
    • 对用户密码进行加密

      1
      2
      // 对用户密码进行bcrypt加密,返回的是加密之后的字符串  hashSync(明文密码,随机盐的长度)方法 进行加密处理
      userinfo.password = bcrypt.hashSync(userinfo.password, 10);
  4. 插入新用户

    编写插入新用户的sql以及业务处理逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // ...省略
    // 对用户密码进行bcrypt加密,返回的是加密之后的字符串 hashSync(明文密码,随机盐的长度)方法 进行加密处理
    userinfo.password = bcrypt.hashSync(userinfo.password, 10);
    // 添加新用户
    let addSql = 'insert into ev_users set ?'
    db.query(addSql, {username: userinfo.username, password: userinfo.password}, function (err, results) {
    if (err) return res.send({status: 1, message: err.message})
    // sql执行成功 但影响行数不为1
    if (results.affectedRows !== 1) {
    return res.send({status: 1, message: "注册用户失败,请稍后再试!"})
    }
    // 注册成功
    return res.send({status: 0, message: '注册成功!'})
    })

优化代码

  1. 优化res.send()代码(统一响应体)

    app.js中,所有路由之前,生命一个全局中间件,为res对象挂载一个res.cc()函数

    注意事项:一定要在所有路由之前,否则会失效

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 统一响应体的中间件
    app.use(function (req, res, next) {
    // 默认状态是1 为错误的情况
    res.cc = function (err, status = 1) {
    res.send({
    status,
    message: err instanceof Error ? err.message : err
    })
    }
    next()
    })

    改造代码(片段)如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 改造前
    if (!userinfo.username || !userinfo.password) {
    return res.send({
    status: 1,
    message: "用户名或密码不能为空!"
    })
    }
    // 改造后
    if (!userinfo.username || !userinfo.password) {
    return res.cc('用户名或密码不能为空!')
    }
  2. 优化表单数据验证

    表单验证原则:前端验证为辅,后端验证为主。对于后端而言,前端传递过来的数据永远是不可信的状态

    现状

    单纯使用if..else...的形式进行数据合法性验证,效率底下、出错率高、维护性差。

    改进方案

    采用第三方数据验证模块来降低出错率、提高验证的效率和可维护性,让后端开发专注于业务逻辑的处理。

    • 安装@hapi/joi包,为表单中携带的每个数据项,定义验证规则

      1
      npm install [email protected]
    • 安装@escook/express-joi中间件,来实现自动对表单数据进行验证的功能

      1
      npm install @escook/express-joi
    • 新建/schema/user.js用户验证规则模块

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      let joi = require('joi');

      // 用户名的验证规则
      let username = joi.string().alphanum().min(1).max(10).required();
      // 密码的验证规则
      let password = joi.string().pattern(/^[\S]{6,15}$/).required()

      // 注册和登录表单的验证规则对象
      exports.reg_login_schema = {
      body: {
      username,
      password
      }
      }
    • 改造/router/user.js

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      let express = require('express');
      // 创建路由对象
      let router = express.Router();

      let userHandler = require("../router_handler/user");

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

      // 注册新用户 插入参数校验的局部中间件
      router.post('/register', expressJoi(reg_login_schema), userHandler.register)

      // 将路由共享出去
      module.exports = router
    • app.js的路由之后,定义全局异常处理中间件

      1
      2
      3
      4
      5
      6
      7
      8
      // ...省略
      // 在路由之后 定义全局异常处理中间件
      app.use(function (err, req, res, next) {
      // 验证失败导致的错误
      if (err instanceof joi.ValidationError) res.cc(err)
      // 未知的错误
      res.cc(err)
      })

登录

  1. 检测登录表单数据是否合法

    router/user.js改造路由代码,添加参数校验的局部中间件

    1
    2
    // 登录
    router.post('/login', expressJoi(reg_login_schema), userHandler.login)
  2. 根据用户名查询用户的数据

    router_handler/user.js改造login函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 用户登录的处理函数
    exports.login = (req, res) => {
    let userinfo = req.body
    let sql = 'select * from ev_users where username = ?'
    db.query(sql, userinfo.username, function (err, results) {
    // 执行sql失败
    if (err) return res.cc(err)
    // 执行sql成功,但数量不等于1
    if (results.length !== 1) return res.cc(err)
    // TODO 判断用户输入的登录密码是否和数据库中的密码一致
    })
    res.send('login ok')
    }
  3. 判断用户输入的密码是否正确

    核心思路:调用bcrypt.compareSync(用户提交的密码,数据库中的加密密码)方法比较密码是否一致。true则一致

    router_handler/user.js改造login函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 用户登录的处理函数
    exports.login = (req, res) => {
    let userinfo = req.body
    let sql = 'select * from ev_users where username = ?'
    db.query(sql, userinfo.username, function (err, results) {
    // 执行sql失败
    if (err) return res.cc(err)
    // 执行sql成功,但数量不等于1
    if (results.length !== 1) return res.cc('登录失败!')
    let compareResult = bcrypt.compareSync(userinfo.password, results[0].password);
    if (!compareResult) return res.cc('登录失败!')
    // TODO 在服务端生成Token的字符串
    res.send('login ok')
    })
    }
  4. 生成JWTToken字符串

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

    • 通过ES6的高级语法,快速提出密码头像的值

      1
      2
      // 剔除用户敏感信息(ES6写法)
      let user = {...results[0], password: '', user_pic: ''}
    • 安装生成Token的包

      1
      npm i jsonwebtoken
    • /router_handler/user.js模块的头部区域,导入jsonwebtoken

      1
      let jwt = require('jsonwebtoken');
    • 创建config.js文件,并向外共享加密和还原Token的jwtSecretKey的字符串

      api_server根目录下新建一个config.js全局配置文件

      1
      2
      3
      4
      5
      6
      7
      // 这是一个全局配置文件
      module.exports = {
      // 加密和还原Token的`jwtSecretKey`的字符串
      jwtSecretKey: 'gcoder 009M ~~',
      // token过期时间
      expiresIn: '1h'
      }
    • 将用户信息对象加密成token字符串

      1
      2
      // 生成token字符串
      let tokenStr = jwt.sign(user, config.jwtSecretKey, {expiresIn: config.expiresIn});
    • 将生成的token字符串响应给客户端

      1
      2
      3
      4
      5
      6
      // 返回token
      res.send({
      status: 0,
      message: '登录成功!',
      token: 'Bearer ' + tokenStr
      })
  5. 配置解析Token的中间件

    • 安装解析Token的中间件

      1
      npm i express-jwt
    • app.js中注册路由之前,配置解析Token的中间件

      1
      2
      3
      4
      // 导入全局配置文件
      let config = require('./config');
      // 解析token的中间件
      let {expressjwt: jwt, UnauthorizedError} = require('express-jwt');
    • app.js中的错误级别中间件里面,捕获并处理Token认证失败后的错误

      1
      2
      // 身份认证失败导致的错误
      if (err instanceof UnauthorizedError) return res.cc('身份认证失败!')

写在最后

以上就是关于用node.js搭建一个express后端项目的记录了。希望大家能够从中学习到点东西~


记录创建一个node项目的全过程
https://gcoder5.com/2023/07/13/记录创建一个node项目的全过程/
作者
Gcoder
发布于
2023年7月13日
许可协议