- 本文记录一下实现一个全栈项目,前端使用vue框架、后端使用express框架、数据库使用mysql。
- 此项目的意义不仅仅有助于我们复习nodejs相关知识、更有助于带前端新人,使其快速从整体全局角度中,理解常规后台管理系统,大致业务需求工作流程形态
- 因为大多数的后台管理系统常见需求也无外乎:增删改查、上传下载、排序分页。
- 当然复杂需求不在本篇文章的考虑范畴内
产品需求分析
产品经理说,我需要做一个web人员管理项目,项目包含以下功能:
- 用户可以在页面上新建数据,新建的数据内容有,人名、年龄、家乡、以及此人的备注
- 用户可以删除之前新建的人员信息,删除只做逻辑删除,不做物理删除(不删数据库数据)
- 用户可以修改之前新建的人员信息,人名、年龄、家乡、备注均可以修改
- 页面中要有一个输入框做模糊搜索功能,比如用户搜索一个“海”这个字,所有和这个字关联的数据都要呈现筛选出来。无论人名、家乡、还是备注中包含了这个字。
- 用户可以勾选行,做excel导出功能
- 使用表格呈现数据,做分页管理、所有字段都支持排序
说白了,就是增删改查分页排序导出功能。我们了解需求以后,就可以设计数据库表、以及字段了
预览一下效果图
开始设计数据库之前,我们先预览最终的效果图,最终的前后端代码以及mysql表,文末会给到Gitee的地址,欢迎大家下载瞅瞅看看
数据库设计
这里因为功能比较简单,所以我们设计一张表即可,在一张表里存储项目中需要的字段即可。当然建表之前,我们要先新建一个数据库。新建数据库之前,我们要连接自己的MySql数据库。这里我用的是Navicat工具管理Mysql数据库。
连接数据库
新建数据库
数据库的名字叫做person_manage,后续express中配置数据库连接池需要用到。
新建表(新建字段)
新建的这张表的名字是people_table
这里就不赘述,Mysql的下载和Navcat的下载了。大家谷歌(百度)一下,有很多下载安装的教程的。
前端页面开发
前端使用vue项目,所以我们从0到1简单搭建一个vue项目。
- 提前安装好node软件、node中自带npm node -v和npm-v分别查看对应版本号,我的版本分别是v12.18.0和6.14.4
- 配置淘宝镜像 npm install -g cnpm –registry=https://registry.npm.taobao.org,这样下载包的时候会快一些
- 使用vue-cli快速生成一个vue项目架构。其实vue-cli脚手架其实也是一个npm包,所以要使用npm下载vue-cli脚手架,执行命令npm install -g @vue/cli全局下载vue-cli脚手架,执行命令vue -V可以查看相应的vue-cli的版本号
- 最后执行vue create 项目名创建一个项目,这里我创建的是mydemo项目,即vue create mydemo
项目创建好了以后,我们还需要下载axios用法发请求,下载vue-router用来做路由,下载element-ui用来快速开发,下载nprogress做个进度条等相关插件包。然后引入并使用这些包,最后我们按照项目需要,修改一下项目的目录,最终是下面的结构。
前端项目结构图
接着贴出各个文件夹代码
api文件夹部分
axios的二次封装(api.js)
// 引入axios包,并创建一个axios实例
import axios from "axios";
const http = axios.create({})
import NProgress from 'nprogress' // 引入NProgress 当然,要提前npm下载一下
import 'nprogress/nprogress.css' // 引入NProgress和对应的样式
NProgress.configure({ showSpinner: false }) // 隐藏右侧的旋转进度条
// 给这个axios实例加上请求拦截器和相应拦截器
//请求拦截
http.interceptors.request.use(
(config) => {
NProgress.start() // 开启进度条
return config;
},
(err) => {
return Promise.reject(err);
}
)
//响应拦截
http.interceptors.response.use(
(res) => {
NProgress.done() // 关闭进度条
return res.data
},
(error) => {
NProgress.done() // 关闭进度条
return Promise.reject(error);
}
)
/*
暴露一个函数,在函数中使用刚创建的且加上拦截器的axios实例去发请求。
因为发请求需要指定请求方式、请求地址、请求参数、请求头、响应类型等相关信息
所以,需要接收相应的method、url、data、headers、responseType等参数
这里最终暴露的api函数是留给lss/index.js文件中定义的接口函数使用的
*/
export default (method, url, data = null, headers = 'application/json;charset=UTF-8', responseType) => {
if (method == "post") {
return http({
method: 'post',
url: url,
data: data,
headers: {
'Content-Type': headers,
},
responseType: responseType
});
} else if (method == "get") {
return http({
method: 'get',
url: url,
params: data,
headers: {
'Content-Type': headers
},
responseType: responseType
});
} else if (method == "put"){
// ......
return;
} else if (method == "delete"){
// .....
return;
}
}
axios的二次封装以后,我们需要在接口函数中引入,并定义接口函数
接口函数的定义(lss/index.js)
// 导入http中创建的axios实例
import http from '../api';
export const getTableData = (params) => http("get", `/api/getTableData`,params)//分页查询获取人物信息
export const getTotalCount = () => http("get", `/api/getTotalCount`)//分页查询获取人物信息总条目数
export const deleteData = (params) => http("get", `/api/deleteData`,params)//删除人物信息
export const addData = (params) => http("post", `/api/addData`,params)//新增人物信息
export const editData = (params) => http("post", `/api/editData`,params)//编辑人物信息
export const exportExcel = (params) => http("post", `/api/exportExcel`,params,'application/json; charset=UTF-8',"arraybuffer")//导出表格数据
接口函数定义了以后,我们在(api/index.js)文件中做统一管理,暴露出去,在main.js文件中再引入最终注册到Vue的原型链上去。
接口函数的统一管理并暴露(api/index.js)
// 统一管理理一下请求接口
import {
getTableData,
getTotalCount,
deleteData,
addData,
editData,
exportExcel,
} from './lss/index'
export default {
getTableData,
getTotalCount,
deleteData,
addData,
editData,
exportExcel,
}
main.js中将接口函数注册到Vue原型上
看倒数第五行、倒数第六行
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false // 关闭提示
import "./assets/css/reset.css"; //引入重置样式表
import ElementUI from 'element-ui'; //引入饿了么包
import 'element-ui/lib/theme-chalk/index.css'; //引入饿了么样式
Vue.use(ElementUI); // 使用饿了么UI
import router from './router/index' // 引入路由
import api from "./api/index" // 引入封装的axios的接口函数
Vue.prototype.$api = api // 将封装的axios接口函数注册到原型链上
new Vue({
render: h => h(App),
router // 挂载路由
}).$mount('#app')
assets文件夹部分
assets文件夹存放的一般是静态资源文件,比如重置样式表,比如404页面的图片等。当然重置样式表,也是要在main.js中引入并使用的。这里就不赘述了
router文件夹部分(router.index.js)
路由表我们这样配置,因为小项目,所以只做两个页面。homepage页面和404页面接口,代码如下:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter) // 注册路由
const router = new VueRouter({
mode: 'history', // hash
routes: [
{
path: "/",
redirect: "/homepage",
},
{
path: "/homepage",
component: resolve => require(['@/views/homepage.vue'], resolve),
meta: {
name: "homepage"
}
},
{
// 会匹配所有路径,这里定义一个404页面
path: '*',
component: resolve => require(['@/views/404.vue'], resolve),
meta: {
name: "404"
},
// redirect: "/homepage", // 当然,也可以重定向到主页页面
}
]
})
export default router
当然路由表也是要引入到main.js中挂载到vue实例上去的。
views文件夹部分
views文件夹存放的是我们所写的页面对应的vue相关组件,这里就不赘述了。主要就是注意,路由表的层级和router-view的层级对应
因为nodejs主要是我们前端使用去做后端的事情,所以,就用多点篇幅讲述nodejs框架express的使用
后端接口开发
我们后端使用的是express框架,这里为了让过程清晰点,就不使用express-generator这个插件了,我们一步步的书写即可。
express-generator是一个node的自动化创建项目工具,类似于vue-cli,也算是一个express脚手架
搭建项目步骤
第一步 npm初始化项目
执行npm init --yes
会创建一个基础的项目,会在文件夹中生成一个package.json文件,命令行会有如下提示,图片如下:
第二步 创建app.js入口文件
安装相关的依赖包,这里我们使用express框架、使用nodemon插件,不用重启后端服务、使用mysql插件连接数据库、使用node-xlsx插件做表格导出。所以执行如下命令:cnpm i express nodemon mysql node-xlsx --save。npm命令执行以后,package.json文件就会把相关的包添加到依赖中,同时也会多一个node_modules文件夹,用于存放我们下载的相关包。如下:
// package.json文件
{
"name": "expressdemo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"mysql": "^2.18.1",
"node-xlsx": "^0.17.1",
"nodemon": "^2.0.12"
}
}
第三步 先创建一个简单的服务
在与package.json文件同级目录上,创建一个app.js文件,这个文件是express服务入口文件。贴上如下代码:
const express = require('express') // 引入express包
const app = express() // 创建express实例
app.get('/', (req, res) => { // 当请求为get请求,url路径为 / 的时候,返回如下数据
res.send('这个是用express搭建的服务')
})
// 在9999端口上启动后端服务
app.listen(9999, (req, res) => {
console.log('后端服务端口地址为:localhost://9999');
})
这样的话,一个简单的后端服务就搞好了。我们在浏览器地址栏,本机localhost:9999访问一下,就可以看到效果啦,如下图:
当然实际的后端服务不会怎么简单,所以我们需要再添加代码。
- 比如app.js中管理路由要做模块化拆分(中间件形式)app.use(Router)
- 比如需要使用使用body-parser中间件做post请求的参数解析cnpm i body-parser
- 比如需要使用mysql插件做数据库连接管理cnpm i mysql
- 比如项目中有下载导出表格功能需要使用cnpm i mysql node-xlsx
- 当然,为了方便后端调试,我们也顺带下载nodemon用一下cnpm i mysql nodemon
第四步 最终的express目录和代码
后端服务目录图如下
package.json文件
/* package.json文件配置信息 */
{
"name": "expressdemo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"mysql": "^2.18.1",
"node-xlsx": "^0.17.1",
"nodemon": "^2.0.12"
}
}
app.js程序主入口文件
/* app.js文件 */
// 引入express插件包并生成一个实例app
const express = require('express')
const app = express()
// 使用body-parser中间件解析post请求主体
app.use(express.urlencoded({ extended: false }))
app.use(express.json())
const Router = require('./router') // 引入分模块管理的路由
// 路由分模块
app.use(Router)
// 在9999端口上启动后端服务
app.listen(9999, (req,res) => {
console.log('后端服务端口地址为:localhost://9999');
})
mysql连接池配置文件
/* sql/index.js文件 */
// 引入mysql数据库
var mysql = require('mysql')
// 数据库连接池的配置
var pool = mysql.createPool({
connectionLimit: 10, // 连接池的大小
host: 'localhost', // 主机名
user: 'root', // 用户名
password: '123456', // 密码
database: 'person_manage' // 数据库名称 在数据库里面建立了一个person_manage数据库,里面有很多表格
});
// 暴露连接池
module.exports = pool
接口开发
我们在router.js文件中写接口
分页排序接口(带模糊搜索查询)
const express = require('express') // 引入express
const route = express.Router() // 实例化一个路由对象
// 引入node-xlsx包
const xlsx = require('node-xlsx')
// 引入连接池
const pool = require('./sql/index')
// 分页排序接口(带有模糊搜索查询)
route.get('/getTableData', (req, res) => {
// console.log('请求参数', req.query);
/* 一般的分页参数一共有四个:排序字段、排序方式、第几页、每页几条 */
// 分页查询格式: select * from table limit (start-1)*limit,limit;
// let sql = "select * from people_table ORDER BY age ASC LIMIT 0,2"
// let sql = "select * from people_table"
// 又因为这个接口带有模糊查询,所以使用like模糊查询searchWord
let sql = null // 拼接sql语句
if (req.query.sortWord == "age" | req.query.sortWord == "id") { // 数字类型的使用默认排序 比如年龄和id
sql = `select * from people_table WHERE (name LIKE '%${req.query.searchWord}%' OR home LIKE '%${req.query.searchWord}%' OR remark LIKE '%${req.query.searchWord}%') AND is_delete_status <> 0 ORDER BY ${req.query.sortWord} ${req.query.sortOrder} LIMIT ${(req.query.pageIndex - 1) * (req.query.pageSize)},${req.query.pageSize}`
} else { // 汉字类型的,强制使用GBK排序,这样的话,就能够按照汉语拼音排序了
sql = `select * from people_table WHERE (name LIKE '%${req.query.searchWord}%' OR home LIKE '%${req.query.searchWord}%' OR remark LIKE '%${req.query.searchWord}%') AND is_delete_status <> 0 ORDER BY CONVERT( ${req.query.sortWord} USING gbk ) ${req.query.sortOrder} LIMIT ${(req.query.pageIndex - 1) * (req.query.pageSize)},${req.query.pageSize}`
}
// console.log('拼接好的sql语句',sql);
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(sql, function (error, results, fields) {
connection.release()
let apiRes = {
code: 0,
msg: "成功",
data: results
}
res.send(apiRes)
})
})
})
// 其他接口...
module.exports = route // 暴露出去方便管理
查询总条目数接口
route.get("/getTotalCount", (req, res) => {
console.log(req.query);
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(`select count(*) AS total from people_table WHERE (name LIKE '%${req.query.searchWord}%' OR home LIKE '%${req.query.searchWord}%' OR remark LIKE '%${req.query.searchWord}%') AND is_delete_status <> 0`, function (error, results, fields) {
connection.release()
let apiRes = {
code: 0,
msg: "成功",
data: results[0].total
}
res.send(apiRes)
})
})
})
逻辑删除数据接口(单条删除)
route.get("/deleteData", (req, res) => {
let sql = `UPDATE people_table SET is_delete_status = 0 WHERE id = ${req.query.id}`
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(sql, function (error, results, fields) {
connection.release()
if (results.affectedRows == 1) { // 说明影响了一行,即 将逻辑删除字段修改了,删掉了一行
let apiRes = {
code: 0,
msg: "成功",
data: "恭喜您,删除成功啦..."
}
res.send(apiRes)
} else {
let apiRes = {
code: 0,
msg: "成功",
data: "很抱歉,删除失败了..."
}
res.send(apiRes)
}
})
})
})
物理删除数据接口(单条删除)
route.get("/trueDeleteData", (req, res) => {
let sql = `DELETE from people_table WHERE id = ${req.query.id}`
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(sql, function (error, results, fields) {
connection.release()
if (results.affectedRows == 1) { // 说明影响了一行,即 真删掉了一行
let apiRes = {
code: 0,
msg: "成功",
data: "恭喜您,删除成功啦..."
}
res.send(apiRes)
} else {
let apiRes = {
code: 0,
msg: "成功",
data: "很抱歉,删除失败了..."
}
res.send(apiRes)
}
})
})
})
逻辑删除数据接口(批量删除)
route.get("/selectDelete",(req,res)=>{
console.log(req.query.ids);
let sql = `UPDATE people_table SET is_delete_status = 0 WHERE id in (${req.query.ids})`
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(sql, function (error, results, fields) {
console.log('结果',results);
connection.release()
if (results.affectedRows != 0) { // 说明影响了一行,即 将逻辑删除字段修改了,删掉了一行
let apiRes = {
code: 0,
msg: "成功",
data: "恭喜您,批量删除成功啦..."
}
res.send(apiRes)
} else {
let apiRes = {
code: 0,
msg: "失败",
data: "很抱歉,批量删除失败了..."
}
res.send(apiRes)
}
})
})
})
新增数据接口
/**
* 值得一提的是,我这里没有做封装sql语句函数
* 所以一些sql语句是动态拼接上去的
* 实际工作中还是要根据项目需求封装一下sql语句的
* 主要是方便能一步步的看懂
* */
route.post("/addData", (req, res) => {
// console.log('新增接口请求参数',req.body);
// console.log('新增接口请求参数',Object.keys(req.body));
// console.log('新增接口请求参数',Object.values(req.body));
// 正确sql语句: INSERT INTO myTale(NAME,sex,borndate) VALUES('白骨精','女','2021-05-16');
// 错误sql语句: INSERT INTO myTale(NAME,sex,borndate) VALUES(白骨精,女,2021-05-16);
// 因为往数据库中写入数据需要注意引号的问题,即VALUES()中需要带上引号,所以下方的拼接sql语句要使用转义字符 \'
let str = "" // 这里大家可以打断点、或者打印看看就晓得了
Object.values(req.body).forEach((item) => {
str = str + "\'" + item + "\'" + ","
})
// console.log('截取一下',str.substr(0,str.length -1)); // 截取一下,不要最后的一个逗号
let editStr = str.substr(0, str.length - 1)
let sql = `INSERT INTO people_table (${Object.keys(req.body).toString()}) VALUES(${editStr})`
// console.log("看看使用转义字符拼接好的sql语句", sql);
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(sql, function (error, results, fields) {
connection.release()
if (results.affectedRows == 1) {
let apiRes = {
code: 0,
msg: "成功",
data: "恭喜您,新增成功啦..."
}
res.send(apiRes)
} else {
let apiRes = {
code: 0,
msg: "失败",
data: "抱歉新增失败"
}
res.send(apiRes)
}
})
})
})
编辑数据接口
route.post("/editData", (req, res) => {
// 将参数加工一下
let id = req.body.id
delete req.body.id
// console.log('原始参数', req.body);
// console.log('keys数组参数', Object.keys(req.body));
// console.log('values数组参数', Object.values(req.body));
let keysArr = Object.keys(req.body)
let valuesArr = Object.values(req.body)
let str = ""
for (let i = 0; i < keysArr.length; i++) {
for (let j = 0; j < valuesArr.length; j++) {
if (i == j) {
str = `${str},${keysArr[i]}=${"\'" + valuesArr[i] + "\'"}`
}
}
}
str = str.substr(1, str.length)
let sql = `UPDATE people_table SET ${str} WHERE id=${id}`
// console.log("看看拼接好的sql语句-->",sql);
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(sql, function (error, results, fields) {
connection.release()
console.log('编辑数据库结果--->', results);
if (results.affectedRows == 1) {
let apiRes = {
code: 0,
msg: "成功",
data: "恭喜您,编辑成功啦..."
}
res.send(apiRes)
} else {
let apiRes = {
code: 0,
msg: "失败",
data: "抱歉编辑失败了!"
}
res.send(apiRes)
}
})
})
})
excel勾选导出接口
const xlsx = require('node-xlsx') // 引入node-xlsx包
route.post('/exportExcel', (req, res) => {
let sql = `SELECT name,home,age,remark FROM people_table WHERE FIND_IN_SET(id,'${req.body.ids}')`
// console.log('拼接好的sql语句-->', sql);
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(sql, function (error, results, fields) {
connection.release()
// console.log('编辑数据库结果--->', results);
let data = []
data.push(Object.keys(results[0])) // excel表格表头
results.forEach((item) => {
data.push(Object.values(item))
})
// console.log('加工的data数据',data);
let sheetArr = [ // excel表格内容数据
{
name: "sheet123", // 指定sheet的名字
data: data // 对应sheet的内容
},
]
let optionArr = { // excel表格内容配置数据
"!cols": [
{ wch: 15 },
{ wch: 15 },
{ wch: 10 },
{ wch: 30 },
],
}
// build方法用来生成一个表格,并以二进制文件的形式传递给前端
// 这里要用end方法,如果使用send方法就会报错
res.end(xlsx.build(sheetArr, optionArr))
})
})
})
excel导出可以参考之前的文章:https://segmentfault.com/a/1190000040614995
前后端联调
前后端联调就比较简单了,前端根据后端提供的api接口,发请求给后端,后端接到相应的请求以后,再去做相应的数据库操作,最终返回相应的数据给前端。这里就不赘述了。详细的情况可以clone我上传到Gitee的代码哈,可以顺手给个star哈,谢谢喽
项目Gitee地址
总结
我个人觉得前端的道友们,还是要学一下node和mysql的,毕竟学好node和mysql以后,整个项目的流程就跑通了,我们就会有整体的思维。而且有助于我们配合后端联调项目,提升项目整体开发效率。
工作闲暇之余,抽空写的一个全栈项目,可能不太完善,欢迎各位大佬批评指正,多交流、共进步哈