邊學邊用--使用React下的Material UI框架開發一個簡單的仿MetaMask的網頁版以太坊錢包(二)

一、前言

        今天我們接着上次的內容學習,本次學習的主要內容是React中路由的使用和創建導入賬號界面。由於這次學習系列中全部使用React函數組件和Hook,所以之前使用類組件的讀者需要切換過來。這裏是React Hook的官方文檔:=> https://react.docschina.org/docs/hooks-intro.html

二、React路由

        React路由主要涉及到react-routerreact-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中,元素主要分成三類:

  1. 路由定義。有三種 <BrowserRouter><HashRouter><MemoryRouter>。各有不同的使用場景,我們平常主要使用<BrowserRouter>,它使用通常的url來保存路由;而<HashRouter>使用哈希#來保存路由;<MemoryRouter>將路由保存在內存中,適用於無瀏覽器環境。
  2. 路由匹配。比如<Route><Switch>。和switch case的用法類似。
  3. 路由導航。比如<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運行起來,如果提示缺少相關模塊,直接進行安裝即可。
在這裏插入圖片描述
        點擊導入已有賬號就可以進入導入賬號界面,在該頁面點擊取消就回退到主界面了。當然你也可以直接點擊瀏覽器的前進、後退功能鍵。

        本次學習到此就結束了,下一章計劃編寫登錄頁面和實現錢包的保存。

        懇請大家留言指正或者提出改進意見。

        碼雲地址:=> https://gitee.com/TianCaoJiangLin/khwallet

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章