Taro 小程序開發大型實戰(三):實現微信和支付寶多端登陸

歡迎繼續閱讀《Taro 小程序開發大型實戰》系列,前情回顧:css

而在這一篇中,咱們將實現微信和支付寶多端登陸。若是你但願直接從這一篇開始,請運行如下命令:html

git clone -b third-part https://github.com/tuture-dev/ultra-club.git
cd ultra-club
本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦~

在正式開始以前,咱們但願你已經具有如下知識:react

  • 基本的 React 框架知識,可參考這篇文章進行學習
  • 對經常使用的 React Hooks (useStateuseEffect)有所瞭解,後面圖雀社區將推出 「一杯茶的時間,上手 React Hooks」,敬請期待!

除此以外,你還須要下載並安裝支付寶開發者工具,登陸後建立本身的小程序 ID。git

多端登陸,羣魔亂舞

與普通的 Web 應用相比,小程序可以在所在的平臺實現一鍵登陸,很是方便。這一步,咱們也將實現多端登陸(主要包括微信登陸和支付寶登陸)。之因此標題取爲「羣魔亂舞」,不只受了「震驚」小編們的啓發,也是由於當今各平臺處理登陸和鑑權的方式差別很大,很遺憾的是在 Taro 框架下咱們依然須要踩不少「坑」才能真正實現「多端登陸」。github

準備工做

組件設計規劃

這一節的代碼很長,在正式開始以前咱們先查看一下組件設計的規劃,便於你對接下來咱們要作的工做有清晰的瞭解。npm

能夠看到「個人」頁面總體拆分紅了 HeaderFooter小程序

  • Header 包括 LoggedMine(我的信息),若是在未登陸狀態下則還有 LoginButton(普通登陸按鈕)、WeappLoginButton(微信登陸按鈕,僅在微信小程序中出現)以及 AlipayLoginButton(支付寶登陸按鈕,僅在支付寶小程序中出現)
  • Footer 則用來顯示是否已登陸的文字,在已登陸的狀況下會顯示 Logout(退出登陸按鈕)

配置 Babel 插件

從這一步開始,咱們將首次開始寫異步代碼。本項目將採用流行的 async/await 來編寫異步邏輯,所以咱們配置一下相應的 Babel 插件:segmentfault

npm install babel-plugin-transform-runtime --save-dev
# yarn add babel-plugin-transform-runtime -D

而後在 config/index.js 中爲 config.babel.plugins 添加相應的配置以下:後端

const config = {
  // ...
  babel: {
    // ...
    plugins: [
      // ...
      [
        'transform-runtime',
        {
          helpers: false,
          polyfill: false,
          regenerator: true,
          moduleName: 'babel-runtime',
        },
      ],
    ],
  },
  // ...
}

// ...

各組件的實現

實現 LoginButton

首先,咱們來實現普通登陸按鈕 LoginButton 組件。建立 src/components/LoginButton 目錄,在其中建立 index.js,代碼以下:微信小程序

import Taro from '@tarojs/taro'
import { AtButton } from 'taro-ui'

export default function LoginButton(props) {
  return (
    <AtButton type="primary" onClick={props.handleClick}>
      普通登陸
    </AtButton>
  )
}

咱們使用了 Taro UI 的 AtButton 組件,並定義了一個 handleClick 事件,後面在使用時會傳入。

實現 WeappLoginButton

接着咱們實現微信登陸按鈕 WeappLoginButton。建立 src/components/WeappLoginButton 目錄,在其中分別建立 index.jsindex.scssindex.js 代碼以下:

import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'

import './index.scss'

export default function LoginButton(props) {
  const [isLogin, setIsLogin] = useState(false)

  async function onGetUserInfo(e) {
    setIsLogin(true)

    const { avatarUrl, nickName } = e.detail.userInfo
    await props.setLoginInfo(avatarUrl, nickName)

    setIsLogin(false)
  }

  return (
    <Button
      openType="getUserInfo"
      onGetUserInfo={onGetUserInfo}
      type="primary"
      className="login-button"
      loading={isLogin}
    >
      微信登陸
    </Button>
  )
}

