一、项目搭建
安装脚手架
npm i -g @vue/cli
npm i -g nodemon
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fl6ILs3N-1659141767327)(01/02.PNG)]
1.server 空文件夹
初始化
cd server
npm init -y
新建服务的入口文件 index.js
在package.json中配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3kDUNWD-1659141767331)(01/02-2.PNG)]
这样启动server项目的时候用 npm run serve
启动
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ThEseTv1-1659141767332)(01/03.PNG)]
2.web vue项目
用vue2.0 默认
vue create web
选择默认的就好
3.admin vue项目
vue create admin
总的目录:
node-vue-moba
server
admin
web
二、admin界面初始化
cd admin 项目
启动项目 npm run serve
在此项目中新开一个命令行窗口,添加一些依赖
vue add element
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8WpiY1fs-1659141767335)(01/04-1.PNG)]
vue add router
选择no
报错01
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N4JugLaL-1659141767336)(01/报错01.PNG)]
解决:
原来是用vue3.0 将版本降为2.0的
使用element
vue 下新建Main.vue
复制element 官网下的布局
在index.js中引用
安装axios
npm i axios
和main.js同级新建http.js
import axios from 'axios'
const http = axios.create({
baseURL: 'http://localhost:3000/admin/api' // 暂时的
})
export default http
main.js中 应用
import http from './http' ;Vue.prototype.$http = http
在el-menu 中添加 rouer 属性,
el-menu-item index=“/” // 就可以跳转到设置的路由
el-main 里面 这样访问的页面就是在mian 里显示
server初始化
安装一些插件
npm i express@next mongoose cors
分类功能的增删查改
1.视图准备
admin项目中 准备视图
根据喜好在element组件中自行寻找
Main.vue
<template>
<el-container style="height: 100vh;">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu router :default-openeds="['1', '3']">
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>内容管理</template>
<el-menu-item-group>
<template slot="title">分类</template>
<el-menu-item index="/categories/create">新建分类</el-menu-item>
<el-menu-item index="/categories/list">分类列表</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>
<el-main>
<router-view/>
</el-main>
</el-container>
</el-container>
</template>
CategoryEdit.vue
<template>
<div>
<h1>{{id? '编辑':'新建'}}分类</h1>
<el-form label-width="120px" @submit.native.prevent="save">
<el-form-item label="名称">
<el-input v-model="model.name"></el-input>
</el-form-item>
<el-form-item >
<el-button type="primary" native-type="submit">保存</el-button>
</el-form-item>
</el-form>
</div>
</template>
CategoryList.vue
<template>
<div>
<el-table :data="items">
<el-table-column prop="_id" label="ID" width="250">
</el-table-column>
<el-table-column prop="name" label="姓名">
</el-table-column>
<el-table-column fixed="right" label="操作" width="100">
<template slot-scope="scope">
<!-- 点击跳转到编辑页携带参数 -->
<el-button type="text" size="small"
@click="$router.push(`/categories/edit/${scope.row._id}`)">编辑</el-button>
<el-button type="text" size="small"
@click="remove(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
2.配置前端路由
http.js中配置了跨域的请求
import axios from 'axios'
const http = axios.create({
baseURL: 'http://localhost:3000/admin/api'
})
export default http
index.js中配置子路由
import Main from '../views/Main.vue'
import CategoryEdit from "../views/CategoryEdit"
import CategoryList from "../views/CategoryList"
const routes = [
{
path: '/',
name: 'Main',
component: Main,
children: [
{path: '/categories/create',component: CategoryEdit},
{path: '/categories/edit/:id',component: CategoryEdit,props: true},
{path: '/categories/list',component: CategoryList},
]
}
]
3.编写后端接口
server项目中
models目录是各种集合的schema
Category.js
const mongoose = require('mongoose')
// 定义schema
const schema = new mongoose.Schema({
name:{type:String}
})
// 导出
module.exports = mongoose.model('Category',schema,'category')
plugins目录下的db.js是连接数据库的
db.js
module.exports = app =>{
const mongoose = require('mongoose')
// 连接数据库
mongoose.connect('mongodb://127.0.0.1:27017/node-vue-moba')
}
index.js 配置了中间件
const express = require("express")
const app = express()
// 配置中间件
app.use(require('cors')()) // 跨域
app.use(express.json())
// 引入
require('./routes/admin')(app)
require('./plugins/db')(app)
app.listen(3000,()=>{
console.log("http://localhost:3000");
})
在router/admin 目录下编写接口文件
admin目录下的index.js
module.exports = app =>{
const express = require("express")
const router = express.Router()
const Category = require('../../models/Category')
// 创建分类
router.post('/categories',async(req,res)=>{
const model = await Category.create(req.body);
res.send(model)
});
// 修改分类
router.put('/categories/:id',async(req,res)=>{
const model = await Category.findByIdAndUpdate(req.params.id,req.body);
res.send(model);
})
// 获取分类
router.get('/categories',async(req,res)=>{
const items = await Category.find().limit(10);
res.send(items)
});
// 根据id获取分类
router.get('/categories/:id',async(req,res)=>{
const model = await Category.findById(req.params.id);
res.send(model)
});
// 删除
router.delete('/categories/:id',async(req,res)=>{
await Category.findByIdAndDelete(req.params.id);
res.send({
success: true
})
});
app.use('/admin/api',router)
}
访问localhost:3000/admin/api/categories
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NJgt58Qq-1659141767338)(01/分类01.PNG)]
4.数据交互
admin项目的main.js 中配置了http,所有在项目中可以使用this.$http来使用
import http from './http'
Vue.prototype.$http = http
CategoryList.vue中的数据绑定与方法编写
<script>
export default {
data(){
return {
items: []
}
},
methods: {
// 获取全部数据
async fetch(){
const res = await this.$http.get('categories');
this.items = res.data;
},
// 删除
remove(row){
this.$confirm(`是否确定删除 ${row.name}`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await this.$http.delete(`categories/${row._id}`);
this.$message({
type: 'success',
message: '删除成功!'
});
// 删除成功后 重新加载表格
this.fetch()
})
}
},
// 加载时调用
created(){
this.fetch();
}
}
</script>
CategoryEdit.vue中的数据绑定与方法编写
<script>
export default {
props: {
id: {}
},
data(){
return {
model: {},
}
},
methods: {
async save(){
if(this.id){
// id存在 说明是修改后的保存
await this.$http.put(`categories/${this.id}`,this.model);
}else {
// id不存在 说明是新建后的保存
// 使用axios 发送请求 路由categories 数据 model
await this.$http.post('categories',this.model)
}
// 页面跳转
this.$router.push('/categories/list');
// 返回信息
this.$message({
type:'success',
message:'保存成功'
})
},
async fetch(){
// get 请求 带参数
const res = await this.$http.get(`categories/${this.id}`);
this.model = res.data
}
},
created(){
// 初始化时 当id存在,调用方法
this.id && this.fetch();
}
}
</script>
5.实际效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mva5Tw5l-1659141767340)(01/分类02.PNG)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZpKxqh6S-1659141767341)(01/分类03.PNG)]
通用CRUD接口设计
设计思路
- 前端请求的路由不同,所有将最后的路由作为动态参数进行处理
- 根据动态参数判断是要对哪个类进行crud,从而引用哪个类
- 将引入的类挂载到req中,继续处理
- 根据特殊情况设定参数集,其他具体的类.crud操作统一使用刚刚挂载到req的参数调用
module.exports = app =>{
const express = require("express")
const router = express.Router()
// 创建分类
router.post('/',async(req,res)=>{
const model = await req.Model.create(req.body);
res.send(model)
});
// 修改分类
router.put('/:id',async(req,res)=>{
const model = await req.Model.findByIdAndUpdate(req.params.id,req.body);
res.send(model);
})
// 获取分类
router.get('/',async(req,res)=>{
// 特定参数处理
const queryOptions = {};
// console.log(req.Model);//Model { Category }
// console.log(req.Model.modelName);//Category
if(req.Model.modelName ==='Category'){
queryOptions.populate = 'parent'
}
const items = await req.Model.find().setOptions(queryOptions).limit(10);
res.send(items)
});
// 根据id获取分类
router.get('/:id',async(req,res)=>{
const model = await req.Model.findById(req.params.id);
res.send(model)
});
// 删除
router.delete('/:id',async(req,res)=>{
await req.Model.findByIdAndDelete(req.params.id);
res.send({
success: true
})
});
// 将最后的路由设为动态参数
// 中间件处理这个参数
app.use('/admin/api/rest/:resource',async(req,res,next)=>{
// 用inflection插件 处理参数,将小写改为大写,复数改单数,就是类的名称
const Model = require('inflection').classify(req.params.resource);
//根据处理后的参数对应引用文件 挂载到reqa的Model参数中
req.Model = require(`../../models/${Model}`);
// 放行
next();
},router)
}
登录及权限
1.登录功能
1)准备登录视图
Login.vue
<template>
<div clss="login-container">
<el-card header="请先登录" class="login-card">
<el-form @submit.native.prevent="login">
<el-form-item label="用户名">
<el-input v-model="model.username"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input type="password" v-model="model.password"></el-input>
</el-form-item>
<el-form-item >
<el-button type="primary" native-type="submit">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
export default {
data(){
return {
model:{}
}
},
methods:{
async login(){
// 向后台发送登录请求
const res = await this.$http.post('login',this.model);
console.log(res.data.token);
// 存储token
localStorage.token = res.data.token
console.log(localStorage.token);
// sessionStorage.token
// 跳转页面
this.$router.push('/')
this.$message({
type:"success",
message:"登录成功"
})
}
}
}
</script>
<style >
.login-card{
width: 25rem;
margin: 6rem auto;
}
</style>
2)在前端路由里配置
import Vue from 'vue'
import Router from 'vue-router'
import Login from '../views/Login'
Vue.use(Router)
const router =new Router({
routes:[
// 定义了一个meta 方便前端权限验证
{path: '/login',name:'Login',component:Login, meta: { isPublic: true } },
]})
// vue router文档
router.beforeEach((to, from ,next) => {
// 如果要去的页面是不公开的 并且token为空
if (!to.meta.isPublic && !localStorage.token) {
//跳转到登录
return next('/login')
}
// 正常情况放行
next()
})
export default router
3)服务端编写登录接口
用到了一个第三方插件 http-assert
npm i http-assert
作用:当不满足条件时,向前端发送报错信息
app.post('/admin/api/login',async (req,res)=>{
//解构
const {username,password} = req.body;
// 1. 根据用户名判断用户是否存在
// 查的时候取出password字段
const user = await AdminUser.findOne({username}).select('+password');
// if(!user){
// // 如果用户不存在,返回 设置状态码为422
// return res.status(422).send({
// message: '用户不存在'
// })
// }
//当user为空时,状态码是422,提示信息是 用户不存在
assert(user,422,'用户不存在') // 效果与上相同
// 2.校验密码
const isValid = require('bcryptjs').compareSync(password,user.password)
// if(!isValid){
// return res.status(422).send({
// message:'密码错误'
// })
// }
assert(isValid,422,'密码错误')
//3.返回token
//生成一个token 第2个参数是一个密钥,用来验证token是否篡改过
const token = jwt.sign({id:user._id},app.get('secret'))
// console.log(token)
res.send({token})
})
//错误处理
app.use(async (err,req,res,next)=>{
res.status(err.statusCode || 500).send({
message: err.message
})
})
前端封装axios的文件中处理响应
http.js
import axios from 'axios'
import Vue from 'vue'
import router from './router'
const http = axios.create({
baseURL: 'http://localhost:3000/admin/api'
})
// 全局处理
// https://www.npmjs.com/package/axios#interceptors 文档查看具体使用
// 处理响应
http.interceptors.response.use(res =>{
return res
},err =>{
if(err.response.data.message){
// 处理 错误
// vue组件弹出消息
// err.response.data.message 取出服务端返回的错误信息
Vue.prototype.$message({
type: 'error',
message: err.response.data.message
})
if (err.response.status === 401) {
router.push('/login')
}
}
return Promise.reject(err)
})
export default http
2.权限验证
当登录成功时,服务端返回了一个token给前端
所以在登录时获取到token后要把它放到请求头里,
http.js
import axios from 'axios'
import Vue from 'vue'
import router from './router'
const http = axios.create({
baseURL: 'http://localhost:3000/admin/api'
})
// 全局处理
// https://www.npmjs.com/package/axios#interceptors 文档查看具体使用
// 处理请求
http.interceptors.request.use(function (config) {
// Do something before request is sent
if (localStorage.token) {
config.headers.Authorization = 'Bearer ' + localStorage.token
}
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// 处理响应
http.interceptors.response.use(res =>{
return res
},err =>{
if(err.response.data.message){
// 处理 错误
// vue组件弹出消息
// err.response.data.message 取出服务端返回的错误信息
Vue.prototype.$message({
type: 'error',
message: err.response.data.message
})
if (err.response.status === 401) {
router.push('/login')
}
}
return Promise.reject(err)
})
export default http
后端接口中,当请求数据时,先判断请求头是否携带token并解析token是否正确,验证通过才返回数据,验证不通过则提示请先登录
使用中间件处理
封装middleware->auth.js
module.exports = options => {
const assert = require('http-assert')
const jwt = require('jsonwebtoken')
const AdminUser = require('../models/AdminUser')
return async (req, res, next) => {
// 获取并截取token
const token = String(req.headers.authorization || '').split(' ').pop()
assert(token, 401, '请先登录')
//根据规格验证获得id
const { id } = jwt.verify(token, req.app.get('secret'))
assert(id, 401, '请先登录')
//通过id去数据库验证是否正确
req.user = await AdminUser.findById(id)
assert(req.user, 401, '请先登录')
await next()
}
}
然后在需要使用拦截的地方引用就ok
const authMiddleware = require('../../middleware/auth')
app.use('/admin/api/rest/:resource', authMiddleware(), router)