NodeJS全栈开发一个功能完善的Express项目(附完整源码)

31,281 阅读11分钟

一. 前言

Node.js对前端来说无疑具有里程碑意义,与其越来越流行的今天,掌握 Node.js 技术已经不仅仅是加分项,而是前端攻城师们必须要掌握的一项技能。而 Express 基于 Node.js 平台,快速、开放、极简的 Web 开发框架,成为 Node.js 最流行的框架,所以使用 Express 进行 web 服务端的开发是个不错且可信赖的选择。但是 Express 初始化后,并不马上就是一个开箱即用,各种功能完善的 web 服务端项目,例如:MySQL 连接、jwt-token 认证、md5 加密、中间件路由模块、异常错误处理、跨域配置、自动重启等一系列常见的功能,需要开发者自己手动配置安装插件和工具来完善功能,如果你对 web 服务端开发或者 Express 框架不熟悉,那将是一项耗费巨大资源的工作。

本文作者根据项目实战经验已将底层服务架构搭建完成,并且本项目已在 github 开源,供大家学习参考使用(如有不足,还请批评指正),希望能减轻大家的工作量,更高效的完成工作,有更多时间提升自己的能力。

后端 API 接口源码地址 👉:github.com/jackchen012…

前端界面源码地址 👉:github.com/jackchen012…

如果觉得本文还不错,记得点个 👍 或者给个 ❤️,你们的赞和 star 是作者编写更多更精彩文章的动力!

分享之前我们先来认识一下Node.js、Express都是什么东东。

Node.js

简单的说 Node.js 就是运行在服务端的 JavaScript。

Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。

Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境,基于 Google 的 V8 引擎,V8 引擎执行 Javascript 的速度非常快,性能非常好。

Express

Express 是一个简洁而灵活的 Node.js Web 应用框架,提供了一系列强大特性帮助你创建各种 Web 应用,和丰富的 HTTP 工具。使用 Express 可以快速地搭建一个完整功能的网站。

Express 框架核心特性:

  • 可以设置中间件来响应 HTTP 请求。
  • 定义了路由表用于执行不同的 HTTP 请求动作。
  • 可以通过向模板传递参数来动态渲染 HTML 页面。

二. 前后端分离

前端项目采用的技术栈是基于Vue + iView,用 vue-cli 构建前端界面,后端项目采用的技术栈是基于Node.js + Express + MySQL,用 Express 搭建的后端服务器。

Web3研习社 👉:54web3.cc/

部分效果截图

三. 前端部分

3.1 基础环境

开发前准备工作,相关运行环境配置如下:

工具名称版本号
node.js10.16.2
npm6.9.0

运行项目

1> 下载安装依赖

git clone github.com/jackchen012…
cd todo-vue-admin
npm install 或 yarn

2> 开发模式

npm run serve

运行之后,访问地址:http://localhost:8082

3> 生产环境打包

npm run build


3.2 目录结构

│  package.json                      // npm包管理所需模块及配置信息
│  vue.config.js                     // webpack配置
├─public
│      favicon.ico                   // 图标
│      index.html                    // 入口html文件
└─src
    │  App.vue                       // 根组件
    │  main.js                       // 程序入口文件
    ├─assets                         // 存放公共图片文件夹
    ├─components
    │      Footer.vue                // 页面底部公用组件
    │      Header.vue                // 页面头部公用组件
    ├─router
    │      index.js                  // 单页面路由注册组件
    ├─store
    │  │  index.js                   // 状态管理仓库入口文件
    │  └─modules
    │          userInfo.js           // 用户模块状态管理文件
    ├─styles
    │      base.scss                 // 基础样式文件
    ├─utils
    │      api.js                    // 统一封装API接口调用方法
    │      network.js                // axios封装与拦截器配置
    │      url.js                    // 自动部署服务器环境
    │      valid.js                  // 统一封装公用模块方法
    └─views
            Home.vue                 // 首页界面
            Login.vue                // 登录界面

3.3 技术栈

  • vue2.6
  • vue-router
  • vuex
  • axios
  • webpack
  • ES6/7
  • flex
  • iViewUI

3.4 功能模块

  • 登录(登出)
  • 注册
  • 记住密码
  • 忘记密码(修改密码)
  • todoList 增删改查
  • 点亮红星标记
  • 查询条件筛选

3.5 代码实现

3.5.1 全局安装 vue-cli4

npm install -g @vue/cli
#安装指定版本
npm install -g @vue/cli@4.4.0
#OR
yarn global add @vue/cli

3.5.2 vue-cli4 创建项目及运行

vue create todo-vue-admin
cd todo-vue-admin
npm run serve

3.5.3 开发配置

在项目根目录新增vue.config.js文件,配置信息如图所示:

3.5.4 其他事项