能夠看到,微信登陸按鈕和以前的普通登陸按鈕多了不少東西:

  • 添加了 isLogin 狀態,用於表示是否在等待登陸中,以及修改狀態的 setIsLogin 函數
  • 實現了 onGetUserInfo async 函數,用於處理在用戶點擊登陸按鈕、獲取到信息以後的邏輯。其中,咱們將獲取到的用戶信息傳入 props 中的 setLoginInfo,從而修改整個應用的登陸狀態
  • 添加了 openType(微信開放能力)屬性,這裏咱們輸入的是 getUserInfo(獲取用戶信息),欲查看全部支持的 open-type,請查看微信開放文檔對應部分
  • 添加了 onGetUserInfo 這個 handler,用於編寫在獲取到用戶信息後的處理邏輯,這裏就是傳入剛剛實現的 onGetUserInfo

WeappLoginButton 的樣式 index.scss 代碼以下:

.login-button {
  width: 100%;
  margin-top: 40px;
  margin-bottom: 40px;
}

實現 AlipayLoginButton

讓咱們來實現支付寶登陸按鈕組件。建立 src/components/AlipayLoginButton 目錄,在其中分別建立 index.jsindex.scssindex.js 代碼以下:

import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'

import './index.scss'

export default function LoginButton(props) {
  const [isLogin, setIsLogin] = useState(false)

  async function onGetAuthorize(res) {
    setIsLogin(true)
    try {
      let userInfo = await Taro.getOpenUserInfo()

      userInfo = JSON.parse(userInfo.response).response
      const { avatar, nickName } = userInfo

      await props.setLoginInfo(avatar, nickName)
    } catch (err) {
      console.log('onGetAuthorize ERR: ', err)
    }

    setIsLogin(false)
  }

  return (
    <Button
      openType="getAuthorize"
      scope="userInfo"
      onGetAuthorize={onGetAuthorize}
      type="primary"
      className="login-button"
      loading={isLogin}
    >
      支付寶登陸
    </Button>
  )
}

