最近闲来无事,便把早就想学的koa2+mongodb好好的学了一下,也算是为自己以后学习数据结构与算法搭了个可以随时练习的架子吧,代码大部分是直接从一位博友那里扒来的(文末会注明出入),不过拿过来用和改的时候遇到了一些小问题,好在百度上有各路大神指点,总算是一一克服了,在这里分享给大伙,如有问题,不妨评论区一起来探讨一二。
前言
目的
打造一个具备登录注册功能,且可以在登录以后,查看文章列表,以及对文章进行增、删、改、查等功能的js全栈项目
用到的前端技术
element-ui + vue-router + vuex
用到的后端技术
koa2 + mongodb + adminmongo
前端部分
这里我们先搭建好vue-cli脚手架并启动它,然后再install我们本项目需要用到的前端组件(vuex、vue-router、element-ui、axios)具体怎么安装启动我这里就不细说了。
接下来就是前端组件,主要有启动页(App.vue)、登录(Login.vue)、注册(Register.vue)、主页(Hone.vue)、关于(AboutView.vue)、列表页(List.vue)、新增/编辑页(Edit.vue);
前端部分所涉及的js文件有启动文件(main.js)、配置文件(vue.config.js)、请求拦截器以及API请求文件(api.js)、路由配置(router/index.js)、状态管理文件(store/index.js)。
目录结构
启动文件(main.js)
这个文件没什么好介绍的,主要是引入相关文件,直接贴代码
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.use(ElementUI)
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
启动页(App.vue)
也没啥好说的,直接以路由的方式简单讲首页和关于我们页列出来
<template>
<div id="app">
<nav>
<router-link to="/home">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
}
</style>
请求拦截器以及API请求文件(api.js)
这里边需要注意的是接口向后端的传参写法,比如get方式获取文章详情时需要向后端传id,可以用instance.get(‘/api/list/detail’, {params: {id: ‘asdfsdfsewfsd’})的方式传参,也可以用instance.get(‘/api/list/detail/’+id)的方式,需要注意的是,不同的前端传参方式分别对应着不同的后端接受参数的方式,具体请查看代码中注释部分代码。
import axios from 'axios'
import store from './store'
import router from './router'
// 全局设置
axios.defaults.timeout = 10000 // 时间超时设置10s
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'
// 创建一个axios的实列
const instance = axios.create()
instance.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'
axios.interceptors.request.use = instance.interceptors.request.use
// request拦截器,每次发送请求的时候拦截下来
instance.interceptors.request.use(
config => {
// 每次发送请求,检查 vuex 中是否有token,如果有放在headers中
if (store.state.token) {
config.headers.Authorization = store.state.token
}
return config
},
err => {
return Promise.reject(err)
}
)
// respone拦截器
instance.interceptors.response.use(
response => {
return response
},
// 除了200以外的请求到这里来,,这里的200不是我们设置的那个code200,,我这里是,没有登录才会不返回200
error => {
console.log(error)
let { response } = error
if (response != null) {
// 这里为什么处理401错误,详见,server/untils/token check_token这个函数
if (response.status === 401) {
let msg = response.data || '请重新登录!'
alert(msg)
store.commit('LOGOUT') // token过期,清除
router.replace({ // 跳转到登录页面
path: '/login',
// 添加一个重定向后缀,等登录以后再到这里来
query: { redirect: router.currentRoute.fullPath }
})
return Promise.reject(error.response)
}
} else {
console.log(error)
}
}
)
// 添加API请求
export default {
// 用户注册
userRegister (data) {
return instance.post('/api/user/register', data)
},
// 用户登录
userLogin (data) {
return instance.post('/api/user/login', data)
},
// 获取用户
getUser () {
return instance.get('/api/user')
},
// 删除用户
delUser (data) {
return instance.post('/api/user/delete', data)
},
// 文章列表
getLists (data) {
return instance.get('/api/list', {params: data})
},
// 文章详情
getDetail (data) {
return instance.get('/api/list/detail', {params: data})
// return instance.get('/api/list/detail/'+data.id) // 这是另一种写法
},
// 新增/编辑文章
editLists (data) {
return instance.post('/api/list/edit', data)
},
// 删除文章
delLists (data) {
return instance.post('/api/list/delete', data)
}
}
配置文件(vue.config.js)
这里的话主要需要关注的是跨域配置,因为后端接口用的不同的服务端口,所以必须要配置跨域才能正常访问
module.exports = {
devServer: {
open: false, //vue项目启动时自动打开浏览器
host: 'localhost', //本机
port: 8080, //端口号
https: false,
//以上的ip和端口是我们本机的;下面为需要跨域的
proxy: { //配置跨域
'/api': {
target: 'http://localhost:3000/api/', //这里是后台的地址
ws: true,
changOrigin: true, //允许跨域
pathRewrite: {
'^/api': '' //请求的时候使用这个api就可以
}
}
}
},
transpileDependencies: true,
lintOnSave: false
}
路由配置(router/index.js)
这里我对部分页面进行了登录校验,通过对requiresAuth是否为true来判断该页面是否需要登录后才能访问,新增编辑页面我用的同一个组件来实现,通过meta.title来控制页面显示不同的标题。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import store from '../store'
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'home',
component: Home,
// component: (resolve) => require([ '@/views/Home' ], resolve),
meta: {
requiresAuth: true // 需要登录才能访问的页面
}
},
{
path: '/login',
name: 'login',
component: (resolve) => require([ '@/views/Login' ], resolve),
meta: {
requiresAuth: false
}
},
{
path: '/register',
name: 'register',
component: (resolve) => require([ '@/views/Register' ], resolve),
meta: {
requiresAuth: false
}
},
{
path: '/about',
name: 'about',
component: (resolve) => require([ '@/views/AboutView' ], resolve),
meta: {
requiresAuth: false
}
},
{
path: '/list',
name: 'list',
component: (resolve) => require([ '@/views/List' ], resolve),
meta: {
requiresAuth: true
}
},
{
path: '/add',
name: 'add',
component: (resolve) => require([ '@/views/Edit' ], resolve),
meta: {
requiresAuth: true,
title: '新增'
}
},
{
path: '/edit',
name: 'edit',
component: (resolve) => require([ '@/views/Edit' ], resolve),
meta: {
requiresAuth: true,
title: '编辑'
}
}
]
const router = new VueRouter({
routes
})
// 注册全局钩子用来拦截导航
router.beforeEach((to, from, next) => {
let token = store.state.token
// console.log(to.name, to.meta, token)
if (to.meta.requiresAuth) {
if (token) {
next()
} else {
next({ name: 'login' })
}
} else {
console.log(to.name)
next()
}
})
export default router
状态管理文件(store/index.js)
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: window.sessionStorage.getItem('token') || '',
user: {
_id: window.sessionStorage.getItem('_id') || '',
user_name: window.sessionStorage.getItem('user_name') || ''
}
},
mutations: {
LOGIN: (state, data) => {
state.token = data.token
window.sessionStorage.setItem('token', data.token)
},
LOGOUT: (state) => {
state.token = null
window.sessionStorage.removeItem('token')
},
USERINFOR: (state, data) => {
state.user.user_name = data.user_name
state.user._id = data._id
window.sessionStorage.setItem('_id', data._id)
window.sessionStorage.setItem('user_name', data.user_name)
}
},
actions: {
UserLogin ({ commit }, data) {
commit('LOGIN', data)
},
UserLogout ({ commit }) {
commit('LOGOUT')
},
UserInfo ({ commit }, data) {
commit('USERINFOR', data)
}
}
})
注册组件(Register.vue)
注册成功以后,我们会通过更新用户的状态,直接跳转到主页(Home.vue)
<template>
<div class="login-ctn">
账号注册
<el-form
:model="ruleForm"
:rules="rules2"
status-icon
ref="ruleForm"
label-width="100px"
class="login-form"
>
<el-form-item label="用户名" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="pass">
<el-input type="password" v-model="ruleForm.pass" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="checkPass">
<el-input type="password" v-model="ruleForm.checkPass" auto-complete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button class="register-form-button" type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button class="register-form-button" @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
<div class="desc">
已有账号,
<span class="go-register" @click="goLogin">去登录</span>
</div>
</div>
</template>
<script>
import api from '../api' // permission control
export default {
name: 'Login',
data () {
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.ruleForm.pass) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
return {
ruleForm: {
pass: '',
checkPass: '',
name: ''
},
rules2: {
pass: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 3, max: 8, message: '长度在 3 到 8 个字符', trigger: 'blur' }
],
checkPass: [{ validator: validatePass, trigger: 'blur' }],
name: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}
}
},
methods: {
submitForm (formName) {
this.$refs[formName].validate(valid => {
if (valid) {
api.userRegister(this.ruleForm).then(res => {
if (res.data.status) {
this.$message({
message: '注册成功!',
type: 'success'
})
this.$store.dispatch('UserInfo', res.data.data)
this.$router.push('/home')
} else {
this.$message({
message: '注册失败',
type: 'error'
})
}
})
} else {
return false
}
})
},
resetForm (formName) {
this.$refs[formName].resetFields()
},
goLogin () {
this.$router.push('/login')
}
}
}
</script>
<style lang="less">
.login-ctn {
padding: 100px;
display: flex;
justify-content: center;
flex-direction: column;
}
.login-form,
.register-form {
width: 40%;
padding-top: 30px;
padding-right: 100px;
max-width: 400px;
margin: 20px auto;
}
.register-form-button {
width: 45%;
font-size: 16px;
text-decoration: none;
white-space: normal;
}
.desc {
font-size: 16px;
color: #666;
.go-register {
color: #985e6d;
cursor: pointer;
}
}
</style>
登录(Login.vue)
<template>
<div class="login-ctn">
用户登录
<el-form
:model="ruleForm2"
:rules="rules2"
status-icon
ref="ruleForm2"
label-width="100px"
class="login-form"
>
<el-form-item label="用户名" prop="name">
<el-input v-model="ruleForm2.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="pass">
<el-input type="password" v-model="ruleForm2.pass" auto-complete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button class="login-form-button" type="primary" @click="submitForm('ruleForm2')">登录</el-button>
</el-form-item>
</el-form>
<div class='desc'>还没有账号?<span class='go-register' @click="goRegister">立即注册</span></div>
</div>
</template>
<script>
import api from '../api' // permission control
export default {
name: 'Login',
data () {
return {
ruleForm2: {
pass: '',
name: ''
},
rules2: {
pass: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 3, max: 8, message: '长度在 3 到 8 个字符', trigger: 'blur' }
],
name: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}
}
},
methods: {
submitForm (formName) {
this.$refs[formName].validate(valid => {
if (valid) {
api.userLogin(this.ruleForm2).then(res => {
if (res.data.status) {
this.$message({
message: '登录成功!',
type: 'success'
})
this.$store.dispatch('UserLogin', res.data.data)
this.$router.push('/home')
} else {
this.$message({
message: '登录失败',
type: 'error'
})
}
})
} else {
console.log('error submit!!')
return false
}
})
},
resetForm (formName) {
this.$refs[formName].resetFields()
},
goRegister () {
this.$router.push('/register')
}
}
}
</script>
<style lang="less">
.login-ctn {
padding: 100px;
display: flex;
justify-content: center;
flex-direction: column;
}
.login-form,
.register-form {
width: 40%;
padding-top: 30px;
padding-right: 100px;
max-width: 400px;
margin: 20px auto;
}
.login-form-button {
width: 100%;
font-size: 16px;
text-decoration: none;
white-space: normal;
}
.desc {
font-size: 16px;
color: #666;
.go-register {
color: #985e6d;
cursor: pointer;
}
}
</style>
主页(Hone.vue)
<template>
<div class="home">
<p>你好啊,现在是已经登录的状态</p>
<p><router-link to="/list">文章列表</router-link></p>
<el-button @click="logout">退出登录</el-button>
</div>
</template>
<script>
export default {
name: 'home',
methods: {
logout () {
this.$store.dispatch('UserLogout')
this.$router.push('/login')
}
}
}
</script>
关于(AboutView.vue)
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
登录注册功能到这里就算是做完了,接下来就是文章的增删改查部分。
列表页(List.vue)
<template>
<div class="about">
<h1>This is an list</h1>
<el-form
:model="formData"
status-icon
ref="ruleForm"
label-width="100px"
class="login-form"
>
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title"></el-input>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" auto-complete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button class="register-form-button" type="primary" @click="submitForm('ruleForm')">搜索</el-button>
<el-button class="register-form-button" @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
<p><router-link to="/add">新增</router-link></p>
<ul class="list">
<li v-for="item in dataList" :key="item.id">{{ item.index }}、{{ item.title }}<a href="javascript:void(0)" @click="del(item.id)">删除</a> <router-link :to="'/edit?id='+item.id">编辑</router-link></li>
</ul>
</div>
</template>
<script>
import api from '../api'
export default{
name: 'List',
data() {
return {
formData: {},
dataList: [],
}
},
created() {
this.getLists()
},
methods: {
getLists() {
api.getLists(this.formData).then((res, err) => {
if (res.data.success) {
this.dataList = res.data.data.map((item, index) => {
item.index = index + 1
return item
})
} else {
this.$message({
message: res.msg,
type: 'error'
})
}
}).catch(err => {
this.$message({
message: res.msg,
type: 'error'
})
})
},
del(id) {
api.delLists({id}).then(res => {
if (res.data.success) {
this.$message({
message: res.data.msg,
type: 'success'
})
this.getLists()
} else {
this.$message({
message: res.data.msg,
type: 'error'
})
}
})
},
submitForm() {
this.getLists()
},
resetForm (formName) {
this.$refs[formName].resetFields()
}
}
}
</script>
<style lang="less">
.list {
width: 50%;
margin: 0 auto;
padding: 10px;
border: 1px solid #ccc;
li {
list-style: none;
width: 100%;
text-align: left;
line-height: 28px;
}
a {
float: right;
margin-left: 10px;
color: rgb(8, 167, 246);
text-decoration: underline;
cursor: pointer;
font-size: 14px;
}
}
.login-ctn {
padding: 100px;
display: flex;
justify-content: center;
flex-direction: column;
}
.login-form,
.register-form {
width: 40%;
padding-top: 30px;
padding-right: 100px;
max-width: 400px;
margin: 20px auto;
}
.register-form-button {
width: 25%;
font-size: 16px;
text-decoration: none;
white-space: normal;
}
.desc {
font-size: 16px;
color: #666;
.go-register {
color: #985e6d;
cursor: pointer;
}
}
</style>
新增/编辑页(Edit.vue)
<template>
<div class="about">
<h3>{{$route.meta.title}}</h3>
<el-form
:model="ruleForm"
:rules="rules2"
status-icon
ref="ruleForm"
label-width="100px"
class="login-form"
>
<el-form-item label="标题" prop="title">
<el-input v-model="ruleForm.title"></el-input>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="ruleForm.description" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input type="textarea" :rows="3" v-model="ruleForm.content" auto-complete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button class="register-form-button" type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button class="register-form-button" @click="resetForm('ruleForm')">重置</el-button>
<el-button class="register-form-button" @click="back">返回</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import api from '../api'
export default{
name: 'Edit',
data() {
return {
ruleForm: {
title: '',
description: '',
content: ''
},
rules2: {
title: [
{ required: true, message: '请输入标题', trigger: 'blur' },
{ min: 3, max: 50, message: '长度在 3 到 50 个字符', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入描述', trigger: 'blur' },
{ min: 3, max: 80, message: '长度在 3 到 80 个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入内容', trigger: 'blur' },
{ min: 3, message: '长度不得少于3个字符', trigger: 'blur' }
],
}
}
},
created() {
if (this.$route.query.id) {
this.getDetail(this.$route.query.id)
}
},
methods: {
getDetail(id) {
api.getDetail({id}).then(res => {
if (res.data.success) {
this.ruleForm = res.data.data
} else {
this.$message({
message: res.data.msg,
type: 'error'
})
}
})
},
submitForm (formName) {
this.$refs[formName].validate(valid => {
if (valid) {
api.editLists(this.ruleForm).then(res => {
if (res.data.success) {
this.$message({
message: res.data.msg,
type: 'success'
})
this.$router.push('/list')
} else {
this.$message({
message: res.data.msg,
type: 'error'
})
}
})
} else {
return false
}
})
},
resetForm (formName) {
this.$refs[formName].resetFields()
},
back() {
this.$router.push('/list')
}
}
}
</script>
<style lang="less">
.login-ctn {
padding: 100px;
display: flex;
justify-content: center;
flex-direction: column;
}
.login-form,
.register-form {
width: 40%;
padding-top: 30px;
padding-right: 100px;
max-width: 400px;
margin: 20px auto;
}
.register-form-button {
width: 25%;
font-size: 16px;
text-decoration: none;
white-space: normal;
}
.desc {
font-size: 16px;
color: #666;
.go-register {
color: #985e6d;
cursor: pointer;
}
}
</style>
前端部分到这里就结束了,因为对于本人以及大多数前端开发人员来说,前端部分都是我们吃饭的本钱,当然再熟悉不过了,我用的写法也是非常简单基础的,所以前端部分我就直接贴代码了,接下来对于本项目的重点后端部分,我会尽量解说的更详细一些。
后端部分
首先我们先要安装相关的组件包
npm i koa
npm i koa-router -S
npm i koa-bodyparser -S
// koa-bodyparser: 支持x-www-form-urlencoded, application/json等格式的请求体,但不支持form-data的请求体, 用来解析body,比如通过post来传递表单、json或上传文件,数据不容易获取,通过koa-bodyparser解析之后,在koa中this.body就能获取到数据
接下来创建和修改相关文文件,主要涉及以下几个:主入口文件(index.js)、token验证文件(utils/token.js)、配置文件(utils/config.js)、路由配置文件(routes/index.js)、数据库连接文件(db/index.js)以及最主要的接口定义文件(controller/index.js)。
目录结构
主入口文件(index.js)
这个文件主要用于引入相关的依赖包和路由等文件,以及定义服务器的端口为3000
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
// koa-bodyparser: 支持x-www-form-urlencoded, application/json等格式的请求体,但不支持form-data的请求体
// 用来解析body,比如通过post来传递表单、json或上传文件,数据不容易获取,通过koa-bodyparser解析之后,在koa中this.body就能获取到数据
const json = require('koa-json') // 告诉客户端『返回的是 JSON 数据』
const logger = require('koa-logger') // 提供了输出请求日志的功能,包括请求的url、状态码、响应时间、响应体大小等信息
const onerror = require('koa-onerror') // koa有error事件,当发生错误,可以通过error事件,对错误统一处理
const staticServe = require('koa-static') // 用于koa的静态文件指定映射路径
const {check_token} = require('./utils/token')
const app = new Koa();
onerror(app)
app.use(bodyParser());
app.use(json())
app.use(logger())
app.use(staticServe(__dirname + '/public'))
// 添加token 验证中间件
app.use(check_token);
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// routes
const index = require('./routes/index')
app.use(index.routes(), index.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
app.listen(3000, () => {
console.log('The server is running at http://localhost:' + 3000);
});
token验证文件(utils/token.js)
这个文件主要起到一个中间件的作用,用于你在访问需要校验登录的页面前,先对其进行token的校验,当然咯,它里边只对post请求的接口进行校验,对get请求的接口设置了不拦截的处理。
const jwt = require('jsonwebtoken') // 用于签发、解析 token
const { TOKEN_ENCODE_STR, URL_YES_PASS } = require('./config')
const User = require('../db').User
module.exports = {
// 生成登录 token
create_token (str) {
const token = jwt.sign({ str }, TOKEN_ENCODE_STR, {
expiresIn: '1h'
})
return token
},
/*
验证登录 token 是否正确 => 写成中间件
get 请求与设置的请求不拦截验证,其余均需登录
*/
async check_token (ctx, next) {
let url = ctx.url
// console.log('ctx.url:::', ctx.url);
if (ctx.method != 'GET' && !URL_YES_PASS.includes(url)) {
let token = ctx.get('Authorization')
if (token === '') {
ctx.response.status = 401
ctx.response.body = '你还没有登录,快去登录吧!'
return
}
try {
// 验证token是否过期
let { str = '' } = await jwt.verify(token, TOKEN_ENCODE_STR)
// 验证token与账号是否匹配
let res = await User.find({ user_name: str, token })
if (res.length === 0) {
ctx.response.status = 401
ctx.response.body = '登录过期,请重新登录!'
return
}
// 保存用户的_id
ctx._id = res[0]._id
} catch (e) {
ctx.response.status = 401
ctx.response.body = '登录已过期请重新登录!'
return
}
}
await next()
}
}
配置文件(utils/config.js)
这个文件别看代码就这么几行,却非常的重要,除了配置加密密钥外,对于不需要登录的请求都要写入URL_YES_PASS,否则你在请求时会提示你请先登录。
module.exports = {
// 用户密码加密字符串
PWD_ENCODE_STR: "pawn_user_encode_str",
// token 加密字符串,
TOKEN_ENCODE_STR: "pawn_token_encode_str",
// 添加非get请求通过的连接
URL_YES_PASS: ['/api/user/login', '/api/user/register']
}
路由配置文件(routes/index.js)
这里主要用于定义前端向后端发起请求的接口名称以及传参,注意,这里定义的接口名称需要跟前端保持一致,否则将无法成功联调。
const router = require('koa-router')()
const controller = require('../controller')
router.get('/', async (ctx, next) => {
ctx.body = "hello,armor"
})
.post("/api/user/register", controller.user.register) // 用户注册
.post("/api/user/login", controller.user.login) // 用户登录
// .get('/api/user',controller.allUser) // 获取所有用户
.post('/api/user/delete',controller.user.deleteUser) // 获取所有用户
.get('/api/list',controller.list.findList) // 获取文章列表
.get('/api/list/detail',controller.list.getDetail) // 获取文章详情
// .get('/api/list/detail/:id',controller.list.getDetail) // 获取文章详情,此写法对应前端instance.get('/api/list/detail/'+data.id)的写法
.post('/api/list/edit',controller.list.edit) // 新增、编辑文章
.post('/api/list/delete',controller.list.deleteList) // 删除文章
module.exports = router
接口定义文件(controller/index.js)
作为前端开发人员,以前我一直很好奇后端返回给我的那些接口和数据到底是从哪里来的,这里就是我要的答案了,定义接口,选择我想要给前端的数据,设置好返回的结构,然后抛出它们,这就是一个接口了,当然咯,里边有很多需要注意的细节,以及多种不同的写法,我通过注释的方式列出了一两种,注释部分代码就是我的尝试,大家也可以切换着试试。
const User = require('../db').User
const List = require('../db').List
const sha1 = require('sha1') // 用于密码加密
const { PWD_ENCODE_STR } = require('../utils/config')
const { create_token } = require('../utils/token')
const xss = require('xss')
//下面这两个包用来生成时间
const moment = require('moment')
const objectIdToTimestamp = require('objectid-to-timestamp')
//根据用户名查找用户
const findUser = (name) => {
return new Promise((resolve, reject) => {
User.findOne({ user_name: name }, (err, doc) => {
if (err) {
reject(err)
}
resolve(doc)
})
})
}
//删除某个用户
const delUser = function (id) {
return new Promise((resolve, reject) => {
User.findOneAndRemove({ _id: id }, (err) => {
if (err) {
reject(err)
}
console.log('删除用户成功')
resolve()
})
})
}
// 用户注册
const register = async (ctx) => {
console.log('注册:::,', ctx.request.body);
let { name = '', pass = '' } = ctx.request.body
try {
if (name === '' || pass === '') {
ctx.body = {
code: 401,
msg: '注册失败,请填写完整表单!'
}
return
}
if (pass.length < 3) {
ctx.body = {
code: 401,
msg: '注册失败,密码最少为3位!'
}
return
}
let doc = await findUser(name)
console.log('doc::', doc);
if (doc) {
ctx.body = {
code: 409,
msg: '注册失败,该用户名已经存在!'
}
return
} else {
pass = sha1(sha1(pass + PWD_ENCODE_STR))
// 防止xss攻击, 转义
name = xss(name)
console.log('pass;::', pass);
console.log('name;::', name);
let token = create_token(name)
let user = new User({
user_name: name,
password: pass,
token //创建token并存入数据库
})
user.create_time = moment(objectIdToTimestamp(user._id)).format('YYYY-MM-DD HH:mm:ss') // 将objectid转换为用户创建时间
console.log('user;::', user);
await new Promise((resolve, reject) => {
user.save((err) => {
if (err) {
reject(err)
}
resolve()
})
})
console.log('注册成功')
ctx.status = 200
ctx.body = {
status: true,
data: {
id: user._id,
token,
user_name: name
}
}
}
} catch (e) {
console.log(e,111)
ctx.body = {
code: 500,
msg: '注册失败,服务器异常!'
}
}
}
// 用户登录
const login = async (ctx) => {
let { name = '', pass = '' } = ctx.request.body
try {
if (name === '' || pass === '') {
ctx.body = {
code: 401,
msg: '登录失败,请输入登录账号或密码!'
}
return
}
// 解密
pass = sha1(sha1(pass + PWD_ENCODE_STR))
console.log('pass1::', pass);
let res = await findUser(name)
console.log('pass2::', res);
if (!res) {
ctx.body = {
code: 401,
msg: '登录失败,用户名或者密码错误!'
}
return
} else if (pass === res.password) {
let token = create_token(name)
res.token = token
await new Promise((resolve, reject) => {
res.save((err) => {
if (err) {
reject(err)
}
resolve()
})
})
ctx.status = 200
ctx.body = {
status: true,
data: {
id: res._id,
token,
user_name: name
}
}
} else {
ctx.status = 200
ctx.body = {
success: false
}
}
} catch (e) {
console.log(e)
ctx.body = {
code: 500,
msg: '登录失败,服务器异常!'
}
}
}
const deleteUser = async (ctx) => {
//拿到要删除的用户id
let id = ctx.request.body.id
await delUser(id)
ctx.status = 200
ctx.body = {
success: '删除成功'
}
}
//根据条件查找文章列表
const findList = async (ctx, next) => {
const query = ctx.query
query.title = new RegExp(query.title)
return new Promise((resolve, reject) => {
List.find(query).then(res => {
// console.log(res)
const data = res.map(item => {
const obj = {
id: item._id,
title: item.title,
description: item.description,
content: item.content,
createTime: item.create_time
}
return obj
})
ctx.body = {
success: true,
msg: 'success',
data: data
}
resolve(ctx.body)
}).catch(err => {
reject(err)
})
// List.find(query, (err, doc) => {
// if (err) {
// reject(err)
// }
// console.log(doc)
// const data = doc.map(item => {
// const obj = {
// id: item._id,
// title: item.title,
// description: item.description,
// content: item.content,
// createTime: item.create_time
// }
// return obj
// })
// ctx.body = {
// success: true,
// msg: 'success',
// data
// }
// resolve(ctx.body)
// })
})
}
//根据id查找文章详情
const getDetail = async (ctx, next) => {
return new Promise((resolve, reject) => {
// List.findOne({_id: ctx.query.id}, (err, doc) => {
List.findById(ctx.query.id, (err, doc) => {
if (err) {
reject(err)
}
const data = {
id: doc._id,
title: doc.title,
description: doc.description,
content: doc.content,
createTime: doc.create_time
}
ctx.body = {
success: true,
msg: 'success',
data
}
resolve(ctx.body)
})
})
}
//删除某篇文章
const delList = function (id) {
return new Promise((resolve, reject) => {
List.deleteOne({ _id: id }, (err) => {
if (err) {
reject(err)
}
console.log('删除文章成功')
resolve()
})
})
}
const deleteList = async (ctx) => {
//拿到要删除的文章id
let id = ctx.request.body.id
await delList(id)
ctx.status = 200
ctx.body = {
success: true,
msg: '删除成功'
}
}
// 用户编辑
const edit = async (ctx) => {
let { title = '', description = '', content = '', id = '' } = ctx.request.body
try {
if (title === '' || description === '' || content === '') {
ctx.body = {
code: 401,
msg: '编辑失败,请填写完整表单!'
}
return
}
if (title.length < 3 || title.length > 80) {
ctx.body = {
code: 401,
msg: '编辑失败,标题最少为3位字符,最大为80个字符!'
}
return
}
if (description.length < 3 || description.length > 80) {
ctx.body = {
code: 401,
msg: '编辑失败,描述最少为3位字符,最大为80个字符!'
}
return
}
if (content.length < 3) {
ctx.body = {
code: 401,
msg: '编辑失败,内容最少为3位字符!'
}
return
}
// 防止xss攻击, 转义
title = xss(title)
// let list = new List({
// title,
// description,
// content
// })
const updateObj = {
title,
description,
content
}
if (id) {
await List.updateOne({_id: id}, updateObj).then(res => {
ctx.body = {
success: true,
msg: '编辑成功'
}
}).catch(err => {
console.log(err)
ctx.body = {
success: false,
msg: '编辑出错'
}
})
} else {
//创建token并存入数据库
const token = create_token(title)
const create_time = moment().format('YYYY-MM-DD HH:mm:ss') // 将objectid转换为用户创建时间
const addObj = {
title,
description,
content,
token,
create_time
}
await List.create(addObj).then(res => {
ctx.body = {
success: true,
msg: '新增成功'
}
}).catch(err => {
console.log(err)
ctx.body = {
success: false,
msg: '新增出错'
}
})
// list.token = token
// list.create_time = moment(objectIdToTimestamp(list._id)).format('YYYY-MM-DD HH:mm:ss') // 将objectid转换为用户创建时间
// await new Promise((resolve, reject) => {
// list.save((err) => {
// if (err) {
// reject(err)
// }
// resolve()
// })
// })
}
} catch (e) {
ctx.body = {
code: 500,
success: false,
msg: `${id ? '编辑失败' : '新增失败'},服务器异常!`
}
}
}
//根据id查找文章是否存在
const findDetail = async (id) => {
return new Promise((resolve, reject) => {
List.findOne({_id: id}, (err, doc) => {
if (err) {
reject(err)
}
resolve(true)
})
})
}
module.exports = {
user: {
register,
login,
deleteUser
},
list: {
findList,
deleteList,
edit,
getDetail
}
}
数据库连接文件(db/index.js)
显然,这个文件就是用来连接数据库以及设置表结构的,在这之前,我们需要先安装数据库,作为初学者,选择一个可视化的数据库管理工具显然是有必要的,这里我选择了adminMongo,直接拉取GitHub上的代码就可以了。
1、git clone https://github.com/mrvautin/adminMongo.git && cd adminMongo
2、npm install
3、npm run serve or node app
4、直接访问http://127.0.0.1:1234 , 至此,adminmongo 安装完成。
const mongoose = require('mongoose');
const db = mongoose.connect("mongodb://localhost:27017/kao-demo", {useNewUrlParser:true}, function(err){
if(err){
console.log(err)
}else{
console.log("Connection success!")
}
})
const Schema = mongoose.Schema;
// 用户
let userSchema = new Schema({
user_name: String,
user_id: String,
password: String,
create_time: Date,
token: {
type: String,
default: ""
}
})
// 列表
let listSchema = new Schema({
title: String,
description: String,
content: String,
create_time: Date,
token: {
type: String,
default: ""
}
})
exports.User = mongoose.model('User', userSchema);
exports.List = mongoose.model('Lists', listSchema);
致此,该项目就算是完成了,技术非常粗浅,只适合初学者做练习只用,如果觉得对你有帮助,不妨给我点个赞,给我些继续分享下去的动力。
参考链接:https://juejin.cn/post/6844904053529378823#comment