按照上面的步骤完成脚手架搭建后,把需要的 axios、vue-router、view-design、sass-loader、node-sass 等依赖库安装配置好,准备开始上膛。

3.5.5 实现前端登录注册功能

首先在 views 文件夹下面新建login.vue组件,编写一个静态的登录注册页面。登录成功后将登录返回的 token 保存到浏览器端并跳转到主页。views 文件夹下面新建home.vue组件,显示登录成功后的页面,并获取用户基本信息,主页右上角显示用户头像、修改密码、退出登录等功能。代码如下:

<template>
  <div class="login-container">
      <div class="pageHeader">
        <img src="../assets/logo.png" alt="logo">
        <span>TODOList区块链管理平台</span>
      </div>
      <div class="login-box">
        <div class="login-text" v-if="typeView != 2">
          <a href="javascript:;" :class="typeView == 0 ? 'active' : ''" @click="handleTab(0)">登录</a>
          <b>·</b>
          <a href="javascript:;" :class="typeView == 1 ? 'active' : ''" @click="handleTab(1)">注册</a>
        </div>
        <!-- 登录模块 -->
        <div class="right-content" v-show="typeView == 0">
          <div class="input-box">

            <input
              autocomplete="off"
              type="text"
              class="input"
              v-model="formLogin.userName"
              placeholder="请输入登录邮箱/手机号"
            />
            <input
              autocomplete="off"
              type="password"
              class="input"
              v-model="formLogin.userPwd"
              maxlength="20"
              @keyup.enter="login"
              placeholder="请输入登录密码"
            />
          </div>
          <Button
            class="loginBtn"
            type="primary"
            :disabled="isDisabled"
            :loading="isLoading"
            @click.stop="login">
            立即登录
          </Button>

          <div class="option">
            <Checkbox class="remember" v-model="checked" @on-change="checkChange">
              <span class="checked">记住我</span>
            </Checkbox>
            <span class="forget-pwd" @click.stop="forgetPwd">忘记密码?</span>
          </div>
        </div>

        <!-- 注册模块 -->
        <div class="right_content" v-show="typeView == 1">
          <div class="input-box">
            <input
              autocomplete="off"
              type="text"
              class="input"
              v-model="formRegister.userName"
              placeholder="请输入注册邮箱/手机号"
            />
            <input
              autocomplete="off"
              type="password"
              class="input"
              v-model="formRegister.userPwd"
              maxlength="20"
              @keyup.enter="register"
              placeholder="请输入密码"
            />
            <input
              autocomplete="off"
              type="password"
              class="input"
              v-model="formRegister.userPwd2"
              maxlength="20"
              @keyup.enter="register"
              placeholder="请再次确认密码"
            />
          </div>
          <Button
            class="loginBtn"
            type="primary"
            :disabled="isRegAble"
            :loading="isLoading"
            @click.stop="register">
            立即注册
          </Button>
        </div>
      </div>
  </div>
