vue+koa2实现最简单的注册登录功能

作为一个前端,工作之中总离不开前后端的数据联调,奈何前端的数据一直只知道怎么用,却不知道是怎么出来的,正好借着放假的期间,琢磨琢磨后端的一些逻辑。先从最最基本的注册登录来看一个完整的数据交互流程,初步的窥探一下全栈的世界。

因为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)  //&emsp;用户注册
.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上的代码就可以,

  1. git clone https://github.com/mrvautin/adminMongo.git && cd adminMongo
  2. npm install
  3. npm start or node app
  4. 直接访问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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值