vue + koa2 + mongodb 前后端分离登陆demo

259 阅读5分钟

世界上最遥远的距离就是从星期一到星期五,Oh,boss我太困了,我需要休息。boss:下次吧!根据墨菲定律,人们通常下次吧的意思就是星期八可以让你休息。当然啦这是不能的,我们的boss 可不是这样的人,老板会说没事困了就play ping-pang吧 ,哈哈哈所以有了公司的乒乓球台,我也是满脸问号???,至于为什么今天才来吐槽,是因为今天路过看见长久不用的乒乓球桌在那静静的静静地在那睡觉,睡的多么香啊,好想和它一起哈哈哈哈。有人肯定要问了一个学习的博客天天开场乱七八糟的东西,那我就不客气啦,博客技术肯定是很正经啊,但是写博客的人正不正经别人不知道哇哈哈哈,好了闲话少说,我们上一篇讲了mongodb的搭建,这一篇讲一下vue + koa2 + mongodb 的前后端小demo的实践吧

  1. 前端登陆页面的搭建

    vue脚手架不用多说了吧?直接来一个项目结构,写一个前端登陆页面 ,话不多说上代码

    // router 的index.js
    import Vue from "vue";
    import VueRouter from "vue-router";
    import Login from '@/components/Login'
    Vue.use(VueRouter);
    const routes = [
      {
        path: "/login",
        name: "Login",
        component: Login
      },
      {
        path: "/register",
        name: "Register",
        component: Register
      }
    ];
    
    const router = new VueRouter({
      mode: "history",
      base: process.env.BASE_URL,
      routes
    });
    
    export default router;
    
    // Login.vue 
    <template>
    <el-form :model="ruleForm" :rules="rules" ref="ruleForm" class="demo-ruleForm">
      <div class="login_form_input" label="用户" style="margin-top: 58px;">
        <el-form-item prop="username">
          <el-input v-model="ruleForm.username" placeholder="请输入用户名或手机号"></el-input>
      </el-form-item>
      </div>
      <div class="login_form_input" label="密码" style="margin-bottom:7px ;">
        <el-form-item prop="userpass">
          <el-input type="password" v-model="ruleForm.userpass" placeholder="请输入用户密码"></el-input>
      </el-form-item>
      </div>
      <div class="login_btn">
        <input class="login_submit" type="button" @click="submitForm('ruleForm')" value="登录" />
      </div>
      </el-form>
    </template>
    <script>
    import axios from 'axios'
    export default {
      data() {
        return {
          ruleForm: {
            username: "",
            userpass: ""
          },
          rules: {
            username: [
              { required: true, message: "用户名或手机号", trigger: "blur" },
            ],
            userpass: [
                { required: true, message: "请选择活动区域", trigger: "change" },
                { min: 6, max: 18, message: "长度在 3 到 18 个字符", trigger: "blur" }
            ]
          }
        };
      },
      methods: {
        async submitForm(formName) {
          this.$refs[formName].validate(valid => {
            if (valid) {
              // 点击登陆进行的提交 肯定会用axios  
                this.$api.login({
                    username:this.ruleForm.username,
                    password:this.ruleForm.userpass
                }).then(res => {
                    console.log(res);
                }).catch((err) => {
                    console.log(err);
                })
            } else {
              console.log("error submit!!");
              return false;
            }
          });
        }
      }
    };
    </script>
    
    
  2. axios的二次封装

    上边提到了使用axios进行请求,我们平常都是直接在组件中直接用axios进行请求,这样造成当我们需要修改某一个接口的时候需要去组件里边找,下面我们就对axios进行封装

    • 目录结构

      -src

      ​ -- http

      ​ --api.js

      ​ --config.js

      ​ --index.js

      ​ --insterface.js

    • config.js : axios配置

      export default {
          method:'post',
          //基础url前缀
          baseURL:'/api',
          //请求信息头
          headers:{
              'Content-Type':'application/json;charset=UTF-8'
          },
          //参数
          data:{},
          //设置超时时间
          timeout:10000,
          // 表示跨域请求时是否需要使用凭证
          withCredentilas:true,
          // `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
          responseType: 'json', // default
      }
      
    • api.js :具体封装 包含拦截、处理数据、返回服务器信息

      import axios from 'axios'
      import config from './config'  //倒入默认配置
      import qs from 'qs'  //序列化请求数据,视服务端的要求
      import { Message } from 'element-ui'
      import router from './../router'
      
      export default function $axios(options) {
          return new Promise((resolve, reject) => {
              const instance = axios.create({
                  baseURL: config.baseURL,
                  headers: {},
                  transformRequest: [function (data) {
                      console.log(data)
                  }]
              })
      
              //request 拦截器
              instance.interceptors.request.use(
                  config => {
                      // Tip 1 :请求开始的时候可以结合vuex开启全屏的loading动画
      
                      // Tip2 :带上token ,可以结合vuex 或者重localStorage
      
                      // if (store.getter.token) {
                      //     config.headers['X-Token'] = getToken() // 让每个请求携带token -- ['X-Token']为自定义key 请根据实际情况自行修改
                      // }else{
                      //     重定向到登陆页面
                      // }
                      // Tip3 :根据请求方法,序列化传过来的参数,根据后端需求是否序列化
      
                      if (config.method.toLocaleLowerCase() === 'post' || config.method.toLocaleLowerCase() === 'put' || config.method.toLocaleLowerCase() === 'delete') {
                          config.data = qs.stringify(config.data)
                      }
                      return config
                  },
                  error => { // 请求错误时做些事(接口错误、超时等)
                      // Tip: 4 关闭loadding  
                      console.log('request:', error);
                      //  1. 判断请求超时
                      if (error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1) {
                          console.log('根据你设置的timeout/真的请求超时 判断请求现在超时了,你可以在这里加入超时的处理方案')
                          // return service.request(originalRequest);//例如再重复请求一次
                      }
                      // 2. 需要重定向到错误页面
                      const errorInfo = error.response;
                      console.log(errorInfo);
                      if (errorInfo) {
                          // error =errorInfo.data//页面那边catch的时候就能拿到详细的错误信息,看最下边的Promise.reject
                          const errorStatus = errorInfo.status; // 404 403 500 ... 等
                          router.push({
                              path: `/error/${errorStatus}`
                          })
                      }
                      return Promise.reject(error);   //在调用的那边可以拿到(catch)你想返回的错误信息
                  })
      
              // response 拦截器
      
              instance.interceptors.response.use(
                  response => {
                      let data;
                      // IE9时response.data是undefined,因此需要使用response.request.responseText(Stringify后的字符串)
                      if (response.data == undefined) {
                          data = response.request.responseText
                      } else {
                          data = response.data
                      }
      
                      // 根据返回的code 值来做不同的处理 和后端进行约定
      
                      switch (data.code) {
                          case '':
      
                              break;
      
                          default:
                              break;
                      }
                      // 如果不是正确的返回code ,且已经登陆,那就抛出错误
                      // const err = new Error(data.description)
                      // err.data = data
                      // err.response = response
                      // throw err
                      return data
                  },
                  error => {
                      if (error && error.response) {
                          switch (error.response.status) {
                              case 400:
                                  error.message = '请求错误'
                                  break;
                              case 401:
                                  error.message = '未授权,请登录'
                                  break;
                              case 403:
                                  error.message = '拒绝访问'
                                  break;
                              case 404:
                                  error.message = `请求地址出错: ${error.response.config.url}`
                                  break;
                              case 408:
                                  error.message = '请求超时'
                                  break
      
                              case 500:
                                  error.message = '服务器内部错误'
                                  break
      
                              case 501:
                                  error.message = '服务未实现'
                                  break
      
                              case 502:
                                  error.message = '网关错误'
                                  break
      
                              case 503:
                                  error.message = '服务不可用'
                                  break
      
                              case 504:
                                  error.message = '网关超时'
                                  break
      
                              case 505:
                                  error.message = 'HTTP版本不受支持'
                                  break
                              default:
                                  break;
                          }
                      }
                      console.error(error)
                      // 此处我使用的是 element UI 的提示组件
                      Message.error(`ERROR: ${error}`)
                      return Promise.reject(error) // 返回接口返回的错误信息
                  }
              )
              //请求处理 执行 instance 的方法
              instance(options)
                  .then((res) => {
                      resolve(res)
                      return false
                  })
                  .catch((error) => {
                      reject(error)
                  })
      
          })
      
      }
      
    • interface.js :接口统一管理

      import axios from './api'  
      /**
       * 讲所有接口统一起来管理
       * 如果项目很大可以将url 独立成文件,接口分成不同的模块
       */
       export const login = params =>{  // 其中的一个接口
          return axios({
              url: '/mongotest',
              method: 'get',
              params
          })
       }
       // 注意post 请求啊,坑了我好久
       //const Register = params =>{
         // return axios({
              //url:'/users/register',
              //method:'POST',
              //data:params
          //})
      //}
      
       export default {
          login
       } 
      
    • 给Vue添加方法,然后进行挂载

      import apiList from './interface';
      const install  = Vue =>{
          if (install.installed) return ;
          install.installed = true;
          Object.defineProperties(Vue.prototype,{
              $api:{
                  get(){
                      return apiList
                  }
              }
          })
      }
      export default install
      
    • Main.js:进行使用挂在

      import api from './http/index'
      Vue.use(api);
      
    • 组件中进行使用

       this.$api.login({
         username:this.ruleForm.username,
         password:this.ruleForm.userpass
       }).then(res => {
         console.log(res);
       }).catch((err) => {
         console.log(err);
       })
      
  3. vue 本地调试跨域请求的代理

    我服务端是localhost:3000,客户端是:localhost:3001 这样进行请求那肯定涉及到跨域问题啊,身为小白菜的我,在这卡了半天啊, 开始的时候我在koa使用 koa-cors 进行后台的跨域开放,很轻松的完成了,但是我轴啊,我就非要代理来进行,我要和你死磕到底哈哈哈哈,这是多大仇这是。在vue.config.js中进行proxy代理。这块我捣鼓了好一会。axiso封装的配置config.js ,baseURL:'/api',就是这样 ,然后接口直接写成 url: '/mongotest',对应好了跨域就不存在问题了。

    module.exports = {
        devServer: {
            // 本地运行地址
            host: 'localhost',
            // 本地运行端口
            port: '3001',
            // 代理配置
            proxy: {
                // 匹配拦截路由  那就请求的接口是localhost:3000/api/XXX
                '/api': {   
                    target: 'http://localhost:3000/',
                    changeOrigin: true,
                    ws: false,
                    pathRewrite: {
                        '^/api': ''
                    }
                }
            }
        }
    }
    
  4. 登陆数据token的

    这一块真的是考虑良久或许我很菜吧,请大佬勿喷,但是绝对是很全面的,我自己肯定是很细节的走了一遍。

    • 先来讲前端登陆,前端登陆页面发送登陆请求,后台返回token

        this.$api.Login({
          username:this.ruleForm.username,
          password:this.ruleForm.userpass
        }).then((res) => {
          let loginType = '';
          console.log(res);
          if (res.code===200) {
            //登陆之后进行token的储存
            //localStorage.setItem('USER_TOKEN', JSON.stringify(res.token));
            (res.token)&&(this.$store.commit('setUserToken',res.token)); 
      
            // 对登陆状态的cookie 存储
            this.$cookie.setCookie({username:res.username},2/144);
            loginType ='success' ;
      
          }else{
            loginType ='error' ;
          }
          });
        }).catch((err) => {
          console.log(err)
        })
      
    • 后台koa 进行接收,聊一下思路,请短发送请求,1.后台需要再路由处有接口,2.有了接口肯定会去对比用户名和密码对吧?不用别人点头那肯定的啊。3.进行mongo查询 4. 返回查询到信息生成token返回

      // 接口处进行接收
      router.post('/login', async function(ctx, next){
        //console.log(ctx.request.body);
        let {username, password} = ctx.request.body;
        if(!username || !password) return ctx.body = {code: 4020,msg: '请填写用户名或密码'};
      
        let args = {username, password};
        const userData = await getUsers.userLogin(args)
        ctx.body = userData
        console.log(userData);
        ctx.body = (userData.code === 200) 
           ? {code: 200, msg: '登陆成功',username:userData.username, token: jwt._createToken(userData)} 
           : userData
      })
      
      //对比密码你肯定不可能直接输入了就对比吧?那不得进行密码加密啊 我们使用bcrypt 进行密码加密
      usersSchema.methods = {
          comparePassword: (_pass, password) => { //验证方法
              // _pass传递过来的密码,password是数据库中的密码
              return new Promise((res, rej) => {
              bcrypt.compare(_pass, password, (err, isMath) => { //compare官方方法
                      if (!err) {
                              res(isMath); // isMath返回true和false,true代表验证通过
                      } else {
                          rej(err);
                      }
                  });
              });
          },
      };
      // 进行调用密码加密的方法
       let result = await usersDoc.comparePassword(password, usersDoc.password).then((isMath) => {
         if (isMath) { // 返回true账户密码存在
           return isMath
         } else { // 否则是账户存在密码错误
           console.log(isMath, "密码不存在............");
         }
       }).catch((error) => {
         console.log("服务器出现异常,请重启服务器......");
       });
      
      //console.log(result);
      return !result ? {code:-2 ,msg :'密码不正确'} : {code:200, _id: usersDoc._id,username: usersDoc.username,avatar: usersDoc.avatar, mobile: usersDoc.mobile,email: usersDoc.email}
      
      // 进行mongo 查询 使用mongoose ,我们新建一个db目录,需要 有对mongose 配置文件,数据的模型model文件,contoller文件,出口调用的index文件
      // db 目录结构详细为:
      //	--index.js
      //	--db.js
      //	--models
      //  	--users.js
      //	--controllers
      //    --user.js
      
      
      // 配置文件 db.js
      const mongoose = require('mongoose');
      // 连接
      const DB_URL = 'mongodb://localhost:27017/msjx'
      mongoose.set('useCreateIndex', true)
      mongoose.connect(DB_URL)
      
      mongoose.connection.on('connected',function(){
          console.log('连接成功 ' + DB_URL);
      })
      /**
       * 链接异常error 数据库链接错误
       */
      mongoose.connection.on('error',function(err){
          console.log('Mongoose connection disconnected');
      })
      
      /**
       * 连接断开disconnected 连接异常断开
       */
      mongoose.connection.on('disconnected',function(){
          console.log('链接断开'+ DB_URL)
      })
      module.exports = mongoose 
      
      // 模型文件
      const mongoose  =require('../db');
      const bcrypt  = require("bcrypt")
      const stringRandom = require('string-random');
      
      const { model ,Schema} = require('mongoose');
       // 定义加密密码计算强度
      const SALT_WORK_FACTOR = 10;
      const usersSchema = new Schema({
          username:{
              type:String,
              default:stringRandom(8),
              unique:true
          },
          alias_name:String,
          mobile: String,
          password: String,
          userType: Number,  // 用户角色
          school_type:String,
          email: String,
          family_name: String,
          reg_time:{
              type:Date,
              default:Date.now
          },
          avatar:{
              type:String,
              default:""
          }
      })
      // 保存之前的操作 
      usersSchema.pre('save', function(next) {
          var user = this;
          // 仅当密码被修改(或新)时才对其进行散列
          if (!user.isModified('password')) return next();
          // 进行加盐
          bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
              if (err) return next(err);
              // 使用加盐密码之后的密码进行hash
              bcrypt.hash(user.password, salt, function(err, hash) {
                  if (err) return next(err);
                  // 用散列后的密码覆盖明文密码
                  user.password = hash;
                  next();
              });
          });
      });
      usersSchema.methods = {
          comparePassword: (_pass, password) => { //验证方法
              // _pass传递过来的密码,password是数据库中的密码
              return new Promise((res, rej) => {
              bcrypt.compare(_pass, password, (err, isMath) => { //compare官方方法
                      if (!err) {
                              res(isMath); // isMath返回true和false,true代表验证通过
                      } else {
                          rej(err);
                      }
                  });
              });
          },
      };
      const userModel  = model("users",usersSchema);
      module.exports = userModel
      
      // control 控制模型文件
      const UsersModel  = require('./../models/users');
      
      class UsersCtl {
          constructor(){
      
          }
          query () {  // 用户列表查询接口
              return new Promise((resolve,reject)=>{
                  UsersModel.find({},(err,res)=>{
                      if (err) {
                          reject(err)
                      }
                      resolve(res)  // res就是一个doc
                  })
              })
          }
          async userLogin(obj) { // 用户登陆的方法
              let {username,password}  = obj;
              
              const usersDoc = await UsersModel.findOne({"$or" :  [ {'mobile':username} , {'username':username}]});
              if (!usersDoc) {
                  return {
                     code:0,
                     msg:"该用户尚未注册" 
                  }
              }
              let result = await usersDoc.comparePassword(password, usersDoc.password).then((isMath) => {
                  if (isMath) { // 返回true账户密码存在
                      return isMath
                  } else { // 否则是账户存在密码错误
                      console.log(isMath, "密码不存在............");
                  }
              }).catch((error) => {
                console.log("服务器出现异常,请重启服务器......");
              });
              
              //console.log(result);
              return !result ? {code:-2 ,msg :'密码不正确'} : {code:200, _id: usersDoc._id,username: usersDoc.username,avatar: usersDoc.avatar, mobile: usersDoc.mobile,email: usersDoc.email}
          }
          async userRegister(obj) { // 用户登陆的方法
              let {alias_name,password,mobile,school_type}  = obj;
              const usersDoc = await UsersModel.findOne({mobile});
              if (usersDoc) {
                  return {
                     code:0,
                     msg:"该用户已经注册" 
                  }
              }
              let userRegister = new UsersModel({alias_name,password,mobile,school_type});
              let userInfo = await userRegister.save();
              return {
                  code:200,
                  msg:"注册成功"            
              }
      
          }
          save(obj){ 
              const m = new UsersModel(obj)
              return new Promise((resolve,reject)=>{
                  m.save((err,res)=>{
                      if (err) {
                          reject(err)
                      }
                      resolve(res)
                      //console.log(res)
                  })
              })
          }
      }
      
      
      module.exports =  new UsersCtl()
      
      // index .js 文件
      const UsersCtl  =require('./controllers/user')
      module.exports = {
          getUsers: UsersCtl
      }
      
  5. 生成token单独拿出来

    // createToken
    const jwt = require('jsonwebtoken');
    const _createToken = (userInfo) =>{
        return jwt.sign({userInfo},'10000@qq.com',{expiresIn:'60'})
    }
    module.exports = {
        _createToken
    }
    
    // checkToken
    const jwt =require('jsonwebtoken');
    const _checkToken = function (req,res,next){ // 获取请求头文件中的token信息
        let token = req.body.token || req.query.token || req.headers['authorization'];
        console.log(token); 
    
        if (token) {
            //确认token是否正确
            let decoded = jwt.decode(token,'10000@qq.com');
            console.log(decoded,4444); // 验证token是否过期
            if (token && decode.exp<new Date()/1000) {
                return res.json({success:false,message:'token令牌已过期'})
            }else{
                return next();
            }
    
        }else{  // 如果没有token
            return res.status(403).send({
                success:false,
                message:'没有提供token!'
            })
        }
    
    }
    module.exports = _checkToken;
    

我的项目 github地址 敬请关注