作为一个前端,工作之中总离不开前后端的数据联调,奈何前端的数据一直只知道怎么用,却不知道是怎么出来的,正好借着放假的期间,琢磨琢磨后端的一些逻辑。先从最最基本的注册登录来看一个完整的数据交互流程,初步的窥探一下全栈的世界。
因为vue项目使用较多,前端项目直接使用了vue,避免造轮子,UI框架也直接使用了常用的Element-UI,后端使用koa2,数据库引入mongodb,都是前端熟悉的JavaScript和json。
前端
- 基本项目搭建部分忽略(可以直接使用vue-cli3进行项目的启动)
- 本项目包括vue-router、vuex
- 本项目为了尽量简化流程,只展示注册、登录、首页三个部分,首页包括退出登录的功能
基本页面
引入Element-UI、router等
src目录下的main.js需加上:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
Vue.use(ElementUI)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
复制代码
1. 登录界面
<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>
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) {
alert('submit!')
} 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>
复制代码
2.注册界面
<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>
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) {
alert('submit!')
} else {
console.log('error submit!!')
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>
复制代码
3. 首页部分
4. router部分
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: '/',
name: 'home',
component: Home,
meta: {
requiresAuth: true // 需要登录才能访问的页面
}
},
{
path: '/login',
name: '/login',
component: (resolve) => require([ '@/views/Login' ], resolve)
},
{
path: '/register',
name: '/register',
component: (resolve) => require([ '@/views/Register' ], resolve)
}
]
const router = new VueRouter({
routes
})
// 注册全局钩子用来拦截导航
router.beforeEach((to, from, next) => {
let token = store.state.user.token
console.log('token:::', token)
if (to.meta.requiresAuth) {
if (token) {
next()
} else {
next({
path: '/login',
query: { redirect: to.fullPath } // 将刚刚要去的路由path(却无权限)作为参数,方便登录成功后直接跳转到该路由
})
}
} else {
next()
}
})
export default router
复制代码
5. store部分
- 在main.js中引入store,main.js文件内容上面已给出,为什么这么简单的一个项目还需要vuex呢?解释一下,因为在真实的项目中,各个组件、页面的操作基本都需要获取到token进行验证,如果不通过vuex进行通信,那就需要各个组件之间记住传递状态,这就比较麻烦了,
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
window.sessionStorage.setItem('token', data)
},
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('USERINFO', data)
}
}
})
复制代码
6. axios的封装
- 这一部分主要实现登录注册的接口请求,以及使用拦截器对是否登录进行一个验证,如果登录状态过期或未登录时,后端返回的code为401,则自动跳转到登录界面,同时清除token信息
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.user.token) {
config.headers.Authorization = store.state.user.token
}
return config
},
err => {
return Promise.reject(err)
}
)
// respone拦截器
instance.interceptors.response.use(
response => {
return response
},
// 除了200以外的请求到这里来,,这里的200不是我们设置的那个code200,,我这里是,没有登录才会不返回200
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)
}
}
复制代码
koa部分
到目前为止,前端vue部分的工作做好准备了,现在开始服务端部分:
1. 入口文件
在项目根目录下面新建server文件夹,作为整个server端项目目录,server目录下初始化package.json文件,npm init -y
命令,并引入koa及其他中间件。
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 :
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);
});
复制代码
2. 路由
新建一个文件夹,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.user.allUser) // 获取所有用户
.post('/api/user/delete',controller.user.deleteUser) // 获取所有用户
module.exports = router
复制代码
- 以上四个路由正是之前前端请求的四个接口,处理逻辑我们放在controller文件夹中
3. JWT验证
- 建立utils文件夹,新建token.js文件,声明create_token和check_token,用于验证登录
- config配置文件,需要区别出不需要登录的接口,比如注册和登录,不需要鉴权返回401
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_id: 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()
}
}
复制代码
config.js:
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']
}
复制代码
数据库mongodb
1. 建表
首先需要安装mongodb,可以在网上找到很多下载的方式,这里略过。
至于数据库可视化工具,我这里使用的是adminMongo,直接拉取GitHub上的代码就可以,
git clone https://github.com/mrvautin/adminMongo.git && cd adminMongo
npm install
npm start
ornode app
- 直接访问http://127.0.0.1:1234 , 至此,adminmongo 安装完成。
建立db文件夹,作为连接数据库的入口,建立用户表:
const mongoose = require('mongoose');
const db = mongoose.connect("mongodb://localhost:27017/test", {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: ""
}
})
exports.User = mongoose.model('User', userSchema);
复制代码
2. 操作数据库
建立controller文件夹,主要对数据库进行操作
const User = require('../db').User
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)
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: '删除成功'
}
}
module.exports = {
register,
login,
deleteUser
}
复制代码
完善交互逻辑
完成后端以及数据库的开发之后,可以开始整合前端的页面,获取接口返回的响应数据,并进行相应的操作
如,注册页面,提交注册的逻辑如下:
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('UserRegister', res.data.data)
this.$router.push('/home')
} else {
this.$message({
message: '注册失败',
type: 'error'
})
}
})
} else {
return false
}
})
},
复制代码
登录逻辑:
submitForm (formName) {
this.$refs[formName].validate(valid => {
if (valid) {
api.userLogin(this.ruleForm).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
}
})
},
复制代码
现在可以完成首页的设计了,这里只设置了一个退出功能的按钮,具体内容可以继续完善。
首页:Home.vue
<template>
<div class="home">
<p>你好啊,现在是已经登录的状态</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>
复制代码
至此,最简单的一个注册登录demo完成了,具备注册、登录、退出登录三个最基本的功能,技术不难,主要为熟悉整个流程,还不能完全用于实际项目中,还需要去更多的完善功能。
作者:Armor
链接:https://juejin.im/post/5e2fa4b7e51d451c7a436856
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。