边学边用--使用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. 路由定义。有三种 。各有不同的使用场景,我们平常主要使用,它使用通常的url来保存路由;而使用哈希#来保存路由;将路由保存在内存中,适用于无浏览器环境。
  2. 路由匹配。比如。和switch case的用法类似。
  3. 路由导航。比如。平常使用

        react-router-dom使用时有几点注意事项:

  • 路由定义(Router)一般在元素顶层使用,注意,可以有不只一个Router(虽然通常不这样做),路由导航会被离它最近的Router捕获。
  • 路由匹配是匹配的URL的开始字符串,所以要把特殊匹配放在前面,通用匹配比如\放在后面,也可以使用exact属性来指定精确匹配
  • 使用了BrowserRouter之后通常需要在服务器端作一些配置(一般是重定向),这个可以参考我的一篇文章:React路由与Apache配置,使用HashRouter不需要服务器端配置,因它是要访问的同一个页面。

        好了,介绍完毕,让我们先安装它们。

npm install react-router
npm install react-router-dom

三、创建导入账号界面

我们计划完成后的界面如下图:
边学边用--使用React下的Material UI框架开发一个简单的仿MetaMask的网页版以太坊钱包(二)_第1张图片
        从这里可以看到它和创建账号的界面很像,因此可以从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

        这里可以看到,我们使用了导入已有账号这行代码来进行路由导航从而转到导入账号界面。

五、增加路由匹配

        修改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运行起来,如果提示缺少相关模块,直接进行安装即可。
边学边用--使用React下的Material UI框架开发一个简单的仿MetaMask的网页版以太坊钱包(二)_第2张图片
        点击导入已有账号就可以进入导入账号界面,在该页面点击取消就回退到主界面了。当然你也可以直接点击浏览器的前进、后退功能键。

        本次学习到此就结束了,下一章计划编写登录页面和实现钱包的保存。

        恳请大家留言指正或者提出改进意见。

        码云地址:=> https://gitee.com/TianCaoJiangLin/khwallet

你可能感兴趣的:(以太坊钱包)