歡迎繼續閱讀《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倉庫加星❤️哦~