技術(shù)棧
目標:了解開發(fā)本項目所要使用的各類框架塑娇、庫
腳手架工具:
create-react-app
組件編寫方式:
函數(shù)組件
+Hooks
路由組件庫:
react-router-dom
全局狀態(tài)庫:
redux + redux-thunk
網(wǎng)絡(luò)請求庫:
axios
UI組件庫:
antd-mobile
、以及一些用來實現(xiàn)特定功能的第三方組件(如:formik
坤邪、react-content-loader
嫩絮、react-window-infinite-loader
等)
項目準備
創(chuàng)建新項目
目標:使用腳手架命令創(chuàng)新項目
操作步驟
- 通過命令行創(chuàng)建項目
create-react-app geek-park
- 修改頁面模板
public/index.html
中的頁面標題
<title>極客園 App</title>
- 刪除
src
目錄中的所有文件 - 新增文件
/src
/assets 項目資源文件丛肢,比如,圖片 等
/components 通用組件
/pages 頁面
/utils 工具剿干,比如,token穆刻、axios 的封裝等
App.js 根組件
index.scss 全局樣式
index.js 項目入口
公用樣式
目標:將本項目要用的公用樣式文件放入合適的目錄置尔,并調(diào)用
【重要說明】
在本課程發(fā)放的資料中,有個 `資源 > src代碼文件 > assets` 目錄氢伟,里面存放著公用樣式文件和圖片資源榜轿,可直接復制到你的代碼中使用。
操作步驟
- 將上面提到的
assets
目錄朵锣,直接拷貝到新項目的src
目錄下
- 在主入口
index.js
中導入公用樣式文件
import './assets/styles/index.scss'
配置 SASS 支持
目標:讓項目樣式支持使用 SASS/SCSS 語法編寫
操作步驟
- 安裝
sass
yarn add sass --save-dev
配置 UI 組件庫
目標:安裝本項目使用的 UI 組件庫 Ant Design Mobile谬盐,并通過 Babel 插件實現(xiàn)按需加載
https://mobile.ant.design/index-cn
操作步驟
- 安裝
antd-mobile
yarn add antd-mobile
- 導入樣式
import 'antd-mobile/dist/antd-mobile.css'
- 使用組件
import { Button, Toast } from 'antd-mobile'
export default function App() {
return (
<div className="app">
<Button
type="primary"
onClick={() => Toast.success('Load success !!!', 1)}
>
default disabled
</Button>
</div>
)
}
antd-按需加載
https://mobile.ant.design/docs/react/use-with-create-react-app-cn
craco
實現(xiàn)思路:
- 使用 customize-cra 來添加和覆蓋腳手架的 webpack 配置
- 使用
react-app-rewired
來打包和運行代碼
- 使用 babel-plugin-import, babel-plugin-import 是一個用于按需加載組件代碼和樣式的 babel 插件
操作步驟
- 安裝
customize-cra
和react-app-rewired
yarn add customize-cra react-app-rewired babel-plugin-import -D
- 在項目根目錄中創(chuàng)建
config-overrides.js
,并編寫如下代碼:
const { override, fixBabelImports } = require('customize-cra')
// 導出要進行覆蓋的 webpack 配置
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd-mobile',
style: 'css',
})
)
- 修改啟動命令
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
- 刪除index.js的樣式導入
- import 'antd-mobile/dist/antd-mobile.css'
- 重啟項目測試
配置快捷路徑 @
目標:讓代碼中支持以
@/xxxx
形式的路徑來導入文件
操作步驟
- 在項目根目錄中創(chuàng)建
config-overrides.js
诚些,并編寫如下代碼:
const path = require('path')
const { override, fixBabelImports, addWebpackAlias } = require('customize-cra')
const babelPlugins = fixBabelImports('import', {
libraryName: 'antd-mobile',
style: 'css',
})
const webpackAlias = addWebpackAlias({
'@': path.resolve(__dirname, 'src'),
'@scss': path.resolve(__dirname, 'src', 'assets', 'styles'),
})
// 導出要進行覆蓋的 webpack 配置
module.exports = override(babelPlugins, webpackAlias)
- 在項目根目錄中創(chuàng)建
jsconfig.json
飞傀,并編寫如下代碼,為了路徑有提示
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"],
"@scss/*": ["src/assets/styles/*"]
}
}
}
配置視口單位插件
目標:通過 webpack 插件將 px 單位自動轉(zhuǎn)換成視口長度單位 vw/vh诬烹,實現(xiàn)頁面對不同屏幕的自動適配
實現(xiàn)思路:
使用 postcss-px-to-viewport
插件砸烦,可讓我們直接在代碼中按設(shè)計稿的 px 值來編寫元素尺寸,它們最終會自動轉(zhuǎn)換成 vw/vh 長度單位绞吁。
操作步驟
- 安裝
postcss-px-to-viewport
yarn add postcss-px-to-viewport -D
- 在
config-overrides.js
中添加配置代碼:
const path = require('path')
const { override, addWebpackAlias, addPostcssPlugins } = require('customize-cra')
const px2viewport = require('postcss-px-to-viewport')
// 配置路徑別名
// ...
// 配置 PostCSS 樣式轉(zhuǎn)換插件
const postcssPlugins = addPostcssPlugins([
// 移動端布局 viewport 適配方案
px2viewport({
// 視口寬度:可以設(shè)置為設(shè)計稿的寬度
viewportWidth: 375,
// 白名單:不需對其中的 px 單位轉(zhuǎn)成 vw 的樣式類類名
// selectorBlackList: ['.ignore', '.hairlines']
})
])
// 導出要進行覆蓋的 webpack 配置
module.exports = override(alias, postcssPlugins)
配置路由管理器
目標:安裝 react-router-dom幢痘,創(chuàng)建 App 根組件并在該組件中配置路由
操作步驟
- 安裝
react-router-dom
yarn add react-router-dom
- 創(chuàng)建兩個組件
pages/Home/index.js
pages/Login/index.js
- 創(chuàng)建
App.js
,編寫根組件:
import React, { Suspense } from 'react'
import {
BrowserRouter as Router,
Route,
Switch,
Redirect,
} from 'react-router-dom'
import './App.scss'
const Login = React.lazy(() => import('@/pages/Login'))
const Home = React.lazy(() => import('@/pages/Home'))
export default function App() {
return (
<Router>
<div className="app">
{/* <Link to="/login">登錄</Link>
<Link to="/home">首頁</Link> */}
<Suspense fallback={<div>loading...</div>}>
<Switch>
<Redirect exact from="/" to="/home"></Redirect>
<Route path="/login" component={Login}></Route>
<Route path="/home" component={Home}></Route>
</Switch>
</Suspense>
</div>
</Router>
)
}
配置 Redux
目標:安裝
redux
和redux-thunk
相關(guān)的依賴包家破,并創(chuàng)建 Redux Store 實例后關(guān)聯(lián)到應(yīng)用上
所要用到的依賴包:
- redux
- react-redux
- redux-thunk
- redux-devtools-extension
操作步驟
- 安裝依賴包
yarn add redux react-redux redux-thunk redux-devtools-extension
- 創(chuàng)建
store
目錄及它的子目錄actions
颜说、reducers
购岗,專門存放 redux 相關(guān)代碼
- 創(chuàng)建
store/reducers/index.js
,用來作為組合所有 reducers 的主入口:
import { combineReducers } from 'redux'
// 組合各個 reducer 函數(shù)门粪,成為一個根 reducer
const rootReducer = combineReducers({
// 一個測試用的 reducer喊积,避免運行時因沒有 reducer 而報錯
test: (state = 0, action) => (state)
// 在這里配置有所的 reducer ...
})
// 導出根 reducer
export default rootReducer
- 創(chuàng)建
store/index.js
,編寫 Redux Store 實例:
import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import rootReducer from './reducers'
// 創(chuàng)建 Store 實例
const store = createStore(
// 參數(shù)一:根 reducer
rootReducer,
// 參數(shù)二:初始化時要加載的狀態(tài)
{},
// 參數(shù)三:增強器
composeWithDevTools(
applyMiddleware(thunk)
)
)
// 導出 Store 實例
export default store
- 在主入口
index.js
中庄拇,配置 Redux Provider
import '@scss/index.scss'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from '@/store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
頁面
字體圖標的基本使用
- 如果使用class類名的方式注服,彩色圖標無法生效
- 可以通過js的方式來使用圖標
1. 引入js
<script src="http://at.alicdn.com/t/font_2791161_ymhdfblw14.js"></script>
2. 樣式
/* 字體圖標 */
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
3. 使用
<svg className="icon" aria-hidden="true">
<use xlinkHref="#icon-mianxingfeizhunan"></use>
</svg>
封裝 svg 圖標小組件
//at.alicdn.com/t/font_2503709_f4q9dl3hktl.js
目標:實現(xiàn)一個用于在頁面上顯示 svg 小圖標的組件,方便后續(xù)開發(fā)中為界面添加小圖標
實現(xiàn)思路:
- 在組件中措近,輸出一段使用 <use> 標簽引用事先準備好的 SVG 圖片資源的 <svg> 代碼
- 組件需要傳入 SVG 圖片的名字溶弟,用來顯示不同的圖標
- 組件可以設(shè)置額外的樣式類名、及點擊事件監(jiān)聽
操作步驟
- 安裝
classnames
瞭郑,輔助組件的開發(fā)
yarn add classnames
- 在
public/index.html
中引入 svg 圖標資源:
<script src="http://at.alicdn.com/t/font_2503709_f4q9dl3hktl.js"></script>
- 創(chuàng)建
components/Icon/index.js
辜御,編寫圖標組件:
import React from 'react'
import classNames from 'classnames'
import PropTypes from 'prop-types'
// ``
function Icon({ type, className, ...rest }) {
return (
<svg {...rest} className={classNames('icon', className)} aria-hidden="true">
<use xlinkHref={`#${type}`}></use>
</svg>
)
}
Icon.propTypes = {
type: PropTypes.string.isRequired,
}
export default Icon
- 測試組件,確認能否正確顯示出圖標
<Icon
type="iconbtn_share"
className="test-icon"
onClick={() => { alert('clicked') }}
/>
實現(xiàn)頂部導航欄組件
- 基礎(chǔ)結(jié)構(gòu)
import React from 'react'
import Icon from '@/components/Icon'
import styles from './index.module.scss'
export default function Login() {
return (
<div className={styles.root}>
{/* 后退按鈕 */}
<div className="left">
<Icon type="iconfanhui" />
</div>
{/* 居中標題 */}
<div className="title">我是標題</div>
{/* 右側(cè)內(nèi)容 */}
<div className="right">右側(cè)內(nèi)容</div>
</div>
)
}
- 樣式
.root {
position: relative;
display: flex;
align-items: center;
height: 46px;
width: 100%;
// padding: 0 42px;
background-color: #fff;
border-bottom: 1px solid #ccc;
:global {
.left {
padding: 0 12px 0 16px;
line-height: 46px;
}
.icon {
font-size: 16px;
}
.title {
flex: 1;
margin: 0 auto;
color: #323233;
font-weight: 500;
font-size: 16px;
text-align: center;
}
.right {
padding-right: 16px;
// position: absolute;
// right: 16px;
}
}
}
移動端 1px 像素邊框
- 參考 antd-mobile 的實現(xiàn)
- 實現(xiàn)方式參考
- 實現(xiàn)原理:偽元素 + transform 縮放
- 偽元素
::after
或::before
獨立于當前元素屈张,可以單獨對其縮放而不影響元素本身的縮放
- 偽元素
// src/assets/styles/hairline.scss
@mixin scale-hairline-common($color, $top, $right, $bottom, $left) {
content: '';
position: absolute;
display: block;
z-index: 1;
top: $top;
right: $right;
bottom: $bottom;
left: $left;
background-color: $color;
}
// 添加邊框
/*
用法:
// 導入
@import '@scss/hairline.scss';
// 在類中使用
.a {
@include hairline(bottom, #f0f0f0);
}
*/
@mixin hairline($direction, $color: #000, $radius: 0) {
@if $direction == top {
border-top: 1px solid $color;
// min-resolution 用來檢測設(shè)備的最小像素密度
@media (min-resolution: 2dppx) {
border-top: none;
&::before {
@include scale-hairline-common($color, 0, auto, auto, 0);
width: 100%;
height: 1px;
transform-origin: 50% 50%;
transform: scaleY(0.5);
@media (min-resolution: 3dppx) {
transform: scaleY(0.33);
}
}
}
} @else if $direction == right {
border-right: 1px solid $color;
@media (min-resolution: 2dppx) {
border-right: none;
&::after {
@include scale-hairline-common($color, 0, 0, auto, auto);
width: 1px;
height: 100%;
background: $color;
transform-origin: 100% 50%;
transform: scaleX(0.5);
@media (min-resolution: 3dppx) {
transform: scaleX(0.33);
}
}
}
} @else if $direction == bottom {
border-bottom: 1px solid $color;
@media (min-resolution: 2dppx) {
border-bottom: none;
&::after {
@include scale-hairline-common($color, auto, auto, 0, 0);
width: 100%;
height: 1px;
transform-origin: 50% 100%;
transform: scaleY(0.5);
@media (min-resolution: 3dppx) {
transform: scaleY(0.33);
}
}
}
} @else if $direction == left {
border-left: 1px solid $color;
@media (min-resolution: 2dppx) {
border-left: none;
&::before {
@include scale-hairline-common($color, 0, auto, auto, 0);
width: 1px;
height: 100%;
transform-origin: 100% 50%;
transform: scaleX(0.5);
@media (min-resolution: 3dppx) {
transform: scaleX(0.33);
}
}
}
} @else if $direction == all {
border: 1px solid $color;
border-radius: $radius;
@media (min-resolution: 2dppx) {
position: relative;
border: none;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 200%;
height: 200%;
border: 1px solid $color;
border-radius: $radius * 2;
transform-origin: 0 0;
transform: scale(0.5);
box-sizing: border-box;
pointer-events: none;
}
}
}
}
// 移除邊框
@mixin hairline-remove($position: all) {
@if $position == left {
border-left: 0;
&::before {
display: none !important;
}
} @else if $position == right {
border-right: 0;
&::after {
display: none !important;
}
} @else if $position == top {
border-top: 0;
&::before {
display: none !important;
}
} @else if $position == bottom {
border-bottom: 0;
&::after {
display: none !important;
}
} @else if $position == all {
border: 0;
&::before {
display: none !important;
}
&::after {
display: none !important;
}
}
}
- 需要導入這個scss
// 導入另一個scss文件
@import '@scss/hiarline.scss';
.root {
position: relative;
display: flex;
align-items: center;
height: 46px;
width: 100%;
// padding: 0 42px;
background-color: #fff;
// border-bottom: 1px solid red;
@include hairline('bottom', red);
實現(xiàn)頂部導航欄組件-封裝
目標:封裝頂部導航欄組件擒权,可以用來顯示頁面標題、后退按鈕阁谆、及添加額外的功能區(qū)域
圖例一:
<img src="極客園移動端1.assets/image-20210831163053705.png" alt="image-20210831163053705" style="zoom:30%;" />
圖例二:
<img src="極客園移動端1.assets/image-20210831163126729.png" alt="image-20210831163126729" style="zoom:30%;" />
圖例三:
<img src="極客園移動端1.assets/image-20210831205954290.png" alt="image-20210831205954290" style="zoom:30%;" />
實現(xiàn)思路:
- 組件布局分為:左碳抄、中、右三個區(qū)域
- 可通過組件屬性傳入內(nèi)容场绿,填充中間和右邊區(qū)域
- 可為左邊的“后退”按鈕添加事件監(jiān)聽
操作步驟
- 創(chuàng)建
components/NavBar/index.js
剖效,并在該目錄拷貝入資源包中的樣式文件,然后編寫組件代碼:
import React from 'react'
import Icon from '@/components/Icon'
import styles from './index.module.scss'
import { useHistory } from 'react-router'
// import { withRouter } from 'react-router-dom'
// 1. withRouter的使用
// history match location: 這個組件必須是通過路由配置的 <Route></Route>
// 自己渲染的組件焰盗,無法獲取到路由信息 <NavBar></NavBar>
// 2. 路由提供了幾個和路由相關(guān)的hook
// useHistory useLocation useParams
function NavBar({ children, extra }) {
const history = useHistory()
const back = () => {
// 跳回上一頁
history.go(-1)
}
return (
<div className={styles.root}>
{/* 后退按鈕 */}
<div className="left">
<Icon type="iconfanhui" onClick={back} />
</div>
{/* 居中標題 */}
<div className="title">{children}</div>
{/* 右側(cè)內(nèi)容 */}
<div className="right">{extra}</div>
</div>
)
}
export default NavBar
- 測試組件功能
<NavBar
onLeftClick={() => alert(123)}
rightContent={
<span>右側(cè)內(nèi)容</span>
}
>
標題內(nèi)容
</NavBar>
效果:
<img src="極客園移動端1.assets/image-20210831212932784.png" alt="image-20210831212932784" style="zoom:50%;" />
表單基本結(jié)構(gòu)
- 結(jié)構(gòu)
import React from 'react'
import NavBar from '@/components/NavBar'
import styles from './index.module.scss'
export default function Login() {
return (
<div className={styles.root}>
<NavBar>登錄</NavBar>
<div className="content">
{/* 標題 */}
<h3>短信登錄</h3>
<form>
{/* 手機號輸入框 */}
<div className="input-item">
<div className="input-box">
<input
className="input"
name="mobile"
placeholder="請輸入手機號"
autoComplete="off"
/>
</div>
<div className="validate">手機號驗證錯誤信息</div>
</div>
{/* 短信驗證碼輸入框 */}
<div className="input-item">
<div className="input-box">
<input
className="input"
name="code"
placeholder="請輸入驗證碼"
maxLength={6}
autoComplete="off"
/>
<div className="extra">獲取驗證碼</div>
</div>
<div className="validate">驗證碼驗證錯誤信息</div>
</div>
{/* 登錄按鈕 */}
<button type="submit" className="login-btn">
登錄
</button>
</form>
</div>
</div>
)
}
- 樣式
@import '@scss/hairline.scss';
.root {
:global {
.iconfanhui {
font-size: 20px;
}
.content {
padding: 0 32px;
h3 {
padding: 30px 0;
font-size: 24px;
}
.input-item {
position: relative;
&:first-child {
margin-bottom: 17px;
}
.input-box {
position: relative;
@include hairline(bottom, #ccc);
.input {
width: 100%;
height: 58px;
padding: 0;
font-size: 16px;
&::placeholder {
color: #a5a6ab;
}
}
.extra {
position: absolute;
right: 0;
top: 50%;
margin-top: -8px;
color: #999;
}
}
}
.validate {
position: absolute;
color: #ee0a24;
font-size: 12px;
}
.login-btn {
width: 100%;
height: 50px;
margin-top: 38px;
border-radius: 8px;
border: 0;
color: #fff;
background: linear-gradient(315deg, #fe4f4f, #fc6627);
}
.disabled {
background: linear-gradient(315deg, #ff9999, #ffa179);
}
}
}
}
實現(xiàn)能顯示額外內(nèi)容的Input組件
目標:將原生的 input 標簽進行封裝璧尸,使得該組件可在 input 右側(cè)放置額外內(nèi)容元素
<img src="極客園移動端1.assets/image-20210831213942878.png" alt="image-20210831213942878" style="zoom:50%;" />
實現(xiàn)思路:
- 左右布局:左側(cè)
<input>
,右側(cè)是一個可自定義的內(nèi)容區(qū)域 - 將封裝的組件傳入的屬性熬拒,全部傳遞到
<input>
標簽上爷光,使得能充分利用原標簽的功能
操作步驟
- 創(chuàng)建
components/Input/index.js
,并在該目錄拷貝入資源包中的樣式文件澎粟,然后編寫組件代碼:
import React from 'react'
import styles from './index.module.scss'
export default function Input({ extra, onExtraClick, ...rest }) {
return (
<div className={styles.root}>
<input className="input" {...rest} />
{extra && (
<div className="extra" onClick={onExtraClick}>
{extra}
</div>
)}
</div>
)
}
登錄頁面的靜態(tài)結(jié)構(gòu)
目標:實現(xiàn)登錄頁的頁面靜態(tài)結(jié)構(gòu)和樣式
登錄頁面布局分解:
<img src="極客園移動端1.assets/image-20210831220941555.png" alt="image-20210831220941555" style="zoom:50%;" />
【特別說明】
本案例中蛀序,表單盡量不使用 antd-mobile 組件庫里的表單組件來實現(xiàn),因為它的表單組件并不好用捌议,尤其是當要實現(xiàn)表單驗證時比較麻煩哼拔。
因此,我們會使用原生的表單標簽來實現(xiàn)瓣颅。
操作步驟
- 將資源包中登錄頁面的樣式文件拷貝到
pages/Login
目錄中倦逐,然后在pages/Login/index.js
中編寫如下代碼:
import NavBar from '@/components/NavBar'
import styles from './index.module.scss'
export default function Login() {
return (
<div className={styles.root}>
{/* 導航條 */}
<NavBar>登錄</NavBar>
{/* 內(nèi)容 */}
<div className="content">
<h3>短信登錄</h3>
<form>
<div className="input-item">
<input type="text" />
<div className="validate">手機號驗證錯誤信息</div>
</div>
<div className="input-item">
<input type="text" />
<div className="validate">驗證碼驗證錯誤信息</div>
</div>
{/* 登錄按鈕 */}
<button type="submit" className="login-btn">
登錄
</button>
</form>
</div>
</div>
)
}
登錄表單的數(shù)據(jù)綁定
目標:為登錄表單中的輸入組件進行數(shù)據(jù)綁定,收集表單數(shù)據(jù)
實現(xiàn)思路:
- 使用
formik
庫進行表單的數(shù)據(jù)綁定
操作步驟
- 安裝
formik
yarn add formik
- 使用
formik
庫中提供的 Hook 函數(shù)創(chuàng)建 formik 表單對象
import { useFormik } from 'formik'
// ...
// Formik 表單對象
const form = useFormik({
// 設(shè)置表單字段的初始值
initialValues: {
mobile: '13900001111',
code: '246810'
},
// 提交
onSubmit: values => {
console.log(values)
}
})
- 綁定表單元素和 formik 表單對象
<form onSubmit={form.handleSubmit}>
<Input
name="mobile"
placeholder="請輸入手機號"
value={form.values.mobile}
onChange={form.handleChange}
/>
<Input
name="code"
placeholder="請輸入驗證碼"
extra="發(fā)送驗證碼"
maxLength={6}
value={form.values.code}
onChange={form.handleChange}
/>
登錄表單的數(shù)據(jù)驗證-基本
- 給useFormik提供validate函數(shù)進行校驗
const formik = useFormik({
initialValues: {
mobile: '',
code: '',
},
// 當表單提交的時候,會觸發(fā)
onSubmit(values) {
console.log(values)
},
validate(values) {
const errors = {}
if (!values.mobile) {
errors.mobile = '手機號不能為空'
}
if (!values.code) {
errors.code = '驗證碼不能為空'
}
return errors
},
})
- 需要給每一個表單元素綁定一個事件 onBlur,,,目的是為了區(qū)分那些輸入框是被點擊過的
<Input
placeholder="請輸入手機號"
value={mobile}
name="mobile"
autoComplete="off"
onChange={handleChange}
onBlur={handleBlur}
></Input>
- 通過formik可以解構(gòu)出來兩個屬性 touched 和 errors,,,,控制錯誤信息的展示
{touched.mobile && errors.mobile ? (
<div className="validate">{errors.mobile}</div>
) : null}
登錄表單的數(shù)據(jù)驗證
目標:驗證表單中輸入的內(nèi)容的合法性
<img src="極客園移動端1.assets/image-20210901091137594.png" alt="image-20210901091137594" style="zoom:50%;" />
實現(xiàn)思路:
- 使用
formik
自帶的表單驗證功能 - 使用
yup
輔助編寫數(shù)據(jù)的驗證規(guī)則 - 驗證不通過時檬姥,在輸入項下顯示驗證后得到的實際錯誤信息
- 驗證不通過時曾我,禁用提交按鈕
操作步驟
- 安裝
yup
npm i yup --save
- 在創(chuàng)建
formik
表單對象時,添加表單驗證相關(guān)參數(shù)
import * as Yup from 'yup'
// Formik 表單對象
const form = useFormik({
// 表單驗證
validationSchema: Yup.object().shape({
// 手機號驗證規(guī)則
mobile: Yup.string()
.required('請輸入手機號')
.matches(/^1[3456789]\d{9}$/, '手機號格式錯誤'),
// 手機驗證碼驗證規(guī)則
code: Yup.string()
.required('請輸入驗證碼')
.matches(/^\d{6}$/, '驗證碼6個數(shù)字')
}),
// ...
})
- 處理驗證錯誤信息
// 原先的兩處錯誤信息代碼
<div className="validate">手機號驗證錯誤信息</div>
<div className="validate">{form.errors.code}</div>
// 改造成如下代碼
{form.errors.mobile && form.touched.mobile && (
<div className="validate">{form.errors.mobile}</div>
)}
{form.errors.code && form.touched.code && (
<div className="validate">{form.errors.code}</div>
)}
- 驗證出錯時禁用登錄按鈕
import classnames from 'classnames'
<button
type="submit"
className={classnames('login-btn', form.isValid ? '' : 'disabled')}
disabled={!form.isValid}
>
登錄
</button>
初步封裝網(wǎng)絡(luò)請求模塊
目標:將 axios 封裝成公用的網(wǎng)絡(luò)請求模塊健民,方便后續(xù)調(diào)用后端接口
(本章節(jié)中暫不處理 token 和 token 續(xù)期)
操作步驟
- 安裝
axios
npm i axios --save
- 創(chuàng)建
utils/request.js
抒巢,并編寫如下代碼
import axios from 'axios'
// 1. 創(chuàng)建新的 axios 實例
const http = axios.create({
baseURL: 'http://geek.itheima.net/v1_0'
})
// 2. 設(shè)置請求攔截器和響應(yīng)攔截器
http.interceptors.request.use(config => {
return config
})
http.interceptors.response.use(response => {
return response.data
}, error => {
return Promise.reject(error)
})
// 3. 導出該 axios 實例
export default http
發(fā)送手機驗證碼
目標:點擊登錄界面中的發(fā)送驗證碼按鈕,調(diào)用后端接口進行驗證碼的發(fā)送
<img src="極客園移動端1.assets/image-20210901092838694.png" alt="image-20210901092838694" style="zoom:50%;" />
實現(xiàn)思路:
- 實現(xiàn)一個 redux action 函數(shù)秉犹,請求發(fā)送驗證碼后端接口
- 在驗證碼的 Input 組件的
onExtraClick
事件監(jiān)聽函數(shù)中調(diào)用 action
操作步驟
- 創(chuàng)建
store/actions/login.js
蛉谜,并實現(xiàn)一個 Action 函數(shù)
import http from '@/utils/http'
/**
* 發(fā)送短信驗證碼
* @param {string} mobile 手機號碼
* @returns thunk
*/
export const sendValidationCode = (mobile) => {
return async (dispatch) => {
const res = await http.get(`/sms/codes/${mobile}`)
console.log(res)
}
}
- 為驗證碼輸入框組件添加
onExtraClick
事件監(jiān)聽
<Input
{/* ... */}
onExtraClick={sendSMSCode}
/>
- 實現(xiàn)事件監(jiān)聽函數(shù),調(diào)用 Action
import { useDispatch } from 'react-redux'
// 獲取 Redux 分發(fā)器
const dispatch = useDispatch()
// 發(fā)送短信驗證碼
const sendSMSCode = () => {
try {
// 手機號
const mobile = form.values.mobile
// 獲取 Action
const action = sendValidationCode(mobile)
// 調(diào)用 Action
dispatch(action)
} catch (e) { }
}
驗證碼倒計時功能
const onExtraClick = async () => {
if (time > 0) return
// 先對手機號進行驗證
if (!/^1[3-9]\d{9}$/.test(mobile)) {
formik.setTouched({
mobile: true,
})
return
}
try {
await dispatch(sendCode(mobile))
Toast.success('獲取驗證碼成功', 1)
// 開啟倒計時
setTime(5)
let timeId = setInterval(() => {
// 當我們每次都想要獲取到最新的狀態(tài)崇堵,需要寫成 箭頭函數(shù)的形式
setTime((time) => {
if (time === 1) {
clearInterval(timeId)
}
return time - 1
})
}, 1000)
} catch (err) {
if (err.response) {
Toast.info(err.response.data.message, 1)
} else {
Toast.info('服務(wù)器繁忙型诚,請稍后重試')
}
}
}
函數(shù)組件的特性
React 中的函數(shù)組件是通過函數(shù)來實現(xiàn)的,函數(shù)組件的公式:f(state) => UI
鸳劳,即:數(shù)據(jù)到視圖的映射狰贯。
函數(shù)組件本身很簡單,但因為是通過函數(shù)實現(xiàn)的赏廓,所以涵紊,在使用函數(shù)組件時,就會體現(xiàn)出函數(shù)所具有的特性來幔摸。
函數(shù)組件的特性說明:
- 對于函數(shù)組件來說摸柄,每次狀態(tài)更新后,組件都會重新渲染既忆。
- 并且塘幅,每次組件更新都像是在給組件拍照。每張照片就代表組件在某個特定時刻的狀態(tài)尿贫。快照
- 或者說:
組件的每次特定渲染踏揣,都有自己的 props/state/事件處理程序
等庆亡。 - 這些照片記錄的狀態(tài),從代碼層面來說捞稿,是通過 JS 中函數(shù)的閉包機制來實現(xiàn)的又谋。
這就是 React 中函數(shù)組件的特性,更加的函數(shù)式(利用函數(shù)的特性)
import { useState } from 'react'
import ReactDOM from 'react-dom'
// 沒有 hooks 的函數(shù)組件:
const Counter = ({ count }) => {
// console.log(count)
const showCount = () => {
setTimeout(() => {
console.log('展示 count 值:', count)
}, 3000)
}
return (
<div>
<button onClick={showCount}>點擊按鈕3秒后顯示count</button>
</div>
)
}
const App = () => {
const [count, setCount] = useState(0)
return (
<div>
<h1>計數(shù)器:{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<hr />
{/* 子組件 */}
<Counter count={count} />
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
useRef高級用法
登錄并獲取 Token
目標:點擊登錄按鈕后娱局,發(fā)送表單數(shù)據(jù)到后端登錄接口彰亥,獲取登錄憑證 Token
實現(xiàn)思路:
- 實現(xiàn)一個 Action,去調(diào)用后端登錄接口
- 在
formik
的表單提交方法onSubmit
中調(diào)用 Action
操作步驟
- 在
store/actions/login.js
中衰齐,添加一個 Action 函數(shù)
/**
* 登錄
* @param {{ mobile, code }} values 登錄信息
* @returns thunk
*/
export const login = params => {
return async dispatch => {
const res = await http.post('/authorizations', params)
const tokenInfo = res.data.data
console.log(tokenInfo)
}
}
- 在登錄頁面組件中的
formik
表單對象的onSubmit
方法中任斋,調(diào)用 Action
import { login, sendValidationCode } from '@/store/actions/login'
// Formik 表單對象
const form = useFormik({
// ...
// 提交
onSubmit: async values => {
await dispatch(login(values))
}
})
如果能成功獲取 Token 信息,控制臺會打印出如下內(nèi)容:
<img src="極客園移動端1.assets/image-20210901101009155.png" alt="image-20210901101009155" style="zoom:50%;" />
保存 Token 到 Redux
目標:將調(diào)用后端接口獲取到的 Token 信息耻涛,放入 Redux 進行維護
實現(xiàn)思路:
- 實現(xiàn)一個 Reducer废酷,用于在 Redux 中操作 Token 狀態(tài)
- 實現(xiàn)一個 Action瘟檩,在該 Action 中調(diào)用 Reducer 來保存 Token 狀態(tài)
- 在上一章節(jié)獲取 Token 的 Action 中,調(diào)用上面的 Action 來保存從后端剛獲取到的 Token
操作步驟
- 創(chuàng)建
store/reducers/login.js
澈蟆,并編寫一個 Reducer 函數(shù)
// 初始狀態(tài)
const initialState = {
token: '',
refresh_token: ''
}
// 操作 Token 狀態(tài)信息的 reducer 函數(shù)
export const login = (state = initialState, action) => {
const { type, payload } = action
switch (type) {
case 'login/token': return { ...payload }
default: return state
}
}
- 在
store/reducers/index.js
中墨辛,將以上的 Reducer 函數(shù)組合進根 Reducer
import { combineReducers } from 'redux'
import { login } from './login'
// 組合各個 reducer 函數(shù),成為一個根 reducer
const rootReducer = combineReducers({
login
})
// 導出根 reducer
export default rootReducer
- 在
store/actions/login.js
中趴俘,實現(xiàn)一個調(diào)用以上 Reducer 的 Action
/**
* 將 Token 信息保存到 Redux 中
* @param {*} tokens
* @returns
*/
export const saveToken = tokenInfo => {
return {
type: 'login/token',
payload: tokenInfo
}
}
- 在原先調(diào)用后端接口獲取 Token 的 Action 中睹簇,調(diào)用
saveToken
Action
// 提交
onSubmit: async (values) => {
try {
await dispatch(login(values))
console.log('登陸成功')
} catch (e) {
console.log(e.response.data.message)
}
},
可以通過 Redux DevTools 插件,查看保存后的值:
<img src="極客園移動端1.assets/image-20210901182935828.png" alt="image-20210901182935828" style="zoom:50%;" />
提示消息優(yōu)化
onSubmit: async (values) => {
try {
await dispatch(login(values))
Toast.success('登陸成功')
history.push('/home')
} catch (e) {
// console.log(e.response.data.message)
Toast.fail(e.response.data.message)
}
},
保存 Token 到本地緩存
目標:將從后端獲取到的 Token 保存到瀏覽器的 LocalStorage 中
實現(xiàn)思路:
- 實現(xiàn)一個工具模塊寥闪,在該模塊中專門操作 LocalStorage 中的 Token 信息
- 在調(diào)用后端接口獲取 Token 的 Action 中太惠,調(diào)用該工具模塊中的方法來存儲 Token
操作步驟
- 創(chuàng)建
utils/storage.js
,并編寫 Token 的設(shè)置橙垢、獲取垛叨、刪除等工具方法
// 用戶 Token 的本地緩存鍵名
const TOKEN_KEY = 'geek-itcast'
/**
* 從本地緩存中獲取 Token 信息
*/
export const getTokenInfo = () => {
return JSON.parse(localStorage.getItem(TOKEN_KEY)) || {}
}
/**
* 將 Token 信息存入緩存
* @param {Object} tokenInfo 從后端獲取到的 Token 信息
*/
export const setTokenInfo = tokenInfo => {
localStorage.setItem(TOKEN_KEY, JSON.stringify(tokenInfo))
}
/**
* 刪除本地緩存中的 Token 信息
*/
export const removeTokenInfo = () => {
localStorage.removeItem(TOKEN_KEY)
}
/**
* 判斷本地緩存中是否存在 Token 信息
*/
export const hasToken = () => {
return !!getTokenInfo().token
}
- 原先調(diào)用后端接口獲取 Token 的 Action 中,調(diào)用以上的本地緩存工具方法來保存 Token 信息
import { http, removeTokens, setTokens } from '@/utils'
export const login = params => {
return async dispatch => {
const res = await http.post('/authorizations', params)
const tokenInfo = res.data.data
// 保存 Token 到 Redux 中
dispatch(saveToken(tokenInfo))
// 保存 Token 到 LocalStorage 中
setTokenInfo(tokenInfo)
}
}
效果:
<img src="極客園移動端1.assets/image-20210901104851802.png" alt="image-20210901104851802" style="zoom:50%;" />
加載緩存的 Token 來初始化 Redux
目標:從緩存中讀取 token 信息柜某,如果存在則設(shè)置為 Redux Store 的初始狀態(tài)
【如果不做本操作的話嗽元,會出現(xiàn)當頁面刷新后,緩存中有值而 Redux 中無值的情況】
操作步驟
- 在
store/index.js
中喂击,調(diào)用緩存工具方法來讀取 Token 信息剂癌,并設(shè)置給createStore
相關(guān)參數(shù):
import { getTokenInfo } from '@/utils/storage'
const store = createStore(
// ...
// 參數(shù)二:初始化時要加載的狀態(tài)
{
login: getTokenInfo()
},
// ...
)
Redux 在實際開發(fā)中的常用模式
目標:根據(jù)上面幾章的 redux 使用情況,總結(jié)實際開發(fā)時的最佳實踐模式
推薦的目錄結(jié)構(gòu)
<img src="極客園移動端1.assets/image-20210901150309264.png" alt="image-20210901150309264" style="zoom:50%;" />
目錄:store/actions
按功能模塊的不同翰绊,拆分若干獨立的文件佩谷,存放 Action Creator 函數(shù)。
- Action Creator 返回函數(shù):用于含有異步行為的操作
export const test1 = params => {
return async dispatch => {
// 執(zhí)行異步業(yè)務(wù)邏輯 ...
// 通過 dispatch 可以再調(diào)用其他 Action ...
}
}
- Action Creator 返回對象:用于同步行為的操作
export const test2 = params => {
// 推薦返回的 action 對象中监嗜,只存放兩個屬性:type谐檀、payload
return {
// 注意命名規(guī)范,推薦規(guī)則為 domain/eventName桐猬。例如:login/token
type: 'abc/hello',
// 所有要傳遞給 reducer 的業(yè)務(wù)數(shù)據(jù)刽肠,都放到 payload 屬性上
payload: {}
}
}
目錄:store/reducers
按功能模塊的不同溃肪,拆分若干獨立文件,存放 Reducer 函數(shù)音五。
最后惫撰,將這些獨立的 Reducer 模塊通過該目錄中的 index.js
合并為根 Reducer。
// 根 Reducer
const rootReducer = combineReducers({
login,
profile,
home,
// ...
})
文件:store/index.js
用于創(chuàng)建和配置 Redux Store躺涝。
在組件中調(diào)用 Redux 的極簡流程
// 第一步:使用 useDispatch() 獲取分發(fā)器
const dispatch = useDispatch()
// 第二步:調(diào)用 Action Creator 獲取 Action
const action = someActionCreatorFuncion()
// 第三步:通過向分發(fā)器調(diào)用 Action 函數(shù)內(nèi)或 Reducer 函數(shù)內(nèi)的業(yè)務(wù)邏輯
dispatch(action)
為網(wǎng)絡(luò)請求添加 Token 請求頭
目標:在發(fā)送請求時在請求頭中攜帶上 Token 信息厨钻,以便在請求需要鑒權(quán)的后端接口時可以順利調(diào)用
實現(xiàn)思路:
- 在 axios 請求攔截器中,讀取保存在 Redux 或 LocalStorage 中的 Token 信息,并設(shè)置到請求頭上
操作步驟
- 在
utils/reqeust.js
中莉撇,改造請求攔截器:
import { getTokenInfo } from './storage'
// 2. 設(shè)置請求攔截器和響應(yīng)攔截器
http.interceptors.request.use((config) => {
// 獲取緩存中的 Token 信息
const token = getTokenInfo().token
if (token) {
// 設(shè)置請求頭的 Authorization 字段
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
整體布局
實現(xiàn)底部 tab 布局
目標:實現(xiàn)一個帶有底部 tab 導航欄的頁面布局容器組件呢蛤,當點擊底部按鈕后,可切換顯示不同內(nèi)容
<img src="極客園移動端1.assets/image-20210830181305183.png" alt="image-20210830181305183" style="zoom:40%;" />
實現(xiàn)思路:
- 在組件中存在兩個區(qū)域:頁面內(nèi)容區(qū)域棍郎、tab 按鈕區(qū)域
- 定義一個數(shù)組來存放 tab 按鈕相關(guān)數(shù)據(jù)其障,這樣可以方便統(tǒng)一管理按鈕
- 通過遍歷數(shù)組來渲染 tab 按鈕
- 點擊按鈕時,根據(jù)當前訪問的頁面路徑和按鈕本身的路徑涂佃,判斷當前按鈕是否是選中狀態(tài)励翼,并添加高亮樣式
- 點擊按鈕后,進行路由跳轉(zhuǎn)
操作步驟
- 準備基本結(jié)構(gòu)
export default function Home() {
return (
<div className={styles.root}>
{/* 區(qū)域一:點擊按鈕切換顯示內(nèi)容的區(qū)域 */}
<div className="tab-content"></div>
{/* 區(qū)域二:按鈕區(qū)域辜荠,會使用固定定位顯示在頁面底部 */}
<div className="tabbar">
<div className="tabbar-item tabbar-item-active">
<Icon type="iconbtn_home_sel" />
<span>首頁</span>
</div>
<div className="tabbar-item">
<Icon type="iconbtn_qa" />
<span>問答</span>
</div>
<div className="tabbar-item">
<Icon type="iconbtn_video" />
<span>視頻</span>
</div>
<div className="tabbar-item">
<Icon type="iconbtn_mine" />
<span>我的</span>
</div>
</div>
</div>
)
}
將發(fā)放資料中的
資源 > src代碼文件 > layouts > index.module.scss
拷貝到該目錄下在組件定義一個數(shù)組汽抚,代表 tab 按鈕的數(shù)據(jù)
// 將 tab 按鈕的數(shù)據(jù)放在一個數(shù)組中
// - id 唯一性ID
// - title 按鈕顯示的文本
// - to 點擊按鈕后切換到的頁面路徑
// - icon 按鈕上顯示的圖標名稱
const buttons = [
{ id: 1, title: '首頁', to: '/home', icon: 'iconbtn_home' },
{ id: 2, title: '問答', to: '/home/question', icon: 'iconbtn_qa' },
{ id: 3, title: '視頻', to: '/home/video', icon: 'iconbtn_video' },
{ id: 4, title: '我的', to: '/home/profile', icon: 'iconbtn_mine' }
]
- 動態(tài)渲染TabBar
import Icon from '@/components/Icon'
import classnames from 'classnames'
import { useHistory, useLocation } from 'react-router-dom'
import styles from './index.module.scss'
// 將 tab 按鈕的數(shù)據(jù)放在一個數(shù)組中
// ...
/**
* 定義 tab 布局組件
*/
const TabBarLayout = () => {
// 獲取路由歷史 history 對象
const history = useHistory()
// 獲取路由信息 location 對象
const location = useLocation()
return (
<div className={styles.root}>
{/* 區(qū)域一:點擊按鈕切換顯示內(nèi)容的區(qū)域 */}
<div className="tab-content">
</div>
{/* 區(qū)域二:按鈕區(qū)域,會使用固定定位顯示在頁面底部 */}
<div className="tabbar">
{buttons.map(btn => {
// 判斷當前頁面路徑和按鈕路徑是否一致伯病,如果一致則表示該按鈕處于選中狀態(tài)
const selected = btn.to === location.pathname
return (
<div
key={btn.id}
className={classnames('tabbar-item', selected ? 'tabbar-item-active' : '')}
onClick={() => history.push(btn.to)}
>
<Icon type={btn.icon + (selected ? '_sel' : '')} />
<span>{btn.title}</span>
</div>
)
})}
</div>
</div>
)
}
export default TabBarLayout
效果:
<img src="極客園移動端1.assets/image-20210831131915002.png" alt="image-20210831131915002" style="zoom:40%;" />
創(chuàng)建 tab 按鈕頁面并配置嵌套路由
目標:為 tab 布局組件中的 4 個按鈕創(chuàng)建對應(yīng)的頁面造烁;并配置路由,使按鈕點擊后能顯示對應(yīng)頁面
操作步驟
- 創(chuàng)建四個頁面組件:
- 首頁:pages/Home/index.js
- 問答:pages/Question/index.js
- 視頻:pages/Video/index.js
- 我的:pages/Profile/index.js
當前午笛,這些組件的代碼使用最簡單的即可惭蟋,如:
const Home = () => {
return (
<div>首頁</div>
)
}
export default Home
- 在
layouts/TabBarLayout.js
中配置4個頁面的路由
const Home = React.lazy(() => import('@/pages/Home'))
const QA = React.lazy(() => import('@/pages/QA'))
const Video = React.lazy(() => import('@/pages/Video'))
const Profile = React.lazy(() => import('@/pages/Profile'))
// ...
{/* 區(qū)域一:點擊按鈕切換顯示內(nèi)容的區(qū)域 */}
<div className="tab-content">
<Route path="/home/index" exact component={Home} />
<Route path="/home/question" exact component={Question} />
<Route path="/home/video" exact component={Video} />
<Route path="/home/profile" exact component={Profile} />
</div>
效果:
<img src="極客園移動端1.assets/image-20210831161825392.png" alt="image-20210831161825392" style="zoom:40%;" />
創(chuàng)建其他功能頁面并配置路由
目標:事先創(chuàng)建本項目中將要開發(fā)的各個頁面組件苟翻,并配置路由
【本章節(jié)所做的事滋饲,你也可以不一次性做完拳昌,可以一個一個頁面邊開發(fā)邊配置】
說明:這些頁面是除了 tab 底部導航欄上的4個頁面以外的其他功能頁
操作步驟
- 創(chuàng)建以下頁面組件:
- 登錄頁面:pages/Login/index.js
- 搜索頁面:pages/Search/index.js
- 搜索結(jié)果頁面:pages/Search/Result/index.js
- 文章詳情頁面:pages/Article/index.js
- 個人信息編輯頁面:pages/Profile/Edit/index.js
- 用戶反饋頁面:pages/Profile/Feedback/index.js
- 機器人客服聊天頁面:pages/Profile/Chat/index.js
- 404 錯誤頁面:pages/NotFound/index.js
當前肖爵,這些組件的代碼使用最簡單的即可,如:
const Login = () => {
return (
<div>登錄</div>
)
}
export default Login
- 在根組件 App 中冲簿,配置以上頁面的路由:
import Article from "./pages/Article"
import Login from "./pages/Login"
import NotFound from "./pages/NotFound"
import Chat from "./pages/Profile/Chat"
import ProfileEdit from "./pages/Profile/Edit"
import ProfileFeedback from "./pages/Profile/Feedback"
import Search from "./pages/Search"
import SearchResult from "./pages/Search/Result"
// ...
const App = () => {
return (
<Router history={history}>
<Switch>
{/* ... */}
{/* 不使用 tab 布局的界面 */}
<Route path="/login" component={Login} />
<Route path="/search" component={Search} />
<Route path="/article/:id" component={Article} />
<Route path="/search/result" component={SearchResult} />
<Route path="/profile/edit" component={ProfileEdit} />
<Route path="/profile/feedback" component={ProfileFeedback} />
<Route path="/profile/chat" component={Chat} />
<Route component={NotFound} />
</Switch>
</Router>
)
}
個人中心
個人中心主頁的靜態(tài)結(jié)構(gòu)
目標:實現(xiàn)個人中心主頁面的靜態(tài)結(jié)構(gòu)和樣式
頁面布局分解示意:
<img src="極客園移動端1.assets/image-20210901112241300.png" alt="image-20210901112241300" style="zoom:50%;" />
操作步驟
- 將資源包中個人中心頁面的樣式文件拷貝到
pages/Profile
目錄中涣旨,然后在pages/Profile/index.js
中編寫如下代碼:
import Icon from '@/components/Icon'
import { Link, useHistory } from 'react-router-dom'
import styles from './index.module.scss'
const Profile = () => {
const history = useHistory()
return (
<div className={styles.root}>
<div className="profile">
{/* 頂部個人信息區(qū)域 */}
<div className="user-info">
<div className="avatar">
<img src={''} alt="" />
</div>
<div className="user-name">{'xxxxxxxx'}</div>
<Link to="/profile/edit">
個人信息 <Icon type="iconbtn_right" />
</Link>
</div>
{/* 今日閱讀區(qū)域 */}
<div className="read-info">
<Icon type="iconbtn_readingtime" />
今日閱讀 <span>10</span> 分鐘
</div>
{/* 統(tǒng)計信息區(qū)域 */}
<div className="count-list">
<div className="count-item">
<p>{0}</p>
<p>動態(tài)</p>
</div>
<div className="count-item">
<p>{0}</p>
<p>關(guān)注</p>
</div>
<div className="count-item">
<p>{0}</p>
<p>粉絲</p>
</div>
<div className="count-item">
<p>{0}</p>
<p>被贊</p>
</div>
</div>
{/* 主功能菜單區(qū)域 */}
<div className="user-links">
<div className="link-item">
<Icon type="iconbtn_mymessages" />
<div>消息通知</div>
</div>
<div className="link-item">
<Icon type="iconbtn_mycollect" />
<div>收藏</div>
</div>
<div className="link-item">
<Icon type="iconbtn_history1" />
<div>瀏覽歷史</div>
</div>
<div className="link-item">
<Icon type="iconbtn_myworks" />
<div>我的作品</div>
</div>
</div>
</div>
{/* 更多服務(wù)菜單區(qū)域 */}
<div className="more-service">
<h3>更多服務(wù)</h3>
<div className="service-list">
<div className="service-item" onClick={() => history.push('/profile/feedback')}>
<Icon type="iconbtn_feedback" />
<div>用戶反饋</div>
</div>
<div className="service-item" onClick={() => history.push('/profile/chat')}>
<Icon type="iconbtn_xiaozhitongxue" />
<div>小智同學</div>
</div>
</div>
</div>
</div>
)
}
export default Profile
請求個人基本信息
目標:進入個人中心頁面時氏仗,調(diào)用后端接口围辙,獲取個人基本信息數(shù)據(jù)
實現(xiàn)思路:
- 使用 Hook 函數(shù)
useEffect
我碟,在頁面進入時,通過調(diào)用 Action 來調(diào)用后端接口
操作步驟
- 創(chuàng)建
store/actions/profile.js
姚建,并編寫 Action Creator 函數(shù):
import http from "@/utils/http"
/**
* 獲取用戶基本信息
* @returns thunk
*/
export const getUser = () => {
return async dispatch => {
const res = await http.get('/user')
console.log(res);
}
}
- 在
pages/Profile/index.js
中怎囚,使用useEffect
在進入頁面時調(diào)用 Action:
import { getUser } from '@/store/actions/profile'
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
// 在進入頁面時執(zhí)行
useEffect(() => {
dispatch(getUser())
}, [dispatch])
成功調(diào)用后,可在控制臺中查看打印的個人基本信息數(shù)據(jù):
<img src="極客園移動端1.assets/image-20210901175404937.png" alt="image-20210901175404937" style="zoom:40%;" />
將個人基本信息存入 Redux
目標:將從后端獲取到的個人基本信息存入 Redux桥胞,以備用于后續(xù)的個人中心主頁的界面渲染等
實現(xiàn)思路:
- 實現(xiàn)一個 Reducer,用于操作 Store 中的個人基本信息狀態(tài)
- 通過一個 Action 來調(diào)用 Reducer考婴,將個人基本信息保存到 Store 中
操作步驟
- 創(chuàng)建
store/reducers/profile.js
贩虾,編寫操作個人基本信息的 Reducer 函數(shù)
// 初始狀態(tài)
const initialState = {
// 基本信息
user: {},
}
// 操作用戶個人信息狀態(tài)的 reducer 函數(shù)
export const profile = (state = initialState, action) => {
const { type, payload } = action
switch (type) {
// 設(shè)置基本信息
case 'profile/user':
return {
...state,
user: { ...payload }
}
// 默認
default:
return state
}
}
- 在
store/index.js
中配置以上新增的 Reducer
import { profile } from './profile'
const rootReducer = combineReducers({
login,
profile
})
- 在
store/actions/profile.js
中,添加一個可用于調(diào)用以上 Reducer 中的profile/user
邏輯的 Action Creator:
/**
* 設(shè)置個人基本信息
* @param {*} user
* @returns
*/
export const setUser = user => {
return {
type: 'profile/user',
payload: user
}
}
- 在之前調(diào)用后端接口的 Action Creator 函數(shù)
getUser
中沥阱,調(diào)用setUser
將數(shù)據(jù)保存到 Redux Store:
export const getUser = () => {
return async dispatch => {
const res = await http.get('/user')
const user = res.data.data
// 保存到 Redux 中
dispatch(setUser(user))
}
}
在 Redux DevTools 中確認數(shù)據(jù)是否已正確設(shè)置:
<img src="極客園移動端1.assets/image-20210901183426325.png" alt="image-20210901183426325" style="zoom:50%;" />
將個人基本信息渲染到界面
目標:從 Redux Store 中獲取之前存入的用戶基本信息缎罢,并渲染到個人中心頁面的對應(yīng)位置
實現(xiàn)思路:
- 使用
useSelector
從 Redux Store 中獲取狀態(tài) - 將獲取的狀態(tài)渲染到界面上
操作步驟
- 在
pages/Profile/index.js
中,調(diào)用react-redux
提供的 Hook 函數(shù)useSelector
,從 Store 中獲取之前存儲的user
狀態(tài):
import { useDispatch, useSelector } from 'react-redux'
// 獲取 Redux Store 中的個人基本信息
const user = useSelector(state => state.profile.user)
- 使用以上獲取到的數(shù)據(jù)策精,填充界面上的相關(guān)元素
用戶頭像和用戶名:
<div className="avatar">
<img src={user.photo} alt="" />
</div>
<div className="user-name">{user.name}</div>
<img src="極客園移動端1.assets/image-20210902083418445.png" alt="image-20210902083418445" style="zoom:50%;" />
統(tǒng)計信息:
<div className="count-list">
<div className="count-item">
<p>{art_count}</p>
<p>動態(tài)</p>
</div>
<div className="count-item">
<p>{follow_count}</p>
<p>關(guān)注</p>
</div>
<div className="count-item">
<p>{fans_count}</p>
<p>粉絲</p>
</div>
<div className="count-item">
<p>{like_count}</p>
<p>被贊</p>
</div>
</div>
<img src="極客園移動端1.assets/image-20210902083434060.png" alt="image-20210902083434060" style="zoom:50%;" />
個人詳情頁面的靜態(tài)結(jié)構(gòu)
目標:實現(xiàn)個人詳情頁的靜態(tài)結(jié)構(gòu)和樣式
頁面布局分解示意:
<img src="極客園移動端1.assets/image-20210902090634094.png" alt="image-20210902090634094" style="zoom:40%;" />
操作步驟
- 將資源包中個人詳情頁面的樣式文件拷貝到
pages/Profile/Edit/
目錄中舰始,然后在pages/Profile/Edit/index.js
中編寫如下代碼
import NavBar from '@/components/NavBar'
import { DatePicker, List } from 'antd-mobile'
import { useHistory } from 'react-router-dom'
import styles from './index.module.scss'
const ProfileEdit = () => {
const history = useHistory()
return (
<div className={styles.root}>
<div className="content">
{/* 頂部導航欄 */}
<NavBar onLeftClick={() => history.go(-1)}>個人信息</NavBar>
<div className="wrapper">
{/* 列表一:顯示頭像、昵稱咽袜、簡介 */}
<List className="profile-list">
<List.Item arrow="horizontal" extra={
<span className="avatar-wrapper">
<img src={''} alt="" />
</span>
}>頭像</List.Item>
<List.Item arrow="horizontal" extra={'昵稱xxxx'}>昵稱</List.Item>
<List.Item arrow="horizontal" extra={
<span className="intro">{'未填寫'}</span>
}>簡介</List.Item>
</List>
{/* 列表二:顯示性別丸卷、生日 */}
<List className="profile-list">
<List.Item arrow="horizontal" extra={'男'}>性別</List.Item>
<DatePicker
mode="date"
title="選擇年月日"
value={new Date()}
minDate={new Date(1900, 1, 1, 0, 0, 0)}
maxDate={new Date()}
onChange={() => { }}
>
<List.Item arrow="horizontal" extra={'2020-02-02'}>生日</List.Item>
</DatePicker>
</List>
{/* 文件選擇框,用于頭像圖片的上傳 */}
<input type="file" hidden />
</div>
{/* 底部欄:退出登錄按鈕 */}
<div className="logout">
<button className="btn">退出登錄</button>
</div>
</div>
</div>
)
}
export default ProfileEdit
請求個人詳情
目標:進入個人詳情頁面時询刹,調(diào)用后端接口谜嫉,獲取個人詳情數(shù)據(jù)
實現(xiàn)思路:
- 使用 Hook 函數(shù)
useEffect
,在頁面進入時凹联,通過調(diào)用 Action 來調(diào)用后端接口
操作步驟
- 在
store/actions/profile.js
中編寫 Action Creator 函數(shù):
/**
* 獲取用戶詳情
* @returns thunk
*/
export const getUserProfile = () => {
return async dispatch => {
const res = await http.get('/user/profile')
console.log(res)
}
}
- 在
pages/Profile/Edit/index.js
中沐兰,使用useEffect
在進入頁面時調(diào)用 Action:
import { getUserProfile } from '@/store/actions/profile'
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
useEffect(() => {
dispatch(getUserProfile())
}, [dispatch])
成功調(diào)用后,可在控制臺中查看打印數(shù)據(jù):
<img src="極客園移動端1.assets/image-20210902093122663.png" alt="image-20210902093122663" style="zoom:40%;" />
將個人詳情存入 Redux
目標:將從后端獲取到的個人詳情存入 Redux蔽挠,以備用于后續(xù)的個人詳情頁面的界面渲染
實現(xiàn)思路:
- 實現(xiàn)一個 Reducer住闯,用于操作 Store 中的個人詳情狀態(tài)
- 通過一個 Action 來調(diào)用 Reducer,將個人詳情保存到 Store 中
操作步驟
- 在
store/reducers/profile.js
中澳淑,添加個人詳情狀態(tài)比原,以及設(shè)置個人詳情的 Reducer 邏輯:
// 初始狀態(tài)
const initialState = {
// ...
// 詳情信息
userProfile: {}
}
// 操作用戶個人信息狀態(tài)的 reducer 函數(shù)
export const profile = (state = initialState, action) => {
const { type, payload } = action
switch (type) {
// 設(shè)置詳情信息
case 'profile/profile':
return {
...state,
userProfile: { ...payload }
}
// ...
}
}
- 在
store/actions/profile.js
中,添加一個可用于調(diào)用以上 Reducer 中的profile/profile
邏輯的 Action Creator:
/**
* 設(shè)置個人詳情
* @param {*} profile
* @returns
*/
export const setUserProfile = profile => ({
type: 'profile/profile',
payload: profile
})
- 在之前調(diào)用后端接口的 Action Creator 函數(shù)
getUserProfile
中偶惠,調(diào)用setUserProfile
將數(shù)據(jù)保存到 Redux Store:
export const getUserProfile = () => {
return async dispatch => {
const res = await http.get('/user/profile')
const profile = res.data.data
// 保存到 Redux 中
dispatch(setUserProfile(profile))
}
}
將個人詳情渲染到界面
目標:從 Redux Store 中獲取之前保存的個人詳情春寿,并渲染到個人詳情頁的對應(yīng)位置
實現(xiàn)思路:
- 使用
useSelector
從 Redux Store 中獲取狀態(tài) - 將獲取的狀態(tài)渲染到界面上
操作步驟
- 在
pages/Profile/Edit/index.js
中,調(diào)用react-redux
提供的 Hook 函數(shù)useSelector
忽孽,從 Store 中獲取之前存儲的userProfile
狀態(tài):
import { useDispatch, useSelector } from 'react-redux'
// 獲取 Redux Store 中個人詳情
const profile = useSelector(state => state.profile.userProfile)
- 使用以上獲取到的數(shù)據(jù)绑改,填充界面上的相關(guān)元素
頭像、昵稱兄一、簡介:
<List.Item arrow="horizontal" extra={
<span className="avatar-wrapper">
<img src={profile.photo} alt="" />
</span>
}>頭像</List.Item>
<List.Item arrow="horizontal" extra={profile.name}>昵稱</List.Item>
<List.Item arrow="horizontal" extra={
<span className={classnames("intro", profile.intro ? 'normal' : '')}>
{profile.intro || '未填寫'}
</span>
}>簡介</List.Item>
性別厘线、生日:
<List.Item arrow="horizontal" extra={profile.gender === 0 ? '男' : '女'}>性別</List.Item>
<DatePicker
mode="date"
title="選擇年月日"
value={new Date(profile.birthday)}
minDate={new Date(1900, 1, 1, 0, 0, 0)}
maxDate={new Date()}
onChange={() => { }}
>
<List.Item arrow="horizontal" extra={profile.birthday}>生日</List.Item>
</DatePicker>
編輯個人詳情:介紹
目標:了解編輯個人詳情時,對于不同字段的編輯界面形式
當點擊個人信息項時會以滑動抽屜的形式展現(xiàn)輸入界面出革,主要有兩種:
一造壮、從屏幕右側(cè)滑入的:全屏表單抽屜
<img src="極客園移動端1.assets/image-20210902110852864.png" alt="image-20210902110852864" style="zoom:25%;" />
該界面的布局是固定的:頂部導航欄、要編輯的字段名稱骂束、一個內(nèi)容輸入框耳璧。
采用這種界面方式進行編輯的是:昵稱、簡介展箱。
二旨枯、從屏幕底部滑入的:菜單列表抽屜
<img src="極客園移動端1.assets/image-20210902111141816.png" alt="image-20210902111141816" style="zoom:25%;" />
該界面的布局是固定的:一個列表、一個取消按鈕混驰。
采用這種界面方式進行編輯的是:頭像攀隔、性別皂贩。
實現(xiàn)思路
- 將這兩種界面封裝成2個組件
- 向組件傳入配置信息,讓組件按配置信息顯示對應(yīng)的內(nèi)容
編輯個人詳情-抽屜組件基本使用
// 控制抽屜組件的顯示
const [inputOpen, setInputOpen] = useState(false)
{/* 全屏表單抽屜 */}
<Drawer
position="right"
className="drawer"
style={{ minHeight: document.documentElement.clientHeight }}
sidebar={<div onClick={() => setInputOpen(false)}>全屏抽屜</div>}
open={inputOpen}
/>
<List.Item
arrow="horizontal"
extra={profile.name}
onClick={() => setInputOpen(true)}
>
昵稱
</List.Item>
<List.Item
arrow="horizontal"
extra={
<span
className={classNames('intro', profile.intro ? 'normal' : '')}
>
{profile.intro || '未填寫'}
</span>
}
onClick={() => setInputOpen(true)}
>
簡介
</List.Item>
編輯個人詳情-EditInput組件
- 導入樣式
- 準備結(jié)構(gòu)
import React from 'react'
import NavBar from '@/components/NavBar'
import styles from './index.module.scss'
export default function EditInput({ onClose }) {
return (
<div className={styles.root}>
<NavBar
rightContent={<span className="commit-btn">提交</span>}
className="navbar"
onLeftClick={onClose}
>
編輯昵稱
</NavBar>
<div className="content">
<h3>昵稱</h3>
</div>
</div>
)
}
- 父組件渲染
{/* 全屏表單抽屜 */}
<Drawer
position="right"
className="drawer"
style={{ minHeight: document.documentElement.clientHeight }}
sidebar={<EditInput onClose={onClose}></EditInput>}
open={inputOpen}
children={''}
/>
編輯個人詳情-navBar組件修改
編輯個人詳情-同時控制昵稱和簡介
- 修改數(shù)據(jù)格式
// 控制抽屜組件的顯示
const [inputOpen, setInputOpen] = useState({
// 抽屜顯示狀態(tài)
visible: false,
// 顯示的類型
type: '',
})
const onClose = () => {
setInputOpen({
visible: false,
type: '',
})
}
<List.Item
arrow="horizontal"
extra={profile.name}
onClick={() =>
setInputOpen({
visible: true,
type: 'name',
})
}
>
昵稱
</List.Item>
<List.Item
arrow="horizontal"
extra={
<span
className={classNames('intro', profile.intro ? 'normal' : '')}
>
{profile.intro || '未填寫'}
</span>
}
onClick={() =>
setInputOpen({
visible: true,
type: 'intro',
})
}
>
簡介
</List.Item>
{/* 全屏表單抽屜 */}
<Drawer
position="right"
className="drawer"
style={{ minHeight: document.documentElement.clientHeight }}
sidebar={
<EditInput onClose={onClose} type={inputOpen.type}></EditInput>
}
open={inputOpen.visible}
children={''}
/>
編輯個人詳情-封裝包含字數(shù)統(tǒng)計的TextArea
目標:對
<textarea>
進行封裝昆汹,使得在輸入內(nèi)容時可以顯示當前已輸入字數(shù)和允許輸入的總字數(shù)
<img src="極客園移動端1.assets/image-20210902154346207.png" alt="image-20210902154346207" style="zoom:40%;" />
實現(xiàn)思路:
- 聲明一個狀態(tài)明刷,用于記錄輸入字數(shù)
- 在
<textarea>
輸入內(nèi)容觸發(fā)change
事件時,獲取當前最新內(nèi)容得到最新字數(shù)满粗,更新到狀態(tài)中
操作步驟
- 創(chuàng)建
components/Textarea/index.js
辈末,并將資源包中的樣式文件拷貝過來,然后編寫以下代碼:
import classnames from 'classnames'
import { useState } from 'react'
import styles from './index.module.scss'
/**
* 帶字數(shù)統(tǒng)計的多行文本
* @param {String} className 樣式類
* @param {String} value 文本框的內(nèi)容
* @param {String} placeholder 占位文本
* @param {Function} onChange 輸入內(nèi)容變動事件
* @param {String} maxLength 允許最大輸入的字數(shù)(默認100個字符)
*/
const Textarea = ({ className, value, placeholder, onChange, maxLength = 100 }) => {
// 字數(shù)狀態(tài)
const [count, setCount] = useState(value.length || 0)
// 輸入框的 change 事件監(jiān)聽函數(shù)
const onValueChange = e => {
// 獲取最新的輸入內(nèi)容败潦,并將它的長度更新到 count 狀態(tài)
const newValue = e.target.value
setCount(newValue.length)
// 調(diào)用外部傳入的事件回調(diào)函數(shù)
onChange(e)
}
return (
<div className={classnames(styles.root, className)}>
{/* 文本輸入框 */}
<textarea
className="textarea"
maxLength={maxLength}
placeholder={placeholder}
value={value}
onChange={onValueChange}
/>
{/* 當前字數(shù)/最大允許字數(shù) */}
<div className="count">{count}/{maxLength}</div>
</div>
)
}
export default Textarea
編輯個人詳情-昵稱和簡介的回顯
- 控制顯示昵稱和簡介
import React from 'react'
import NavBar from '@/components/NavBar'
import styles from './index.module.scss'
import Input from '@/components/Input'
import Textarea from '@/components/Textarea'
export default function EditInput({ onClose, type }) {
return (
<div className={styles.root}>
<NavBar
rightContent={<span className="commit-btn">提交</span>}
className="navbar"
onLeftClick={onClose}
>
編輯{type === 'name' ? '昵稱' : '簡介'}
</NavBar>
<div className="content-box">
<h3>{type === 'name' ? '昵稱' : '簡介'}</h3>
{type === 'name' ? (
<div className="input-wrap">
<Input />
</div>
) : (
<Textarea placeholder="請輸入" />
)}
</div>
</div>
)
}
- 數(shù)據(jù)回顯
import Input from '@/components/Input'
import NavBar from '@/components/NavBar'
import Textarea from '@/components/Textarea'
import { useState } from 'react'
import styles from './index.module.scss'
const EditInput = ({ config, onClose, onCommit }) => {
const [value, setValue] = useState(config.value || '')
const { title, type } = config
const onValueChange = (e) => {
setValue(e.target.value)
}
return (
<div className={styles.root}>
<NavBar
className="navbar"
onLeftClick={onClose}
rightContent={
<span className="commit-btn" onClick={() => onCommit(type, value)}>
提交
</span>
}
>
編輯{title}
</NavBar>
<div className="content">
<h3>{title}</h3>
{type === 'name' ? (
<div className="input-wrap">
<Input value={value} onChange={onValueChange} />
</div>
) : (
<Textarea
placeholder="請輸入"
value={value}
onChange={onValueChange}
/>
)}
</div>
</div>
)
}
export default EditInput
編輯個人詳情:完成昵稱和簡介的修改
目標:在抽屜表單中編輯昵稱或簡介后本冲,將表單返回的數(shù)據(jù)提交到后端進行更新,并更新到 Redux
實現(xiàn)思路:
- 編寫用于在 Redux 中更新個人詳情字段的 Reducer
- 編寫用于通過調(diào)用后端接口即 Reducer 來更新個人詳情字段的 Action
- 在抽屜表單提交數(shù)據(jù)時調(diào)用 Action
操作步驟
- 在
store/actions/profile.js
中劫扒,編寫 Action Creator:
/**
* 修改個人詳情:昵稱檬洞、簡介、生日沟饥、性別 (每次修改一個字段)
* @param {String} name 要修改的字段名稱
* @param {*} value 要修改的字段值
* @returns thunk
*/
export const updateProfile = (name, value) => {
return async dispatch => {
// 調(diào)用接口將數(shù)據(jù)更新到后端
const res = await http.patch('/user/profile', { [name]: value })
// 如果后端更新成功添怔,則再更新 Redux 中的數(shù)據(jù)
if (res.data.message === 'OK') {
dispatch(getUserProfile())
}
}
}
- 為抽屜表單組件設(shè)置
onCommit
回調(diào)函數(shù),并在該函數(shù)中調(diào)用以上的 Action:
<EditInput
// ...
onCommit={onFormCommit}
/>
import { getUserProfile, updateProfile } from '@/store/actions/profile'
// 抽屜表單的數(shù)據(jù)提交
const onFormCommit = (name, value) => {
// 調(diào)用 Action 更新數(shù)據(jù)
dispatch(updateProfile(name, value))
// 關(guān)閉抽屜
toggleDrawer(false)
}
編輯個人詳情-準備性別和頭像的抽屜組件
// 關(guān)閉昵稱和簡介的顯示
const onClose = () => {
setInputOpen({
visible: false,
type: '',
})
setListOpen({
visible: false,
type: '',
})
}
// 控制頭像和性別
const [listOpen, setListOpen] = useState({
visible: false,
type: '',
})
{/* 頭像贤旷、性別 */}
<Drawer
className="drawer-list"
position="bottom"
sidebar={<div>性別和頭像</div>}
open={listOpen.visible}
onOpenChange={onClose}
>
{''}
</Drawer>
</div>
<List.Item
arrow="horizontal"
onClick={() =>
setListOpen({
visible: true,
type: 'avatar',
})
}
extra={
<span className="avatar-wrapper">
<img src={profile.photo} alt="" />
</span>
}
>
頭像
</List.Item>
<List.Item
arrow="horizontal"
extra={profile.gender === 0 ? '男' : '女'}
onClick={() =>
setListOpen({
visible: true,
type: 'avatar',
})
}
>
性別
</List.Item>
編輯個人詳情-EditList組件
- 準備樣式
- 準備結(jié)構(gòu)
import styles from './index.module.scss'
const EditList = () => {
return (
<div className={styles.root}>
<div className="list-item">男</div>
<div className="list-item">女</div>
<div className="list-item">取消</div>
</div>
)
}
export default EditList
編輯個人詳情-控制顯示
- 父組件提供數(shù)據(jù)
const config = {
avatar: [
{
title: '拍照',
onClick: () => {},
},
{
title: '本地選擇',
onClick: () => {},
},
],
gender: [
{
title: '男',
onClick: () => {},
},
{
title: '女',
onClick: () => {},
},
],
}
- 傳遞給子組件
{/* 頭像广料、性別 */}
<Drawer
className="drawer-list"
position="bottom"
sidebar={<EditList config={config} type={listOpen.type}></EditList>}
open={listOpen.visible}
onOpenChange={onClose}
>
{''}
</Drawer>
- 子組件渲染
import styles from './index.module.scss'
const EditList = ({ type, config, onClose }) => {
const list = config[type]
return (
<div className={styles.root}>
{list.map((item) => (
<div className="list-item" key={item.title}>
{item.title}
</div>
))}
<div className="list-item" onClick={onClose}>
取消
</div>
</div>
)
}
export default EditList
編輯個人詳情:抽屜上的列表組件
目標:封裝用于顯示在列表抽屜中的列表組件,它可通過配置的方式顯示不同列表項
<img src="極客園移動端1.assets/image-20210902171500980.png" alt="image-20210902171500980" style="zoom:30%;" />
<img src="極客園移動端1.assets/image-20210902171524407.png" alt="image-20210902171524407" style="zoom:30%;" />
實現(xiàn)思路:
- 界面主要由一個列表和一個取消按鈕組成
- 界面中的列表數(shù)據(jù)通過組件屬性傳入
- “取消” 按鈕的監(jiān)聽函數(shù)通過組件屬性傳入
操作步驟
- 創(chuàng)建
pages/Profile/Edit/components/EditList/
目錄幼驶,并將資源包中的樣式文件拷貝進來
- 創(chuàng)建
pages/Profile/Edit/components/EditList/index.js
艾杏,編寫組件:
//【說明】:組件的 config 屬性是一個對象,包含以下內(nèi)容:
{
"字段1": {
name: '數(shù)組字段名',
items: [
{
title: '選項一',
value: '選項一的值'
},
{
title: '選項二',
value: '選項二的值'
}
]
},
// 其他字段...
}
組件代碼:
import styles from './index.module.scss'
/**
* 個人信息項修改列表
* @param {Object} config 配置信息對象
* @param {Function} onSelect 選擇列表項的回調(diào)函數(shù)
* @param {Function} onClose 取消按鈕的回調(diào)函數(shù)
*/
const EditList = ({ config = {}, onSelect, onClose }) => {
return (
<div className={styles.root}>
{/* 列表項 */}
{config.items?.map((item, index) => (
<div
className="list-item"
key={index}
onClick={() => onSelect(config.name, item, index)}
>
{item.title}
</div>
))}
{/* 取消按鈕 */}
<div className="list-item" onClick={onClose}>取消</div>
</div>
)
}
export default EditList
編輯個人詳情:完成性別的修改
目標:在從點擊性別進入的抽屜列表中選擇一項后盅藻,將選中的數(shù)據(jù)提交到后端進行更新购桑,并更新到 Redux
實現(xiàn)思路:
- 借助之前實現(xiàn)的更新個人詳情的 Action 封裝的魅力
const config = {
gender: [
{
title: '男',
onClick: () => {
onCommit('gender', 0)
},
},
{
title: '女',
onClick: () => {
onCommit('gender', 1)
},
},
],
}
編輯個人詳情:完成頭像的修改
目標:在從點擊頭像進入的抽屜列表中選擇一項后,從彈出的文件選擇器中選取一張圖片上傳到后端氏淑,并將新頭像地址更新到 Redux
實現(xiàn)思路:
- 實現(xiàn)一個用于調(diào)用接口進行頭像上傳勃蜘、及將上傳后的新圖片地址更新到 Redux 的 Action
- 使用 Hook 函數(shù)
useRef
操作文件輸入框元素<input type="file">
,觸發(fā)文件輸入彈框 - 監(jiān)聽文件輸入框的
onChange
事件假残,在文件變化時調(diào)用 Action 進行上傳
操作步驟
- 在
store/actions/profile.js
中缭贡,實現(xiàn)用于上傳頭像的 Action Creator:
/**
* 更新頭像
* @param {FormData} formData 上傳頭像信息的表單數(shù)據(jù)
* @returns thunk
*/
export const updateAvatar = (formData) => {
return async (dispatch) => {
// 調(diào)用接口進行上傳
const res = await http.patch('/user/photo', formData)
// 如果后端更新成功,則再更新 Redux 中的數(shù)據(jù)
if (res.data.message === 'OK') {
dispatch(getUserProfile())
}
}
}
- 創(chuàng)建 ref 對象辉懒,并關(guān)聯(lián)到文件輸入框元素
import { useEffect, useRef, useState } from 'react'
const fileRef = useRef()
<input type="file" hidden ref={fileRef} />
- 修改config對象
const config = {
avatar: [
{
title: '拍照',
onClick: () => {
fileRef.current.click()
},
},
{
title: '本地選擇',
onClick: () => {
fileRef.current.click()
},
},
],
gender: [
{
title: '男',
onClick: () => {
onCommit('gender', 0)
},
},
{
title: '女',
onClick: () => {
onCommit('gender', 1)
},
},
],
}
- 為文件輸入框添加
onChange
監(jiān)聽函數(shù)阳惹,并在該函數(shù)中獲取選中的文件后調(diào)用 Action 進行上傳和更新
<input type="file" hidden ref={fileRef} onChange={onAvatarChange} />
import { getUserProfile, updateAvatar, updateProfile } from '@/store/actions/profile'
const onAvatarChange = (e) => {
// 獲取選中的圖片文件
const file = e.target.files[0]
// 生成表單數(shù)據(jù)
const formData = new FormData()
formData.append('photo', file)
// 調(diào)用 Action 進行上傳和 Redux 數(shù)據(jù)更新
dispatch(updateAvatar(formData))
Toast.success('頭像上傳成功')
onClose()
}
編輯個人詳情:完成生日的修改
目標:在從點擊生日進入的日期選擇器中選擇新日期后,將選中數(shù)據(jù)提交到后端進行更新眶俩,并更新到 Redux
實現(xiàn)思路:
- 借助之前實現(xiàn)的更新個人詳情的 Action
操作步驟
- 為日期選擇器組件設(shè)置
onChange
回調(diào)函數(shù)穆端,在該函數(shù)執(zhí)行對應(yīng) Action
<DatePicker
// ...
onChange={onBirthdayChange}
>
const onBirthdayChange = async (value) => {
const year = value.getFullYear()
const month = value.getMonth() + 1
const day = value.getDate()
const dateStr = `${year}-${month}-${day}`
// 調(diào)用 Action 更新數(shù)據(jù)
await dispatch(updateProfile('birthday', dateStr))
Toast.success('修改生日成功')
}
退出登錄
目標:點擊 “退出登錄” 按鈕后返回到登錄頁面
<img src="極客園移動端1.assets/image-20210902104308053.png" alt="image-20210902104308053" style="zoom:40%;" />
實現(xiàn)思路:
- 點擊 “退出登錄” 后,需要彈信息框讓用戶確認
- 確認退出仿便,則清空 Redux 和 LocalStorage 中的 Token 信息
- 清空 Token 后跳轉(zhuǎn)頁面到登錄頁
操作步驟
- 為“退出登錄”按鈕添加點擊事件体啰,并在監(jiān)聽函數(shù)中彈出確認框:
<button className="btn" onClick={onLogout}>退出登錄</button>
import { DatePicker, List, Modal } from 'antd-mobile'
// 退出登錄
const onLogout = () => {
// 彈出確認對話框
Modal.alert('溫馨提示', '你確定退出嗎?', [
// 取消按鈕
{ text: '取消' },
// 確認按鈕
{
text: '確認',
style: { color: '#FC6627' },
onPress: () => {
console.log('執(zhí)行登出....')
}
}
])
}
- 在
store/reducers/login.js
中嗽仪,添加刪除 Token 信息的 Reducer 邏輯:
switch (type) {
case 'login/logout': return {}
// ...
}
- 在
store/action/login.js
中荒勇,添加用于從 Redux 和 LocalStorage 中刪除 Token 信息的 Action Creator:
/**
* 退出
* @returns
*/
export const logout = () => {
return (dispatch) => {
removeTokenInfo()
dispatch({
type: 'login/logout',
})
}
}
- 在“退出登錄”的彈框回調(diào)
onPress
中調(diào)用以上 Action 刪除 Token 后,跳轉(zhuǎn)到登錄頁:
import { logout } from '@/store/actions/login'
onPress: () => {
// 刪除 Token 信息
dispatch(logout())
// 跳轉(zhuǎn)到登錄頁
history.replace('/login')
}
小智同學
websocket
WebSocket 是一種數(shù)據(jù)通信協(xié)議闻坚,類似于我們常見的 http 協(xié)議沽翔。
為什么需要 WebSocket?
初次接觸 WebSocket 的人窿凤,都會問同樣的問題:我們已經(jīng)有了 HTTP 協(xié)議仅偎,為什么還需要另一個協(xié)議?它能帶來什么好處雳殊?
答案很簡單橘沥,因為 HTTP 協(xié)議有一個缺陷:通信只能由客戶端發(fā)起。http基于請求響應(yīng)實現(xiàn)夯秃。
舉例來說座咆,我們想了解今天的天氣,只能是客戶端向服務(wù)器發(fā)出請求仓洼,服務(wù)器返回查詢結(jié)果介陶。HTTP 協(xié)議做不到服務(wù)器主動向客戶端推送信息。
這種單向請求的特點色建,注定了如果服務(wù)器有連續(xù)的狀態(tài)變化哺呜,客戶端要獲知就非常麻煩。我們只能使用"輪詢":每隔一段時候箕戳,就發(fā)出一個詢問某残,了解服務(wù)器有沒有新的信息。最典型的場景就是聊天室漂羊。
輪詢的效率低驾锰,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)走越。因此椭豫,工程師們一直在思考,有沒有更好的方法旨指。WebSocket 就是這樣發(fā)明的赏酥。
websocket簡介
WebSocket 協(xié)議在2008年誕生,2011年成為國際標準谆构。所有瀏覽器都已經(jīng)支持了裸扶。
它的最大特點就是,服務(wù)器可以主動向客戶端推送信息搬素,客戶端也可以主動向服務(wù)器發(fā)送信息呵晨,是真正的雙向平等對話魏保,屬于服務(wù)器推送技術(shù)的一種。
典型的websocket應(yīng)用場景:
- 即時通訊摸屠,谓罗,,客服
- 聊天室 廣播
- 點餐
websocket使用-原生
基本步驟
- 瀏覽器發(fā)出鏈接請求
- 服務(wù)器告知鏈接成功
- 雙方進行雙向通訊
- 關(guān)閉連接
核心api
// 打開websocket連接
// WebSocket 是瀏覽器的內(nèi)置對象
var ws = new WebSocket('wss://echo.websocket.org') // 建立與服務(wù)端地址的連接
// 如果與服務(wù)器建立連接成功, 調(diào)用 websocket實例的 回調(diào)函數(shù) onopen
ws.onopen = function () {
// 如果執(zhí)行此函數(shù) 表示與服務(wù)器建立關(guān)系成功
}
// 發(fā)送消息
ws.send('消息')
// 接收消息
ws.onmessage = function (event) {
// event中的data就是服務(wù)器發(fā)過來的消息
}
ws.close()
// 關(guān)閉連接成功
ws.onclose = function () {
// 關(guān)閉連接成功
}
示例demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>體驗websocket</title>
<style>
#contanier {
width: 500px;
height: 400px;
border: 2px dashed #7575e7;
overflow-y: auto;
}
</style>
</head>
<body>
<div id="contanier"></div>
<!-- 1 建立連接 (撥號) -->
<!-- 2 發(fā)消息 接消息 -->
<!-- 3 關(guān)閉連接 -->
<input type="text" id="message" />
<button onclick="openWS()">建立連接</button>
<button onclick="sendMessage()">發(fā)送消息</button>
<button onclick="closeWS()">關(guān)閉連接</button>
<script>
var dom = document.getElementById('contanier')
var inputDom = document.getElementById('message')
var isOpen = false // 表示是否已經(jīng)建立了撥號
var ws // 別的方法 也需要使用ws
// 打開websocket連接
var openWS = function() {
/// 網(wǎng)絡(luò)上提供的一個測試websocket功能的服務(wù)器地址季二。
/// 它的效果是檩咱,你向服務(wù)器發(fā)什么消息 ,它就完全回復還給你胯舷。
ws = new WebSocket('wss://echo.websocket.org') // 建立與服務(wù)器的聯(lián)系
// onopen是webSocket約定事件名
// 當本地客戶端瀏覽器與服務(wù)器建立連接之后刻蚯,就會執(zhí)行onopen的回調(diào)
ws.onopen = function(event) {
isOpen = true
// 建立成功
dom.innerHTML = dom.innerHTML + `<p>與服務(wù)器成功建立連接</p>`
}
// 接收消息
// onmessage是webSocket約定事件名
// 如果從服務(wù)器上發(fā)過來了消息,則會進入onmessage的回調(diào)
ws.onmessage = function(event) {
// 由于 我們先給服務(wù)器發(fā)了消息 服務(wù)器給我們回了消息
dom.innerHTML =
dom.innerHTML + `<p style='color: blue'>服務(wù)器說:${event.data}</p>`
}
// onclose是webSocket約定事件名
ws.onclose = function() {
// 此函數(shù)表示 關(guān)閉連接成功
isOpen = false // 把狀態(tài)關(guān)閉掉
dom.innerHTML = dom.innerHTML + `<p>與服務(wù)器連接關(guān)閉</p>`
}
}
// 發(fā)送消息 接收消息
var sendMessage = function() {
if (inputDom.value && isOpen) {
// 發(fā)消息 要等到 連接成功才能發(fā) 而且內(nèi)容不為空
// 發(fā)消息就是send
ws.send(inputDom.value) // 發(fā)送消息
// 發(fā)完之后 添加到 當前視圖上
dom.innerHTML =
dom.innerHTML + `<p style='color: red'>我說:${inputDom.value}</p>`
inputDom.value = ''
}
}
// 關(guān)閉連接
var closeWS = function() {
ws.close() // 關(guān)閉連接
}
</script>
</body>
</html>
聊天客服:小智同學頁面的靜態(tài)結(jié)構(gòu)
目標:實現(xiàn)小智同學頁面的靜態(tài)結(jié)構(gòu)和樣式
頁面布局結(jié)構(gòu)分析:
<img src="極客園移動端1.assets/image-20210903174406559.png" alt="image-20210903174406559" style="zoom:40%;" />
操作步驟
- 將資源包中的樣式文件拷貝到
pages/Profile/Chat/
目錄下桑嘶,然后在該目錄中的index.js
里編寫:
import Icon from '@/components/Icon'
import Input from '@/components/Input'
import NavBar from '@/components/NavBar'
import { useHistory } from 'react-router-dom'
import styles from './index.module.scss'
const Chat = () => {
const history = useHistory()
return (
<div className={styles.root}>
{/* 頂部導航欄 */}
<NavBar className="fixed-header" onLeftClick={() => history.go(-1)}>
小智同學
</NavBar>
{/* 聊天記錄列表 */}
<div className="chat-list">
{/* 機器人的消息 */}
<div className="chat-item">
<Icon type="iconbtn_xiaozhitongxue" />
<div className="message">你好炊汹!</div>
</div>
{/* 用戶的消息 */}
<div className="chat-item user">
<img src={'http://toutiao.itheima.net/images/user_head.jpg'} alt="" />
<div className="message">你好?</div>
</div>
</div>
{/* 底部消息輸入框 */}
<div className="input-footer">
<Input
className="no-border"
placeholder="請描述您的問題"
/>
<Icon type="iconbianji" />
</div>
</div>
)
}
export default Chat
- 配置路由規(guī)則
聊天客服:動態(tài)渲染聊天記錄列表
目標:將聊天數(shù)據(jù)存在數(shù)組狀態(tài)中不翩,再動態(tài)渲染到界面上
操作步驟
- 聲明一個數(shù)組狀態(tài)
import { useEffect, useRef, useState } from 'react'
// 聊天記錄
const [messageList, setMessageList] = useState([
// 放兩條初始消息
{ type: 'robot', text: '親愛的用戶您好兵扬,小智同學為您服務(wù)。' },
{ type: 'user', text: '你好' }
])
- 從 Redux 中獲取當前用戶基本信息
import { useSelector } from 'react-redux'
// 當前用戶信息
const user = useSelector(state => state.profile.user)
- 根據(jù)數(shù)組數(shù)據(jù)口蝠,動態(tài)渲染聊天記錄列表
{/* 聊天記錄列表 */}
<div className="chat-list">
{messageList.map((msg, index) => {
// 機器人的消息
if (msg.type === 'robot') {
return (
<div className="chat-item" key={index}>
<Icon type="iconbtn_xiaozhitongxue" />
<div className="message">{msg.text}</div>
</div>
)
}
// 用戶的消息
else {
return (
<div className="chat-item user" key={index}>
<img src={user.photo || 'http://toutiao.itheima.net/images/user_head.jpg'} alt="" />
<div className="message">{msg.text}</div>
</div>
)
}
})}
</div>
效果:
<img src="極客園移動端1.assets/image-20210904085509862.png" alt="image-20210904085509862" style="zoom:50%;" />
聊天客服:建立與服務(wù)器的連接
目標:使用 socket.io 客戶端與服務(wù)器建立 WebSocket 長連接
本項目聊天客服的后端接口器钟,使用的是基于 WebSocket 協(xié)議的 socket.io 接口。我們可以使用專門的 socket.io 客戶端庫妙蔗,就能輕松建立起連接并進行互相通信傲霸。
實現(xiàn)思路:
- 借助
useEffect
,在進入頁面時調(diào)用客戶端庫建立 socket.io 連接
操作步驟
- 安裝 socket.io 客戶端庫:
socket.io-client
npm i socket.io-client --save
- 在進入機器人客服頁面時眉反,創(chuàng)建 socket.io 客戶端
import io from 'socket.io-client'
import { getTokenInfo } from '@/utils/storage'
// 用于緩存 socket.io 客戶端實例
const clientRef = useRef(null)
useEffect(() => {
// 創(chuàng)建客戶端實例
const client = io('http://toutiao.itheima.net', {
transports: ['websocket'],
// 在查詢字符串參數(shù)中傳遞 token
query: {
token: getTokenInfo().token
}
})
// 監(jiān)聽連接成功的事件
client.on('connect', () => {
// 向聊天記錄中添加一條消息
setMessageList(messageList => [
...messageList,
{ type: 'robot', text: '我現(xiàn)在恭候著您的提問昙啄。' }
])
})
// 監(jiān)聽收到消息的事件
client.on('message', data => {
console.log('>>>>收到 socket.io 消息:', data)
})
// 將客戶端實例緩存到 ref 引用中
clientRef.current = client
// 在組件銷毀時關(guān)閉 socket.io 的連接
return () => {
client.close()
}
}, [])
正常情況,一進入客服頁面寸五,就能在控制臺看到連接成功的信息:
<img src="極客園移動端1.assets/image-20210903181934664.png" alt="image-20210903181934664" style="zoom:40%;" />
聊天客服:給機器人發(fā)消息
目標:將輸入框內(nèi)容通過 socket.io 發(fā)送到服務(wù)端
實現(xiàn)思路:
- 使用 socket.io 實例的
emit()
方法發(fā)送信息
操作步驟
- 聲明一個狀態(tài)梳凛,并綁定消息輸入框
// 輸入框中的內(nèi)容
const [message, setMessage] = useState('')
<Input
className="no-border"
placeholder="請描述您的問題"
value={message}
onChange={e => setMessage(e.target.value)}
/>
- 為消息輸入框添加鍵盤事件,在輸入回車時發(fā)送消息
<Input
// ...
onKeyUp={onSendMessage}
/>
// 按回車發(fā)送消息
const onSendMessage = e => {
if (e.keyCode === 13) {
// 通過 socket.io 客戶端向服務(wù)端發(fā)送消息
clientRef.current.emit('message', {
msg: message,
timestamp: Date.now()
})
// 向聊天記錄中添加當前發(fā)送的消息
setMessageList(messageList => [
...messageList,
{ type: 'user', text: message }
])
// 發(fā)送后清空輸入框
setMessage('')
}
}
聊天客服:接收機器人回復的消息
目標:
通過 socket.io 監(jiān)聽回復的消息梳杏,并添加到聊天列表中韧拒;
且當消息較多出現(xiàn)滾動條時,有后續(xù)新消息的話總將滾動條滾動到最底部十性。
實現(xiàn)思路:
- 使用 socket.io 實例的
message
事件接收信息 - 在聊天列表數(shù)據(jù)變化時叛溢,操作列表容器元素來設(shè)置滾動量
操作步驟
- 在 socket.io 實例的
message
事件中,將接收到的消息添加到聊天列表:
// 監(jiān)聽收到消息的事件
client.on('message', data => {
// 向聊天記錄中添加機器人回復的消息
setMessageList(messageList => [
...messageList,
{ type: 'robot', text: data.msg }
])
})
- 聲明一個 ref 并設(shè)置到聊天列表的容器元素上
// 用于操作聊天列表元素的引用
const chatListRef = useRef(null)
<div className="chat-list" ref={chatListRef}>
- 通過
useEffect
監(jiān)聽聊天數(shù)據(jù)變化劲适,對聊天容器元素的 scrollTop 進行設(shè)置:
// 監(jiān)聽聊天數(shù)據(jù)的變化楷掉,改變聊天容器元素的 scrollTop 值讓頁面滾到最底部
useEffect(() => {
chatListRef.current.scrollTop = chatListRef.current.scrollHeight
}, [messageList])
權(quán)限控制
封裝鑒權(quán)路由組件
目標:基于 Route 組件,封裝一個判斷存在 token 才能正常渲染指定 component 的路由組件
本項目中有些頁面需要登錄后才可訪問霞势,如:個人中心的所有頁面
因此我們需要為 Route 組件添加額外的邏輯烹植,使得在路由匹配后進行界面展示時斑鸦,可以按條件決定如何渲染。
實現(xiàn)思路:
- 使用 Router 組件的
render-props
機制
操作步驟
- 創(chuàng)建
components/AuthRoute/index.js
草雕,編寫組件代碼:
import { hasToken } from '@/utils/storage'
import { Redirect, Route } from 'react-router-dom'
/**
* 鑒權(quán)路由組件
* @param {*} component 本來 Route 組件上的 component 屬性
* @param {Array} rest 其他屬性
*/
const AuthRoute = ({ component: Component, ...rest }) => {
return (
<Route {...rest} render={props => {
// 如果有 token鄙才,則展示傳入的組件
if (hasToken) {
return <Component />
}
// 否則調(diào)用 Redirect 組件跳轉(zhuǎn)到登錄頁
return (
<Redirect to={{
pathname: '/login',
state: {
from: props.location.pathname
}
}} />
)
}} />
)
}
export default AuthRoute
- 在
App.js
和layouts/TabBarLayout.js
中,使用AuthRoute
組件替代某些Route
:
import AuthRoute from '@/components/AuthRoute'
<AuthRoute path="/profile/edit" component={ProfileEdit} />
<AuthRoute path="/profile/feedback" component={ProfileFeedback} />
<AuthRoute path="/profile/chat" component={Chat} />
<AuthRoute path="/home/profile" exact component={Profile} />
替代后促绵,如果未經(jīng)登錄訪問個人中心的頁面,就會直接跳到登錄頁嘴纺。
修改Router的history
- 新增文件 utils/history.js
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
export default history
- 修改App.js
import { Router, Route, Switch, Redirect } from 'react-router-dom'
import history from '@/utils/history'
export default function App() {
return (
<Router history={history}>
// ....
}
Token 的失效處理和無感刷新
目標:了解當請求后端接口時败晴,如果發(fā)生了由于 Token 失效而產(chǎn)生的請求失敗,應(yīng)該如何進行處理
token: 訪問令牌栽渴,通過這個token就能夠訪問項目
- 有效時間都不會很長尖坤,一般就是一個小時或者2個小時
- token過期的處理
- 重新登錄(適合PC端的管理系統(tǒng))
- 對于移動端資訊類的項目用戶體驗不好。
refresh_token: 刷新令牌闲擦,沒有訪問的功能慢味,通過刷新令牌能夠獲取到一個新的訪問令牌。
- 刷新令牌:有效時間會比較長
常用的處理流程:
思想總結(jié):
- 無 Token墅冷,直接跳到登錄頁
- 有 Token纯路,則用 Refresh Token 換新 Token:換成功則用新 Token 重發(fā)原先的請求,沒換成功則跳到登錄頁
這一系列操作寞忿,可以在封裝的 http 請求模塊中完成驰唬。
操作步驟
// 配置響應(yīng)攔截器
instance.interceptors.response.use(
(response) => {
// 對響應(yīng)做點什么...
return response.data
},
async (err) => {
// 如果是網(wǎng)絡(luò)錯誤
if (!err.response) {
Toast.info('網(wǎng)絡(luò)繁忙,請稍后重試')
return Promise.reject(err)
}
// 如果有響應(yīng),但是不是401錯誤
if (err.response.status !== 401) {
Toast.info(err.response.data.message)
return Promise.reject(err)
}
const { token, refresh_token } = getTokenInfo()
// 如果是401錯誤
// 如果沒有token或者刷新token
if (!token || !refresh_token) {
// 跳轉(zhuǎn)到登錄頁腔彰,并攜帶上當前正在訪問的頁面叫编,等登錄成功后再跳回該頁面
history.replace('/login', {
from: history.location.pathname || '/home',
})
return Promise.reject(err)
}
// 如果有token,且是401錯誤
try {
// 通過 Refresh Token 換取新 Token
// 特別說明:這個地方發(fā)請求的時候霹抛,不能使用新建的 http 實例去請求搓逾,要用默認實例 axios 去請求!
// 否則會因 http 實例的請求攔截器的作用杯拐,攜帶上老的 token 而不是 refresh_token
const res = await axios.put(`${err.config.baseURL}authorizations`, null, {
headers: {
Authorization: `Bearer ${refresh_token}`,
},
})
// 將新?lián)Q到的 Token 信息保存到 Redux 和 LocalStorage 中
const tokenInfo = {
token: res.data.data.token,
refresh_token,
}
setTokenInfo(tokenInfo)
store.dispatch(saveToken(tokenInfo))
// 重新發(fā)送之前因 Token 無效而失敗的請求
return instance(err.config)
} catch (error) {
// 如果換取token失敗
store.dispatch(logout())
// 跳轉(zhuǎn)到登錄頁霞篡,并攜帶上當前正在訪問的頁面,等登錄成功后再跳回該頁面
history.replace('/login', {
from: history.location,
})
Toast.info('登錄信息失效')
return Promise.reject(error)
}
}
)
效果測試:
按下圖修改 LocalStorage 中的 token藕施,修改后刷新頁面寇损,成功執(zhí)行的話,可以該token被替換成了新的 token
<img src="極客園移動端1.assets/image-20210903153923341.png" alt="image-20210903153923341" style="zoom:50%;" />
處理登錄后的頁面跳轉(zhuǎn)
目標:當進行登錄獲取到 Token 后裳食,應(yīng)當將頁面跳到合適的頁面去
操作步驟
- 在登錄頁面表單對象的
onSubmit
方法中矛市,在獲取 Token 后添加頁面跳轉(zhuǎn)邏輯
import { useHistory, useLocation } from 'react-router-dom'
// 獲取路由信息 location 對象
const location = useLocation()
// Formik 表單對象
const form = useFormik({
// ...
// 提交
onSubmit: async values => {
await dispatch(login(values))
// 登錄后進行頁面跳轉(zhuǎn)
const { state } = location
if (!state) {
// 如果不是從其他頁面跳到的登錄頁,則登錄后默認進入首頁
history.replace('/home/index')
} else {
// 否則跳回到之前訪問的頁面
history.replace(state.from)
}
}
})
404 錯誤頁面
目標:實現(xiàn)當用戶訪問不存在的頁面路徑時诲祸,所要顯示的錯誤提示頁
<img src="極客園移動端1.assets/image-20210903172102078.png" alt="image-20210903172102078" style="zoom:40%;" />
實現(xiàn)思路:
- 使用一個數(shù)字類型的狀態(tài)浊吏,記錄當前倒計時的秒數(shù)
- 使用一個 ref 狀態(tài)而昨,引用延時器
- 在延時器中判斷是否倒計時結(jié)束,未結(jié)束則秒數(shù)減一找田;結(jié)束則清理延時器并跳轉(zhuǎn)頁面
操作步驟
- 在
pages/NotFound/index.js
中歌憨,編寫以下代碼:
import React, { useEffect, useState } from 'react'
import { Link, useHistory } from 'react-router-dom'
export default function NotFound() {
const [time, setTime] = useState(3)
const history = useHistory()
useEffect(() => {
setTimeout(() => {
setTime(time - 1)
}, 1000)
if (time === 0) {
history.push('/home')
}
}, [time, history])
return (
<div>
<h1>對不起,你訪問的內(nèi)容不存在...</h1>
<p>
{time} 秒后墩衙,返回<Link to="/home">首頁</Link>
</p>
</div>
)
}