• SpringCloud微服务实战——搭建企业级开发框架(四十七):【移动开发】整合uni-app搭建移动端快速开发框架-添加Axios并实现登录功能


      uni-app自带uni.request用于网络请求,因为我们需要自定义拦截器等功能,也是为了和我们后台管理保持统一,这里我们使用比较流行且功能更强大的axios来实现网络请求。

      Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

    Axios特性:
    • 从浏览器创建 XMLHttpRequests
    • 从 node.js 创建 http 请求
    • 支持 Promise API
    • 拦截请求和响应
    • 转换请求和响应数据
    • 取消请求
    • 自动转换JSON数据
    • 客户端支持防御XSRF

    一、安装axios和axios-auth-refresh组件

    1、新增uni-app自定义常量配置文件

      HBuilderX有针对于uni-app的通用配置,很多通用常量可以直接配置在manifest.json文件中,且HBuilderX提供图形化配置界面。但我们有许多业务系统相关的常量配置,那么就需要一个自定义常量配置文件。
      uni-app中定义全局常量有多种方式,在vue.js框架中也可以使用App.vue里面的globalData,根据业务需求,自定义常量可能会很多,不便于和官方配置融合在一起,所以这里使用新增配置project.config.js文件并挂载Vue.prototype的方式来实现常量配置。

    • 在工程的根目录下新增project.config.js
     module.exports = {
            # 配置请求后台地址
    	APP_API_BASE_URL: 'http://127.0.0.1:8080',
    	# 多租户项目,这里是默认的租户id
    	APP_TENANT_ID: '0',
    	# OAuth2授权的用户名密码
    	APP_CLIENT_ID: 'gitegg-admin',
    	# client_id:client_secret加密后的值,直接传,不需要再进行BASE64加密
    	APP_CLIENT_SECRET: 'Z2l0ZWdnLWFkbWluOjEyMzQ1Ng=='
    }
    
    • 在main.js中导入、挂载project.config.js
     // 导入js文件
    import ProjectConfig from './project.config'
     // 挂载
    Vue.prototype.$ProjectConfig = ProjectConfig
    
    • 在项目中引用
    this.$ProjectConfig.APP_API_BASE_URL
    
    • 如果是在APP挂在前引用,那么使用以下方法引用
    import ProjectConfig from './project.config'
    
    2、打开HBuilderX终端命令窗口,用于执行yarn安装命令

    HBuilderX默认没有开启终端命令窗口,选中项目,有两种方式打开命令窗口:

    • 按快捷键Ctrl+Alt+T打开终端窗口
    • 菜单栏中,选择 视图 > 显示终端(C)
    3、执行安装axios(http请求拦截)和 axios-auth-refresh(强大的token刷新)组件命令
    yarn add axios
    yarn add axios-auth-refresh
    
    4、在目录/common/utils新建axios.js,创建Axios 实例
    const VueAxios = {
      vm: {},
      // eslint-disable-next-line no-unused-vars
      install (Vue, instance) {
        if (this.installed) {
          return
        }
        this.installed = true
        if (!instance) {
          // eslint-disable-next-line no-console
          console.error('You have to install axios')
          return
        }
        Vue.axios = instance
        Object.defineProperties(Vue.prototype, {
          axios: {
            get: function get () {
              return instance
            }
          },
          $http: {
            get: function get () {
              return instance
            }
          }
        })
      }
    }
    export {
      VueAxios
    }
    
    5、在目录/common/utils新建request.js,自定义Axios拦截器

    在这里定义拦截器主要用于:自动设置token、token过期刷新、统一异常提示、返回数据处理等功能。

    import axios from 'axios'
    import createAuthRefreshInterceptor from 'axios-auth-refresh'
    import store from '@/store'
    import { serialize } from '@/common/util'
    import { VueAxios } from './axios'
    import { ACCESS_TOKEN, REFRESH_ACCESS_TOKEN } from '@/store/mutation-types'
     // 导入js文件
    import ProjectConfig from '@/project.config.js'
    
    // uni-app适配
    axios.defaults.adapter = function(config) {
      return new Promise((resolve, reject) => {
          var settle = require('axios/lib/core/settle');
          var buildURL = require('axios/lib/helpers/buildURL');
          uni.request({
              method: config.method.toUpperCase(),
              url: config.baseURL + buildURL(config.url, config.params, config.paramsSerializer),
              header: config.headers,
              data: config.data,
              dataType: config.dataType,
              responseType: config.responseType,
              sslVerify: config.sslVerify,
              complete: function complete(response) {
                  response = {
                      data: response.data,
                      status: response.statusCode,
                      errMsg: response.errMsg,
                      header: response.header,
                      config: config
                  };
                  settle(resolve, reject, response);
              }
          })
      })
    }
    
    // 创建 axios 实例
    const request = axios.create({
      // API 请求的默认前缀
      baseURL: ProjectConfig.APP_API_BASE_URL,
      timeout: 30000 // 请求超时时间
    })
    
    // 当token失效时,需要调用的刷新token的方法
    const refreshAuthLogic = failedRequest =>
      axios.post(ProjectConfig.APP_API_BASE_URL + '/oauth/token',
      serialize({
          grant_type: 'refresh_token',
          refresh_token: uni.getStorageSync(REFRESH_ACCESS_TOKEN)
        }),
        {
          headers: { 'TenantId': ProjectConfig.APP_TENANT_ID, 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + ProjectConfig.APP_CLIENT_SECRET },
          skipAuthRefresh: true // 刷新token请求过期,不再进行刷新
        }
        ).then(tokenRefreshResponse => {
          if (tokenRefreshResponse.status === 200 && tokenRefreshResponse.data && tokenRefreshResponse.data.success) {
            const result = tokenRefreshResponse.data.data
            uni.setStorageSync(ACCESS_TOKEN, result.tokenHead + result.token, result.expiresIn * 1000)
            uni.setStorageSync(REFRESH_ACCESS_TOKEN, result.refreshToken, result.refreshExpiresIn * 1000)
            failedRequest.response.config.headers['Authorization'] = result.tokenHead + result.token
          } else if (tokenRefreshResponse.status === 200 && tokenRefreshResponse.data &&
            !tokenRefreshResponse.data.success && tokenRefreshResponse.data.code === 401) {
              store.dispatch('Timeout').then(async () => {
    			uni.navigateTo({
    				url: '/pages/login/login'
    			})
            })
          }
          return Promise.resolve()
    })
    
    // 初始化刷新token拦截器
    createAuthRefreshInterceptor(request, refreshAuthLogic, {
      pauseInstanceWhileRefreshing: true // 当刷新token执行时,暂停其他请求
    })
    
    // 异常拦截处理器
    const errorHandler = (error) => {
      if (error.response) {
        const data = error.response.data
        if (error.response.status === 403) {
    	  uni.showToast({
    	  	title: '您没有权限访问此接口',
    	  	icon:'error',
    		duration: 2000
    	  });
        } else if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
           // 当刷新token超时,则调到登录页面
    	   uni.showModal({
    		title: '登录超时',
    	   	content: '由于您长时间未操作, 为确保安全, 请重新登录系统进行后续操作 !',
    		confirmText: '重新登录',
    	   	showCancel: false,
    		success: (res) => {
    			if(res.confirm) {  
    				store.dispatch('Timeout').then(() => {
    				    uni.navigateTo({
    				    	url: '/pages/login/login'
    				    })
    				})
    			} 
    		} 
    	   })
        }
      }
      return Promise.reject(error)
    }
    
    // request interceptor
    request.interceptors.request.use(config => {
      const token = uni.getStorageSync(ACCESS_TOKEN)
      // 如果 token 存在
      // 让每个请求携带自定义 token 请根据实际情况自行修改
      if (token && config.authenticationScheme !== 'Basic') {
        config.headers['Authorization'] = token
      }
      config.headers['TenantId'] = ProjectConfig.APP_TENANT_ID
      return config
    }, errorHandler)
    
    // response interceptor
    request.interceptors.response.use((response) => {
      const res = response.data
      if (res && res.code) {
        if (res.code !== 200) {
    	  uni.showToast({
    	  	title: '操作失败: ' + res.msg,
    	  	icon:'error',
    	  	duration: 2000
    	  });
          return Promise.reject(res || 'Error')
        } else {
          return response.data
        }
      } else {
        return response
      }
    }, errorHandler)
    
    const installer = {
      vm: {},
      install (Vue) {
        Vue.use(VueAxios, request)
      }
    }
    
    export default request
    
    export {
      installer as VueAxios,
      request as axios
    }
    
    

    二、请求后台接口并实现登录功能

    1、新建api目录,用于存放所有后台请求的接口,在api目录下新建login目录,存放用于登录的相关接口
    2、在/api/login目录下新增login.js
    import request from '@/common/utils/request'
    import ProjectConfig from '@/project.config.js'
    
    const loginApi = {
      // 登录
      Login: '/oauth/token',
      // 退出登录
      Logout: '/oauth/logout',
      // 获取系统配置的验证码类型
      CaptchaType: '/oauth/captcha/type',
      // 获取图片验证码
      ImageCaptcha: '/oauth/captcha/image',
      // 发送短信验证码
      SendSms: '/oauth/sms/captcha/send',
      // 获取用户信息
      UserInfo: '/system/account/user/info',
      // 第三方登录
      SocialLoginUrl: '/oauth/social/login/',
      // 第三方登录回调
      SocialLoginCallback: '/oauth/social/',
      // 第三方用户绑定---通过手机号验证码绑定
      SocialBindMobile: '/oauth/social/bind/mobile',
      // 第三方用户绑定---通过账号密码绑定
      SocialBindAccount: '/oauth/social/bind/account',
      // 发送短信验证码
      SmsSend: '/extension/sms/code/send',
      // 校验短信验证码
      SmsCheckPre: '/extension/sms/check/code/pre',
      // 校验短信验证码
      SmsCheck: '/extension/sms/check/code',
      // 发送注册短信
      SmsRegisterSend: '/system/account/register/sms/send',
      // 账户注册
      Register: '/system/account/register',
      // 校验用户是否存在
      CheckUserExist: '/system/account/register/check'
    }
    
    export default loginApi
    
    /**
     * OAuth2登录
     * @param parameter
     * @returns {*}
     */
    export function login (parameter) {
      return request({
        url: loginApi.Login,
        authenticationScheme: 'Basic',
        method: 'post',
        headers: { 'Authorization': 'Basic ' + ProjectConfig.APP_CLIENT_SECRET },
        skipAuthRefresh: true,
        data: parameter
      })
    }
    
    /**
     * OAuth2退出登录
     * @param parameter
     * @returns {*}
     */
    export function logout (parameter) {
      return request({
        url: loginApi.Logout,
        method: 'post',
        skipAuthRefresh: true,
        data: parameter
      })
    }
    
    /**
     * 获取验证码类型
     * @param parameter
     * @returns {*}
     */
    export function getCaptchaType () {
      return request({
        url: loginApi.CaptchaType,
        method: 'get'
      })
    }
    
    /**
     * 获取图片验证码
     * @param parameter
     * @returns {*}
     */
    export function getImageCaptcha () {
      return request({
        url: loginApi.ImageCaptcha,
        method: 'get'
      })
    }
    
    /**
     * 获取短信验证码
     * @param parameter
     * @returns {*}
     */
    export function getSmsCaptcha (parameter) {
      return request({
        url: loginApi.SendSms,
        method: 'post',
        data: parameter
      })
    }
    
    /**
     * 获取用户信息
     * @param parameter
     * @returns {*}
     */
    export function getInfo () {
      return request({
        url: loginApi.UserInfo,
        method: 'get'
      })
    }
    
    /**
     * 获取第三方登录的URL
     * @param {Object} socialType
     */
    export function getSocialLoginUrl (socialType) {
      return request({
        url: loginApi.SocialLoginUrl + socialType,
        method: 'get'
      })
    }
    
    /**
     * 第三方登录回调地址
     * @param {Object} socialType
     * @param {Object} parameter
     */
    export function socialLoginCallback (socialType, parameter) {
      return request({
        url: loginApi.SocialLoginCallback + socialType + '/callback',
        method: 'get',
        params: parameter
      })
    }
    
    /**
     * 发送短信验证码
     * @param {Object} parameter
     */
    export function sendSmsCode (parameter) {
      return request({
        url: loginApi.SmsSend,
        method: 'post',
        data: parameter
      })
    }
    
    /**
     * 校验短信验证码
     * @param {Object} parameter
     */
    export function checkSmsCode (parameter) {
      return request({
        url: loginApi.SmsCheckPre,
        method: 'get',
        params: parameter
      })
    }
    
    /**
     * 发送注册短信验证码
     * @param {Object} parameter
     */
    export function smsRegisterSend (parameter) {
      return request({
        url: loginApi.SmsRegisterSend,
        method: 'post',
        data: parameter
      })
    }
    
    /**
     * 校验用户是否存在
     * @param {Object} parameter
     */
    export function checkUserExist (parameter) {
      return request({
        url: loginApi.CheckUserExist,
        method: 'post',
        data: parameter
      })
    }
    
    /**
     * 用户注册
     * @param {Object} parameter
     */
    export function userRegister (parameter) {
      return request({
        url: loginApi.Register,
        method: 'post',
        data: parameter
      })
    }
    
    /**
     * 第三方用户绑定---通过手机号验证码绑定
     * @param {Object} parameter
     */
    export function userBindMobile (parameter) {
      return request({
        url: loginApi.SocialBindMobile,
        method: 'post',
        data: parameter
      })
    }
    
    /**
     * 第三方用户绑定---通过账号密码绑定
     * @param {Object} parameter
     */
    export function userBindAccount (parameter) {
      return request({
        url: loginApi.SocialBindAccount,
        method: 'post',
        data: parameter
      })
    }
    

    3、在/pages目录下创建login目录,新增login.vue登录页面,用于登录。

    
    <template>
    	<view class="login-bg">
    		<br /><br /><br /><br /><br /><br /><br />
    		<view class="t-login">
    			<form class="cl">
    				<view class="t-a">
    					<image src="@/static/login/user.png">image>
    					<input type="text" name="username" placeholder="请输入手机号码" maxlength="11" v-model="username" />
    				view>
    				<view class="t-a">
    					<image src="@/static/login/pwd.png">image>
    					<input type="password" name="password" maxlength="100" placeholder="请输入密码" v-model="password" />
    				view>
    				<button @tap="login()">登 录button>
    				<view class="t-c">
    					<text class="t-c-txt" @tap="reg()">注册账号text>
    					<text @tap="forgotPwd()">忘记密码text>
    				view>
    			form>
    			<view class="t-f"><text>—————— 其他登录方式 ——————text>view>
    			<view class="t-e cl">
    				<view class="t-g" @tap="wxLogin()"><image src="@/static/login/wx2.png">image>view>
    				<view class="t-g" @tap="zfbLogin()"><image src="@/static/login/qq2.png">image>view>
    				<view class="t-g" @tap="zfbLogin()"><image src="@/static/login/wb.png">image>view>
    			view>
    		view>
    		<image class="img-a" src="@/static/login/bg1.png">image>
    	view>
    template>
    <script>
    import md5 from '@/common/md5.min.js';
    import { mapActions } from 'vuex'
    import { ACCESS_TOKEN, REFRESH_ACCESS_TOKEN } from '@/store/mutation-types'
    export default {
    	data() {
    		return {
    			username: '',
    			password: '',
    			grant_type: 'password'
    		};
    	},
    	onLoad() {},
    	methods: {
    		...mapActions(['Login', 'Logout']),
    		login() {
    			var that = this;
    			if (!that.username) {
    				uni.showToast({ title: '请输入手机号', icon: 'none' });
    				return;
    			}
    			if (!/^[1][3,4,5,7,8,9][0-9]{9}$/.test(that.username)) {
    				uni.showToast({ title: '请输入正确手机号', icon: 'none' });
    				return;
    			}
    			if (!that.password) {
    				uni.showToast({ title: '请输入密码', icon: 'none' });
    				return;
    			}
    			const loginParams = {}
    			loginParams.username = that.username
    			loginParams.grant_type = 'password'
    			loginParams.password = md5(that.password)
    			that.Login(loginParams)
    			  .then((res) => this.loginSuccess(res))
    			  .catch(err => this.requestFailed(err))
    			  .finally(() => {
    
    			})
    		},
    		loginSuccess (res) {
    		  // 判断是否记住密码
    		  uni.showToast({ title: '登录成功!', icon: 'none' });
    		  uni.switchTab({
    		  	url: '/pages/tabBar/component/component',
    			fail(err) {
    				console.log(err)
    			}
    		  })
    		},
    		requestFailed (res) {
    		 // 判断是否记住密码
    		  uni.showToast({ title: '登录失败:' + res.msg, icon: 'none' });
    		},
    		//忘记密码
    		forgotPwd() {
    			uni.showToast({ title: '忘记密码', icon: 'none' });
    		},
    		//立刻注册
    		reg() {
    			uni.showToast({ title: '注册账号', icon: 'none' });
    		}
    	}
    };
    script>
    <style>
    .img-a {
    	width: 100%;
    	position: absolute;
    	bottom: 0;
    }
    .login-bg {
    	height: 100vh;
    	background-image: url(/static/login/bg3.png);
    }
    .t-login {
    	width: 580rpx;
    	padding: 55rpx;
    	margin: 0 auto;
    	font-size: 28rpx;
    	background-color: #ffffff;
    	border-radius: 20rpx;
    	box-shadow: 0 5px 7px 0 rgba(0, 0, 0, 0.15);
    	z-index: 9;
    }
    .t-login button {
    	font-size: 28rpx;
    	background: linear-gradient(to right, #ff8f77, #fe519f);
    	color: #fff;
    	height: 90rpx;
    	line-height: 90rpx;
    	border-radius: 50rpx;
    }
    
    .t-login input {
    	padding: 0 20rpx 0 120rpx;
    	height: 90rpx;
    	line-height: 90rpx;
    	margin-bottom: 50rpx;
    	background: #f6f6f6;
    	border: 1px solid #f6f6f6;
    	font-size: 28rpx;
    	border-radius: 50rpx;
    }
    .t-login .t-a {
    	position: relative;
    }
    .t-login .t-a image {
    	width: 40rpx;
    	height: 40rpx;
    	position: absolute;
    	left: 40rpx;
    	top: 28rpx;
    	padding-right: 20rpx;
    }
    .t-login .t-b {
    	text-align: left;
    	font-size: 46rpx;
    	color: #000;
    	padding: 300rpx 0 120rpx 0;
    	font-weight: bold;
    }
    .t-login .t-d {
    	text-align: center;
    	color: #999;
    	margin: 80rpx 0;
    }
    .t-login .t-c {
    	text-align: right;
    	color: #666666;
    	margin: 30rpx 30rpx 40rpx 0;
    }
    .t-login .t-c .t-c-txt {
    	margin-right: 300rpx;
    }
    .t-login .t-e {
    	text-align: center;
    	width: 600rpx;
    	margin: 40rpx auto 0;
    }
    .t-login .t-g {
    	float: left;
    	width: 33.33%;
    }
    
    .t-login .t-e image {
    	width: 70rpx;
    	height: 70rpx;
    }
    .t-login .t-f {
    	text-align: center;
    	margin: 80rpx 0 0 0;
    	color: #999;
    }
    .t-login .t-f text {
    	margin-left: 20rpx;
    	color: #b9b9b9;
    	font-size: 27rpx;
    }
    .t-login .uni-input-placeholder {
    	color: #aeaeae;
    }
    .cl {
    	zoom: 1;
    }
    .cl:after {
    	clear: both;
    	display: block;
    	visibility: hidden;
    	height: 0;
    	content: '\20';
    }
    style>
    
    4、将页面中用到的图片,复制到/static/login目录下
    5、配置pages.json文件,将新增的login.vue文件目录加入到配置中。pages.json类似于vue.js工程下的路由页面配置
    6、在App.vue文件的onLaunch方法中新增判断,当token为空时,跳转到我们刚刚新建的登录界面。
    			const token = uni.getStorageSync(ACCESS_TOKEN)
    			if(!token || token === ''){
    				uni.navigateTo({
    					url: '/pages/login/login'
    				})
    			} else {
    				console.log('已登录');
    			}
    

    三、在手机模拟器中运行并预览登录界面

      上文中介绍了如果配置HBuilderX连接手机模拟器,预览并调试uni-app项目,这里我们通过以上配置和编写,实现了登录界面,现在我们可以在手机模拟器中查看刚刚写的登录页面了。

    1、启动手机模拟器 > 双击桌面的nox_adb快捷方式
    2、在HBuilder X中依次点击 运行 -> 运行到手机或模拟器 -> 运行到Android App基座

    image.png

    3、弹出框会显示我们已连接的模拟器,点击运行,HBuilderX就可以自动打包app发布到模拟器中运行,并可以在HBuilderX控制台查看运行日志。

    image.png

    4、在手机模拟器展示的登录界面中,输入我们系统用户的手机号码 + 密码,登录成功后即可跳转到登录后的界面。

    image.png
    image.png

    源码地址:

    Gitee: https://gitee.com/wmz1930/GitEgg

    GitHub: https://github.com/wmz1930/GitEgg

  • 相关阅读:
    靠“山寨”发家的名创优品,如今是什么模样
    阿里云交互式建模(PAI-DSW)训练并微调推理ChatGLM模型
    全新出炉:7 月编程语言最新排行榜
    如何替换模型的骨干网络(backbone)
    高速DSP系统设计参考指南(二)传输线(TL)效应
    C\C++ 使用RapidJSON库,轻松解析和生成JSON
    ZooKeeper的权限控制--ACL
    数据库系统原理与应用教程(045)—— MySQL 查询(七):聚合函数
    Oracle之ADG与DG的区别?
    12.88万的小魔驼2.0量产交付,末端物流自动配送从概念走向现实
  • 原文地址:https://www.cnblogs.com/FullStackProgrammer/p/16831832.html