能夠看到,內容與以前的微信登陸按鈕基本類似,可是有如下差異:

  • 實現 onGetAuthorize 回調函數。與以前微信的回調函數不一樣,這裏咱們要調用 Taro.getOpenUserInfo 手動獲取用戶基礎信息(實際上調用的是支付寶開放平臺 my.getOpenUserInfo
  • Button 組件的 openType(支付寶開放能力)設置成 getAuthorize(小程序受權)
  • 在設定開放能力爲 getAuthorize 時,須要添加 scope 屬性爲 userInfo,讓用戶能夠受權小程序獲取支付寶會員的基礎信息(另外一個有效值是 phoneNumber,用於獲取手機號碼)
  • 傳入 onGetAuthorize 回調函數
提示

關於支付寶小程序登陸按鈕的細節,能夠查看官方文檔

樣式文件 index.scss 的代碼以下:

.login-button {
  width: 100%;
  margin-top: 40px;
}

實現 LoggedMine

接着咱們實現已經登陸狀態下的 LoggedMine 組件。建立 src/components/LoggedMine 目錄,在其中分別建立 index.jsxindex.scssindex.jsx 代碼以下:

import Taro from '@tarojs/taro'
import { View, Image } from '@tarojs/components'
import PropTypes from 'prop-types'

import './index.scss'
import avatar from '../../images/avatar.png'

export default function LoggedMine(props) {
  const { userInfo = {} } = props
  function onImageClick() {
    Taro.previewImage({
      urls: [userInfo.avatar],
    })
  }

  return (
    <View className="logged-mine">
      <Image
        src={userInfo.avatar ? userInfo.avatar : avatar}
        className="mine-avatar"
        onClick={onImageClick}
      />
      <View className="mine-nickName">
        {userInfo.nickName ? userInfo.nickName : '圖雀醬'}
      </View>
      <View className="mine-username">{userInfo.username}</View>
    </View>
  )
}

LoggedMine.propTypes = {
  avatar: PropTypes.string,
  nickName: PropTypes.string,
  username: PropTypes.string,
}

這裏咱們添加了點擊頭像能夠預覽的功能,能夠經過 Taro.previewImage 函數實現。

LoggedMine 組件的樣式文件以下:

.logged-mine {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.mine-avatar {
  width: 200px;
  height: 200px;
  border-radius: 50%;
}

.mine-nickName {
  font-size: 40;
  margin-top: 20px;
}

.mine-username {
  font-size: 32px;
  margin-top: 16px;
  color: #777;
}

實現 Header 組件

在全部的「小零件」所有實現後,咱們就實現整個登陸界面的 Header 部分。建立 src/components/Header 目錄,在其中分別建立 index.jsindex.scssindex.js 代碼以下:

import Taro from '@tarojs/taro'
import { View } from '@tarojs/components'
import { AtMessage } from 'taro-ui'

import LoggedMine from '../LoggedMine'
import LoginButton from '../LoginButton'
import WeappLoginButton from '../WeappLoginButton'
import AlipayLoginButton from '../AlipayLoginButton'

import './index.scss'

export default function Header(props) {
  const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
  const isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY

  return (
    <View className="user-box">
      <AtMessage />
      <LoggedMine userInfo={props.userInfo} />
      {!props.isLogged && (
        <View className="login-button-box">
          <LoginButton handleClick={props.handleClick} />
          {isWeapp && <WeappLoginButton setLoginInfo={props.setLoginInfo} />}
          {isAlipay && <AlipayLoginButton setLoginInfo={props.setLoginInfo} />}
        </View>
      )}
    </View>
  )
}

能夠看到,咱們根據 Taro.ENV_TYPE 查詢當前所在的平臺(微信、支付寶或其餘),而後肯定是否顯示相應平臺的登陸按鈕。

提示

你也許發現了,setLoginInfo 仍是要等待父組件的傳入。雖然 Hooks 簡化了狀態的定義和更新方式,可是卻沒有簡化跨組件修改狀態的邏輯。在接下來的一步,咱們將用 Redux 進行簡化。

Header 組件的樣式代碼以下:

.user-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
}

.login-button-box {
  margin-top: 60px;
  width: 100%;
}

實現 LoginForm

接着咱們實現用於普通登陸的 LoginForm 組件。因爲本系列教程的目標是講解 Taro,所以這裏簡化了註冊/登陸的流程,用戶能夠直接輸入用戶名並上傳頭像進行註冊/登陸,無需設置密碼和其餘驗證過程。建立 src/components/LoginForm 目錄,在其中分別建立 index.jsxindex.scssindex.jsx 代碼以下:

import Taro, { useState } from '@tarojs/taro'
import { View, Form } from '@tarojs/components'
import { AtButton, AtImagePicker } from 'taro-ui'

import './index.scss'

export default function LoginForm(props) {
  const [showAddBtn, setShowAddBtn] = useState(true)

  function onChange(files) {
    if (files.length > 0) {
      setShowAddBtn(false)
    }

    props.handleFilesSelect(files)
  }

  function onImageClick() {
    Taro.previewImage({
      urls: [props.files[0].url],
    })
  }

  return (
    <View className="post-form">
      <Form onSubmit={props.handleSubmit}>
        <View className="login-box">
          <View className="avatar-selector">
            <AtImagePicker
              length={1}
              mode="scaleToFill"
              count={1}
              files={props.files}
              showAddBtn={showAddBtn}
              onImageClick={onImageClick}
              onChange={onChange}
            />
          </View>
          <Input
            className="input-nickName"
            type="text"
            placeholder="點擊輸入暱稱"
            value={props.formNickName}
            onInput={props.handleNickNameInput}
          />
          <AtButton formType="submit" type="primary">
            登陸
          </AtButton>
        </View>
      </Form>
    </View>
  )
}

這裏咱們使用 Taro UI 的 ImagePicker 圖片選擇器組件,讓用戶可以選擇圖片進行上傳。AtImagePicker 最重要的屬性就是 onChange 回調函數,這裏咱們經過父組件傳進來的 handleFilesSelect 函數來搞定。

LoginForm 組件的樣式代碼以下:

.post-form {
  margin: 0 30px;
  padding: 30px;
}

.input-nickName {
  border: 1px solid #eee;
  padding: 10px;
  font-size: medium;
  width: 100%;
  margin-top: 40px;
  margin-bottom: 40px;
}

.avatar-selector {
  width: 200px;
  margin: 0 auto;
}

實現 Logout

在登陸以後,咱們還須要退出登陸的按鈕。建立 src/components/Logout/index.js 文件,代碼以下:

import Taro from '@tarojs/taro'
import { AtButton } from 'taro-ui'

export default function LoginButton(props) {
  return (
    <AtButton
      type="secondary"
      full
      loading={props.loading}
      onClick={props.handleLogout}
    >
      退出登陸
    </AtButton>
  )
}

實現 Footer

全部的子組件所有實現以後,咱們就來實現 Footer 組件。建立 src/components/Footer 目錄,在其中分別建立 index.jsxindex.scssindex.jsx 代碼以下:

import Taro, { useState } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { AtFloatLayout } from 'taro-ui'

import Logout from '../Logout'
import LoginForm from '../LoginForm'
import './index.scss'

export default function Footer(props) {
  // Login Form 登陸數據
  const [formNickName, setFormNickName] = useState('')
  const [files, setFiles] = useState([])

  async function handleSubmit(e) {
    e.preventDefault()

    // 鑑權數據
    if (!formNickName || !files.length) {
      Taro.atMessage({
        type: 'error',
        message: '您還有內容沒有填寫!',
      })

      return
    }

    // 提示登陸成功
    Taro.atMessage({
      type: 'success',
      message: '恭喜您,登陸成功!',
    })

    // 緩存在 storage 裏面
    const userInfo = { avatar: files[0].url, nickName: formNickName }
    await props.handleSubmit(userInfo)

    // 清空表單狀態
    setFiles([])
    setFormNickName('')
  }

  return (
    <View className="mine-footer">
      {props.isLogged && (
        <Logout loading={props.isLogout} handleLogout={props.handleLogout} />
      )}
      <View className="tuture-motto">
        {props.isLogged ? 'From 圖雀社區 with Love ❤' : '您還未登陸'}
      </View>
      <AtFloatLayout
        isOpened={props.isOpened}
        title="登陸"
        onClose={() => props.handleSetIsOpened(false)}
      >
        <LoginForm
          formNickName={formNickName}
          files={files}
          handleSubmit={e => handleSubmit(e)}
          handleNickNameInput={e => setFormNickName(e.target.value)}
          handleFilesSelect={files => setFiles(files)}
        />
      </AtFloatLayout>
    </View>
  )
}

Footer 組件的樣式文件代碼以下:

.mine-footer {
  font-size: 28px;
  color: #777;
  margin-bottom: 20px;
}

.tuture-motto {
  margin-top: 40px;
  text-align: center;
}

全部小組件都搞定以後,咱們在 src/components 中只需暴露出 HeaderFooter。修改 src/components/index.jsx,代碼以下:

import PostCard from './PostCard'
import PostForm from './PostForm'
import Footer from './Footer'
import Header from './Header'

export { PostCard, PostForm, Footer, Header }

更新「個人」頁面

是時候用上寫好的 HeaderFooter 組件了,但在此以前,咱們先來說一下咱們須要用到的 useEffect Hooks。

useEffect Hooks

useEffect Hooks 是用來替代原 React 的生命週期鉤子函數的,咱們能夠在裏面發起一些 「反作用」 操做,好比異步獲取後端數據、設置定時器或是進行 DOM 操做等:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 和 componentDidMount 以及 componentDidUpdate 相似:
  useEffect(() => {
    // 使用瀏覽器 API 更新 document 的標題
    document.title = `你點擊了 ${count} 次`;
  });

  return (
    <div>
      <p>你點擊了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        點我
      </button>
    </div>
  );
}