</template>
<style lang="scss" scoped>
.login-container {
  background-image: url('../assets/bg.png');
  background-position: center;
  background-size: cover;
  position: relative;
  width: 100%;
  height: 100%;

  .pageHeader {
    padding-top: 30px;
    padding-left: 40px;

    img {
      vertical-align: middle;
      display: inline-block;
      margin-right: 15px;
    }

    span {
      font-size: 18px;
      display: inline-block;
      vertical-align: -4px;
      color: rgba(255, 255, 255, 1);
    }
  }

  .login-box {
    position: absolute;
    left: 64vw;
    top: 50%;
    -webkit-transform: translateY(-50%);
    transform: translateY(-50%);
    box-sizing: border-box;
    text-align: center;
    box-shadow: 0 1px 11px rgba(0, 0, 0, 0.3);
    border-radius: 2px;
    width: 420px;
    background: #fff;
    padding: 45px 35px;
    .option {
      text-align: left;
      margin-top: 18px;
      .checked {
        padding-left: 5px;
      }
      .forget-pwd, .goback {
        float: right;
        font-size: 14px;
        font-weight: 400;
        color: #4981f2;
        line-height: 20px;
        cursor: pointer;
      }
      .protocol {
        color: #4981f2;
        cursor: pointer;
      }
    }

    .login-text {
      width: 100%;
      text-align: center;
      padding: 0 0 30px;
      font-size: 24px;
      letter-spacing: 1px;
      a {
        padding: 10px;
        color: #969696;
        &.active {
          font-weight: 600;
          color: rgba(73, 129, 242, 1);
          border-bottom: 2px solid rgba(73, 129, 242, 1);
        }
        &:hover {
          border-bottom: 2px solid rgba(73, 129, 242, 1);
        }
      }
      b {
        padding: 10px;
      }
    }
    .title {
      font-weight: 600;
      padding: 0 0 30px;
      font-size: 24px;
      letter-spacing: 1px;
      color: rgba(73, 129, 242, 1);
    }

    .input-box {
      .input {
        &:nth-child(1) {
          /*margin-top: 10px;*/
        }
        &:nth-child(2),
        &:nth-child(3) {
          margin-top: 20px;
        }
      }
    }

    .loginBtn {
      width: 100%;
      height: 45px;
      margin-top: 40px;
      font-size: 15px;
    }

    .input {
      padding: 10px 0px;
      font-size: 15px;
      width: 350px;
      color: #2c2e33;
      outline: none; // 去除选中状态边框
      border: 1px solid #fff;
      border-bottom-color: #e7e7e7;
      background-color: rgba(0, 0, 0, 0); // 透明背景
    }

    input:focus {
      border-bottom-color: #0f52e0;
      outline: none;
    }
    .button {
      line-height: 45px;
      cursor: pointer;
      margin-top: 50px;
      border: none;
      outline: none;
      height: 45px;
      width: 350px;
      background: rgba(216, 216, 216, 1);
      border-radius: 2px;
      color: white;
      font-size: 15px;
    }
  }

  // 滚动条样式
  ::-webkit-scrollbar {
    width: 10px;
  }
  ::-webkit-scrollbar-track {
    -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.3);
    border-radius: 8px;
  }
  ::-webkit-scrollbar-thumb {
    border-radius: 10px;
    background: rgba(0, 0, 0, 0.2);
    -webkit-box-shadow: inset006pxrgba(0, 0, 0, 0.5);
  }
  ::-webkit-scrollbar-thumb:window-inactive {
    background: rgba(0, 0, 0, 0.4);
  }
  //设置更改input 默认颜色
  ::-webkit-input-placeholder {
    /* WebKit browsers */
    color: #bebebe;
    font-size: 16px;
  }
  ::-moz-placeholder {
    /* Mozilla Firefox 19+ */
    color: #bebebe;
    font-size: 16px;
  }
  :-ms-input-placeholder {
    /* Internet Explorer 10+ */
    color: #bebebe;
    font-size: 16px;
  }
  input:-webkit-autofill {
    box-shadow: 0 0 0px 1000px rgba(255, 255, 255, 1) inset;
    -webkit-box-shadow: 0 0 0px 1000px rgba(255, 255, 255, 1) inset;
    -webkit-text-fill-color: #2c2e33;
  }
  .ivu-checkbox-wrapper {
    margin-right: 0;
  }
}
</style>

请求登录成功后,根据需求将用户信息保存到浏览器端,通过vuex-persistedstate插件使用浏览器的本地存储(localstorage)对状态(state)进行持久化。

npm install -S vuex-persistedstate

配置信息在 store 文件夹下面新建 index.js 文件,代码如下:

import Vue from 'vue'
import Vuex from 'vuex'
import userInfo from './modules/userInfo' // 用户模块信息
import createPersistedState from 'vuex-persistedstate'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: { // 采用模块化状态管理
  	userInfo
  },
	getters: {
		isLogined: state => {
			return state.userInfo.isLogined
		}
	},
	plugins: [createPersistedState({ // 插件配置信息
	    key: 'store', // key对象存储的key值可以自定义
	    storage: window.localStorage, // storage对象存储的value值,采用HTML5中的新特性localStorage属性实现
	})]
})

在 modules 文件夹下面新建userInfo.js文件,用作用户状态管理成员配置,将 token 保存到 vuex 中,代码如下:

const userInfo = {
  namespaced: true,
  state: {
    data: {},
    isLogined: false
  },

  getters: {
    userInfo: state => {
      return state.data
    }
  },

  mutations: {
    // 设置用户信息
    setUserInfo(state, userInfo) {
      state.data = userInfo
      state.isLogined = true
    },
    // 清除用户信息
    clearUserInfo(state,info) {
      state.data = info
      state.isLogined = false
    },
    // 修改用户信息
    modifyUserInfo(state, newInfo) {
      state.data = Object.assign(state.data, newInfo)
    }

  },

  actions: {
    // 保存用户信息
    saveInfo({ commit }, result) {
      commit('setUserInfo', result)
    },
    // 退出登录
    logout({commit}) {
      commit('clearUserInfo', {})
      location.href = '/login'
    }
  }
}

export default userInfo

