欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:css
而在这一篇中,咱们将实现微信和支付宝多端登陆。若是你但愿直接从这一篇开始,请运行如下命令:html
git clone -b third-part https://github.com/tuture-dev/ultra-club.git cd ultra-club
本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦~
在正式开始以前,咱们但愿你已经具有如下知识:react
useState
、useEffect
)有所了解,后面图雀社区将推出 “一杯茶的时间,上手 React Hooks”,敬请期待!除此以外,你还须要下载并安装支付宝开发者工具,登陆后建立本身的小程序 ID。git
与普通的 Web 应用相比,小程序可以在所在的平台实现一键登陆,很是方便。这一步,咱们也将实现多端登陆(主要包括微信登陆和支付宝登陆)。之因此标题取为“群魔乱舞”,不只受了“震惊”小编们的启发,也是由于当今各平台处理登陆和鉴权的方式差别很大,很遗憾的是在 Taro 框架下咱们依然须要踩不少“坑”才能真正实现“多端登陆”。github
这一节的代码很长,在正式开始以前咱们先查看一下组件设计的规划,便于你对接下来咱们要作的工做有清晰的了解。npm
能够看到“个人”页面总体拆分红了 Header
和 Footer
:小程序
Header
包括 LoggedMine
(我的信息),若是在未登陆状态下则还有 LoginButton
(普通登陆按钮)、WeappLoginButton
(微信登陆按钮,仅在微信小程序中出现)以及 AlipayLoginButton
(支付宝登陆按钮,仅在支付宝小程序中出现)Footer
则用来显示是否已登陆的文字,在已登陆的状况下会显示 Logout
(退出登陆按钮)从这一步开始,咱们将首次开始写异步代码。本项目将采用流行的 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
组件。建立 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
。建立 src/components/WeappLoginButton
目录,在其中分别建立 index.js
和 index.scss
。index.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; }
让咱们来实现支付宝登陆按钮组件。建立 src/components/AlipayLoginButton
目录,在其中分别建立 index.js
和 index.scss
。index.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
组件。建立 src/components/LoggedMine
目录,在其中分别建立 index.jsx
和 index.scss
。index.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
部分。建立 src/components/Header
目录,在其中分别建立 index.js
和 index.scss
。index.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
组件。因为本系列教程的目标是讲解 Taro,所以这里简化了注册/登陆的流程,用户能够直接输入用户名并上传头像进行注册/登陆,无需设置密码和其余验证过程。建立 src/components/LoginForm
目录,在其中分别建立 index.jsx
和 index.scss
。index.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; }
在登陆以后,咱们还须要退出登陆的按钮。建立 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
组件。建立 src/components/Footer
目录,在其中分别建立 index.jsx
和 index.scss
。index.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
中只需暴露出 Header
和 Footer
。修改 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 }
是时候用上写好的 Header
和 Footer
组件了,但在此以前,咱们先来说一下咱们须要用到的 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
建立了四个状态:用户有关信息(nickName
和 avatar
),登陆弹出层是否打开(isOpened
),是否登陆成功(isLogged
),以及相应的更新函数useEffect
Hook 尝试从本地缓存中获取用户信息(Taro.getStorage),并用来更新 nickName
和 avatar
状态setLoginInfo
函数,其中咱们不只更新了 nickName
和 avatar
的状态,还把用户数据存入本地缓存(Taro.getStorage),确保下次打开时保持登陆状态handleLogout
函数,其中不只更新了相关状态,还去掉了本地缓存中的数据(Taro.removeStorage)handleSubmit
函数,内容基本上与 setLoginInfo
一致Header
和 Footer
组件,传入相应的状态和回调函数调整 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仓库加星❤️哦~