img

这个章节主要实现以下功能:

  1. 页面开发
  2. 账号登录
  3. 授权登录

# 页面开发

<template>
    <view>
        <view class="login-form">
            <input class="input-row js-input-numer" v-model="account" type="number" maxlength="11" placeholder="输入手机号"/>
            <input class="input-row" v-model="password" @confirm="bindLogin" type="password" maxlength="20" placeholder="输入密码" confirm-type="登录"/>
            <button type="button" class="btn login-btn" @click="bindLogin">登录</button>
            <view class="bot flex-box">
                <navigator class="sign" hover-class="none">立即注册</navigator>
                <navigator class="forget" hover-class="none">忘记密码?</navigator>
            </view>
        </view>
        <view class="auth-row" v-if="hasProvider">
            <view class="auth-image" v-for="provider in providerList" :key="provider.value">
                <!-- #ifdef APP-PLUS -->
                <image class="img" :src="provider.image" @click="authLogin(provider.value)"></image>
                <!-- #endif -->
                <!-- 小程序处理 -->
                <!-- #ifdef MP-WEIXIN -->
                <button v-if="provider.value == 'weixin'" class="log-btn" open-type="getUserInfo" @getuserinfo="getUserInfo">
                    <image class="img" :src="provider.image"></image>
                </button>
                <!-- #endif -->
            </view>
        </view>
    </view>
</template>
<script>

export default {
    data() {
        return {
            hasProvider: false, // 是否有服务商平台可登录
            providerList: [],
            account: '',
            password: ''
        };
    },
    onReady() {
        this.getProvider();
        // #ifdef H5
        this.$nextTick(() => {
            document.querySelector('.js-input-numer input').setAttribute("pattern", "[0-9]*")
        })
        // #endif
    },
    methods: {
        // 获取服务供应商
        getProvider() {
            const  providerList = ['weixin', 'qq', 'sinaweibo'];
            // 判断是否用某种登录工具登录
            uni.getProvider({
                service: 'oauth',
                success: res => {
                    if (res.provider && res.provider.length) {
                        for (let i = 0; i < res.provider.length; i++) {
                            var tempProvider = res.provider[i];
                            if (~providerList.indexOf(tempProvider)) {
                                this.providerList.push({
                                    value: tempProvider,
                                    image: '../../static/image/login/' + tempProvider + '.png'
                                });
                            }
                        }
                        this.hasProvider = true;
                    }
                },
                fail: err => {
                    console.error('获取服务供应商失败:' + JSON.stringify(err));
                }
            });
        },
        // 账号登录
        bindLogin() {
            /**
             * 客户端对账号信息进行一些必要的校验。
             * 实际开发中,根据业务需要进行处理,这里仅做示例。
             */
            const account = this.account.trim()
            const password = this.password.trim()
            if (account == '') {
                this.alert('请输入手机号')
                return;
            }
            if(!(/^1[3456789]\d{9}$/.test(account))){
                this.alert("手机号码有误,请重填");
                return false;
            }
            if (password.length < 6) {
                uni.showToast({
                    icon: 'none',
                    title: '密码最短为 6 个字符'
                });
                return;
            }

            uni.showLoading({
                title: '登录中'
            });

            const params = {
                phone: this.account,
                password: this.password
            };
            // 登录请求业务
            //
            // ...
        },
        // 授权登录
        authLogin(value) {
            uni.login({
                provider: value,
                success: res => {
                    uni.getUserInfo({
                        provider: value,
                        success: infoRes => {
                            console.log(infoRes)
                            // 存储个人信息
                            // 在store存储token, 用户信息
                            // ...

                            // 登录完之后到首页
                            uni.switchTab({
                                url: '/pages/index/index'
                            });
                        }
                    });
                },
                fail: err => {
                    console.error('授权失败:' + JSON.stringify(err));
                }
            });
        },
        // 验证报错
        alert (msg) {
            uni.showToast({
                icon: 'none',
                title: msg
            });
        }
    }
};
</script>
<style lang="scss"></style>

在 H5,还要区分是安卓用户,还是 ios 用户,所以这样写:在安卓端设置 input 类型为 number,可限制键盘只输入数字,在 ios 端,要加入 pattern 验证输入字段的模式,才能限制数字输入。

<input type='number' pattern="[0-9]*"/>

pattern 属性规定用于验证输入字段的模式。模式指的是正则表达式,只出现数字键盘。由于 Uniapp 的 input 组件并没有 pattern 属性传递,编译成 H5 不会在 input 控件中查看到 pattern,需要条件编译一下,插入 pattern:

onReady() {
    // #ifdef H5
    this.$nextTick(() => {
        document.querySelector('.js-input-numer input').setAttribute("pattern", "[0-9]*")
    })
    // #endif
},

img

在 input 控件中,使用 v-model 指令双向绑定使用,直接对应修改 data 中的数据。在这里的登录功能以手机号登录的 api 为账号登录,限定了类型 type="number",长度 maxlength="11"(手机号码最大长度11位)。

第二个密码控件多了 @confirm="bindLogin" 属性,可以给用户提供输入完密码后确认登录的体验。

