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…

此項目其實還有不少不足或優化的地方,也指望與你們一塊兒交流學習。

獲取更多項目實戰經驗及各類源碼資源

請關注我的公衆號:懶人碼農

相關文章
相關標籤/搜索