一、前言
今天我們接着上次的內容學習,本次學習的主要內容是React中路由的使用和創建導入賬號界面。由於這次學習系列中全部使用React函數組件和Hook,所以之前使用類組件的讀者需要切換過來。這裏是React Hook的官方文檔:=> https://react.docschina.org/docs/hooks-intro.html
二、React路由
React路由主要涉及到react-router
和react-router-dom
這兩個庫,第一個庫是讓你可以在代碼中進行路由導航;第二個庫是個和節點相關的路由庫(從名字就可以看出來),它用來進行路由定義、導航、匹配等。我們可以看到react-router-dom
這個庫已經有路由導航功能了,爲什麼還要使用react-router
呢?因爲它們的使用範圍不同,react-router-dom
主要用於節點靜態導航,比如直接點擊鏈接來跳轉;而react-router
用於代碼中導航,比如可以在回調函數中使用。下面我們簡要的介紹一下react-router-dom
這個庫。大家也可以直接去看它官方的文檔,這裏放出地址: => https://reacttraining.com/react-router/web/guides/quick-start
react-router-dom
中,元素主要分成三類:
- 路由定義。有三種
<BrowserRouter>
、<HashRouter>
和<MemoryRouter>
。各有不同的使用場景,我們平常主要使用<BrowserRouter>
,它使用通常的url來保存路由;而<HashRouter>
使用哈希#
來保存路由;<MemoryRouter>
將路由保存在內存中,適用於無瀏覽器環境。 - 路由匹配。比如
<Route>
和<Switch>
。和switch case
的用法類似。 - 路由導航。比如
<Link>
、<NavLink>
和<Redirect>
。平常使用<Link>
和<Redirect>
。
react-router-dom
使用時有幾點注意事項:
- 路由定義(Router)一般在元素頂層使用,注意,可以有不只一個Router(雖然通常不這樣做),路由導航會被離它最近的Router捕獲。
- 路由匹配是匹配的URL的開始字符串,所以要把特殊匹配放在前面,通用匹配比如
\
放在後面,也可以使用exact
屬性來指定精確匹配 - 使用了
BrowserRouter
之後通常需要在服務器端作一些配置(一般是重定向),這個可以參考我的一篇文章:React路由與Apache配置,使用HashRouter
不需要服務器端配置,因它是要訪問的同一個頁面。
好了,介紹完畢,讓我們先安裝它們。
npm install react-router
npm install react-router-dom
三、創建導入賬號界面
我們計劃完成後的界面如下圖:
從這裏可以看到它和創建賬號的界面很像,因此可以從src\views\CreateWallet.js
進行簡單修改得來。下面有一個助記詞導入和私鑰導入的切換按鈕,可以切換兩種導入方式,點擊取消可以退回到創建賬號界面。同樣,導入功能並沒有實現,這個放在後面統一做。
新建src/views/ImportWallet.js
代碼如下:
import React, {useState} from 'react';
import {makeStyles} from '@material-ui/core/styles';
import {useSimpleSnackbar} from 'contexts/SimpleSnackbar.jsx';
import TextField from '@material-ui/core/TextField';
import FormControl from '@material-ui/core/FormControl';
import HowToReg from '@material-ui/icons/HowToReg';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import Avatar from '@material-ui/core/Avatar';
import { withRouter } from "react-router";
import { Link } from "react-router-dom";
const minLength = 12;
const useStyles = makeStyles(theme => ({
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
},
title: {
marginTop: theme.spacing(1),
fontSize: 20
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(1),
textAlign: 'center'
},
submit: {
fontSize: 18,
width: "40%",
marginTop: theme.spacing(2)
},
import: {
margin: theme.spacing(2),
color:"#f44336",
fontSize: 18,
textDecoration: "none"
},
wallet: {
textAlign: "center",
fontSize: 18
},
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: theme.spacing(3)
}
}));
function ImportWallet({history}) {
const classes = useStyles();
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [isPrivateKey,setIsPrivateKey] = useState(true)
const [key,setKey] = useState('')
const showSnackbar = useSimpleSnackbar()
const changeKeyType = e => {
e.preventDefault()
setIsPrivateKey(isPrivate => !isPrivate)
}
const updatePassword = e => {
let _password = e.target.value;
setPassword(_password)
};
const updateConfirmPassword = e => {
let _confirmPassword = e.target.value;
setConfirmPassword(_confirmPassword)
}
const updateKey = e => {
setKey(e.target.value)
};
const cancelImport = e => {
history.push('/')
};
const onSubmit = e => {
e.preventDefault()
if (password !== confirmPassword) {
return showSnackbar("前後兩次密碼不一致", "error");
}
if (password.length < minLength) {
return showSnackbar("密碼至少12位", "error");
}
}
return(
<div className={classes.container}>
<Avatar className={classes.avatar}>
<HowToReg/>
</Avatar>
<Typography className={classes.title}>
{ isPrivateKey ? "請輸入你的私鑰": "請輸入你的助記詞" }
</Typography>
<form className={classes.form} onSubmit={onSubmit}>
<FormControl margin="normal" fullWidth>
<TextField id="key-password-input"
label={isPrivateKey ? "私鑰" : "助記詞"}
required
type="password"
value={key}
onChange={updateKey}/>
</FormControl>
<FormControl margin="normal" fullWidth>
<TextField id="standard-password-input"
label="設置密碼"
required
type="password"
autoComplete="current-password"
value={password}
onChange={updatePassword}/>
</FormControl>
<FormControl margin="normal" fullWidth>
<TextField id="confirm-password-input"
label="請再次輸入密碼"
required
type="password"
autoComplete="current-password"
value={confirmPassword}
onChange={updateConfirmPassword}/>
</FormControl>
<Button type='submit' variant="contained" color="primary" className={classes.submit}>
導入
</Button>
</form>
<Button variant="contained" color="primary" onClick={cancelImport} className={classes.submit}>
取消
</Button>
<Link to="#" onClick={changeKeyType} className={classes.import}>
{ isPrivateKey ? "從助記詞導入" : "從私鑰導入"}
</Link>
</div>
)
}
export default withRouter(ImportWallet)
上面的代碼有幾點需要解釋的地方:
-
上面的代碼最後導出時使用了
withRouter
,它讓我們可以在函數組件中使用history
屬性,從而在cancelImport
方法裏進行路由導航。 -
請注意以下這段代碼,這裏的
Link
其實並不是發揮路由導航功能。原本應該是一個Button
的,但是爲了達到在桌面端鼠標懸停時顯示手形效果,硬生生的改成了Link
,希望大家能改進一下。
<Link to="#" onClick={changeKeyType} className={classes.import}>
{ isPrivateKey ? "從助記詞導入" : "從私鑰導入"}
</Link>
- 這裏還要注意
changeKeyType
這個方法,它裏面的state設置setIsPrivateKey
傳入了一個函數,而不是具體的值。這個函數的參數就是需要改變的state的上一個值,它返回一個新值。可以查看上面提到的Hook文檔來學習詳細的用法。
const changeKeyType = e => {
e.preventDefault()
setIsPrivateKey(isPrivate => !isPrivate)
}
四、爲創建賬號界面增加路由導航
修改src\views\CreateWallet.js
,增加對賬號導入界面的導入和路由導航,修改後的代碼如下:
import React, {useState} from 'react';
import {makeStyles} from '@material-ui/core/styles';
import Avatar from '@material-ui/core/Avatar';
import AddIcon from '@material-ui/icons/PersonAdd';
import Button from '@material-ui/core/Button';
import FormControl from '@material-ui/core/FormControl';
import Typography from '@material-ui/core/Typography';
import { Link } from "react-router-dom";
import {useSimpleSnackbar} from 'contexts/SimpleSnackbar.jsx';
import TextField from '@material-ui/core/TextField';
const minLength = 12;
const useStyles = makeStyles(theme => ({
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
},
title: {
marginTop: theme.spacing(1),
fontSize: 20
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(1),
textAlign: 'center'
},
submit: {
fontSize: 20,
width: "50%",
marginTop: theme.spacing(5)
},
import: {
fontSize: 18,
textDecoration:"none",
color:"#f44336",
margin: theme.spacing(4),
},
wallet: {
textAlign: "center",
marginTop: theme.spacing(0.5),
fontSize: 18
},
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: theme.spacing(3)
}
}));
function CreateWallet() {
const classes = useStyles();
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const showSnackbar = useSimpleSnackbar()
const updatePassword = e => {
let _password = e.target.value;
setPassword(_password)
};
const updateConfirmPassword = e => {
let _confirmPassword = e.target.value;
setConfirmPassword(_confirmPassword)
}
const onSubmit = e => {
e.preventDefault();
if (password !== confirmPassword) {
return showSnackbar("前後兩次密碼不一致", "error");
}
if (password.length < minLength) {
return showSnackbar("密碼至少12位", "error");
}
}
return (<div className={classes.container}>
<Avatar className={classes.avatar}>
<AddIcon/>
</Avatar>
<Typography className={classes.title}>
創建一個新賬號
</Typography>
<form className={classes.form} onSubmit={onSubmit}>
<FormControl margin="normal" fullWidth>
<TextField id="standard-password-input"
label="設置密碼"
required
type="password"
autoComplete="current-password"
value={password}
onChange={updatePassword}/>
</FormControl>
<FormControl margin="normal" fullWidth>
<TextField id="confirm-password-input"
label="再次輸入密碼"
required
type="password"
autoComplete="current-password"
value={confirmPassword}
onChange={updateConfirmPassword}/>
</FormControl>
<Button type='submit' variant="contained" color="primary" className={classes.submit}>
創建
</Button>
</form>
<Link to="/import" className={classes.import}>導入已有賬號</Link>
<Typography color='secondary' className={classes.wallet}>
KHWallet,簡單安全易用的
</Typography>
<Typography color='secondary' className={classes.wallet}>
以太坊錢包
</Typography>
</div>)
}
export default CreateWallet
這裏可以看到,我們使用了<Link to="/import" className={classes.import}>導入已有賬號</Link>
這行代碼來進行路由導航從而轉到導入賬號界面。
五、增加路由匹配
修改src/views/Main.js
,增加路由定義與路由匹配,修改完成後的代碼如下:
import React,{lazy,Suspense} from 'react';
import Grid from '@material-ui/core/Grid';
import {makeStyles} from '@material-ui/core/styles';
import WalletBar from 'components/WalletBar';
import Paper from '@material-ui/core/Paper';
import { isMobile } from 'react-device-detect';
import { BrowserRouter as Router, Route, Switch} from "react-router-dom";
const ImportWallet = lazy(() => import('./ImportWallet'));
const CreateWallet = lazy(() => import('./CreateWallet'));
const useStyles = makeStyles(theme => ({
root: {
marginTop: theme.spacing(isMobile ? 8 :10),
display: "flex",
justifyContent: "center"
}
}));
function SwitchPage() {
return (
<Suspense fallback ='loading'>
<Switch>
<Route path="/import" component={ImportWallet}/>
<Route path="/" component={CreateWallet}/>
</Switch>
</Suspense >
)
}
export default function Main() {
const classes = useStyles();
return (<div className={classes.root}>
<Grid item xs={12} sm={12} md={3}>
<Paper style={{
height: 600,
mixHeight: 600
}}>
<Router >
<WalletBar/>
<SwitchPage />
</Router>
</Paper>
</Grid>
</div>)
}
注意到這行代碼:const ImportWallet = lazy(() => import('./ImportWallet'));
,這裏使用了一個延遲導入功能。就是需要用到導入賬號界面時纔去裝載這個界面。使用延時功能時需要使用Suspense
來進行包裝。因爲兩個界面的Header(頭部)是一樣的,所以我們只對下面的Body部分使用了路由匹配。
好了,所有的修改結束了。讓我們npm start
運行起來,如果提示缺少相關模塊,直接進行安裝即可。
點擊導入已有賬號就可以進入導入賬號界面,在該頁面點擊取消就回退到主界面了。當然你也可以直接點擊瀏覽器的前進、後退功能鍵。
本次學習到此就結束了,下一章計劃編寫登錄頁面和實現錢包的保存。
懇請大家留言指正或者提出改進意見。