微信小程序電商實(shí)戰(zhàn)-登錄模塊設(shè)計(jì)

本項(xiàng)目基于ztaro腳手架快速搭建開(kāi)發(fā)環(huán)境,在看這篇文章之前建議首先查看ztaro文檔,或者ztaro介紹,并且對(duì)于taro,zoro有所了解

本系列文章后臺(tái)api部分均以數(shù)據(jù)mock方式完成,僅保證整體流程跑通,不阻礙前端系統(tǒng)開(kāi)發(fā)即可,同時(shí)也希望有經(jīng)驗(yàn)后臺(tái)開(kāi)發(fā)人員可以完善該系統(tǒng)api部分

該項(xiàng)目代碼托管于github,weapp-clover,該項(xiàng)目用到了阿里云oss,請(qǐng)自行解決賬號(hào),配置方法查看ztaro

前言

微信小程序業(yè)務(wù)愈加龐大,像那種脫離用戶信息的小程序已經(jīng)越來(lái)越少了,更多時(shí)候,我們希望能增加用戶的粘性,基于用戶信息做推薦,基于這個(gè)原因,當(dāng)我們?cè)谠O(shè)計(jì)系統(tǒng)時(shí),會(huì)發(fā)現(xiàn)絕大部分的API接口設(shè)計(jì)都是基于用戶已經(jīng)登錄的前提下,這會(huì)對(duì)我們前端設(shè)計(jì)造成極大的影響

大多數(shù)情況下,我們會(huì)在微信onLoad函數(shù)中去發(fā)起request請(qǐng)求,獲取當(dāng)前頁(yè)面的數(shù)據(jù),如果在此時(shí),用戶還未登錄完成,那獲取接口將會(huì)是失敗的

我們通常的解決辦法如下:

  1. 專門為此設(shè)計(jì)一個(gè)登錄頁(yè)面,當(dāng)小程序加載時(shí)跳轉(zhuǎn)登錄頁(yè),登錄完成后跳轉(zhuǎn)回來(lái)

這種方式優(yōu)點(diǎn)是,我們無(wú)需在每個(gè)頁(yè)面監(jiān)聽(tīng)登錄回調(diào)后在調(diào)用數(shù)據(jù)接口,缺點(diǎn)是用戶對(duì)于登錄的感知明顯,無(wú)法做到靜默登錄

  1. 在每個(gè)頁(yè)面中注冊(cè)登錄回調(diào)事件,僅當(dāng)?shù)卿浗涌诔晒Ψ祷睾蟛奴@取數(shù)據(jù)

該方式優(yōu)點(diǎn)是,無(wú)需額外的登錄頁(yè)面,靜默登錄,用戶無(wú)感知,缺點(diǎn)是開(kāi)發(fā)維護(hù)成本較大,需要每個(gè)頁(yè)面多做額外的處理,系統(tǒng)龐大之后,容易遺忘

  1. 第三種方式,也是我們今天主要介紹的方式,通過(guò)異步阻塞的方式等待登錄完成,優(yōu)點(diǎn)是靜默登錄,用戶絕大部分時(shí)間無(wú)感知,開(kāi)發(fā)維護(hù)成本較小,缺點(diǎn)是需要兜底重登錄頁(yè)

流程

微信登錄流程這里不再給出了,不了解微信登錄的直接查看官方文檔
微信登錄

登錄整體設(shè)計(jì)流程


登錄控制流程.png

實(shí)現(xiàn)

要完成一個(gè)完整的前端模塊功能,我們總需要經(jīng)歷三個(gè)步驟:

  1. 完成模擬接口
  2. 編寫數(shù)據(jù)模型
  3. 組織界面

依照上面的步驟,我們首先編寫登錄接口模擬,代碼及解釋如下:(mocks/user.js)

const faker = require('faker')

