仿京东 项目笔记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

相关推荐
Li_RuiQi12 小时前
DAgger方法演进_独立学习笔记
笔记·学习·计算机视觉
Li_RuiQi16 小时前
DAgger与交互式模仿学习_ACT学习笔记
笔记·学习
He BianGu16 小时前
【笔记】C# 中BindingFlags的详细作用
笔记·c#
AI视觉网奇16 小时前
turtle 入门教程笔记
笔记
跨知易知识产权17 小时前
美国专利确权是什么,和专利诉讼有什么不一样?
笔记
杨逢昌工厂6S管理17 小时前
126-杨逢昌6S三环体系:越做越乱的现场熵增机理与基准化治理——一线现场诊断笔记
经验分享·笔记·职场和发展·学习方法
悟道书生17 小时前
若依 RBAC 权限控制学习笔记
java·网络·笔记·sql·学习·vue
陈哈哈111118 小时前
折腾了半天,总算把 Vim 9.2 装明白了
笔记
陈哈哈111118 小时前
折腾了半天,总算把 OpenGL 的开发环境给装明白了
笔记
深圳老胡19 小时前
STM32F103 硬件 SPI 与软件 IO 模拟 SPI 最高速度差异详解
笔记·stm32·单片机·代码规范·1024程序员节