本項目基于ztaro腳手架快速搭建開發環境,在看這篇文章之前建議首先查看ztaro文檔,或者ztaro介紹,并且對于taro,zoro有所了解
本系列文章后臺api部分均以數據mock方式完成,僅保證整體流程跑通,不阻礙前端系統開發即可,同時也希望有經驗后臺開發人員可以完善該系統api部分
該項目代碼托管于github,weapp-clover,該項目用到了阿里云oss,請自行解決賬號,配置方法查看ztaro
前言
微信小程序業務愈加龐大,像那種脫離用戶信息的小程序已經越來越少了,更多時候,我們希望能增加用戶的粘性,基于用戶信息做推薦,基于這個原因,當我們在設計系統時,會發現絕大部分的API接口設計都是基于用戶已經登錄的前提下,這會對我們前端設計造成極大的影響
大多數情況下,我們會在微信onLoad函數中去發起request請求,獲取當前頁面的數據,如果在此時,用戶還未登錄完成,那獲取接口將會是失敗的
我們通常的解決辦法如下:
- 專門為此設計一個登錄頁面,當小程序加載時跳轉登錄頁,登錄完成后跳轉回來
這種方式優點是,我們無需在每個頁面監聽登錄回調后在調用數據接口,缺點是用戶對于登錄的感知明顯,無法做到靜默登錄
- 在每個頁面中注冊登錄回調事件,僅當登錄接口成功返回后才獲取數據
該方式優點是,無需額外的登錄頁面,靜默登錄,用戶無感知,缺點是開發維護成本較大,需要每個頁面多做額外的處理,系統龐大之后,容易遺忘
- 第三種方式,也是我們今天主要介紹的方式,通過異步阻塞的方式等待登錄完成,優點是靜默登錄,用戶絕大部分時間無感知,開發維護成本較小,缺點是需要兜底重登錄頁
流程
微信登錄流程這里不再給出了,不了解微信登錄的直接查看官方文檔
微信登錄
登錄整體設計流程
實現
要完成一個完整的前端模塊功能,我們總需要經歷三個步驟:
- 完成模擬接口
- 編寫數據模型
- 組織界面
依照上面的步驟,我們首先編寫登錄接口模擬,代碼及解釋如下:(mocks/user.js)
const faker = require('faker')
function userLogin(req, res) {
// 獲取前端傳遞過來的code
// 然后根據小程序appid,appsecret,code訪問微信服務器api獲取session_key,openid,這一步驟,無需模擬
// 根據session_key,openid關聯自定義登錄態,生成token,并創建匿名用戶
const { code } = req.body
res.status(200).json({
code: 'success',
message: '登錄成功',
// token已經利用express啟動時設置于locals中,這里只需獲取即可
token: req.app.locals.token,
})
}
module.exports = {
'POST /v1/user/login': userLogin,
}
接下來我們需要實現登錄攔截器,新增effect攔截器(app.js)
// 利用taro全局事件機制,等待登錄事件觸發
function waitLogin() {
return new Promise(resolve => {
Taro.eventCenter.on('login', resolve)
})
}
/**
* 由于后臺絕大部分接口都需要用戶預先登錄才可以獲取數據
* 并且前端所有的接口調用都發生在頁面中,難以在頁面中統一控制接口必須在登錄完成后才觸發調用
* 因此在這里設置登錄攔截器,攔截所有需要預先登錄的接口,等待登錄完成后返回
*/
app.intercept.effect(async action => {
// 我們通過action.meta字段來標記是否需要進行授權
if (action.meta && action.meta.noAuth) return action
try {
// 檢測本地是否存在token,存在則無需等待登錄
const token = Taro.getStorageSync(TOKEN_KEY)
if (!token) {
await waitLogin()
}
} catch (error) {
await waitLogin()
}
})
登錄請求實現(src/requests/user.js)
import request from '../utils/request'
export function userLogin(data) {
return request({
url: '/v1/user/login',
data,
header: {
noAuth: true,
},
method: 'POST',
})
}
user model實現(src/models/user.js)
import Taro from '@tarojs/taro'
import { userLogin } from '../requests/user'
import { TOKEN_KEY } from '../constants/common'
import { getAuthorize } from '../utils/tools'
export default {
namespace: 'user',
mixins: ['common'],
state: {
memberId: '',
memberInfo: {},
},
// 該函數會在model初始化的時候調用setup
async setup({ put }) {
try {
// 首先檢測微信登錄是否過期,過期則重新登錄
// 也因此我們在設計后臺token有效期時,應大于微信登錄態有效期
await Taro.checkSession()
// 微信登錄未過期,嘗試獲取本地token
const token = await Taro.getStorage({ key: TOKEN_KEY })
if (!token) {
// 本地無token存在時重新發起登錄,并在登錄完成后觸發login全局事件,打通攔截器
await put({ type: 'login', meta: { noAuth: true } })
Taro.eventCenter.trigger('login')
} else {
// 存在則直接觸發login全局事件
Taro.eventCenter.trigger('login')
}
} catch (error) {
// 調用失敗時重新發起登錄,并在登錄完成后觸發login全局事件,打通攔截器
await put({ type: 'login', meta: { noAuth: true } })
Taro.eventCenter.trigger('login')
}
},
effects: {
async login() {
const { code } = await Taro.login()
const { token } = await userLogin({ code })
await Taro.setStorage({ key: TOKEN_KEY, data: token })
},
}
注冊user model即可(src/models/index.js)
import user from './user'
export default [user]
修改request帶上token(src/utils/request.js)
export default function request(options) {
const { url } = options
Taro.showNavigationBarLoading()
// 獲取token
const token = Taro.getStorageSync(TOKEN_KEY)
return Taro.request(
resolveParams({
...options,
url: `${CONFIG.SERVER}${url}`,
mode: 'cors',
header: {
'content-type': 'application/json',
// 給每一個請求帶上token字段
Authorization: `Bearer ${token}`,
...options.header,
},
}),
)
.then(checkHttpStatus)
.then(checkSuccess)
.catch(throwError)
}
登錄兜底
登錄主流程已完成,但是我們還沒有處理異常情況,當本地存儲的token過期時,我們需要重新登錄小程序,這只是極少數情況下會發生,因為我們判斷了微信session,并且服務器的token有效期設置大于微信有效期,這個異常處理只是一個兜底
首先我們需要屏蔽所有的因授權失敗的報出的錯誤(app.js)
const app = zoro({
onError(error) {
// 屏蔽用戶登錄過期信息,因為當用戶登錄過期時會跳轉自動登錄
if (error.response && error.response.statusCode === 401) return
if (error.message) {
Taro.showToast({
icon: 'none',
title: error.message,
duration: 2000,
})
}
},
})
接下來需要攔截接口401狀態(src/utils/request.js)
// 通過截流函數,確保1秒內僅觸發一次
const redirectToRelogin = throttle(async function() {
// 這里的輪詢是為了確保,頁面已經ready,然后進行跳轉
// 因為登錄檢測發生在app onLaunch,此時頁面并沒有真正ready,調用跳轉會失敗
while (true) {
const { url, isTabbar } = getCurrentPageTypeAndUrlWithArgs()
const redirectUrl = encodeURIComponent(`/${url}`)
if (url) {
Taro.redirectTo({
url: `/pages/relogin/relogin?isTabbar=${isTabbar}&redirectUrl=${redirectUrl}`,
})
break
}
await delay(500)
}
}, 1000)
function checkHttpStatus(response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
Taro.hideNavigationBarLoading()
return response.data
}
// 新增攔截401狀態進行跳轉登錄
if (response.statusCode === 401) {
redirectToRelogin()
}
const message =
HTTP_ERROR[response.statusCode] || `ERROR CODE: ${response.statusCode}`
const error = new Error(message)
error.response = response
throw error
}
最后編寫我們的relogin頁面(src/pages/relogin/relogin.js)
頁面樣式直接查看源碼,在此不列出,僅列出關鍵函數
class PageRelogin extends Component {
state = {
error: false,
}
componentWillMount() {
this.handleLogin()
}
handleLogin = () => {
this.setState({ error: false })
// 獲取登錄完成回跳地址
const { params: { redirectUrl, isTabbar } = {} } = this.$router
const url = decodeURIComponent(redirectUrl)
dispatcher.user
.login()
.then(() => {
// 登錄完成后回跳
if (isTabbar) {
Taro.switchTab({ url })
} else {
Taro.redirectTo({ url })
}
})
.catch(() => {
this.setState({ error: true })
})
}
}
用戶實名
上面我們僅僅是實現了登錄,但是并未獲取到用戶信息,也就是登錄僅僅是個匿名用戶而已
微信獲取用戶信息授權wx.getUserInfo接口已經逐步在放棄,取而代之的是通過button開放能力來實現,因此我們需要實現一個通用授權組件,引入在必要用戶信息的頁面中
明確了目標,我們依舊按照功能模塊三步驟:
編寫上傳用戶信息的接口模擬(mocks/user.js)
const faker = require('faker')
function userUploadInfo(req, res) {
// 前端傳遞rawData, signature, encryptedData, iv校驗和解析用戶信息
// 詳見https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html
const { rawData, signature, encryptedData, iv } = req.body
res.status(200).json({
code: 'success',
message: '上傳成功',
})
}
function userGetInfo(req, res) {
res.status(200).json({
code: 'success',
message: '獲取用戶信息成功',
memberInfo: {
memberId: faker.random.uuid(),
nickName: 'Faure',
avatarUrl: 'https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJS8AiaqOQqE1j3qHCbiaNKF9D9BgtQuE6gFXoXPKUibRMeWvTO55TSeblaMIzFfp3lGdJt3qUPCibBTQ/132',
city: '成都',
province: '四川',
country: '中國',
gender: 1,
}
})
}
module.exports = {
'POST /v1/user/info': userUploadInfo,
'GET /v1/user/info': userGetInfo,
}
編寫上傳用戶信息的request請求(src/requests/user.js)
import request from '../utils/request'
export function userUploadInfo(data) {
return request({
url: '/v1/user/info',
data,
method: 'POST',
})
}
export function userGetInfo() {
return request({
url: '/v1/user/info',
method: 'GET',
})
}
編寫user model中的上傳用戶信息部分(src/models/user.js)
import Taro from '@tarojs/taro'
import { userUploadInfo, userGetInfo } from '../requests/user'
import { getAuthorize } from '../utils/tools'
export default {
namespace: 'user',
mixins: ['common'],
state: {
authorize: true, // 新增授權字段,默認初始化為已授權
memberInfo: {}, // 新增用戶信息
},
async setup({ put }) {
try {
// 檢測用戶是否授權
const authorize = await getAuthorize('userInfo')
if (!authorize) {
Taro.hideTabBar()
} else {
// 已授權,則調用獲取用戶數據
put({ type: 'getInfo' })
}
put({ type: 'update', payload: { authorize } })
} catch (error) {
Taro.hideTabBar()
put({ type: 'update', payload: { authorize: false } })
}
// 省略之前的登錄部分
},
effects: {
// 上傳用戶信息
async uploadInfo(
{
payload: { rawData, signature, encryptedData, iv },
},
{ put },
) {
await userUploadInfo({ rawData, signature, encryptedData, iv })
put({ type: 'getInfo' })
},
// 獲取用戶信息
async getInfo(action, { put }) {
const { memberInfo } = await userGetInfo()
put({ type: 'update', payload: { memberInfo } })
},
},
}
編寫界面,僅列出關鍵代碼,樣式及界面請查看倉庫源碼(src/components/login/login.js)
import Taro, { Component } from '@tarojs/taro'
import { View, Text, Button } from '@tarojs/components'
import { connect } from '@tarojs/redux'
import { dispatcher } from '@opcjs/zoro'
import ComponentCommonModal from '../modal/modal'
import { weappApiFail } from '../../../utils/tools'
import './login.scss'
@connect(({ user }) => ({
authorize: user.authorize,
}))
class ComponentCommonLogin extends Component {
handleUploadUserInfo = ({
detail: { errMsg, rawData, signature, encryptedData, iv },
}) => {
if (!weappApiFail(errMsg)) {
dispatcher.user.uploadInfo({ rawData, signature, encryptedData, iv })
dispatcher.user.update({ authorize: true })
Taro.showTabBar()
}
}
render() {
const { authorize } = this.props
return (
<ComponentCommonModal visible={!authorize}>
<View className="login">
<View className="logo" />
<Text className="title">歡迎加入四葉草莊園</Text>
<Button
className="btn"
openType="getUserInfo"
lang="zh_CN"
onGetUserInfo={this.handleUploadUserInfo}
>
微信登錄
</Button>
</View>
</ComponentCommonModal>
)
}
}
export default ComponentCommonLogin
最后只需將login組件引入到頁面中(src/pages/home/home.js)
import Taro, { Component } from '@tarojs/taro'
import { View, Text } from '@tarojs/components'
import ComponentCommonLogin from '../../components/common/login/login'
import './home.scss'
class PageHome extends Component {
config = {
navigationBarTitleText: '四葉草莊園',
}
state = {
// 請到README.md中查看此參數說明
__TAB_PAGE__: true, // eslint-disable-line
}
componentDidMount() {}
render() {
return (
<View className="home">
<ComponentCommonLogin />
<Text>首頁</Text>
</View>
)
}
}
export default PageHome
最后來看看最終效果吧
該系列文章會持續更新,由于工作忙碌,為了保證文章質量,更新較為緩慢,如果你是小程序愛好者,歡迎加我微信Faure5,備注小程序開發交流,這里主要是探討小程序開發遇到的各種填坑辦法