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

一. 前言

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

本文做者根据项目实战经验已将底层服务架构搭建完成,而且本项目已在github开源,供你们学习参考使用(若有不足,还请批评指正),但愿能减轻你们的工做量,更高效的完成工做,有更多时间提高本身的能力。🤭css

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

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

若是以为本文还不错,记得点个👍赞或者给个❤️star,大家的赞和star是做者编写更多更精彩文章的动力!vue

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

Node.js

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

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

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

Express

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

Express框架核心特性:

  • 能够设置中间件来响应HTTP请求。
  • 定义了路由表用于执行不一样的HTTP请求动做。
  • 能够经过向模板传递参数来动态渲染HTML页面。

二. 先后端分离

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

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

部分效果截图

三. 前端部分

3.1 基础环境

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

工具名称 版本号
node.js 10.16.2
npm 6.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 基础环境

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

工具名称 版本号
express 4.17.1
mysql 5.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地址:github.com/jackchen012…

此项目其实还有不少不足或优化的地方,也指望与你们一块儿交流学习。

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

请关注我的公众号:懒人码农

相关文章
相关标签/搜索