09-用户登录
用户登录
9-1:开篇
在上一章中,我们说到:如果想要完成:关注、收藏、点赞、评论 这些功能的话,那么需要首先完成 用户登录 的功能。
那么这一章中,我们就来看一下,我们应该如何完成 用户登录 的功能实现。
首先我们先来看一下 用户登录 的业务逻辑。
对于 用户登录 来说,主要有两个登录的入口:
- 在 《我的页面》中
- 在调用需要登录权限的功能时
那么在明确了 用户登录 的业务逻辑之后,接下来我们就去实现用户登录的对应功能。
9-2:用户登录 - 登录页面基本样式
my.vue
<template><view class="my-container"><!-- 用户未登录 --><block><image class="avatar avatar-img" src="/static/images/default-avatar.png" mode="scaleToFill" /><view class="login-desc">登录后可同步数据</view><button class="login-btn" type="primary" @click="getUserInfo">微信用户一键登录</button></block></view>
</template><script>
export default {name: 'my-login',data() {return {};}
};
</script><style lang="scss" scoped>
.my-container {display: flex;flex-direction: column;align-items: center;padding-top: 25%;.avatar-img {width: 78px;height: 78px;}.login-desc {color: $uni-text-color-grey;font-size: $uni-font-size-base;margin-top: $uni-spacing-col-big;}.login-btn {margin-top: $uni-spacing-col-big;width: 85%;}
}
</style>
9-3:用户登录 - 封装登录组件
在开篇中,我们说到,对于 登录 功能来说提供了两个登录的入口。
那么大家想一下,现在我们已经在 我的 这个 tab页
中实现了 登录的 UI
,难道我还需要在另外一个页面中再去实现一遍吗?
这个肯定是不需要的对不对。所以我们希望可以复用登录的 UI
,而复用的方式则可以把 登录的 UI
封装称为一个 组件
创建登录组件 my-login
<template><view class="my-container"><!-- 用户未登录 --><block><image class="avatar avatar-img" src="/static/images/default-avatar.png" mode="scaleToFill" /><view class="login-desc">登录后可同步数据</view><button class="login-btn" type="primary" @click="getUserInfo">微信用户一键登录</button></block></view>
</template><script>
export default {name: 'my-login',data() {return {};}
};
</script><style lang="scss" scoped>
.my-container {display: flex;flex-direction: column;align-items: center;padding-top: 25%;.avatar-img {width: 78px;height: 78px;}.login-desc {color: $uni-text-color-grey;font-size: $uni-font-size-base;margin-top: $uni-spacing-col-big;}.login-btn {margin-top: $uni-spacing-col-big;width: 85%;}
}
</style>
my
<template><my-login />
</template><script>
export default {name: 'login'
};
</script>
在调用需要登录权限的功能时,进入的登录页面 我们先不需要去创建,等到使用的时候,在创建就可以了。
9-4:用户登录 - 明确登录的实现思路
在实现登录的具体功能之前,为了避免一些没有开发经验的同学直接看代码一脸懵逼,我们需要先来明确一下登录的实现基本逻辑。
首先对于登录来说,我们会分为两个不同的端来进行适配实现:
- 微信小程序
- 非微信小程序(在讲解适配时实现)
我们这里先只讲解 【微信小程序的实现】,【非微信小程序】的实现将在后面的 适配环节进行。
微信小程序:
- 想要实现登录功能,那么我们需要调用登录接口来进行实现,而登录接口所需要的参数,我们可以直接通过
getUserProfile
方法进行获取。 - 调用登录接口成功,服务端会返回用户的
token
,这个token
为当前的用户身份令牌。(拥有 token) 则表示用户已经登录了。 - 而此处的
token
,我们需要在多个组件中进行使用,所以token
需要被保存到全局状态管理工具 - vuex
中,同时需要保存的还有通过getUserProfile
获取到的用户基本信息。 - 而当前的用户登录状态,我们希望可以一直保存(PS:不需要每次都进行登录)。所以在登录完成后,我们需要把
token
&&userinfo
保存到 本地存储中 - 最后,为了实现 数据与组件的分离,我们需要把与 与登录相关的逻辑 都封装在
vuex
中进行。
这些业务是 前端用户登录的标准逻辑,大家在以后的前端登录业务处理中,也可以按照此逻辑进行。
那么从下一小节开始,我们就按照这个逻辑实现一下对应的代码。
9-5:用户登录 - 封装 action 调用登录接口
在上一小节中,我们分析了【微信小程序】中进行登录的实现逻辑,那么从这一小节开始,我们就实现对应的功能。
首先我们需要先获取到用户的信息,用作登录时的请求参数:
my-login
<script>
import { mapActions } from 'vuex';
export default {methods: {...mapActions('user', ['login']),/* 获取用户信息*/getUserInfo() {// 展示加载框uni.showLoading({title: '加载中'});uni.getUserProfile({desc: '登录后可同步数据',success: async (obj) => {// 调用 action ,请求登录接口await this.login(obj);},fail: () => {uni.showToast({title: '授权已取消',icon: 'error',mask: true});},complete: () => {// 隐藏loadinguni.hideLoading();}});}}
};
</script>
api/user.js
import request from '../utils/request';/* 用户登录*/
export function login(data) {return request({url: '/sys/login',method: 'POST',data});
}
store/user.js
import { login } from 'api/user';
export default {namespaced: true,state: () => {return {};},actions: {/* 完成登录*/async login(context, userProfile) {console.log(userProfile);// 用户数据const rawData = userProfile.userInfo;// 调用登录接口const { data: res } = await login({signature: userProfile.signature,iv: userProfile.iv,nickName: rawData.nickName,gender: rawData.gender,city: rawData.city,province: rawData.province,avatarUrl: rawData.avatarUrl});// TODO: 登录逻辑console.log(res);}}
};
store/index.js
import user from './modules/user';const store = new Vuex.Store({modules: {user}
});
9-6:用户登录 - 保存用户登录状态
用户的登录状态需要被保存到 vuex
中,同时需要进行 本地存储。
store/user.js
import { login } from 'api/user';
const STORAGE_KEY = 'user-info';
const TOKEN_KEY = 'token';
export default {namespaced: true,state: () => {return {// 用户 tokentoken: uni.getStorageSync(TOKEN_KEY) || '',// 用户信息userInfo: uni.getStorageSync(STORAGE_KEY) || {}};},mutations: {/* 保存 token 到 vuex*/setToken(state, token) {state.token = token;this.commit('user/saveToken');},/* 保存 token 到 本地存储*/saveToken(state) {uni.setStorage({key: TOKEN_KEY,data: state.token});},/* 保存 用户信息 到 vuex*/setUserInfo(state, userInfo) {state.userInfo = userInfo;this.commit('user/saveUserInfo');},/* 保存 用户信息 到 本地存储*/saveUserInfo(state) {uni.setStorage({key: STORAGE_KEY,data: state.userInfo});}},actions: {/* 完成登录*/async login(context, userProfile) {...// 登录逻辑this.commit('user/setToken', res.token);this.commit('user/setUserInfo', JSON.parse(userProfile.rawData));}}
};
9-7:用户登录 - 完成已登录的用户视图
当 token
存在时,表示用户已经登录了,此时需要 展示用户登录完成的视图:
my-login
<template><view class="my-container"><!-- 用户未登录 --><block v-if="!token">...</block><!-- 已登录 --><block v-else><image class="avatar avatar-img" :src="userInfo.avatarUrl" mode="scaleToFill" /><view class="login-desc">{{ userInfo.nickName }}</view><button class="login-btn" type="default" @click="onLogoutClick">退出登录</button></block></view>
</template><script>
import { mapState, mapActions } from 'vuex';
export default {name: 'my-login',data() {return {};},computed: {...mapState('user', ['token', 'userInfo'])},
};
</script>
9-8:用户登录 - 实现退出登录功能
store/user
export default {...mutations: {.../* 删除 token*/removeToken(state) {state.token = '';this.commit('user/saveToken');},.../* 删除用户信息*/removeUserInfo(state) {state.userInfo = {};this.commit('user/saveUserInfo');}},actions: {.../* 退出登录*/logout(context) {this.commit('user/removeToken');this.commit('user/removeUserInfo');}}
};
my-login
<template><view class="my-container">...<!-- 已登录 --><block v-else>...<button class="login-btn" type="default" @click="onLogoutClick">退出登录</button></block></view>
</template><script>
export default {...methods: {...mapActions('user', ['login', 'logout']),.../* 退出登录*/onLogoutClick() {uni.showModal({title: '提示',content: '退出登录将无法同步数据哦~',success: ({ confirm, cancel }) => {if (confirm) {this.logout();}}});}}
};
</script>
9-9:用户登录 - 判断用户登录状态
截止到目前为止, 用户登录 的功能其实就已经全部构建完毕了。
接下来我们就需要实现:
- 关注用户
- 文章点赞
- 文章收藏
- 文章评论
这四个对应的功能。
之前我们说过,想要实现这四个功能, 那么需要有一个前提条件就是:当前用户已登录。
所以说,我们就需要在用户使用这四个功能之前,来判断用户的登录状态。
也就是说,在 用户登录功能完成之后,我们其实还不可以立刻着手这四个功能的开发,我们还需要进行一步操作,那就是 判断用户的登录状态!
想要判断用户的登录状态,我们依然需要在 vuex
中进行:
store/user
export default {...actions: {/* 进行登录判定*/isLogin(context) {if (context.state.token) return true;// TODO: 如果用户未登录,则引导用户进入登录页面return false;}}
};
如果用户未登录,则引导用户进入登录页面,那么这一步功能如何进行实现呢?
9-10:用户登录 - 新建登录页面,处理当前场景
开篇的时候,我们说过,对于 登录 来说,包含有两个入口:
- 在 《我的页面》中
- 在调用需要登录权限的功能时
那么此时,就是使用到第二个场景的时候了。
我们创建一个新页面,叫做 login-page
,在这个页面中,导入 my-login
组件:
<template><my-login />
</template><script>
export default {data() {return {};}
};
</script><style lang="scss"></style>
当 进行登录判定,用户未登录时,进入 login-page
页面:
export default {...actions: {/* 进行登录判定*/async isLogin(context) {if (context.state.token) return true;// 如果用户未登录,则引导用户进入登录页面const [error, res] = await uni.showModal({title: '登录之后才可以进行后续操作',content: '立即跳转到登录页面?(登录后回自动返回当前页面哦~~~)'});const { cancel, confirm } = res;if (confirm) {uni.navigateTo({url: '/subpkg/pages/login-page/login-page'});}return false;}}
};
在 关注 用户时,调用该 action
:
blog-detail.vue
<template>...<!-- 关注按钮 --><button class="follow" size="mini" @click="onFollowClick">关注</button>...
</template><script>
...
import { mapActions } from 'vuex';
export default {...methods: {......mapActions('user', ['isLogin']),/* 关注按钮点击事件*/async onFollowClick() {// 进行登录判定const isLogin = await this.isLogin();if (!isLogin) {return;}}}
};
</script>
9-11:用户登录 - 监听登录成功的状态,返回之前页面
在上一节,我们已经完成了 在调用需要登录权限的功能时,进入登录页面 ,但是当我们登录完成之后,我们 还需要返回之前页面,因为只有这样才能完成我们的功能闭环,所以在这一小节中,我们就去完成这一块的功能:
my-login
:在登录成功后,发送事件
<script>
export default {methods: {/* 获取用户信息*/getUserInfo() {...uni.getUserProfile({desc: '登录后可同步数据',success: async (obj) => {// 调用 action ,请求登录接口await this.login(obj);// 登录成功之后,发送事件this.$emit('onLoginSuccess');}...});},}
};
</script>
login-page
:监听登录成功的事件,并返回上一个页面
<template><my-login @onLoginSuccess="onLoginSuccess" />
</template><script>
export default {methods: {/* 登录成功的事件回调*/onLoginSuccess() {uni.navigateBack({ delta: 1 });}}
};
</script>
9-12:用户登录 - 处理登录时无 loading 的 bug
异步操作没有处理:
/微信一键登录点击事件*/getUserInfo() {// 展示加载框uni.showLoading({title: '加载中'});// 获取用户信息,调用 login 接口uni.getUserProfile({desc: '登录后可同步数据',success: async (obj) => {// 调用登录接口await this.login(obj);// 登录成功,发送事件this.$emit('onLoginSuccess');},fail: () => {uni.showToast({title: '授权已取消',icon: 'error',mask: true});},complate: () => {// 关闭加载uni.hideLoading();}});},
9-13:文章操作 - 关注用户
在彻底完成了 登录 相关的内容之后,接下来就可以回过头去实现这四个功能了。
首先我们先去实现 关注用户 的功能:
定义 api
接口:
api/user
/* 关注用户*/
export function userFollow(data) {return request({url: '/user/follow',data});
}
关注用户接口需要传递 token
请求头,所以我们可以在 request.js
中,传递当前的 token
:
import store from '../store';
function request({ url, data, method }) {return new Promise((resolve, reject) => {uni.request({header: {Authorization: store.state.user.token},});});
}export default request;
在 blog-detail
中调用 接口:
<template>...
<!-- 关注按钮 -->
<buttonclass="follow"size="mini":type="articleData.isFollow ? 'primary' : 'default'":loading="isFollowLoading"@click="onFollowClick">{{ articleData.isFollow ? '已关注' : '关注' }}</button>
...
</template><script>
...
import { userFollow } from 'api/user';
export default {...data() {return {...// 关注用户的 loadingisFollowLoading: false};},methods: {/* 关注按钮点击事件*/async onFollowClick() {// 进行登录判定const isLogin = await this.isLogin();if (!isLogin) {return;}// 关注用户// 开启 button 的 loadingthis.isFollowLoading = true;const { data: res } = await userFollow({author: this.author,isFollow: !this.articleData.isFollow});// 修改用户数据this.articleData.isFollow = !this.articleData.isFollow;// 关闭 button 的 loadingthis.isFollowLoading = false;}}
};
</script>
9-14:文章操作 - 处理发表评论的 UI
监听 article-operate
中的 输入框点击事件:
<template><view class="operate-container"><!-- 输入框 --><view class="comment-box" @click="onCommitClick">...</view></view>
</template><script>
import { mapActions } from 'vuex';export default {name: 'article-operate',props: {},data() {return {};},methods: {...mapActions('user', ['isLogin']),/* my-search 的点击事件*/async onCommitClick() {// 进行登录判定,登录之后允许发布评论if (!(await this.isLogin())) {return;}this.$emit('commitClick');}}
};
</script>
在 blog-detail
中展示弹出层:
<template>...
<!-- 底部功能区 -->
<article-operate @commitClick="onCommit" />
<!-- 输入评论的popup -->
<uni-popup ref="popup" type="bottom"> 发表评论的弹出层 </uni-popup>...
</template><script>
...
export default {...methods: {.../* 发布评论点击事件*/onCommit() {// 通过组件定义的ref调用uni-popup方法this.$refs.popup.open();}}
};
</script>
创建 发表评论 的弹出层组件 - article-comment-commit
:
<template><view class="comment-container"><uni-easyinputv-model="value"type="textarea"placeholder="说点什么...":maxlength="50":inputBorder="false"></uni-easyinput><button class="commit" type="primary" :disabled="!value" size="mini" @click="onBtnClick">发送</button></view>
</template><script>
export default {name: 'article-comment-commit',data() {return {value: ''};},methods: {/* 发送按钮点击事件*/onBtnClick() {}}
};
</script><style lang="scss" scoped>
.comment-container {background-color: $uni-bg-color;text-align: right;padding: $uni-spacing-row-base;position: relative;
}
</style>
在 blog-detail
中使用 article-comment-commit
组件:
<template>...
<!-- 底部功能区 -->
<article-operate @commitClick="onCommit" />
<!-- 输入评论的popup -->
<uni-popup ref="popup" type="bottom"><article-comment-commit />
</uni-popup>...
</template>
9-15:文章操作 - 处理评论框的显示问题
现在评论框已经可以显示出来了,但是目前 评论框的显示存在两个问题:
- 输入内容之后,关闭评论框,再次展示评论框时,之前输入的内容依然存在
- 在真机中,软键盘会遮挡评论框的展示
那么在本小节中,我们就来处理一下这两个问题:
1. 输入内容之后,关闭评论框,再次展示评论框时,之前输入的内容依然存在:
原因:
当 popup
关闭时,article-comment-commit
组件 并未销毁,依然存在
解决方案:
监听 popup
的关闭事件,通过 v-if
控制 article-comment-commit
组件的销毁
<template>...
<!-- 输入评论的popup --><uni-popup ref="popup" type="bottom" @change="onCommitPopupChange"><article-comment-commit v-if="isShowCommit" /></uni-popup>
</template><script>
...
export default {...data() {return {...// popup 的显示状态isShowCommit: false};},methods: {.../* 发布评论的 popup 切换事件*/onCommitPopupChange(e) {// 修改对应的标记,当 popup 关闭时,为了动画平顺,进行延迟处理if (e.show) {this.isShowCommit = e.show;} else {setTimeout(() => {this.isShowCommit = e.show;}, 200);}}}
};
</script>
2. 在真机中,软键盘会遮挡评论框的展示
原因:
软键盘弹出,占用了底部空间
解决方案:
检测软键盘的弹出事件,动态修改 article-comment-commit
组件的位置
article-comment-commit
<template><view class="comment-container" :style="{ bottom: bottom + 'px' }">...</view>
</template><script>
export default {data() {return {bottom: 0};},created() {// 检测软键盘的变化uni.onKeyboardHeightChange(({ height }) => {this.bottom = height;});},
};
</script>
9-16:文章操作 - 发表评论
在一切准备就绪之后,最后就可以实现 发表评论 的功能了。
在 api/user
中,定义发表评论的接口:
/* 发表评论*/
export function userArticleComment(data) {return request({url: '/user/article/comment',method: 'POST',data});
}
在 article-comment-commit
中调用接口,发表评论:
<template><button class="commit" type="primary" :disabled="!value" size="mini" @click="onBtnClick">发送</button>
</template><script>
import { userArticleComment } from 'api/user';export default {name: 'article-comment-commit',props: {articleId: {type: String,required: true}},...methods: {/* 发送按钮点击事件*/async onBtnClick() {// 展示加载框uni.showLoading({title: '加载中'});// 异步处理即可await userArticleComment({articleId: this.articleId,content: this.value});uni.showToast({title: '发表成功',icon: 'success',mask: true});// 发表成功之后的回调this.$emit('success');}}
};
</script>
在 blog-detail
中传递 id
,处理评论成功之后的操作:
<template><!-- 输入评论的popup --><uni-popup ref="popup" type="bottom" @change="onCommitPopupChange"><article-comment-commitv-if="isShowCommit":articleId="articleId"@success="onSendSuccess"/></uni-popup>
</template><script>export default {... methods: {.../* 发表评论成功*/onSendSuccess() {// 关闭弹出层this.$refs.popup.close();this.isShowCommit = false;}}
};
</script>
9-17:文章操作 - 回显评论数据
article-comment-commit
:评论发布成功,传递评论数据对象
/* 发送按钮点击事件*/async onBtnClick() {...// 异步处理即可const { data: res } = await userArticleComment({articleId: this.articleId,content: this.value});...// 发表成功之后的回调this.$emit('success', res);}
article-comment-list
:增加添加评论项的方法
/* 为 comment 增加一个评论*/addCommentList(data) {this.commentList.unshift(data);}
blog-detail
:评论成功后,调用添加评论项的方法
/* 发表评论成功*/onSendSuccess(data) {...// 显示评论数据this.$refs.mescrollItem.addCommentList(data);}
9-18:文章操作 - 关于点赞和收藏的功能实现
因为 点赞 和 收藏 的功能实现和 关注,几乎一致,所以:
点赞 和 收藏 的功能作为课下作业让大家进行实现,视频中不在进行讲解。
实现代码会在 文档 和 最终代码 中进行体现,供大家进行参考。
点赞功能实现代码:
api/user
/* 用户点赞*/
export function userPraise(data) {return request({url: '/user/praise',data});
}
article-praise
<template><view class="praise-box" @click="onClick"><image class="img" :src="getIsPraise" /><text class="txt">点赞</text></view>
</template><script>
import { mapActions } from 'vuex';
import { userPraise } from 'api/user';
export default {name: 'article-praise',props: {/* 数据源*/articleData: {type: Object,required: true}},computed: {getIsPraise() {return this.articleData && this.articleData.isPraise? '/static/images/praise.png': '/static/images/un-praise.png';}},methods: {...mapActions('user', ['isLogin']),async onClick() {// 进行登录判定,登录之后允许发布评论if (!(await this.isLogin())) {return;}// 展示加载框uni.showLoading({title: '加载中'});await userPraise({articleId: this.articleData.articleId,isPraise: !this.articleData.isPraise});// 关闭加载uni.hideLoading();// 更新数据this.$emit('changePraise', !this.articleData.isPraise);}}
};
</script>
article-operate
<template><!-- 点赞 --><view class="options-box"><article-praise :articleData="articleData" @changePraise="$emit('changePraise', $event)" /></view>
</template><script>
export default {name: 'article-operate',props: {articleData: {type: Object,required: true}},
};
</script>
blog-detail
<template><!-- 底部功能区 --><article-operate:articleData="articleData"@commitClick="onCommit"@changePraise="onChangePraise"/>
</template><script>
export default {methods: {.../* 点赞处理回调*/onChangePraise(isPraise) {this.articleData.isPraise = isPraise;}}
};
</script>
收藏功能实现代码:
api/user
/* 用户收藏*/
export function userCollect(data) {return request({url: '/user/collect',data});
}
article-collect
<template><view class="collect-box" @click="onClick"><image class="img" :src="getIsCollect" /><text class="txt">收藏</text></view>
</template><script>
import { mapActions } from 'vuex';
import { userCollect } from 'api/user';
export default {name: 'article-collect',props: {/* 数据源*/articleData: {type: Object,required: true}},computed: {getIsCollect() {return this.articleData && this.articleData.isCollect? '/static/images/collect.png': '/static/images/un-collect.png';}},methods: {...mapActions('user', ['isLogin']),async onClick() {// 进行登录判定,登录之后允许发布评论if (!(await this.isLogin())) {return;}// 展示加载框uni.showLoading({title: '加载中'});await userCollect({articleId: this.articleData.articleId,isCollect: !this.articleData.isCollect});// 关闭加载uni.hideLoading();// 更新数据this.$emit('changeCollect', !this.articleData.isCollect);}}
};
</script>
article-operate
<template><!-- 收藏 --><view class="options-box"><article-collect :articleData="articleData" @changeCollect="$emit('changeCollect', $event)" /></view>
</template><script>
export default {name: 'article-operate',props: {articleData: {type: Object,required: true}},
};
</script>
blog-detail
<template><!-- 底部功能区 --><article-operate:articleData="articleData"@commitClick="onCommit"@changePraise="onChangePraise"@changeCollect="onChangeCollect"/>
</template><script>
export default {methods: {.../* 收藏处理回调*/onChangeCollect(isCollect) {this.articleData.isCollect = isCollect;}}
};
</script>
9-19:总结
在这一大章中,我们完成了整个的【用户登录】以及【文章详情】的功能。
针对于【用户登录】来说,我们使用 vuex
来对 【组件】和【数据】进行了分离。
所有与【用户登录】相关的数据操作,都被封装到了 vuex
之中,这样做的好处在于:我们可以在多个组件中对数据进行操作,而不需要担心其影响 单向数据流的简洁性。
而【文章详情】的剩余功能,我们在视频中完成了【关注】和【发布评论】两个功能,而把【点赞】和【收藏】留做了课下作业。这样做的目的是 可以让大家有能够独立思考,以及独立完成功能的机会。
那么到现在为止,我们项目还剩余 【热播】模块没有完成,那么从下一章开始,我们就要搞定【热播】模块啦。