NodeJS全栈开发一个功能完善的Express项目(附完整源码)
一. 前言
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,你们的赞和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.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.js
和user.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…
此项目其实还有很多不足或优化的地方,也期望与大家一起交流学习。
获取更多项目实战经验及各种源码资源
请关注个人公众号:懒人码农
转载自:https://juejin.cn/post/6844904198551666701