上面的對 document 標題的修改是具備反作用的操做,在以前的 React 應用中,咱們一般會這麼寫:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `你點擊了 ${this.state.count} 次`;
  }

  componentDidUpdate() {
    document.title = `你點擊了 ${this.state.count} 次`;
  }

  render() {
    return (
      <div>
        <p>你點擊了 {this.state.count} 次</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          點我
        </button>
      </div>
    );
  }
}

若是你想了解 useEffect 具體的詳情,能夠去查看 React 的官方文檔

作的好!瞭解了 useEffect Hooks 的概念以後,咱們立刻來更新「個人」頁面組件 src/pages/mine/mine.jsx,代碼以下:

import Taro, { useState, useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'

import { Header, Footer } from '../../components'
import './mine.scss'

export default function Mine() {
  const [nickName, setNickName] = useState('')
  const [avatar, setAvatar] = useState('')
  const [isOpened, setIsOpened] = useState(false)
  const [isLogout, setIsLogout] = useState(false)

  // 雙取反來構造字符串對應的布爾值,用於標誌此時是否用戶已經登陸
  const isLogged = !!nickName

  useEffect(() => {
    async function getStorage() {
      try {
        const { data } = await Taro.getStorage({ key: 'userInfo' })

        const { nickName, avatar } = data
        setAvatar(avatar)
        setNickName(nickName)
      } catch (err) {
        console.log('getStorage ERR: ', err)
      }
    }

    getStorage()
  })

  async function setLoginInfo(avatar, nickName) {
    setAvatar(avatar)
    setNickName(nickName)

    try {
      await Taro.setStorage({
        key: 'userInfo',
        data: { avatar, nickName },
      })
    } catch (err) {
      console.log('setStorage ERR: ', err)
    }
  }

  async function handleLogout() {
    setIsLogout(true)

    try {
      await Taro.removeStorage({ key: 'userInfo' })

      setAvatar('')
      setNickName('')
    } catch (err) {
      console.log('removeStorage ERR: ', err)
    }

    setIsLogout(false)
  }

  function handleSetIsOpened(isOpened) {
    setIsOpened(isOpened)
  }

  function handleClick() {
    handleSetIsOpened(true)
  }

  async function handleSubmit(userInfo) {
    // 緩存在 storage 裏面
    await Taro.setStorage({ key: 'userInfo', data: userInfo })

    // 設置本地信息
    setAvatar(userInfo.avatar)
    setNickName(userInfo.nickName)

    // 關閉彈出層
    setIsOpened(false)
  }

  return (
    <View className="mine">
      <Header
        isLogged={isLogged}
        userInfo={{ avatar, nickName }}
        handleClick={handleClick}
        setLoginInfo={setLoginInfo}
      />
      <Footer
        isLogged={isLogged}
        isOpened={isOpened}
        isLogout={isLogout}
        handleLogout={handleLogout}
        handleSetIsOpened={handleSetIsOpened}
        handleSubmit={handleSubmit}
      />
    </View>
  )
}

Mine.config = {
  navigationBarTitleText: '個人',
}

能夠看到,咱們作了這麼些工做:

  • 使用 useState 建立了四個狀態:用戶有關信息(nickNameavatar),登陸彈出層是否打開(isOpened),是否登陸成功(isLogged),以及相應的更新函數
  • 經過 useEffect Hook 嘗試從本地緩存中獲取用戶信息(Taro.getStorage),並用來更新 nickNameavatar 狀態
  • 實現了久違的 setLoginInfo 函數,其中咱們不只更新了 nickNameavatar 的狀態,還把用戶數據存入本地緩存(Taro.getStorage),確保下次打開時保持登陸狀態
  • 實現了一樣久違的 handleLogout 函數,其中不只更新了相關狀態,還去掉了本地緩存中的數據(Taro.removeStorage
  • 實現了用於處理普通登陸的 handleSubmit 函數,內容基本上與 setLoginInfo 一致
  • 在返回 JSX 代碼時渲染 HeaderFooter 組件,傳入相應的狀態和回調函數

調整 Mine 組件的樣式 src/pages/mine/mine.scss 代碼以下:

.mine {
  margin: 30px;
  height: 90vh;
  padding: 40px 40px 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

最後在 src/app.scss 中引入相應的 Taro UI 組件的樣式:

@import './custom-theme.scss';

@import '~taro-ui/dist/style/components/button.scss';
@import '~taro-ui/dist/style/components/fab.scss';
@import '~taro-ui/dist/style/components/icon.scss';
@import '~taro-ui/dist/style/components/float-layout.scss';
@import '~taro-ui/dist/style/components/textarea.scss';
@import '~taro-ui/dist/style/components/message.scss';
@import '~taro-ui/dist/style/components/avatar.scss';
@import '~taro-ui/dist/style/components/image-picker.scss';
@import '~taro-ui/dist/style/components/icon.scss';

查看效果

終於到了神聖的驗收環節。首先是普通登陸:

而微信和支付寶登陸,點擊以後就會直接以登陸開發者工具所用的賬號登陸了。下面貼出我微信和支付寶登陸後的界面展現:

登陸後點擊下方的「退出登陸」按鈕,就會將當前登陸賬戶註銷哦。

至此,《Taro 多端小程序開發大型實戰》第三篇也就結束啦。在接下來的第四篇中,咱們將逐步用 Redux 來重構業務數據流,讓咱們如今略顯臃腫的狀態管理變得清晰可控。

想要學習更多精彩的實戰技術教程?來 圖雀社區逛逛吧。

本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦~

相關文章
相關標籤/搜索