登录操作使用 @click 事件触发,有些小伙伴可能会在其他地方看到有使用 @tap 处理事件的,不必有疑惑,Uniapp 对两者的处理是一样的。

在页面下方有几个服务平台的授权,如果不使用账号密码登录,可以选择直接由第三方进行授权登录。但是在页面处理方面我们要做到根据应用处于不用平台的情况,对应不同登录方式可供用户选择:

  • H5端,只可账号密码登录
  • 小程序可微信授权登录
  • App 端可 QQ,微信,微博授权登录

我们可以使用 Uniapp 的 uni.getProvider() 获取服务供应商,判定应用打开的在什么端。注意在 app 平台,可用的服务商,是打包环境中配置的服务商,与手机端安装了什么 app 没有关系。如果你的手机没有安装 QQ 软件,开发的 app 应用会打开 web 进行 QQ 账号密码登录后再授权登录。

根据API uni.getProvider() 直接在视图渲染对应在 /static/image/login/ 目录下的图标,只要我们命名方式 ['weixin', 'qq', 'sinaweibo']与该方法判断名称一致即可,加上切图美化,即可对应。

# 手机账号登录

img

应用的表单提交时可以说是前端的一项重大工程,后台的数据存储都是用户从前端页面操作存储的,如果没有表单的提交,那数据来源就是单向性的。我们前端在给后台传输数据的时候,需要对数据先行处理,对数据进行加工,校验,过滤等操作(当然后台也可以做)。后台也会对数据做校验过滤等操作,因为前端的数据是可见的,并且在某种手段下甚至是可以伪造的,可以说前端的数据是危险的,如果没有后端的校验过滤等操作,那么存入数据库的可能是一个命令。比如,csrf,xss插入伪造信息,更严重是删除数据库。

那么这些校验过滤操作放在前端来做呢?

如果用户的每一次操作不经过前台校验要后台来校验,想想用户每一次输入错误,前台把数据传入到后台需要0.3秒(网络不好甚至是1-3秒),后台再传输返回信息给前端告诉问题所在。那么用户很有可能关闭这个应用,甚至是不再使用这个应用。这样的用户体验是在太差了。

const account = this.account.trim()
const password = this.password.trim()
if (account == '') {
    this.alert('请输入手机号')
    return;
}
if (!(/^1[3456789]\d{9}$/.test(account))) {
    this.alert("手机号码有误,请重填");
    return false;
}

在登录提交这一块,对用户输入数据进行校验过滤,先使用 trim() 过滤掉数据的前后空格,可以避免用户操作输入失误产生的空格,然后对账号进行校验非空处理。这一步可以拦截用户未填入账号进行登录的无意义提交操作。下一步再对用户的输入数据格式(当前设定为手机号,可能有邮箱账户)进行校验。校验均使用 uni.showToast() 进行提示,统一又封装成 alert()。如果有多部操作,提取封装可以减少一定的代码量并提高效率。

# 微信授权登录

授权登录是指通过一套简单、安全的交互流程,让第三方应用可以在不知道用户登录名和密码的情况下,获取到用户的的对应信息,从而实现在第三方应用中的对应客户端的登录并关联第三方平台账户。

也就是说,对于用户来说,当你登录了一个 app 之后,你再使用这个 app 上的任何第三方服务,都不需要再手动输入一遍第三方服务对应的账号密码。而且在完成第一次登陆后,接下来的一段时间内用户再使用此服务都可以实现无感知登录。

第三方授权登录有几个优势点:

  • 缩短用户操作步骤,降低流失率;
  • 统一接入规范,把控登录方式;
  • 获取用户信息,考虑合规要求;
  • 协议管理方便,快速签约解约;

上面提到 Uniapp 的 uni.getProvider() 获取服务供应商就是为了判定该应用是否可以进行第三方授权登录,然后再根据API uni.login()根据不同的服务商进行授权登录。判定登录后再使用API uni.getUserInfo() 获取第三方已经登录的用户信息。

注意:

  1. 虽然第三方授权登录的速度比传统的账号密码登录速度更快,只需要同意授权即可登录。鼓励用户使用第三方的账号登录,无疑是把该平台的资料分享给第三方平台,造成了数据泄露(登录用户数、昵称等信息)。作为核心机密的用户信息,被第三方平台所知悉,这也是不明智的选择。
  2. 为优化用户体验,使用 wx.getUserInfo 接口直接弹出授权框的开发方式将逐步不再支持。目前已经调整为:使用 button 组件,并将 open-type 指定为 getUserInfo 类型,获取用户基本信息。

# 小结

  1. 利用元素标签的属性限定用户操作能让你的应用更加好用,记住用户的所有操作都可能是愚蠢的,用户并不关心你的控件,能越节省用户输入时间,你的应用体验感也就越好,越能赢得客户青睐;
  2. 前端操作的数据是可见的,危险的,并有可能被人伪造,或者恶意操作,真实项目需要一定的校验加密;
  3. 授权登录优化了繁杂的账号密码登录,更方便用户操作。
阅读全文