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仓库加星❤️哦~

相关文章
相关标签/搜索