function userLogin(req, res) {
  // 獲取前端傳遞過(guò)來(lái)的code
  // 然后根據(jù)小程序appid,appsecret,code訪問(wèn)微信服務(wù)器api獲取session_key,openid,這一步驟,無(wú)需模擬
  // 根據(jù)session_key,openid關(guān)聯(lián)自定義登錄態(tài),生成token,并創(chuàng)建匿名用戶
  const { code } = req.body

  res.status(200).json({
    code: 'success',
    message: '登錄成功',
    // token已經(jīng)利用express啟動(dòng)時(shí)設(shè)置于locals中,這里只需獲取即可
    token: req.app.locals.token,
  })
}

module.exports = {
  'POST /v1/user/login': userLogin,
}

接下來(lái)我們需要實(shí)現(xiàn)登錄攔截器,新增effect攔截器(app.js)

// 利用taro全局事件機(jī)制,等待登錄事件觸發(fā)
function waitLogin() {
  return new Promise(resolve => {
    Taro.eventCenter.on('login', resolve)
  })
}

/**
 * 由于后臺(tái)絕大部分接口都需要用戶預(yù)先登錄才可以獲取數(shù)據(jù)
 * 并且前端所有的接口調(diào)用都發(fā)生在頁(yè)面中,難以在頁(yè)面中統(tǒng)一控制接口必須在登錄完成后才觸發(fā)調(diào)用
 * 因此在這里設(shè)置登錄攔截器,攔截所有需要預(yù)先登錄的接口,等待登錄完成后返回
 */
app.intercept.effect(async action => {
  // 我們通過(guò)action.meta字段來(lái)標(biāo)記是否需要進(jìn)行授權(quán)
  if (action.meta && action.meta.noAuth) return action

  try {
    // 檢測(cè)本地是否存在token,存在則無(wú)需等待登錄
    const token = Taro.getStorageSync(TOKEN_KEY)
    if (!token) {
      await waitLogin()
    }
  } catch (error) {
    await waitLogin()
  }
})

登錄請(qǐng)求實(shí)現(xiàn)(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實(shí)現(xiàn)(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: {},
  },
  
  // 該函數(shù)會(huì)在model初始化的時(shí)候調(diào)用setup
  async setup({ put }) {
    try {
      // 首先檢測(cè)微信登錄是否過(guò)期,過(guò)期則重新登錄
      // 也因此我們?cè)谠O(shè)計(jì)后臺(tái)token有效期時(shí),應(yīng)大于微信登錄態(tài)有效期
      await Taro.checkSession()
      // 微信登錄未過(guò)期,嘗試獲取本地token
      const token = await Taro.getStorage({ key: TOKEN_KEY })
      if (!token) {
        // 本地?zé)otoken存在時(shí)重新發(fā)起登錄,并在登錄完成后觸發(fā)login全局事件,打通攔截器
        await put({ type: 'login', meta: { noAuth: true } })
        Taro.eventCenter.trigger('login')
      } else {
        // 存在則直接觸發(fā)login全局事件
        Taro.eventCenter.trigger('login')
      }
    } catch (error) {
      // 調(diào)用失敗時(shí)重新發(fā)起登錄,并在登錄完成后觸發(fā)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 })
    },
}

注冊(cè)u(píng)ser 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',
        // 給每一個(gè)請(qǐng)求帶上token字段
        Authorization: `Bearer ${token}`,
        ...options.header,
      },
    }),
  )
    .then(checkHttpStatus)
    .then(checkSuccess)
    .catch(throwError)
}

登錄兜底

登錄主流程已完成,但是我們還沒(méi)有處理異常情況,當(dāng)本地存儲(chǔ)的token過(guò)期時(shí),我們需要重新登錄小程序,這只是極少數(shù)情況下會(huì)發(fā)生,因?yàn)槲覀兣袛嗔宋⑿舠ession,并且服務(wù)器的token有效期設(shè)置大于微信有效期,這個(gè)異常處理只是一個(gè)兜底

首先我們需要屏蔽所有的因授權(quán)失敗的報(bào)出的錯(cuò)誤(app.js)

