简易网站后台的搭建 — 分类功能的实现
本文章是学习B站全栈之巅系列课程的学习笔记,利用笔记来吸收升华学到的知识。
学习感受是,如果有一定的
nodejs+mongoDB+vue
的基础,加做过一两个相关的练手项目,再学习这个课程,你会发现原来搭建一个完整有基础功能的网站,是如此的简单,会有很大的收获。
参考源码:https://github.com/morningClock/herohoner
视频学习地址:点击这里
1.项目目录
完整的网站项目中,一般会拥有前台网站,后台管理网站,以及服务器端处理程序。
前台网站:提供给用户访问的网站。
后台管理网站:对网站数据进行可视化管理的网站。
服务端处理程序:处理网站请求。
所以我们在项目下,建立三个子目录为
-
/web
使用
vue
搭建的前台网站。 -
/admin
使用
vue
搭建的后台网站 -
/server
提供接口处理。
根据开发的简易程度,进行开发顺序的确认。首先网站的前台项目是项目中最复杂的部分,因为一开始没有数据难以呈现效果,而且涉及到大量细节问题,所以要到最后开发。
我们在开发网站时需要拥有数据,那么我们就需要从后台管理系统,以及后端接口处理,进行着手开发。
2.快速搭建后台基础界面
项目中,使用了Element
的UI库,没接触过UI库,其实也不影响使用,因为UI库是为了简化开发的,所以基本上可以拿来就用。而且后端是面向管理员的,所以对界面的细节要求不高。
Element安装有两种方法:
-
使用
vue add element
-
使用
npm install element-ui
需要手动在
main.js
中引入Element。// 引入element-ui import ElementUI from 'element-ui' // 引入样式库 import 'element-ui/lib/theme-chalk/index.css' // 挂载Element Vue.use(ElementUI)
当然,
ElementUI
还提供了局部组件引用的功能,详细见Element 按需引入。
我们还需要用到vue-router
,进行组件路由。
我们直接使用vue add router
,进行默认的配置。项目中会生成views目录,并且拥有三个组件,此时,我们只需要简单的配置。
-
配置
router.js
import Vue from 'vue' import Router from 'vue-router' import Main from './views/Main.vue' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'main', component: Main, } ] })
-
配置
App.vue
<template> <div id="app"> <!-- 应用路由 --> <router-view/> </div> </template> <style> body{ margin: 0; padding: 0; } </style>
-
主页面
Main.vue
简单的配置并应用Element的布局容器章节中的示例:实例
其中需要修改的是,将组件的高度,设置为100vh,撑开容器的高度。
此时,就已经完成了最简单的后台界面了。接下来就是按照需求,进行对应的修改。
3.分类功能的实现
项目网站中,分类是最基础的功能,比如网站的新闻,活动。这些都是分类。当然分类中,还会有许多子分类。我们要实现一个分类的管理,那么我们要对分类进行增删改查操作,我们先从新增开始。
新增分类
现在我们的后台网站,只有一个主页面,并且显示的内容还是模板中的内容。我们先初始化后台的基础页面,保留一些我们需要展示的内容。
-
后台管理页面的前端修改
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvxm29HX-1573006263218)(F:\Github\myrepositories\learning-notes\全栈之巅学习笔记\lesson1\assets\image-20191104143432627.png)]
我们需要这样的列表,那么我们把其他清空了,只保留一项菜单,新增选项
分类列表
与新建分类
。-
由于我们需要点击选项时,进行路由。使用
element
提供的router
选项,就可以点击跳转到index
属性所指向的组件了,可配置选项点击这里查看。 -
<el-main>
中,现在是写死的表格,我们需要点击选项切换,所以我们要抽离成两个组件(CategoryList.vue 与 CategoryEdit.vue
),并配置子路由。CategoryList
展示分类列表、CategoryEdit
用于分类的新建与编辑。我们预设路由是
/categories/list
与/categories/create
。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Wb3jzmv-1573006263220)(F:\Github\myrepositories\learning-notes\全栈之巅学习笔记\lesson1\assets\image-20191104145518009.png)]
Main.vue
的tempalte
部分<template> <div id="main"> <el-container style="height: 100vh; border: 1px solid #eee;"> <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/list">分类列表</el-menu-item> <el-menu-item index="/categories/create">新建分类</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></router-view> </el-main> </el-container> </el-container> </div> </template>
CategoryList.vue
新建分类页面<template> <div class="about"> <h1>新建分类</h1> <el-form label-width="120px" @submit.native.parent="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> export default { data () { return { model: {} } }, methods: { save () { // 保存新建的分类 } } }
router.js
路由配置import Vue from 'vue' import Router from 'vue-router' import Main from './views/Main.vue' import CategoryEdit from './views/CategoryEdit.vue' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'main', component: Main, children: [ { path: '/categories/create', component: CategoryEdit }, ] } ] })
-
-
新建分类后端接口的实现
后端接口的实现其实非常简单,此时我们进入server目录,进行后端接口的实现。
此项目中我们使用了
express、mongoose
以及跨域处理插件cors
。npm install express mongoose cors
开启监听端口服务
index.js
。const express = require('express') const app = express() // 解决跨域问题 app.use(require('cors')()) app.listen(3000, () => { console.log('http://localhost:3000') })
此时我们就可以run起来,看见提示语句。
为了让层次更分明,我们需要将接口写入
admin/api
,方便管理。const express = require('express') const app = express() app.use(require('cors')()) app.use(express.json()) // 引入admin接口,传入express对象 require('./routes/admin')(app) app.listen(3000, () => { console.log('http://localhost:3000') })
然后在
admin/api
新建一个index.js`的接口处理文件,管理admin中相关的接口。module.exports = app => { const express = require('express') const router = express.Router() // 接口 router.post('/categories', async(req, res) => { res.send(‘請求成功’) }) // 输出router对象,挂载在全局express上 app.use('/admin/api', router) }
此时我们可以配合前端界面,进行接口的测试。
前端安装
axios
插件,并单独新建一个http.js
进行请求方法的管理。import axios from 'axios' const http = axios.create({ baseURL: 'http://localhost:3000/admin/api' }) export default http
main.js
中引用该配置文件,并挂载到Vue
对象上// 引入axios import http from './http' Vue.prototype.$http = http
修改
CategoryEdit.vue
中的save方法,发起请求,测试是否成功async save () { // 保存新建的分类 res = await this.$http.post('categories', this.model) // 输出`请求成功` console.log(res.data) }
此时我们的接口已经正常运作,我们需要配置我们的数据库,用作数据的存储。
当然数据库,我们也是希望用单独的文件来管理,防止index.js中内容过多,不容易后期的管理。
新建
plugins
文件夹,并创建db.js
module.exports = app => { // 连接mongoDB const mongoose = require('mongoose') // herohoner,可以自定义 mongoose.connect('mongodb://127.0.0.1:27017/herohoner', { useNewUrlParser: true }) }
然后再
index.js
入口文件中,引用配置// 引入db require('./plugins/db')(app)
接入数据库的操作就完成,接下来就是将数据写入数据库中,
mongoDB
中,我们要写入数据,必需要对数据进行规范,写Schema约束,然后使用Schema创建model模型,最后再将数据写入model模型中。每一个表都有自己的Schema,所以我们可以单独管理各个Schema创建的model模型。
新建文件夹
models
用于存放不同的模型,在下面创建我们要使用的model管理文件Category.js
。const mongoose = require('mongoose') // 创建约束 const schema = new mongoose.Schema({ name: { type: String } }) // 输出具备约束的model模型 module.exports = mongoose.model('Category', schema)
万事俱备,只欠东风,我们的数据就可以写入数据库了。回到
/routes/admin/api/index.js
中写入接口
// 引入model模型 const Category = require('../../models/Category') /** * 新建分类 */ router.post('/categories', async(req, res) => { // 创建该模型,并填入post中传递过来的数据。 const model = await Category.create(req.body) // 将创建后的模型数据返回到前端 res.send(model) })
前端接受到后端的响应,进行对应的处理
async save () { let res if (this.id) { // 编辑 res = await this.$http.put(`categories/${this.id}`, this.model) } else { // 新建 res = await this.$http.post('categories', this.model) } this.$router.push('/categories/list') this.$message({ type: 'success', message: '保存成功' }) },
查询分类列表
不要害怕,完成新增分类完成后,基本上已经完成了大部分的后端接口功能了,接下来操作都几乎差不多。
CategoryList.vue
我们照样从官网,搬一些基础的模板过来,查询时,我们只需要在页面初始化阶段,发送一个GET请求,请求查询接口就可以完成查询了。
<template>
<div>
<h1>分类列表</h1>
<el-table :data="items">
<el-table-column prop="_id" lable="ID" width="220"></el-table-column>
<el-table-column prop="name" lable="分类名称"></el-table-column>
<el-table-column
fixed="right"
label="操作"
width="100">
<template slot-scope="scope">
<el-button type="text" size="small">编辑</el-button>
<el-button type="text" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
data () {
return {
items: []
}
},
methods: {
async fetch() {
// 请求查询接口
const res = await this.$http.get('categories')
this.items = res.data
}
},
created () {
this.fetch()
}
}
</script>
后端接口的修改非常简单,只需要在admin/api/index.js
中新增接口处理
/**
* 获取分类列表
*/
router.get('/categories', async(req, res) => {
// 这里限制了10条,后期可以使用limit做分页处理
const items = await Category.find().limit(10)
res.send(items)
})
查询也完成了,是不是十分简单,只需要熟悉一下mongoose
中的数据操作方法就可以了。
编辑分类
编辑分类也完全是一样的套路,修改前端,请求接口,新增接口处理,编辑数据库中对应的数据。
当然这次不一样的是,我们需要知道我们在编辑哪个数据,这时候我们要使用到唯一标识_id
。
在前端请求编辑接口时,在url
上添加查询出来的_id
就可以了。非常简单。
其中scope.row
代表了表格中,当前行的数据。
<el-button
type="text"
size="small"
@click="$router.push(`/categories/edit/${scope.row._id}`)">
编辑
</el-button>
当然编辑页我们也需要,但是编辑页和新建页可以复用,所以我们可以修改成如下
<template>
<div class="about">
<h1>{{id? '编辑': '新建'}}分类</h1>
<el-form label-width="120px" @submit.native.parent="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>
<script>
export default {
props: {
id: {}
},
data () {
return {
model: {}
}
},
methods: {
async save () {
let res
if (this.id) {
// 编辑
res = await this.$http.put(`categories/${this.id}`, this.model)
} else {
// 新建
res = await this.$http.post('categories', this.model)
}
this.$router.push('/categories/list')
this.$message({
type: 'success',
message: '保存成功'
})
},
async fetch () {
// 获取详情
const res = await this.$http.get(`categories/${this.id}`)
this.model = res.data
}
},
created () {
// 当编辑时拥有id才会触发fetch初始化model数据。
this.id && this.fetch()
}
}
</script>
还需要新增一个路由。
router.js
import Vue from 'vue'
import Router from 'vue-router'
import Main from './views/Main.vue'
import CategoryEdit from './views/CategoryEdit.vue'
import CategoryList from './views/CategoryList.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'main',
component: Main,
children: [
{ path: '/categories/create', component: CategoryEdit },
// 技巧:注入url参数进入该组件,
// 组件使用路由参数需要使用this.$router.params可实现同样效果
{ path: '/categories/edit/:id', component: CategoryEdit, props: true },
{ path: '/categories/list', component: CategoryList }
]
}
]
})
前端大功告成,到后端新增一个获取详情接口以及更新分类的接口就完事了。
/**
* 编辑分类
*/
router.put('/categories/:id', async(req, res) => {
const model = await Category.findByIdAndUpdate(req.params.id, req.body)
res.send(model)
})
/**
* 获取分类详情
*/
router.get('/categories/:id', async(req, res) => {
const model = await Category.findById(req.params.id)
res.send(model)
})
删除分类
同样的套路,不过我们要保证体验,需要提供一个提示确认窗口
categoryList.vue
中添加
<el-button type="text" size="small" @click="remove(scope.row)">删除</el-button>
async remove (row) {
this.$confirm(`是否确定删除分类${row.name}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await this.$http.delete(`categories/${row._id}`)
this.fetch();
this.$message({
type: 'success',
message: '删除成功!'
});
})
}
后端接口新增删除接口
/**
* 删除分类
*/
router.delete('/categories/:id', async(req, res) => {
await Category.findByIdAndDelete(req.params.id, req.body)
res.send({
success: true
})
})
增加上级分类
分类通常会有一定的关联,所以我们增加在每一个分类中添加上级分类的字段,通过该字段可以查询到上级分类的相关数据。
前端List页和Edit页面增加上级分类的信息。
CategoryList.vue
在table中新增一列,展示获取到分类的parent选项对应的name名称。
<el-table-column prop="parent.name" label="上级分类"></el-table-column>
CategoryEdit.vue
增加一个下拉选项框,选择已有的分类作为父级分类。
<el-form-item label="上级分类">
// 默认选择已有的parent
<el-select v-model="model.parent" clearable placeholder="上级分类">
// 分类列表
<el-option
v-for="item in parents"
:key="item._id"
:label="item.name"
:value="item._id"
></el-option>
</el-select>
</el-form-item>
我们还需要获取到所有列表的信息。
async fetchParents () {
const res = await this.$http.get(`categories`)
this.parents = res.data
}
这样前端部分就完成了。
后端相对简单,只需要在模型中增加一个字段,并用moongoose.SchemaTypes.Object
,并用ref属性关联模型。然后在查询时,使用populate('字段名称'
)方法查询出关联模型的数据,就可以了。
Category.js
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
name: { type: String },
// type为mongoose的唯一id标识mongoose.SchemaTypes.ObjectId
// ref为需要关联的model
parent: { type: mongoose.SchemaTypes.ObjectId, ref: 'Category'}
})
module.exports = mongoose.model('Category', schema)
index.js
/**
* 获取分类列表
*/
router.get('/categories', async(req, res) => {
// populate关联字段
// populate指定的字段会继续找到指定的字段它的model对象
const items = await Category.find().populate('parent').limit(10)
res.send(items)
})
修复侧边栏状态不同步bug
眼尖的你,可能已经发现了,此时,新建后,跳转回列表页,发现侧边栏的高亮,竟然还是新建列表。一阵苦恼怎么做。查看一下ElementUI
的文档最下面选项配置,或者百度一下,都可以轻松解决这个问题。
解决方法:使用default-active
,每次更新组件页面时,动态改变其值。说白了,就是让它值跟随你的url
的$router.path
来变化。
<el-menu router :default-openeds="['1']" :default-active="$route.path">
<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/list">分类列表</el-menu-item>
<el-menu-item index="/categories/create">新建分类</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
到此你就完成了一个拥有简单分类功能的后台了,接下来我们把接口做成通用的增删改查接口。