项目描述:一开始进入登录界面,只有登录成功才可以跳转到主页面,已注册但是忘记密码的进入忘记密码页面,找回密码后进入登录界面。

技术选型:nodejs+vue+stylus

界面效果:

  • 切换登录方式
  • 手机合法检查
  • 倒计时效果
  • 切换显示或隐藏密码
  • 前台验证提示

前后台交互功能

  • 动态一次性图形验证码
  • 动态一次性短信验证码
  • 短信登录
  • 密码登录
  • 获取用户信息,实现自动登录
  • 退出登录

 

技术点讲解

  • 切换登录方式

使用:class绑定样式,如果没有该样式,标签设置为display:none

 

  • 手机的合法检查

使用正则,规则为以1开头,共11位数字。注意这里将方法写在computed中,当用户输入的phone改变时rightPhone才重新计算,减少缓存。



computed: {
        rightPhone () {
          return /^1\d{10}$/.test(this.phone)
        }
      },



 

  • 倒计时效果

当输入手机号后,如果手机号输入正确,“获取验证码”由灰色显示为黑色(color #ccc-> #000),可以按下按钮(:disabled = !rightPhone),发送请求(getCode),同时倒数显示,“获取验证码”字样隐藏。



<template>
....    
        <input type="tel" maxlength="11" placeholder="手机号" v-model="phone">
            <button :disabled="!rightPhone" class="get_verification"
                    :class="{right_phone: rightPhone}" @click.prevent="getCode">
              {{computeTime>0 ? `已发送(${computeTime}s)` : '获取验证码'}}
            </button>
....
</template>
<script>
.....
data(){
    return{
        computeTime:0;
        }
}

methos:{
async getCode () {
          // 如果当前没有计时
          if(!this.computeTime) {
            // 启动倒计时
            this.computeTime = 60
            this.intervalId = setInterval(() => {
              this.computeTime--
              if(this.computeTime<=0) {
                // 停止计时
                clearInterval(this.intervalId)
              }
            }, 1000)
......



 

  • 切换显示或密码隐藏

两个input,类型分别为type=password,text;使用v-if,v-else绑定showPwd(一个布尔值)决定显示哪一个。



<template>
...
<section class="login_verification">
              <input type="text" maxlength="8" placeholder="密码" v-if="showPwd" v-model="pwd">
              <input type="password" maxlength="8" placeholder="密码" v-else v-model="pwd">
              <div class="switch_button" :class="showPwd?'on':'off'" @click="showPwd=!showPwd">
                <div class="switch_circle" :class="{right: showPwd}"></div>
                <span class="switch_text">{{showPwd ? '' : ''}}</span>
              </div>
</section>
...
</template>
<script>
export default {
      name: "login",
      data () {
        return {
          showPwd: false, // 是否显示密码
          pwd:'',
                }
        }
.....
}



 

  • 前台验证提示

用户每次点击获取图形验证码时都会不同,即img中src不能相同,可以在请求地址后面加上请求时间Date.now()



<template>
...
<section class="login_message">
              <input type="text" maxlength="11" placeholder="验证码" v-model="captcha">
              <img class="get_verification" src="http://localhost:4000/captcha" alt="captcha"
                   @click="getCaptcha" ref="captcha">
</section>

<script>
.....
methods:{
    // 获取一个新的图片验证码
        getCaptcha () {
          // 每次指定的src要不一样
          this.$refs.captcha.src = 'http://localhost:4000/captcha?time='+Date.now()
        }
}
}



 

  • 点击登录提交表单

 这里分为短信登录和用户密码登录两个部分,都是提交表单,即为post请求,短信登录中提交的是电话号码和验证码,用户密码登录提交的是手机号、密码和图形验证码。

开始先安装axios

接着封装ajax请求,返回promise对象,



import axios from 'axios'
export default function ajax (url, data={}, type='GET') {

  return new Promise(function (resolve, reject) {
    // 执行异步ajax请求
    let promise
    if (type === 'GET') {
      // 准备url query参数数据
      let dataStr = '' //数据拼接字符串
      Object.keys(data).forEach(key => {
        dataStr += key + '=' + data[key] + '&'
      })
      if (dataStr !== '') {
        dataStr = dataStr.substring(0, dataStr.lastIndexOf('&'))
        url = url + '?' + dataStr
      }
      // 发送get请求
      promise = axios.get(url)
    } else {
      // 发送post请求
      promise = axios.post(url, data)
    }
    promise.then(function (response) {
      // 成功了调用resolve()
      resolve(response.data)
    }).catch(function (error) {
      //失败了调用reject()
      reject(error)
    })
  })
}



根据提供的api编写请求地址



import ajax from './ajax'
//设置跨域,跨域地址为http://localhost:4000
import apiConfig from '../../config/api.config.js'
axios.defaults.baseURL=apiConfig.baseURL

// 用户名密码登陆
export const reqPwdLogin = ({name, pwd, captcha}) => ajax('/login_pwd', {name, pwd, captcha}, 'POST')
// 发送短信验证码
export const reqSendCode = (phone) => ajax('/sendcode', {phone})
// 手机号验证码登陆
export const reqSmsLogin = (phone, code) => ajax('/login_sms', {phone, code}, 'POST')
// 根据会话获取用户信息
export const reqUserInfo = () => ajax('/userinfo')
// 用户登出
export const reqLogout = () => ajax('/logout')



 

这里先配置一下跨域,假设跨域地址为http://localhost:4000,以下同时配置开发环境和生产环境的跨域,这样上线后无需更改也可以请求接口

配置目录 /config/index.js



proxyTable: {
      '/apis':{
        target: 'http://localhost:4000/',  // 后台api
        changeOrigin: true,  //是否跨域
        // secure: true,
        pathRewrite: {
          '^/apis': ''   //需要rewrite的,
        }
      }
    },



在config文件里新建一个js文件api.config.js



//判断是否是生产环境
var isPro = process.env.NODE_ENV === 'production' //process.env.NODE_ENV用于区分是生产环境还是开发环境
//根据环境不同导出不同的baseURL
module.exports = {
    baseURL: isPro ? 'http://localhost:4000/' : '/apis'
}



在axios的默认实例有一个baseURL的属性,配置了baseURL之后,访问接口时就会自动带上,这里对应api地址请求中的



//设置跨域,跨域地址为http://localhost:4000
import apiConfig from '../../config/api.config.js'
axios.defaults.baseURL=apiConfig.baseURL



写好接口请求和配置好跨域后,我们回到登录请求方面。



// 异步登陆
        async login () {
          let result
          // 前台表单验证
          if(this.loginWay) {  // 短信登陆
            const {rightPhone, phone, code} = this
            if(!this.rightPhone) {
              // 手机号不正确
              this.showAlert('手机号不正确')
              return
            } else if(!/^\d{6}$/.test(code)) {
              // 验证必须是6位数字
              this.showAlert('验证必须是6位数字')
              return
            }
            // 发送ajax请求短信登陆
            result = await reqSmsLogin(phone, code)

          } else {// 密码登陆
            const {name, pwd, captcha} = this
            if(!this.name) {
              // 用户名必须指定
              this.showAlert('用户名必须指定')
              return
            } else if(!this.pwd) {
              // 密码必须指定
              this.showAlert('密码必须指定')
              return
            } else if(!this.captcha) {
              // 验证码必须指定
              this.showAlert('验证码必须指定')
              return
            }
            // 发送ajax请求密码登陆
            result = await reqPwdLogin({name, pwd, captcha})
          }

          // 停止计时
          if(this.computeTime) {
            this.computeTime = 0
            clearInterval(this.intervalId)
            this.intervalId = undefined
          }

          // 根据结果数据处理
          if(result.code===0) {
            const user = result.data
            // 将user保存到vuex的state
            this.$store.dispatch('recordUser', user)
            // 跳转首页
            this.$router.replace('/home')
          } else {
            // 显示新的图片验证码
            this.getCaptcha()
            // 显示警告提示
            const msg = result.msg
            this.showAlert(msg)
          }
        },



 

此时我们先暂停以下前台的编写,使用nodejs来编写后台响应。

注意:以下讲到的模块的参数写法具体参考www.npmjs.com中自己搜索模块,这里不会详细讲解每个参数是什么意思。

 在这里我使用的是nodejs+express+mongoose来搭建。首先确定你的电脑上已经配置好nodejs,MongoDB,express。

使用express新建项目server



express -e server



  • 完成用户密码方式登录

首先考虑获取验证码,在这里我们使用svg-captcha



router.get('/captcha', function (req, res) {
  var captcha = svgCaptcha.create({
    ignoreChars: '0o1l',
    noise: 2,//产生线数
    color: true
  });
  req.session.captcha = captcha.text.toLowerCase();
  console.log(req.session.captcha)
  /*res.type('svg');
  res.status(200).send(captcha.data);*/
  res.type('svg');
  res.send(captcha.data)
});



然后我们再连接数据库。注意这里登陆总共存在数据库中的参数有手机号,用户名,密码三个。



/*
包含n个能操作mongodb数据库集合的model的模块
1. 连接数据库
  1.1. 引入mongoose
  1.2. 连接指定数据库(URL只有数据库是变化的)
  1.3. 获取连接对象
  1.4. 绑定连接完成的监听(用来提示连接成功)
2. 定义对应特定集合的Model
  2.1. 字义Schema(描述文档结构)
  2.2. 定义Model(与集合对应, 可以操作集合)
3. 向外暴露获取Model的方法
 */
// 1. 连接数据库
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/order_manager')
const conn = mongoose.connection
conn.on('connected', function () {
  console.log('数据库连接成功!')
})

// 2. 得到对应特定集合的Model: UserModel
const userSchema = mongoose.Schema({
  // 用户名
  'name': {type: String},
  // 密码
  'pwd': {type: String},
  // 类型
  'phone': {'type': String}
})
UserModel = mongoose.model('user', userSchema)

// 3. 向外暴露
module.exports = {
  getModel(name) {
    return mongoose.model(name)
  }
}



接着我们来验证用户提交的用户密码登录的信息。



*
密码登陆
 */
router.post('/login_pwd', function (req, res) {
  const name = req.body.name
  const pwd = md5(req.body.pwd)
  const captcha = req.body.captcha.toLowerCase()
  console.log('/login_pwd', name, pwd, captcha, req.session)

  // 可以对用户名/密码格式进行检查, 如果非法, 返回提示信息
  if(captcha!==req.session.captcha) {
    return res.send({code: 1, msg: '验证码错误'})
  }
  // 删除保存的验证码
  delete req.session.captcha

  UserModel.findOne({name}, function (err, user) {
    if (user) {
      console.log('findUser', user)
      if (user.pwd !== pwd) {
        res.send({code: 1, msg: '用户名或密码不正确!'})
      } else {
        req.session.userid = user._id
        res.send({code: 0, data: {_id: user._id, name: user.name, phone: user.phone}})
      }
    } else {
      const userModel = new UserModel({name, pwd})
      userModel.save(function (err, user) {
        // 向浏览器端返回cookie(key=value)
        // res.cookie('userid', user._id, {maxAge: 1000*60*60*24*7})
        req.session.userid = user._id
        const data = {_id: user._id, name: user.name}
        // 3.2. 返回数据(新的user)
        res.send({code: 0, data})
      })
    }
  })
})



 

  • 完成用户手机方式登录

首先在网上云之讯中注册新用户(这里我是随便选的,主要是云之讯新用户免费送100条短信测试,所以我就注册了,其他的也可以)。注册成功后你可以获得自己的api接口对接(APPID,ACCOUNT,TOKEN,REST URL)。

这里我们先编写生成随机验证码的函数。



/*
 生成指定长度的随机数
 */
function randomCode(length) {
    var chars = ['0','1','2','3','4','5','6','7','8','9'];
    var result = ""; //统一改名: alt + shift + R
    for(var i = 0; i < length ; i ++) {
        var index = Math.ceil(Math.random()*9);
        result += chars[index];
    }
    return result;
}
// console.log(randomCode(6));
exports.randomCode = randomCode;



 

向指定的号码发送指定的验证码



/*
向指定号码发送指定验证码
 */
function sendCode(phone, code, callback) {
    var ACCOUNT_SID = '自己的用户sid'
    var AUTH_TOKEN = '鉴权密钥';
    var Rest_URL = '请求地址';
    var AppID = '应用ID';
    //1. 准备请求url
    /*
     1.使用MD5加密(账户Id + 账户授权令牌 + 时间戳)。其中账户Id和账户授权令牌根据url的验证级别对应主账户。
     时间戳是当前系统时间,格式"yyyyMMddHHmmss"。时间戳有效时间为24小时,如:20140416142030
     2.SigParameter参数需要大写,如不能写成sig=abcdefg而应该写成sig=ABCDEFG
     */
    var sigParameter = '';
    var time = moment().format('YYYYMMDDHHmmss');
    sigParameter = md5(ACCOUNT_SID+AUTH_TOKEN+time);
    var url = Rest_URL+'/2019-07-12/Accounts/'+ACCOUNT_SID+'/SMS/TemplateSMS?sig='+sigParameter;

    //2. 准备请求体
    var body = {
        to : phone,
        appId : AppID,
        templateId : '1',
        "datas":[code,"1"]
    }
    //body = JSON.stringify(body);

    //3. 准备请求头
    /*
     1.使用Base64编码(账户Id + 冒号 + 时间戳)其中账户Id根据url的验证级别对应主账户
     2.冒号为英文冒号
     3.时间戳是当前系统时间,格式"yyyyMMddHHmmss",需与SigParameter中时间戳相同。
     */
    var authorization = ACCOUNT_SID + ':' + time;
    authorization = Base64.encode(authorization);
    var headers = {
        'Accept' :'application/json',
        'Content-Type' :'application/json;charset=utf-8',
        'Content-Length': JSON.stringify(body).length+'',
        'Authorization' : authorization
    }

    //4. 发送请求, 并得到返回的结果, 调用callback
    // callback(true);
    request({
        method : 'POST',
        url : url,
        headers : headers,
        body : body,
        json : true
    }, function (error, response, body) {
        console.log(error, response, body);
        // callback(body.statusCode==='0');
        callback(true);
    });
}
exports.sendCode = sendCode;



最后我们就可以编写手机号短信验证码登录的路由请求。



/*
发送验证码短信
*/
router.get('/sendcode', function (req, res, next) {
  //1. 获取请求参数数据
  var phone = req.query.phone;
  //2. 处理数据
  //生成验证码(6位随机数)
  var code = sms_util.randomCode(6);
  //发送给指定的手机号
  console.log(`向${phone}发送验证码短信: ${code}`);
  sms_util.sendCode(phone, code, function (success) {//success表示是否成功
    if (success) {
      users[phone] = code
      console.log('保存验证码: ', phone, code)
      res.send({"code": 0})
    } else {
      //3. 返回响应数据
      res.send({"code": 1, msg: '短信验证码发送失败'})
    }
  })
})

/*
短信登陆
*/
router.post('/login_sms', function (req, res, next) {
  var phone = req.body.phone;
  var code = req.body.code;
  console.log('/login_sms', phone, code);
  if (users[phone] != code) {
    res.send({code: 1, msg: '手机号或验证码不正确'});
    return;
  }
  //删除保存的code
  delete users[phone];


  UserModel.findOne({phone}, function (err, user) {
    if (user) {
      req.session.userid = user._id
      res.send({code: 0, data: user})
    } else {
      //存储数据
      const userModel = new UserModel({phone})
      userModel.save(function (err, user) {
        req.session.userid = user._id
        res.send({code: 0, data: user})
      })
    }
  })

})



 

由于篇幅太长了,我分两篇写,要看后续,请看nodejs+vue实现登录界面功能(二)