const app = zoro({
  onError(error) {
    // 屏蔽用戶登錄過(guò)期信息,因?yàn)楫?dāng)用戶登錄過(guò)期時(shí)會(huì)跳轉(zhuǎn)自動(dòng)登錄
    if (error.response && error.response.statusCode === 401) return

    if (error.message) {
      Taro.showToast({
        icon: 'none',
        title: error.message,
        duration: 2000,
      })
    }
  },
})

接下來(lái)需要攔截接口401狀態(tài)(src/utils/request.js)

// 通過(guò)截流函數(shù),確保1秒內(nèi)僅觸發(fā)一次
const redirectToRelogin = throttle(async function() {
  // 這里的輪詢是為了確保,頁(yè)面已經(jīng)ready,然后進(jìn)行跳轉(zhuǎn)
  // 因?yàn)榈卿洐z測(cè)發(fā)生在app onLaunch,此時(shí)頁(yè)面并沒(méi)有真正ready,調(diào)用跳轉(zhuǎn)會(huì)失敗
  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狀態(tài)進(jìn)行跳轉(zhuǎn)登錄
  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頁(yè)面(src/pages/relogin/relogin.js)

頁(yè)面樣式直接查看源碼,在此不列出,僅列出關(guān)鍵函數(shù)

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 })
      })
  }
}

用戶實(shí)名

上面我們僅僅是實(shí)現(xiàn)了登錄,但是并未獲取到用戶信息,也就是登錄僅僅是個(gè)匿名用戶而已

微信獲取用戶信息授權(quán)wx.getUserInfo接口已經(jīng)逐步在放棄,取而代之的是通過(guò)button開(kāi)放能力來(lái)實(shí)現(xiàn),因此我們需要實(shí)現(xiàn)一個(gè)通用授權(quán)組件,引入在必要用戶信息的頁(yè)面中

明確了目標(biāo),我們依舊按照功能模塊三步驟:

編寫上傳用戶信息的接口模擬(mocks/user.js)

const faker = require('faker')

function userUploadInfo(req, res) {
  // 前端傳遞rawData, signature, encryptedData, iv校驗(yàn)和解析用戶信息
  // 詳見(jiàn)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: '中國(guó)',
      gender: 1,
    }
  })
}

module.exports = {
  'POST /v1/user/info': userUploadInfo,
  'GET /v1/user/info': userGetInfo,
}

編寫上傳用戶信息的request請(qǐng)求(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, // 新增授權(quán)字段,默認(rèn)初始化為已授權(quán)
    memberInfo: {}, // 新增用戶信息
  },

  async setup({ put }) {
    try {
      // 檢測(cè)用戶是否授權(quán)
      const authorize = await getAuthorize('userInfo')
      if (!authorize) {
        Taro.hideTabBar()
      } else {
        // 已授權(quán),則調(diào)用獲取用戶數(shù)據(jù)
        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 } })
    },
  },
}

編寫界面,僅列出關(guān)鍵代碼,樣式及界面請(qǐng)查看倉(cāng)庫(kù)源碼(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組件引入到頁(yè)面中(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 = {
    // 請(qǐng)到README.md中查看此參數(shù)說(shuō)明
    __TAB_PAGE__: true, // eslint-disable-line
  }

  componentDidMount() {}

  render() {
    return (
      <View className="home">
        <ComponentCommonLogin />
        <Text>首頁(yè)</Text>
      </View>
    )
  }
}

export default PageHome

最后來(lái)看看最終效果吧


QQ20181207-131915-HD.gif

該系列文章會(huì)持續(xù)更新,由于工作忙碌,為了保證文章質(zhì)量,更新較為緩慢,如果你是小程序愛(ài)好者,歡迎加我微信Faure5,備注小程序開(kāi)發(fā)交流,這里主要是探討小程序開(kāi)發(fā)遇到的各種填坑辦法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容