仿京东 项目笔记2(注册登录)

这里写目录标题

  • [1. 注册页面](#1. 注册页面)
    • [1.1 注册/登录页面------接口请求](#1.1 注册/登录页面——接口请求)
    • [1.2 Vue开发中Element UI的样式穿透](#1.2 Vue开发中Element UI的样式穿透)
      • [1.2.1 ::v-deep的使用](#1.2.1 ::v-deep的使用)
      • [1.2.2 elementUI Dialog内容区域显示滚动条](#1.2.2 elementUI Dialog内容区域显示滚动条)
    • [1.3 注册页面------步骤条和表单联动 steps+form](#1.3 注册页面——步骤条和表单联动 steps+form)
    • [1.4 注册页面------滑动拼图验证](#1.4 注册页面——滑动拼图验证)
    • [1.5 注册页面------element-ui组件Popover 弹出框 条件控制显示和隐藏 滑块验证码](#1.5 注册页面——element-ui组件Popover 弹出框 条件控制显示和隐藏 滑块验证码)
    • [1.6 注册页面------获取手机验证码,进行手机号校验、验证码CD60秒](#1.6 注册页面——获取手机验证码,进行手机号校验、验证码CD60秒)
    • [1.7 注册页面------element-ui组件el-autocomplete带输入建议 自动补全后缀(邮箱地址)](#1.7 注册页面——element-ui组件el-autocomplete带输入建议 自动补全后缀(邮箱地址))
    • [1.8 注册页面------用户、密码强弱校验+密码自定义规则校验、提交验证](#1.8 注册页面——用户、密码强弱校验+密码自定义规则校验、提交验证)
  • [2. 登录页面](#2. 登录页面)
    • [2.1 登录页面------接口](#2.1 登录页面——接口)
    • [2.2 登录------token](#2.2 登录——token)
    • [2.3 登录页面------退出登录](#2.3 登录页面——退出登录)
    • [2.4 登录页面------全局导航守卫](#2.4 登录页面——全局导航守卫)
    • [2.5 路由独享守卫](#2.5 路由独享守卫)
    • [2.6 组件导航守卫](#2.6 组件导航守卫)
    • [2.7 全局封装API](#2.7 全局封装API)
  • [3. 二维码生成](#3. 二维码生成)
  • [4. 二级路由拆分(个人中心)](#4. 二级路由拆分(个人中心))
  • [5. 图片懒加载](#5. 图片懒加载)
  • [6. 路由懒加载](#6. 路由懒加载)
  • [7. 项目上线](#7. 项目上线)
    • [7.1 打包](#7.1 打包)
    • [7.2 购买云服务器](#7.2 购买云服务器)

1. 注册页面

1.1 注册/登录页面------接口请求

api/index.js

javascript 复制代码
/*--------- 用户注册登录 ---------*/

//获取验证码
export const reqGetCode = (phone) => requests({url: `/user/passport/sendCode/${phone}`, method: 'get'})

//用户注册
export const reqUserRegister = (data) => requests({url: '/user/passport/register', method: 'post', data: data})

store/user.js

javascript 复制代码
import { reqGetCode, reqUserRegister } from "@/api"

//home模块的Vuex模块
const state = {
    //state中数据默认初始值别瞎写,根据接口的返回值进行初始化
    code: '',
}
const mutations = {
    GET_CODE(state, code) {
        state.code = code
    },
}
const actions = {
    //获取验证码
    async getCode({commit}, phone){
        //获取验证码的这个接口,把验证码返回,正常情况下,后台把验证码发到用户手机上
        let result  = await reqGetCode(phone)
        // console.log("result",result) 
        if(result.code == 200){
            commit('GET_CODE',result.data)
            return 'ok'
        } else {
            return Promise.reject(new Error("fail"))
        }
    },
    //用户注册
    async userRegister({commit}, userFrom) {
        let result = await reqUserRegister(userFrom) 
        if(result.code == 200) {
            return 'ok'
        } else {
            return Promise.reject(new Error(result.message))
        }
    },
}

export default{
    state,
    getters,
    mutations,
    actions
}

1.2 Vue开发中Element UI的样式穿透

1.2.1 ::v-deep的使用

参考::v-deep的使用

在 vue 项目的开发过程,使用了 ElementUI 组件且样式 style 使用了 scoped 属性,当想要修改组件样式,发现直接修改不了,需去掉 scoped 属性或者使用深度选择器才能修改成功。去掉scoped的话又会影响全局样式,针对这种情况,可以使用深度作用选择器(即样式穿透)

1、当项目中使用的 css 原生样式 ,需要使用 >>> 深度选择器来修改 外用第三方组件的样式

css 复制代码
<style lang="css" scoped>
    .el-button >>> span{
        color: '#f00'
    }
</style>

2、当项目中使用的 css 扩展语言是 less, 需要使用 /deep/ 或者 ::v-deep 深度选择器来修改 外用第三方组件的样式

css 复制代码
<style lang="less" scoped>
    /deep/.el-button{
         span{
                color: '#f00'
         }
    }

    .el-button::v-deep{
         span{
                color: '#f00'
         }
    }
</style>

3、当项目中使用的 css 扩展语言是 node-sass, 需要使用 /deep/ 或者 ::v-deep 深度选择器来修改 外用第三方组件的样式

css 复制代码
<style lang="scss" scoped>
    .el-button::v-deep{
         span{
                color: '#f00'
         }
    }

    /deep/.el-button{
         span{
                color: '#f00'
         }
    }
</style>

4、当项目中使用的 css 扩展语言是 dart-sass, 需要使用 ::v-deep 深度选择器来修改 外用第三方组件的样式,dart-sass不支持 /deep/ 和 >>> 的写法

css 复制代码
<style lang="scss" scoped>
    .el-button::v-deep{
         span{
                color: '#f00'
         }
    }
</style>

注意:

① 操作符 >>> 可能会因为无法编译而报错,可以使用 /deep/

② vue3.0 中使用 /deep/ 会报错,更推荐使用 ::v-deep

③ 对于使用了 css 预处理器(scss 、sass、 less)时,深度选择器 ::v-deep 比较通用

1.2.2 elementUI Dialog内容区域显示滚动条

所以在项目,我要使对话框的内容区域显示滚动条,同时去掉对话框原有的外部滚动条,使用样式穿透,代码如下:

html 复制代码
<el-dialog title="Dialog" class="roll-dialog"> </el-dialog>
css 复制代码
.rolling-dialog {
    overflow: hidden;
    ::v-deep .el-dialog .el-dialog__body {
      overflow-y: scroll;
      height: 400px;
    }
}

实际开发中还有对话框内容区域高度自适应的要求,可以阅读这篇 Element UI 弹窗(Dialog)改成自适应高度,仅body内容部分滚动

效果

1.3 注册页面------步骤条和表单联动 steps+form

element的步骤条整合表单(steps+form)

场景

在vue开发中,注册页面填写的信息过多,如果全部一起呈现,效果不是很好,这时候就可以用到步骤条来分步注册,这里用到了ElementUI里的steps组件和form

页面代码如下:

html 复制代码
<template>

//第一步:定义出4个步骤
<el-steps :active="active" finish-status="success" align-center :space="200" class="steps">
  <el-step title="验证手机号"></el-step>
  <el-step title="填写帐号信息"></el-step>
  <el-step title="注册成功" status="success"></el-step>
</el-steps>

//第二步:定义form表单
<el-form
  ref="registerForm"
  :model="registerForm"
  :rules="rules"
  class="form-body">
    
//第三步:定义3个盒子对象active =>0 到 2
<div v-show="active == 0">
	//第四步:放置表单项
	//...
	<el-form-item class="form-item" prop="phoneNum">
	    <el-input clearable placeholder="建议使用常用手机号" v-model="registerForm.phoneNum">
	</el-form-item>
	
</div>
<div v-show="active == 1"></div>
<div v-show="active == 2"></div>

</el-form>

//第五步:设置上一步和下一步的按钮
<el-button v-if="active < 3" style="margin-top: 12px" @click="next">下一步</el-button>
<el-button v-if="active > 1" style="margin-top: 12px" @click="pre">上一步</el-button>
   
 
</template>

对应的属性和方法

javascript 复制代码
data() {
    return {
    	 //默认第一步
		 active: 0,
	}
},
methods: {
    // 步骤条下一步的方法
    next() {
      if (this.active++ > 2) this.active = 0
    },
     // 步骤条上一步的方法
    pre() {
      if (this.active-- < 0) this.active = 0
    },
    
 }

1.4 注册页面------滑动拼图验证

Vue实现滑块拼图验证,这里使用了vue-monoplasty-slide-verify插件

参考 vue实现登录滑动拼图验证的两种方法,纯前端组件验证以及前后端同时验证

1.5 注册页面------element-ui组件Popover 弹出框 条件控制显示和隐藏 滑块验证码

场景:输入手机号码,手机格式正确,点击按钮验证才可显示弹出框

手动控制el-popver弹窗的显示与隐藏,给el-popver层绑定一个v-model,值为true或是false,这是官网上给的Attributes。

javascript 复制代码
<el-popover v-model="showPopover">

而且显示根本不用控制,el-popover有一个trigger属性,trigger可以为click/focus/hover/manual,默认值是click,所以单击就能触发,主要是弹窗的隐藏问题。

我们使用manul来控制显示

javascript 复制代码
<el-popover trigger="manual">

实际代码:

html 复制代码
<el-popover placement="top" width="320" trigger="manual" v-model="showSliderVerify">
  <div class="popper-title">
    <span>完成拼图验证</span>
    <i class="el-icon-close close-icon" @click="showSliderVerify=false"></i>
  </div>
  <slide-verify :l="42" :r="10" :w="310":h="155" 
                slider-text="向右滑动" 
                @success="onSuccess" 
                @fail="onFail" 
                @refresh="onRefresh">
  </slide-verify>
  <div style="margin-top: 15px">{{ msg }}</div>
  <!--点击控制弹窗的显示-->
  <el-button slot="reference" style="width: 400px" @click="openSliderVerify">
      点击按钮进行验证
  </el-button>
</el-popover>
javascript 复制代码
<script>
  export default {
    data() {
      return {
        showSliderVerify:false,//v-model默认值是false, click激活变成true
        msg: ""
      }
    },
    methods: {
        //滑块按钮点击
        openSliderVerify() {
          //只有手机号码通过才能显示滑块验证码
          this.$refs.registerForm.validateField("phoneNum", async (valid) => {
            if (!valid) {
              //手机号码格式正确,才可以显示滑块验证码
              this.showSliderVerify = true;
            } else {
              return false;
            }
          });
        },
        //滑块验证通过
        onSuccess(times) {
          this.msg = `success, 耗时${(times / 1000).toFixed(1)}s`;
          this.showSliderVerify = false;
          this.showCode = true;
          //验证码一通过,就自动获取验证码
          this.getCode();
        },
        //滑块验证失败
        onFail() {
          this.msg = "验证不通过";
        },
        //滑块验证刷新
        onRefresh() {
          this.msg = "";
          console.log("点击了刷新小图标");
        },
        //滑块验证刷新完成
        onFulfilled() {
          this.msg = "重新验证";
        },
    }
  }
</script>

代码中我还设置了样式,但是样式在当前vue文件下,怎么样调整都不动。

后来看来这个blog

原来是要在App.vue下写css样式,

css 复制代码
<style lang="less">
.popper-title {
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
  font-size: 16px;
  .close-icon {
    cursor: pointer;
    font-size: 20px;
    color: #4f4f4f;
  }
}
</style>

原因可以看下面这张图,你会发现 app 和 el-popover 是平级,又因为我们每个组件的style标签都写有 scoped 属性,所以在组件里写样式不起效

1.6 注册页面------获取手机验证码,进行手机号校验、验证码CD60秒

参考blog

场景

手机输入不能为空且必须为正确格式

点击下一步,如果未完成验证,则不可以进行下一步注册步骤

完成滑块拼图验证后,立即自动发送验证码,60s有效期,

由于注册页面用到了前面的提到的steps,所以手机及验证码的输入框是在 <div v-show="active == 0"></div>内,且只有手机号码输入正确,才可进行滑块拼图验证,滑块拼图验证通过之后,立即发送验证码

页面代码如下

html 复制代码
<el-steps :active="active" finish-status="success" align-center :space="200" class="steps" >
  <el-step title="验证手机号"></el-step>
  <el-step title="填写帐号信息"></el-step>
  <el-step title="注册成功"></el-step>
</el-steps>
<el-form ref="registerForm" :model="registerForm" :rules="rules" class="form-body">
   <!-- 步骤条 active==0 -->
   <div v-show="active == 0">
    <el-form-item class="form-item" prop="phoneNum">
      <el-input clearable placeholder="建议使用常用手机号" v-model="registerForm.phoneNum">
      <el-select
          v-model="registerForm.select"
          placeholder="中国+86"
          slot="prepend"
          style="width: 120px"
        >
          <el-option label="中国+86" value="中国+86"></el-option>
          <el-option label="+40" value="+40"></el-option>
          <el-option label="+111" value="+111"></el-option>
        </el-select>
      </el-input>
    </el-form-item>
    <el-form-item class="form-item" v-show="!showCode">
      <el-popover placement="top" width="320" trigger="manual" v-model="showSliderVerify">
        <div class="popper-title">
          <span>完成拼图验证</span>
          <i class="el-icon-close close-icon" @click="showSliderVerify = false"></i>
        </div>
        <slide-verify :l="42" :r="10" :w="310" :h="155" :imgs="bgimgs"
          slider-text="向右滑动"
          @success="onSuccess"
          @fail="onFail"
          @refresh="onRefresh"
        ></slide-verify>
        <div style="margin-top: 15px">{{ msg }}</div>
        <el-button slot="reference" class="wd400" @click="openSliderVerify">
            点击按钮进行验证
        </el-button>
      </el-popover>
      <div v-show="validateCode" class="el-form-item__error">请完成验证</div>
    </el-form-item>
    <el-form-item prop="code" class="form-item" v-show="showCode">
      <el-input clearable v-model="registerForm.code" placeholder="请输入验证码">
        <template slot="prepend">手机验证码</template>
        <el-button slot="append" :disabled="codeCd" size="samll" @click="getCode">
          <span v-if="codeCd">{{ long }}后重新获取</span>
          <span v-else>获取验证码</span>
        </el-button>
      </el-input>
    </el-form-item>
    <el-form-item class="form-item">
      <el-button class="wd400" @click="next">下一步</el-button>
    </el-form-item>
  </div>
</el-form>

对应的逻辑代码如下

javascript 复制代码
export default {
  name: "Register",
  data() {
    //验证手机号
    const validatePhone = (rule, value, callback) => {
      if (!value) {
        callback(new Error("手机号码不能为空"));
      }
      // 使用正则表达式验证手机号码
      if (!/^1[3456789]\d{9}$/.test(value)) {
        callback(new Error("手机号码格式不正确"));
      }
      //自定义校验规则,需要调用callback()函数
      callback();
    };
    return {
      registerForm: {
        phoneNum: null,
        code: "",
      },
      rules: {
        phoneNum: [
          {
            required: true,
            validator: validatePhone,
            trigger: "blur",
          },
        ],
        code: [
          {
            required: true,
            message: "验证码不能为空!",
            trigger: "blur",
          },
        ],
      },
      //验证码秒数倒计时
      long: 60,
      //验证码是否等候
      codeCd: false,
      //滑块拼图验证码msg
      msg: "",
      //滑块验证码背景图
      bgimgs: [],
      //步骤条的active
      active: 0,
      //是否显示滑块拼图验证
      showSliderVerify: false,
      //是否显示验证码
      showCode: false,
      //验证是否完成拼图滑块
      validateCode: false
     };
  },
  methods: {
    //滑块按钮点击
    openSliderVerify() {
      //只有手机号码通过才能显示滑块验证码
      this.$refs.registerForm.validateField("phoneNum", async (valid) => {
        if (!valid) {
          //手机号码格式正确,才可以显示滑块验证码
          this.showSliderVerify = true;
        } else {
          return false;
        }
      });
    },
    //步骤条下一步
    next() {
      //当前表格是否进行验证
      const { phoneNum, code } = this.registerForm;
      const { showCode } = this;
      //验证手机号
      if (!phoneNum) {
        this.$refs.registerForm.validateField("phoneNum");
        return;
      }
      //验证滑块拼图验证码
      if (!showCode) {
        //展示提示信息
        this.validateCode = true       
        return;
      } //完成滑块验证后,验证是否填写验证码
      else if (showCode && !code) {
        this.$refs.registerForm.validateField("code");
        return;
      }
      //以上都通过才进入下一步
      if (this.active++ > 2) this.active = 0;
    },
    //获取手机验证码
    getCode() {
      this.$refs.registerForm.validateField("phoneNum", async (valid) => {
        // valid是验证手机号码是否通过
        if (!valid) {
          // 获取验证码
          try {
            //发送验证码
            this.$store.dispatch("getCode", this.registerForm.phoneNum);
            //开始计时,60秒倒计时
            this.codeCd = true;
            const timer = setInterval(() => {
              this.long--;
              if (this.long <= 0) {
                this.long = 60;
                this.codeCd = false;
                clearInterval(timer);
              }
            }, 1000);
            //假设手动输入验证码
            this.registerForm.code = this.$store.state.user.code;
          } catch (error) {
            alert(error.message);
          }
        } else {
          return false;
        }
      });
    },
    //滑块验证通过
    onSuccess(times) {
      this.msg = `success, 耗时${(times / 1000).toFixed(1)}s`;
      this.showSliderVerify = false;
      this.showCode = true;
      //验证码一通过,就自动获取验证码
      this.getCode();
    },
    //滑块验证失败
    onFail() {
      this.msg = "验证不通过";
    },
    //滑块验证刷新
    onRefresh() {
      this.msg = "";
      //console.log("点击了刷新小图标");
    },
    //滑块验证刷新完成
    onFulfilled() {
      this.msg = "重新验证";
    },
  1. 进行手机号校验关键在对单个手机号输入框进行校验,需要使用到validateField对部分表单字段进行校验,valid是校验完的提示信息,当valid为空时代表校验成功
  2. 读秒和设置禁用,在校验成功时设置一个60s计时器,读秒过程禁用按钮,用了element-ui的按钮组件,在读秒过程中给按钮增加disabled属性;读秒过程结束,解除按钮禁用

1.7 注册页面------element-ui组件el-autocomplete带输入建议 自动补全后缀(邮箱地址)

效果:实现输入数字,自动补齐邮箱后缀

autocomplete 是一个可带输入建议的输入框组件,fetch-suggestions 是一个返回输入建议的方法属性,如 querySearch(queryString, cb),在该方法中你可以在你的输入建议数据准备好时通过 cb(data) 返回到 autocomplete 组件中。

html 复制代码
<el-autocomplete
    clearable
    v-model="registerForm.email"
    :fetch-suggestions="emailSuffix"
    :trigger-on-focus = 'false'
    @select="selectEmailSuffix"
    class="wd400"
    placeholder="请输入邮箱"
  >
    <template slot="prepend">邮箱验证</template>
  </el-autocomplete>
javascript 复制代码
data(){
    return {
        suffix: []
    }
},
mounted() {
  this.suffix = this.loadAll()
},
methods: {
    //邮箱后缀输入建议
    emailSuffix(queryString, callback) {
      console.log(queryString);
      let suffix = this.suffix
      let results = JSON.parse(JSON.stringify(suffix))
      for(let item in results) {
        results[item].value = queryString + '' + suffix[item].value
      }
      callback(results)
    },
    //选择的哪个值
    selectEmailSuffix(item) {
      console.log(item);
    },
    loadAll() {
      return [
        {"value": "@qq.com"},
        {"value": "@126.com"},
        {"value": "@163.com"},
        {"value": "@sohu.com"},
        {"value": "@Gmail.com"},
        {"value": "@Sina.com"}
      ]
    },
}

1.8 注册页面------用户、密码强弱校验+密码自定义规则校验、提交验证

参考vue3+ts+element-plus密码强弱校验+密码自定义规则校验

博客中用的是Vue3,自己项目中用的Vue2,去掉密码规则中的"是否包含3个及以上键盘连续字符;(横向、斜向都包括)"这一项,其他要求都差不多。

修修改改,实现效果如下:


页面代码

html 复制代码
<el-form ref="registerForm" :model="registerForm" :rules="rules" class="form-body">
    <div v-show="active ==0 ">
       <!-- 验证手机号.... -->
    </div>
    <div v-show="active == 1">
        <el-form-item class="form-item" prop="username">
          <el-input clearable v-model="registerForm.username" placeholder="账户唯一识别,可用来登录">
            <template slot="prepend">账号名</template>
          </el-input>
        </el-form-item>
        <el-form-item class="form-item" prop="password">
          <el-input
            clearable
            v-model="registerForm.password"
            show-password
            placeholder="请输入包含英文字母大小写、数字和特殊符号的 8-16 位组合"
          >
            <template slot="prepend">设置密码</template>
          </el-input>
          <div class="barbox"
               v-if="registerForm.password !== '' && registerForm.password !== undefined">
            <div class="strength" :style="{ color: barColor }">{{ strength }} </div>
            <div class="bar" :style="{ background: barColor, width: width + '%' }"></div>
          </div>
        </el-form-item>
        <el-form-item class="form-item" prop="repassword">
          <el-input
            clearable
            v-model="registerForm.repassword"
            show-password
            placeholder="请再次输入密码"
          >
            <template slot="prepend">确认密码</template>
          </el-input>
        </el-form-item>
        <el-form-item class="form-item" prop="email">
          <el-autocomplete
            clearable
            v-model="registerForm.email"
            :fetch-suggestions="emailSuffix"
            :trigger-on-focus="false"
            @select="selectEmailSuffix"
            class="wd400"
            placeholder="请输入邮箱"
          >
            <template slot="prepend">邮箱验证</template>
          </el-autocomplete>
        </el-form-item>
        <el-form-item class="form-item" prop="emailCode">
          <el-input
            clearable
            v-model="registerForm.emailCode"
            placeholder="请输入邮箱验证码"
          >
            <template slot="prepend">邮箱验证码</template>
            <el-button
              slot="append"
              :disabled="emailCodeCd"
              size="samll"
              @click="getEmailCode"
            >
              <span v-if="emailCodeCd">{{ emaillong }}后重新获取</span>
              <span v-else>获取验证码</span>
            </el-button>
          </el-input>
        </el-form-item>
        <el-form-item class="form-item">
          <el-button class="wd400" @click="submitForm">立即注册</el-button>
        </el-form-item>
    </div>
    <div v-show="active == 2">
        <div class="registerOk">
          <i class="el-icon-time icon"></i>
          <h1>恭喜您 {{ this.registerForm.username }}</h1>
          <span>您已成功注册为京东用户,祝您购物愉快~</span>
          <router-link class="btn" to="/home">去购物</router-link>
        </div>
     </div>
</el-form>

css代码

css 复制代码
.barbox {
    display: flex;
    align-items: center;
    height: 26px;
    .strength {
      font-size: 13px;
      color: #271e25;
      transition: 0.5s all ease;
      margin-right: 5px;
      flex-shrink: 0;
    }
    .bar {
      height: 5px;
      background: red;
      transition: 0.5s all ease;
      max-width: 400px;
    }
}
.registerOk {
  color: #333;
  font-size: 14px;
  display: flex;
  flex-flow: column;
  align-items: center;
  .icon {
    font-size: 40px;
    color: green;
  }
  h1 {
    font-size: 40px;
    margin: 16px 0;
  }
  span {
    margin-bottom: 16px;
  }
  .btn {
    padding: 10px 20px;
    background-color: #c81623;
    color: #fff;
  }
}

对应的逻辑代码

javascript 复制代码
//引入验证方法
import { checkPasswordRule, level } from "./CheckPassword";
export default {
  name: "Register",
  data() {
    //验证手机号
    const validatePhone = (rule, value, callback) => {
      if (!value) {
        callback(new Error("手机号码不能为空"));
      }
      // 使用正则表达式验证手机号码
      if (!/^1[3456789]\d{9}$/.test(value)) {
        callback(new Error("手机号码格式不正确"));
      }
      //自定义校验规则,需要调用callback()函数
      callback();
    };
    //密码验证
    const passwordValidate = (rule, value, callback) => {
      if (!value) {
        callback(new Error("密码不能为空"));
      } else {
        let name =
          this.registerForm.username === "" ? "" : this.registerForm.username;
        const result = checkPasswordRule(value, name);
        if (result === "校验通过") {
          callback();
        } else {
          callback(new Error(result));
        }
      }
      //该部分是只验证密码是否满足reg正则,而不进行强弱校验
      // const reg = /^(?=.*[A-Za-z])(?=.*[0-9])[A-Za-z0-9]{8,16}$/g;
      // if (!reg.test(value)) {
      //   callback(new Error("请输入包含英文字母、数字的 8-16 位组合"));
      // } else {
      //   callback();
      // }
    };
    //密码与确认密码不一样,一定要写在data里,但不是return里
    const repeatValidate = (rule, value, callback) => {
      if (!value) {
        callback(new Error("请再次输入密码"));
      } else if (value !== this.registerForm.password) {
        callback(new Error("两次输入密码不一致"));
      } else {
        callback();
      }
    };
    return {
      registerForm: {
        username: "",
        phoneNum: null,
        code: "",
        email: "",
        password: "",
        repassword: "",
        emailCode: "",
      },
      rules: {
        phoneNum: [
          {
            required: true,
            validator: validatePhone,
            trigger: "blur",
          },
        ],
        code: [
          {
            required: true,
            message: "验证码不能为空!",
            trigger: "blur",
          },
        ],
        password: [
          {
            required: true,
            validator: passwordValidate,
            trigger: "blur",
          },
        ],
        repassword: [
          {
            required: true,
            validator: repeatValidate,
            trigger: "blur",
          },
        ],
        username: [
          {
            required: true,
            message: "账户名不能为空",
            trigger: "blur",
          },
          {
            min: 4,
            max: 30,
            message: "账户名长度在4至30个字符之间",
            trigger: "blur",
          },
        ],
        email: [
          {
            required: true,
            message: "请输入邮箱地址",
            trigger: "blur",
          },
          {
            type: "email",
            message: "请输入正确的邮箱地址",
            trigger: ["blur", "change"],
          },
        ],
        emailCode: [
          {
            required: true,
            message: "邮箱验证码不能为空!",
            trigger: "blur",
          },
        ],
      },
      //验证码秒数倒计时
      long: 60,
      emaillong: 300,
      //密码强度背景色
      barColor: "",
      //密码强度长度
      width: "",
      //密码强度
      strength: "",
      //验证码是否等候
      codeCd: false,
      //邮箱验证码等候
      emailCodeCd: false,
    };
  },
  methods: {
    //提交表单_用户注册
    submitForm() {
      this.$refs["registerForm"].validate(async(valid)=>{
        if(valid) {
           try {
            const { phoneNum, code, password } = this.registerForm;
              phoneNum && code && password && (await this.$store.dispatch("userRegister", {
                phone: phoneNum,
                code: code,
                password:password,
              }));
			
            this.active++;
            // this.$router.push("/login");
          } catch (error) {
            alert(error.message);
          }
        }else {
          return false
        }
      })
    },  
  },
  watch: {
    "registerForm.password"(newVal, oldVal) {
      if (newVal !== "") {
        const res = level(newVal);
        this.strength = res;
        switch (res) {
          case "非常强":
            this.barColor = "#1B8EF8";
            this.width = "100";
            break;
          case "强":
            this.barColor = "green";
            this.width = "80";
            break;
          case "一般":
            this.barColor = "orange";
            this.width = "60";
            break;
          case "弱":
            this.barColor = "#ee795c";
            this.width = "40";
            break;
          case "非常弱":
            this.barColor = "red";
            this.width = "20";
            break;
        }
      }
    },
  },

强弱校验、规则校验 CheckPassword.js:

javascript 复制代码
// 数字
const REG_NUMBER = '.*\\d+.*'
//大写字母
const REG_UPPERCASE = '.*[A-Z].*'
//小写字母
const REG_LOWERCASE = '.*[a-z].*'
//特殊符号
const REG_SYMBOL = ".*[~!@#$%^&*()_+|<>,.?/:;'\\[\\]{}\"]+.*"
/**
 * 校验密码是否符合条件
 * @param password 密码
 * @param username 用户名
 */
export const checkPasswordRule = (password, username) => {
    if(password === '' ) {
        return "密码不能为空"
    }else if (password.length < 8 || password.length > 20) {
        return "密码长度应大于8小于20"
    } 
    if(username && password.indexOf(username) !== -1) {
        return "请勿包含用户名"
    }
    if(isContinuousChar(password)) {
        return "请勿包含3个及以上相同或连续的字符"
    }
    let i = 0
    if(password.match(REG_NUMBER)) i++
    if(password.match(REG_UPPERCASE)) i++
    if(password.match(REG_LOWERCASE)) i++
    if(password.match(REG_SYMBOL)) i++
    if(i<2) {
        return "数字、小写字母、大写字母、特殊字符,至少包含两种";
    }
    return "校验通过"
}

/**
 * 是否包含3个及以上相同或字典连续字符
 */
const isContinuousChar = (password) => {
    let chars = password.split('')
    let charCode = []
    for(let i=0; i<chars.length-2; i++) {
        charCode[i] = chars[i].charCodeAt(0)
    }
    for(let i=0; i<chars.length-2; i++) {
        let n1 = charCode[i]
        let n2 = charCode[i+1]
        let n3 = charCode[i+2]
        //判断重复字符
        if(n1 == n2 && n2 == n3) {
            return true
        }
        //判断连续字符: 正序+倒序
        if((n1 + 1 == n2 && n2 + 2 == n3) || (n1 - 1 == n2 && n2 - 2 == n3)) {
            return true
        }
    }
    return false
}

/**
 * 密码强度校验
 */

/**
 * 长度
 * @param str 
 */
const length = (str) => { 
    if(str.length<5){ 
        return 5;
    }else if(str.length<8){
        return 10;
    }else{
        return 25;
    }
}


/**
 * 字母
 * @param str 
 */
const letters = (str)=> {
    let count1 = 0, count2 = 0
    for(let i=0; i<str.length; i++) {
        if(str.charAt(i) >= 'a' && str.charAt(i) <= 'z'){
            count1++
        }
        if(str.charAt(i) >= 'A' && str.charAt(i) <= 'Z'){
            count2++
        }
    }
    if(count1==0 && count2==0) {
        return 0
    }
    if(count1!=0 && count2!=0) {
        return 20
    }
    return 10
}

/**
 * 数字
 * @param str 
 */
const numbers = (str)=> {
    let count = 0
    for(let i=0; i<str.length; i++) {
        if(str.charAt(i) >= '0' && str.charAt(i) <= '9'){
            count++
        }
    }
    if(count==0) {
        return 0
    }
    if(count==1) {
        return 10
    }
    return 20
}

/**
 * 符号
 * @param str 
 */
const symbols = (str)=> {
    let count = 0
    for(let i=0; i<str.length; i++) {
        if(str.charCodeAt(i)>=0x21 && str.charCodeAt(i)<=0x2F ||
        str.charCodeAt(i)>=0x3A && str.charCodeAt(i)<=0x40 ||
        str.charCodeAt(i)>=0x5B && str.charCodeAt(i)<=0x60 ||
        str.charCodeAt(i)>=0x7B && str.charCodeAt(i)<=0x7E ){
        count++;
    }
    }
    if(count==0) {
        return 0
    }
    if(count==1) {
        return 10
    }
    return 25
}

/**
 * 得分机制
 * @param str 
 */
const rewards = (str) => {
    let letter = letters(str)
    let number = numbers(str)
    let symbol = symbols(str)
    if(letter>0 && number>0 && symbol==0){    //字母和数字
        return 2;
    }
    if(letter==10 && number>0 && symbol>0){    //字母、数字和符号
        return 3;
    }
    if(letter==20 && number>0 && symbol>0){   //大小写字母、数字和符号
        return 5;
    }
    return 0;
}


/**
 * 最终评分
 * @param str 
 */
export const level = (str) => {
    let lengths=length(str);//长度
    let letter=letters(str);//字母
    let number=numbers(str);//数字
    let symbol=symbols(str);//符号
    let reward=rewards(str);//奖励
    let sum = lengths+letter+number+symbol+reward
    if(sum>=80) {
        return '非常强'
    }else if (sum>=60) {
        return "强"
    }else if(sum>=40) {
        return '一般'
    }else if(sum>=25) {
        return '弱'
    }else {
        return "非常弱"
    }
}

常用的密码校验正则Regex 正则表达式

包含英文字母大小写、数字和特殊符号的 8-16 位组合

/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[._~!@#$^&*])[A-Za-z0-9._~!@#$^&*]{8,16}$/g

2. 登录页面

2.1 登录页面------接口

api/index.js

javascript 复制代码
/*--------- 用户登录 ---------*/

//登录
export const reqUserLogin = (data) => requests({url: '/user/passport/login', method: 'post', data: data})

//登陆后获取用户信息(需要带着用户的token向服务器要用户信息)
export const reqUserInfo = () => requests({url: '/user/passport/auth/getUserInfo',method: 'get'})

//退出登录
export const reqLogout = () => requests({url: '/user/passport/logout',method: 'get'})

store/user.js

javascript 复制代码
import { reqUserLogin } from "@/api"

//home模块的Vuex模块
const state = {
    //state中数据默认初始值别瞎写,根据接口的返回值进行初始化
    token: localStorage.getItem("TOKEN"),
    userInfo: {}
}
const mutations = {
    USER_LOGIN(state, token){
        state.token = token
    },
    USER_INFO(state, data) {
        state.userInfo = data
    },
    USER_CLEAR(state) {
      //清除本地数据
      state.token = ""
      state.userInfo = {}
      localStorage.removeItem("TOKEN")
    }
}
const actions = {
    //用户登录
    async userLogin({commit}, userFrom) {
        let result = await reqUserLogin(userFrom) 
        //服务器下发token,用户唯一标识符(uuid)
        //将来经常通过带token找服务器要用户信息进行展示
        if(result.code == 200) {
            //token存入vuex
            commit("USER_LOGIN", result.data.token)
            //持久化存储token
            localStorage.setItem('TOKEN', result.data.token)
            return 'ok'
        } else {
            return Promise.reject(new Error(result.message))
        }
    },
    //获取用户信息
    async getUserInfo({commit}) {
        let result = await reqUserInfo()
        if(result.code == 200) {
            commit("USER_INFO", result.data)
            return 'ok'
        } else {
            return Promise.reject(new Error(result.message))
        }
    },
    //退出登录
    async userLogout({commit}) {
        let result = await reqLogout()
        if(result.code == 200) {
            commit("USER_CLEAR", result.data)
            return 'ok'
        } else {
            return Promise.reject(new Error(result.message))
        }
    }
}

export default{
    state,
    getters,
    mutations,
    actions
}

2.2 登录------token

登录成功的时候,后台为了区分你这个用户是谁-服务器下发token[令牌:唯一标识符]

一般登录成功服务器会下发token,前台持久化存储token,[带着token找服务器要用户信息进行展示]

Vuex不是持久化存储,如果token存储在Vuex,页面一刷新token数据就没有了,所以使用localStorage存储

登陆组件methods登陆函数userLogin

javascript 复制代码
methods: {
      async userLogin() {
        try {
          const {phone, password} = this
          phone && password && await this.$store.dispatch('userLogin', {phone,password})
          //登陆成功,有query参数,就跳到query参数指定的路由,无query参数,跳到home组件
          //这个query参数是导航守卫设置的,next("/login?redirect="+toPath),toPath是原路由
          let toPath = this.$route.query.redirect || '/home'
          this.$router.push(toPath)
        } catch (error) {
           alert(error.message)
        }
        
      }
    }

登录成功后跳转到指定路由或是首页,若是跳转到首页,在首页获取用户信息,向服务器请求用户信息,需要携带token

view/home/index.vue

javascript 复制代码
mounted() {
// 触发vuex的异步action调用, 从mock接口请求数据到state中
this.$store.dispatch("getFloorList")

//获取用户信息在首页展示
this.$store.dispatch("getUserInfo")
},

api/request.js 下的请求拦截器,设置携带token

javascript 复制代码
//配置请求拦截器
requests.interceptors.request.use(config => {
    //config内主要是对请求头Header配置
    //比如添加token
    //1、先判断uuid_token是否为空
    if(store.state.detail.uuid_token) {
        //2、userTempId字段和后端统一
        config.headers['userTempId'] = store.state.detail.uuid_token
    }
    //需要携带token带给服务器
    if(store.state.user.token) {
        config.headers.token = store.state.user.token
    }
    //开启进度条
    nprogress.start()
    return config;
})

登录接口返回的token

获取用户信息接口,携带token

2.3 登录页面------退出登录

javascript 复制代码
methods: {
    //退出登录
    async logout() {
      //1.发请求,通知服务器退出登录(清除数据,如token)
      //2. 清楚项目当中的用户数据(userInfo)
      try {
        //退出成功
        await this.$store.dispatch("userLogout")
        //回到首页
        this.$router.push('/home')
      } catch (error) {
        alert(error.message)
      }
    }
}

2.4 登录页面------全局导航守卫

存在的问题:

  1. 多个组件需要展示用户信息,需要在每一个组件的mounted中触发 获取用户信息接口函数
  2. 用户已经登录,不应该再回登陆页面
  3. 未登录,不允许跳转到购物车和订单

流程
为什么要判断name?

因为store中的token是通过localStorage获取的,token有存放在本地。当页面刷新时,本地token不会消失,所以store中的token也不会消失。但是,store中的其他数据(用户信息等)会清空,此时会出现用户信息不存在,但是有token,这种情况是不可以访问其他页面的,必须先去获取用户信息。由于用户信息是一个对象,所以我们通过它的一个属性name判断用户信息是否存在。

所以不仅要判断token,还要判断用户信息

router/index.js全局前置守卫代码

javascript 复制代码
//设置全局导航前置守卫
router.beforeEach(async(to, from, next) =>  {
    let token = store.state.user.token
    let name = store.state.user.userInfo.name
    //1、有token代表登录,全部页面放行
    if(token){
        //1.1登陆了,不允许前往登录页
        if(to.path==='/login'){
            next('/home')
        } else{
            //1.2、因为store中的token是通过localStorage获取的,token有存放在本地
            // 当页面刷新时,token不会消失,但是store中的其他数据会清空,
            // 所以不仅要判断token,还要判断用户信息

            //1.2.1、判断仓库中是否有用户信息,有放行,没有派发actions获取信息
            if(name)
                next()
            else{
                //1.2.2、如果没有用户信息,则派发actions获取用户信息
                try{
                    await store.dispatch('getUserInfo')
                    next()
                }catch (error){
                    //1.2.3、获取用户信息失败,原因:token过期
                    //清除前后端token,跳转到登陆页面
                    await store.dispatch('logout')
                    next('/login')
                }
            }
        }
    }else{
        //2、未登录,首页或者登录页可以正常访问
        //2. 未登录,支付页面、订单页
       let toPath = to.path
       if(toPath.indexOf('/pay') !== -1 || 
          toPath.indexOf('/trade')!==-1 || 
          toPath.indexOf('/center')!==-1) 
       {
          alert("请先登录")
          //登录成功后,回到原页面,源地址存储在地址栏中
      	  next("/login?redirect="+toPath)
        } else {
          // 其他可以正常访问
          next()
        }
    }
})

2.5 路由独享守卫

全局导航守卫已经帮助我们限制未登录的用户不可以访问相关页面。但是还会有一个问题。

例如:

用户已经登陆,用户在home页直接通过地址栏访问trade结算页面,发现可以成功进入该页面,正常情况,用户只能通过在shopcart页面点击去结算按钮才可以到达trade页面。我们可以通过路由独享守卫解决该问题

路由独享的守卫:只针对一个路由的守卫,所以该守卫会定义在某个路由中。

以上面问题为例,我们可以通过路由独享的守卫解决。

在trade路由信息中加入路由独享守卫

javascript 复制代码
//交易组件
    {
        name: 'Trade',
        path: '/trade',
        meta: {showFooter: true},
        component:  () => import('@/views/Trade'),
        //路由独享首位
        beforeEnter: (to, from, next) => {
            //购物车页面才可进入交易页面
            if(from.path ===  '/shopcart' ){
                next()
            }else{
                next(false)
            }
        }
    },
 //支付组件
	{
    path: '/pay',
    component: () => import('@/views/Pay'),
    meta: {showFooter: true},
    //路由独享守卫
    beforeEnter: (to, from, next) => {
      if(from.path === '/trade'){
        next()
      } else {
        next(false)
      }
    }   
  },

上面的代码已经实现了trade路由只能从shopcart路由跳转。next(false)指回到from路由。

但是,上面的代码还会有bug,就是当我们在shopcart页面通过地址栏访问trade时还是会成功。正常情况应该是只有当我们点击去结算按钮后才可以进入到trade页面。(这只是我个人观点)
解决办法:

在shopcart路由信息meta 中加一个flag ,初始值为false。当点击去结算按钮后,将flag置为true。在trade的独享路由守卫中判断一下flag是否为true,当flag为true时,代表是通过点击去结算按钮跳转的,所以就放行。

shopcart路由信息

javascript 复制代码
 //购物车
    {
        path: "/shopcart",
        component: () => import('@/views/ShopCart'),
        meta:{showFooter: true,flag: false},
    },

shopcart组件去结算按钮触发事件

javascript 复制代码
toTrade(){
    this.$route.meta.flag = true
    this.$router.push('/trade')
}

trade路由信息

javascript 复制代码
{
    name: 'Trade',
    path: '/trade',
    meta: {showFooter: true},
    component:  () => import('@/views/Trade'),
    //路由独享首位
    beforeEnter: (to, from, next) => {
        //购物车页面才可进入交易页面
        if(from.path ===  '/shopcart'&& from.meta.flag === true){
            from.meta.flag = false
            next()
        }else{
            next(false)
        }
    }
},

注意,判断通过后,在跳转之前一定要将flag置为false。

2.6 组件导航守卫

支付成功页面,设置只有通过支付页面后才能访问

javascript 复制代码
<script>
  export default {
    name: 'PaySuccess',
    //组件内守卫:通过路由规则,进入该组件时被调用
    //不能获取组件实例------this,因为守卫执行前,组件实例还未被创建
    beforeRouteEnter (to, from, next) {
      if(from.path == '/pay') {
        next()
      }else {
        next(false)
      }
    },
  }
</script>

2.7 全局封装API

推荐:API封装的具体步骤

若是想在组件里调用请求接口,而不通过Vuex来调用请求接口,该如何统一配置api接口,一次调用即可,而不须一个个引入

api/index.js文件是请求接口

main.js

javascript 复制代码
//统一接口api
import * as api from '@/api'
new Vue({
    render: (h) => h(App),
    beforeCreate() {
    	//全局事件总线
    	Vue.prototype.$bus = this;
        Vue.prototype.$api = api;
  },
})

组件内发请求

javascript 复制代码
methods: {
    //提交订单
    submitOrder() {
      console.log(this.$API.reqSubmitOrder());
    }
}

3. 二维码生成

qrcode

javascript 复制代码
import QRCode from "qrcode"
//立即支付弹出框
async openMsgBox(){
    //生成二维码(地址)
    let code = await QRCode.toDataURL(this.payInfo.codeUrl)
    this.$alert(`<img src=${code} />`, '请微信支付', {
      dangerouslyUseHTMLString: true,
      center: true,
      showCancelButton: true,
      confirmButtonText: '已支付成功',
      cancelButtonText: '支付遇见问题',
      showClose: false
    })
}

4. 二级路由拆分(个人中心)

菜单栏

html 复制代码
<dt><i>·</i> 订单中心</dt>
    <dd>
      <router-link to="/center/myorder">我的订单</router-link>
    </dd>
    <dd>
      <router-link to="/center/grouporder">团购订单</router-link>
    </dd>
</dt>

个人中心路由

javascript 复制代码
//个人中心
{
    path: '/center',
    component:  () => import('@/views/Center'),
    children: [
        {
            //二级路由要么不写/,要么写全:'/center/myorder'
            path: 'myorder',
            component: () => import('@/views/Center/MyOrder')
        },
        {
            path: 'groupbuy',
            component: () => import('@/views/Center/GroupOrder'),
        },
        //默认显示
        {
            path: '',
            redirect: 'myorder'
        }
    ]
}

{ path: '', redirect: 'myorder' }表示当我们访问center路由时,center中的router-view部分默认显示myorder二级路由内容。

我们的子路由最好放在父路由文件夹下,如下所示。

注意:当某个路由有子级路由时,父级路由须要一个默认的路由,因此父级路由不能定义name属性

5. 图片懒加载

在网络不好时,每个图片都有一个基础的默认图片,即在请求服务器结束前 加载默认设置的图片

懒加载vue-lazyload插件官网

插件的使用直接参考官方教程,很简单。

npm i vue-lazyload

vue使用插件的步骤,main.js

javascript 复制代码
import VueLazyload from "vue-lazyload

import loadingImg from '@/assets/images/loading.jpeg'
Vue.use(VueLazyload, {
  //懒加载默认的图片
  loading: loadingImg
})

使用懒加载

html 复制代码
<img v-lazy="good.defaultImg" />

在使用中报错 如下图所示:

因为该 模块 版本问题, 可安装低版本的 vue-lazyload 来解决该问题:

css 复制代码
# 先写在原有的安装
npm uninstall vue-lazyload --save

# 再安装低版本的
npm install vue-lazyload@1.3.3 --save

6. 路由懒加载

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

javascript 复制代码
// 将
// import Center from '@/views/Center'
// 替换成
const Center = () => import('./views/Center')
{
        path: '/center',
        component: Center,
        meta: { showFooter: true },
}

component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。

7. 项目上线

7.1 打包

npm run build

会生成dist打包文件。

dist就是我们打包好的项目文件

dist文件下的js文件存放我们所有的js文件,并且经过了加密,并且还会生成对应的map文件。

**map文件作用:**因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以像未加密的代码一样,准确的输出是哪一行那一列有错。

当然map文件也可以去除(map文件大小还是比较大的)

vue.config.js配置productionSourceMap: false即可。

注意:vue.config.js配置改变,需要重启项目

7.2 购买云服务器

阿里云、腾讯云

记得重置密码

设置安全组 开放端口

利用xshell等工具登录服务器

nginx

1、如何通过服务器IP地址直接访问到项目?

在服务器上部署dist文件地址:/root/project/dist

2、项目数据来自于哪个服务器

通过nginx从数据服务器拿数据

配置Nginx:在etc文件下

shell 复制代码
cd etc
ls

安装nginx:yum install nginx

shell 复制代码
cd nginx

存在文件nginx.conf

编辑vim nginx.conf

解决第一个问题:

shell 复制代码
location /{
root /root/project/dist;
index index.html;
try_files $uri/ /index.html;
}

解决第二个问题:

shell 复制代码
location /api{
	proxy_pass http://39.983123.211;

启动nginx服务器:service nginx start

相关推荐
Nu11PointerException1 小时前
JAVA笔记 | ResponseBodyEmitter等异步流式接口快速学习
笔记·学习
亦枫Leonlew2 小时前
三维测量与建模笔记 - 3.3 张正友标定法
笔记·相机标定·三维重建·张正友标定法
考试宝3 小时前
国家宠物美容师职业技能等级评价(高级)理论考试题
经验分享·笔记·职场和发展·学习方法·业界资讯·宠物
黑叶白树4 小时前
简单的签到程序 python笔记
笔记·python
幸运超级加倍~5 小时前
软件设计师-上午题-15 计算机网络(5分)
笔记·计算机网络
芊寻(嵌入式)6 小时前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
准橙考典7 小时前
怎么能更好的通过驾考呢?
人工智能·笔记·自动驾驶·汽车·学习方法
密码小丑8 小时前
11月4日(内网横向移动(一))
笔记
鸭鸭梨吖9 小时前
产品经理笔记
笔记·产品经理
齐 飞9 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb