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

Posted SHERlocked93

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NodeJS全栈开发一个功能完善的Express项目(附完整源码)相关的知识,希望对你有一定的参考价值。

一. 前言

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

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

后端API接口源码地址????:https://github.com/jackchen0120/todo-nodejs-api

前端界面源码地址????:https://github.com/jackchen0120/todo-vue-admin

如果觉得本文还不错,记得点个????赞或者给个❤️star,你们的赞和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搭建的后端服务器。

在线演示DEMO地址????:http://106.55.168.13:8082/

部分效果截图

三. 前端部分

3.1 基础环境

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

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

运行项目

1> 下载安装依赖

git clone https://github.com/jackchen0120/todo-vue-admin.git
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 https://github.com/jackchen0120/todo-nodejs-api.git
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 https://nodejs.org/dist/v10.16.2/node-v10.16.2-linux-x64.tar.xz
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 https://dev.mysql.com/get/mysql57-community-release-el7-9.noarch.rpm
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 https://github.com/jackchen0120/todo-nodejs-api.git

7.2 运维事项

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

  • 启动命令:pm2 start/restart ecosystem.config.js

  • 运维命令:pm2 log

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

八. 写在最后

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

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

github地址:https://github.com/jackchen0120

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

最后

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 欢迎加我微信「qianyu443033099」拉你进技术群,长期交流学习...

  3. 关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。

点个在看支持我吧,转发就更好了

以上是关于NodeJS全栈开发一个功能完善的Express项目(附完整源码)的主要内容,如果未能解决你的问题,请参考以下文章

重点突破—— Nodejs+Express+MongoDB的使用基础

一个关于vue+mysql+express的全栈项目------ 实时聊天部分socket.io

前端进阶全栈入门级教程nodeJs博客开发(二)安装mysql完善api接口对接mysql

NodeJS框架express的途径映射(路由)功能及控制

2019教你用react全家桶+node.js全栈开发大型电商后台管理系统(视频+源码+课件)

如何用nodejs开发爬虫程序?