在 router 文件夹下面新建index.js文件,用来添加路由信息,代码如下:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: {
      title: '登录界面'
    }
  },
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: {
      title: '首页',
      requireAuth: true
    }
  },
  {
    path: '**',
    redirect: '/'
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

编写完登录注册界面,登录成功后跳转到主页。

// 立即登录
login() {
  if (this.isDisabled || this.isLoading) {
    return false;
  }

  if (!this.$Valid.validUserName(this.formLogin.userName)) {
    this.$Message.error('请输入正确的邮箱/手机号');
    return false;
  }

  if (!this.$Valid.validPass(this.formLogin.userPwd)) {
    this.$Message.error('密码应为8到20位字母或数字!');
    return false;
  }

  // 判断复选框是否被勾选,勾选则调用配置cookie方法
  if (this.checked) {
    // 传入账号名,密码,和保存天数3个参数
    this.setCookie(this.formLogin.userName, this.formLogin.userPwd, 7);
  } else {
    // 清空Cookie
    this.clearCookie();
  }

  this.isLoading = true;

  let form = {
    username: this.formLogin.userName,
    password: this.formLogin.userPwd
  };

  login(form)
  .then(res => {
    console.log('登录===', res);
    this.isLoading = false;
    if (res.code == 0) {
      this.clearInput();
      this.$Message.success('登录成功');
      this.$store.dispatch('userInfo/saveInfo', res.data);
      this.$router.push('/home');
    } else {
      this.$Message.error(res.msg);
    }

  })
  .catch(() => {
    this.isLoading = false;
  });
}

编写主页,头部和底部组件单独引入作为可复用,存放在/src/components 文件夹下面,首页效果如图所示:

// 点击头像下拉菜单选择
changeMenu(name) {
  if (name == 'a') {
    this.modal = true;
    this.$refs['formItem'].resetFields();
  } else if (name == 'b') {
    this.$store.dispatch('userInfo/logout')
  }
}

使用 axios 编写 http 请求和响应拦截器。在 utils 文件夹下新建network.js文件,代码如下:

import Vue from 'vue'
import axios from 'axios'
import { apiUrl } from './url'
import store from '../store'

// 创建实例
const service = axios.create({
  baseURL: apiUrl,
  timeout: 55000
})

// 请求拦截器
service.interceptors.request.use(config => {
  if (store.state.userInfo.data.token) {
    config.headers['authorization'] = store.state.userInfo.data.token;
  }

  return config;
}, error => {
  Promise.reject(error);
})

// 响应拦截器
service.interceptors.response.use(
  response => {
    console.log(response.data)
    // 抛出401错误,因为token失效,重新刷新页面,清空缓存,跳转到登录界面
    if (response.data.code == 401) {
      store.dispatch('userInfo/logout')
      .then(() => {
        location.reload();
      });
    }

    return response.data;
  },
  error => {
    Vue.prototype.$Message.error({
      content: '网络异常,请稍后再试',
      duration: 5
    })

    return Promise.reject(error)
  }
)

export default service;

在 utils 文件夹下新建api.js实现前端 API 接口统一调用,代码如下:

import network from './network';

// 登录
export function login(data) {
  return network({
    url: `/login`,
    method: "post",
    data
  });
}

// 注册
export function register(data) {
  return network({
    url: `/register`,
    method: "post",
    data
  })
}

// 密码重置
export function resetPwd(data) {
  return network({
    url: `/resetPwd`,
    method: "post",
    data
  })
}

// 任务列表
export function queryTaskList(params) {
  return network({
    url: `/queryTaskList`,
    method: "get",
    params
  })
}

// 添加任务
export function addTask(data) {
  return network({
    url: `/addTask`,
    method: "post",
    data
  })
}

// 编辑任务
export function editTask(data) {
  return network({
    url: `/editTask`,
    method: "put",
    data
  })
}

// 操作任务状态
export function updateTaskStatus(data) {
  return network({
    url: `/updateTaskStatus`,
    method: "put",
    data
  })
}

// 点亮红星标记
export function updateMark(data) {
  return network({
    url: `/updateMark`,
    method: "put",
    data
  })
}

// 删除任务
export function deleteTask(data) {
  return network({
    url: `/deleteTask`,
    method: "delete",
    data
  })
}

到这里,前端的登录注册功能就基本实现了。接下来要实现后端的接口部分了。👏

四. MySQL 安装配置

请移步到我的另一篇博客<前端必知必会 MySQL 的那些事儿 - NodeJS 全栈成长之路>有详细介绍。

数据库设计部分

使用MySQL,创建数据库my_test ,创建sys_user用户表。

-- 创建数据库
CREATE DATABASE `my_test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

-- 创建用户表
CREATE TABLE `sys_user` (
    `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
    `username` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '登录帐号,邮箱或手机号',
    `password` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '登录密码',
    `nickname` VARCHAR(50) NULL DEFAULT '' COMMENT '昵称',
    `avator` VARCHAR(50) NULL DEFAULT '' COMMENT '用户头像',
	  `sex` VARCHAR(20) NULL DEFAULT '' COMMENT '性别:u:未知,  m:男,  w:女',
	  `gmt_create` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	  `gmt_modify` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`) USING BTREE,
	  UNIQUE KEY `username_UNIQUE` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='用户表';

五. 后端部分

5.1 基础环境

开发前准备工作,相关运行环境配置如下:

工具名称版本号
express4.17.1
mysql5.7

运行项目

1> 下载安装依赖

git clone github.com/jackchen012…
cd todo-nodejs-api
npm install 或 yarn

2> 开发模式

npm start

运行之后,访问地址:http://localhost:8088

3> 生产环境(后台启动服务)

pm2 start ecosystem.config.js


5.2 目录结构

│  app.js                             // 入口文件
│  ecosystem.config.js                // pm2默认配置文件
│  package.json                       // npm包管理所需模块及配置信息
├─db
│      dbConfig.js                    // mysql数据库基础配置
├─routes
│      index.js                       // 初始化路由信息,自定义全局异常处理
│      tasks.js                       // 任务路由模块
│      users.js                       // 用户路由模块
├─services
│      taskService.js                 // 业务逻辑处理 - 任务相关接口
│      userService.js                 // 业务逻辑处理 - 用户相关接口
└─utils
        constant.js                   // 自定义常量
        index.js                      // 封装连接mysql模块
        md5.js                        // 后端封装md5方法
        user-jwt.js                   // jwt-token验证和解析函数

5.3 技术栈

  • Node.js v10
  • express v4
  • mysql v5.7
  • express-jwt
  • nodemon
  • crypto
  • cors
  • boom
  • pm2

5.4 功能模块

  • 登录(登出)
  • 注册
  • 记住密码
  • 修改密码
  • todoList 增删改查
  • 点亮红星标记
  • 查询条件筛选

5.5 代码实现

后端登录注册功能使用了jwt-token认证模式来实现。使用Express、express-validator、body-parser、boom、cors、jsonwebtoken、express-jwt、MySQL组件库来简化开发。

  • express-validator:一个基于 Express 的数据验证中间件,可以方便的判断传入的表单数据是否合法。
  • body-parser:对 post 请求的请求体进行解析的 express 中间件。
  • boom:处理程序异常状态,boom 是一个兼容 HTTP 的错误对象,他提供了一些标准的 HTTP 错误,比如 400(参数错误)等。
  • cors:实现 Node 服务端跨域的 JS 库。
  • jsonwebtoken:基于jwt的概念实现安全的加密方案库,实现加密 token 和解析 token 的功能。
  • express-jwt:express-jwt 是在 jsonwebtoken 的基础上做了上层封装,基于 Express 框架下认证 jwt 的中间件,来实现 jwt 的认证功能。
  • MySQL:Node.js 连接 MySQL 数据库。

5.5.1 安装相关依赖库

npm i -S express
npm i -S body-parser
npm i -S express-validator
npm i -S boom
npm i -S cors
npm i -S jsonwebtoken
npm i -S express-jwt
npm i -S mysql

5.5.2 后端目录结构

│  app.js                        // 入口文件
│  ecosystem.config.js           // pm2默认配置文件
│  package.json                  // npm包管理所需模块及配置信息
├─db
│      dbConfig.js               // mysql数据库基础配置
├─routes
│      index.js                  // 初始化路由信息,自定义全局异常处理
│      tasks.js                  // 任务路由模块
│      users.js                  // 用户路由模块
├─services
│      taskService.js            // 业务逻辑处理 - 任务相关接口
│      userService.js            // 业务逻辑处理 - 用户相关接口
└─utils
        constant.js              // 自定义常量
        index.js                 // 封装连接mysql模块
        md5.js                   // 后端封装md5方法
        user-jwt.js              // jwt-token验证和解析函数

5.5.3 实现后端功能

5.5.3.1 工具类方法

在 utils 文件夹新建constant.js文件,定义一些常量信息,代码如下:

module.exports = {
  CODE_ERROR: -1, // 请求响应失败code码
  CODE_SUCCESS: 0, // 请求响应成功code码
  CODE_TOKEN_EXPIRED: 401, // 授权失败
  PRIVATE_KEY: 'jackchen', // 自定义jwt加密的私钥
  JWT_EXPIRED: 60 * 60 * 24, // 过期时间24小时
}

在 utils 文件夹新建user-jwt.js文件,定义 jwt-token 验证和 jwt-token 解析函数,代码如下:

const jwt = require('jsonwebtoken'); // 引入验证jsonwebtoken模块
const expressJwt = require('express-jwt'); // 引入express-jwt模块
const { PRIVATE_KEY } = require('./constant'); // 引入自定义的jwt密钥

// 验证token是否过期
const jwtAuth = expressJwt({
  // 设置密钥
  secret: PRIVATE_KEY,
  // 设置为true表示校验,false表示不校验
  credentialsRequired: true,
  // 自定义获取token的函数
  getToken: (req) => {
    if (req.headers.authorization) {
      return req.headers.authorization
    } else if (req.query && req.query.token) {
      return req.query.token
    }
  }
  // 设置jwt认证白名单,比如/api/login登录接口不需要拦截
}).unless({
  path: [
    '/',
    '/api/login',
    '/api/register',
    '/api/resetPwd'
  ]
})

// jwt-token解析
function decode(req) {
  const token = req.get('Authorization')
  return jwt.verify(token, PRIVATE_KEY);
}

module.exports = {
  jwtAuth,
  decode
}

在 utils 文件夹新建md5.js文件,密码使用 md5 加密。代码如下:

const crypto = require('crypto'); // 引入crypto加密模块

function md5(s) {
  return crypto.createHash('md5').update('' + s).digest('hex');
}
module.exports = md5;

在 db 文件夹新建dbConfig.js文件,定义数据库基本配置信息,代码如下:

const mysql = {
  host: 'localhost', // 主机名称,一般是本机
	port: '3306', // 数据库的端口号,如果不设置,默认是3306
	user: 'root', // 创建数据库时设置用户名
	password: '123456', // 创建数据库时设置的密码
	database: 'my_test',  // 创建的数据库
	connectTimeout: 5000 // 连接超时
}

module.exports = mysql;

在 utils 文件夹新建index.js文件,连接 MySQL 数据库,代码如下:

const mysql = require('mysql');
const config = require('../db/dbConfig');

//连接mysql
function connect() {
  const { host, user, password, database } = config;
  return mysql.createConnection({
    host,
    user,
    password,
    database
  })
}

//新建查询连接
function querySql(sql) {
  const conn = connect();
  return new Promise((resolve, reject) => {
    try {
      conn.query(sql, (err, res) => {
        if (err) {
          reject(err);
        } else {
          resolve(res);
        }
      })
    } catch (e) {
      reject(e);
    } finally {
      //释放连接
      conn.end();
    }
  })
}

//查询一条语句
function queryOne(sql) {
  return new Promise((resolve, reject) => {
    querySql(sql).then(res => {
      console.log('res===',res)
      if (res && res.length > 0) {
        resolve(res[0]);
      } else {
        resolve(null);
      }
    }).catch(err => {
      reject(err);
    })
  })
}

module.exports = {
  querySql,
  queryOne
}
5.5.3.2 业务逻辑层

在 services 文件夹下新建userService.js文件,定义用户登录注册查询等 API 接口,代码如下:

const { querySql, queryOne } = require('../utils/index');
const md5 = require('../utils/md5');
const jwt = require('jsonwebtoken');
const boom = require('boom');
const { body, validationResult } = require('express-validator');
const {
  CODE_ERROR,
  CODE_SUCCESS,
  PRIVATE_KEY,
  JWT_EXPIRED
} = require('../utils/constant');
const { decode } = require('../utils/user-jwt');


// 登录
function login(req, res, next) {
  const err = validationResult(req);
  // 如果验证错误,empty不为空
  if (!err.isEmpty()) {
    // 获取错误信息
    const [{ msg }] = err.errors;
    // 抛出错误,交给我们自定义的统一异常处理程序进行错误返回
    next(boom.badRequest(msg));
  } else {
    let { username, password } = req.body;
    // md5加密
    password = md5(password);
    const query = `select * from sys_user where username='${username}' and password='${password}'`;
    querySql(query)
    .then(user => {
    	// console.log('用户登录===', user);
      if (!user || user.length === 0) {
        res.json({
        	code: CODE_ERROR,
        	msg: '用户名或密码错误',
        	data: null
        })
      } else {
        // 登录成功,签发一个token并返回给前端
        const token = jwt.sign(
          // payload:签发的 token 里面要包含的一些数据。
          { username },
          // 私钥
          PRIVATE_KEY,
          // 设置过期时间
          { expiresIn: JWT_EXPIRED }
        )

        let userData = {
          id: user[0].id,
          username: user[0].username,
          nickname: user[0].nickname,
          avator: user[0].avator,
          sex: user[0].sex,
          gmt_create: user[0].gmt_create,
          gmt_modify: user[0].gmt_modify
        };

        res.json({
        	code: CODE_SUCCESS,
        	msg: '登录成功',
        	data: {
            token,
            userData
          }
        })
      }
    })
  }
}


// 注册
function register(req, res, next) {
  const err = validationResult(req);
  if (!err.isEmpty()) {
    const [{ msg }] = err.errors;
    next(boom.badRequest(msg));
  } else {
    let { username, password } = req.body;
    findUser(username)
  	.then(data => {
  		// console.log('用户注册===', data);
  		if (data) {
  			res.json({
	      	code: CODE_ERROR,
	      	msg: '用户已存在',
	      	data: null
	      })
  		} else {
	    	password = md5(password);
  			const query = `insert into sys_user(username, password) values('${username}', '${password}')`;
  			querySql(query)
		    .then(result => {
		    	// console.log('用户注册===', result);
		      if (!result || result.length === 0) {
		        res.json({
		        	code: CODE_ERROR,
		        	msg: '注册失败',
		        	data: null
		        })
		      } else {
            const queryUser = `select * from sys_user where username='${username}' and password='${password}'`;
            querySql(queryUser)
            .then(user => {
              const token = jwt.sign(
                { username },
                PRIVATE_KEY,
                { expiresIn: JWT_EXPIRED }
              )

              let userData = {
                id: user[0].id,
                username: user[0].username,
                nickname: user[0].nickname,
                avator: user[0].avator,
                sex: user[0].sex,
                gmt_create: user[0].gmt_create,
                gmt_modify: user[0].gmt_modify
              };

              res.json({
                code: CODE_SUCCESS,
                msg: '注册成功',
                data: {
                  token,
                  userData
                }
              })
            })
		      }
		    })
  		}
  	})

  }
}

// 重置密码
function resetPwd(req, res, next) {
	const err = validationResult(req);
  if (!err.isEmpty()) {
    const [{ msg }] = err.errors;
    next(boom.badRequest(msg));
  } else {
    let { username, oldPassword, newPassword } = req.body;
    oldPassword = md5(oldPassword);
    validateUser(username, oldPassword)
    .then(data => {
      console.log('校验用户名和密码===', data);
      if (data) {
        if (newPassword) {
          newPassword = md5(newPassword);
				  const query = `update sys_user set password='${newPassword}' where username='${username}'`;
				  querySql(query)
          .then(user => {
            // console.log('密码重置===', user);
            if (!user || user.length === 0) {
              res.json({
                code: CODE_ERROR,
                msg: '重置密码失败',
                data: null
              })
            } else {
              res.json({
                code: CODE_SUCCESS,
                msg: '重置密码成功',
                data: null
              })
            }
          })
        } else {
          res.json({
            code: CODE_ERROR,
            msg: '新密码不能为空',
            data: null
          })
        }
      } else {
        res.json({
          code: CODE_ERROR,
          msg: '用户名或旧密码错误',
          data: null
        })
      }
    })

  }
}

// 校验用户名和密码
function validateUser(username, oldPassword) {
	const query = `select id, username from sys_user where username='${username}' and password='${oldPassword}'`;
  	return queryOne(query);
}

// 通过用户名查询用户信息
function findUser(username) {
  const query = `select id, username from sys_user where username='${username}'`;
  return queryOne(query);
}

module.exports = {
  login,
  register,
  resetPwd
}
5.5.3.3 请求路由处理

在 routes 文件夹下新建index.jsuser.js文件。

index.js 文件是初始化路由信息,自定义全局异常处理,代码如下:

const express = require('express');
// const boom = require('boom'); // 引入boom模块,处理程序异常状态
const userRouter = require('./users'); // 引入user路由模块
const taskRouter = require('./tasks'); // 引入task路由模块
const { jwtAuth, decode } = require('../utils/user-jwt'); // 引入jwt认证函数
const router = express.Router(); // 注册路由

router.use(jwtAuth); // 注入认证模块

router.use('/api', userRouter); // 注入用户路由模块
router.use('/api', taskRouter); // 注入任务路由模块

// 自定义统一异常处理中间件,需要放在代码最后
router.use((err, req, res, next) => {
  // 自定义用户认证失败的错误返回
  console.log('err===', err);
  if (err && err.name === 'UnauthorizedError') {
    const { status = 401, message } = err;
    // 抛出401异常
    res.status(status).json({
      code: status,
      msg: 'token失效,请重新登录',
      data: null
    })
  } else {
    const { output } = err || {};
    // 错误码和错误信息
    const errCode = (output && output.statusCode) || 500;
    const errMsg = (output && output.payload && output.payload.error) || err.message;
    res.status(errCode).json({
      code: errCode,
      msg: errMsg
    })
  }
})

module.exports = router;

user.js 文件是用户路由模块,代码如下:

const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const service = require('../services/userService');

// 登录/注册校验
const vaildator = [
  body('username').isString().withMessage('用户名类型错误'),
  body('password').isString().withMessage('密码类型错误')
]

// 重置密码校验
const resetPwdVaildator = [
  body('username').isString().withMessage('用户名类型错误'),
  body('oldPassword').isString().withMessage('密码类型错误'),
  body('newPassword').isString().withMessage('密码类型错误')
]

// 用户登录路由
router.post('/login', vaildator, service.login);

// 用户注册路由
router.post('/register', vaildator, service.register);

// 密码重置路由
router.post('/resetPwd', resetPwdVaildator, service.resetPwd);

module.exports = router;
5.5.3.4 入口文件配置

在根目录 app.js 程序入口文件中,导入 Express 模块,再引入常用的中间件和自定义 routes 路由的中间件,代码如下:

const bodyParser = require('body-parser'); // 引入body-parser模块
const express = require('express'); // 引入express模块
const cors = require('cors'); // 引入cors模块
const routes = require('./routes'); //导入自定义路由文件,创建模块化路由
const app = express();

app.use(bodyParser.json()); // 解析json数据格式
app.use(bodyParser.urlencoded({extended: true})); // 解析form表单提交的数据application/x-www-form-urlencoded

app.use(cors()); // 注入cors模块解决跨域

app.use('/', routes);

app.listen(8088, () => { // 监听8088端口
	console.log('服务已启动 http://localhost:8088');
})

到此基于 Vue + iView + Express + Node.js + MySQL 实现的前后端功能已基本完成

六. 工具整合

6.1 自动重启服务

每次修改 js 文件,我们都需要重启服务器,这样修改的内容才会生效,但是每次重启比较麻烦,影响开发效果。所以我们在开发环境中引入 nodemon 插件,实现实时热更新,自动重启项目。我们在开发环境中启动项目应该使用npm start命令,因为我们在 package.json 文件中配置了以下命令:

"scripts": {
  "start": "nodemon app.js"
}

6.2 PM2 - Node 进程管理

PM2 是 Node 进程管理工具,可以利用它来简化很多 Node 应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。

下面就对 PM2 进行入门性的介绍,基本涵盖了 PM2 的常用功能和配置:

  • 全局安装 PM2:npm i pm2 -g
  • 监听应用:pm2 start index.js
  • 查看所有进程:pm2 list
  • 查看某个进程:pm2 describe App name/id
  • 停止某个进程:pm2 stop App name/id。
  • 停止所有进程:pm2 stop all
  • 重启某个进程:pm2 restart App name/id
  • 删除某个进程:pm2 delete App name/id

配置文件信息如下:

module.exports = {
  apps : [{
    name: 'todo_node_api',
    script: 'app.js',
    instances: 1,
    autorestart: true,
    watch: false,
    max_memory_restart: '1G',
    env: {
      NODE_ENV: 'development'
    },
    env_production: {
      NODE_ENV: 'production'
    }
  }],
};

这里作者就不详细介绍 pm2,如需了解更多请移步到PM2 实用入门指南 | 博客园 - 程序猿小卡

七. 运维和发布

7.1 部署发布

项目部署发布之前,必须准备好一台服务器和域名以及相关配置。作者购买的服务器是CentOS7操作系统,也要安装对应的工具库。命令如下:

// 系统升级命令
yum update
// 安装 nginx
yum install nginx
// 启动/重启 nginx 服务
nginx / nginx -s reload
// 压缩包 zip 上传下载命令
yum install lrzsz

// 安装 nodejs
wget nodejs.org/dist/v10.16…
tar xf node-v10.16.2-linux-x64.tar.xz
mv node-v10.16.2-linux-x64 nodejs
// 建立软连接
ln -s /usr/local/nodejs/bin/npm /usr/local/bin/
ln -s /usr/local/nodejs/bin/node /usr/local/bin/
// 重启服务,打印显示版本号表示安装成功
node -v

// 安装 pm2
npm install -g pm2
ln -s /usr/local/nodejs/bin/pm2 /usr/local/bin/
// 打印显示版本号表示安装成功
pm2 -v

// 安装 MySQL
wget dev.mysql.com/get/mysql57…
rpm -ivh mysql57-community-release-el7-9.noarch.rpm
yum -y install mysql-community-server
// 启动 MySQL 服务
systemctl start mysqld.service
// 测试访问数据库端口是否开启
netstat -tnlp grep 3306
// 查看数据库初始密码
grep "password" /var/log/mysqld.log
// 连接数据库,输入密码登录
mysql -uroot -p
// 设置字符编码 UTF8
vim /etc/my.cnf
[client]
default-character-set=utf8
[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci
// 重启 MySQL 服务
systemctl restart mysqld.service

前端代码打包命令

npm run build

后端代码直接上传到 github,通过命令将 github 上的代码下载到线上服务器。命令如下:

wget github.com/jackchen012…

7.2 运维事项

我们开发人员将项目部署发布线上后,接下来的工作就交给运维人员进行维护,而需要提供哪些给到运维人员如下:

  • 启动命令:pm2 start/restart ecosystem.config.js
  • 运维命令:pm2 log
  • 运维文档:注意事项比如项目部署的代码程序目录路径,常用命令(启动、重启、查看日志)等等

八. 写在最后

写到这,兴许在前面代码的摧残下,能看到这里的小伙伴已经寥寥无几了,但我坚信我该交代的基本都交代了,不该交代的也交代了~🐶

所以,如果小伙伴看完真觉得不错,那就点个 👍 或者给个 💖 吧!你们的赞和 star 是我编写更多更精彩文章的动力!

github 地址:github.com/jackchen012…

此项目其实还有很多不足或优化的地方,也期望与大家一起交流学习。

获取更多项目实战经验及各种源码资源

请关注个人公众号:懒人码农