边学边用--使用React下的Material UI框架开发一个简单的仿MetaMask的网页版以太坊钱包(三)

        前一篇文章我们学习了React中路由的使用并创建了一个导入界面;这一次我们学习钱包的具体实现和登录界面的开发,并且通过钱包将登录界面、创建钱包和导入钱包这三个UI串连起来。

一、Provider的进一步应用

        在前面的学习中,我们使用了Provider来实现消息条的全局显示和网络切换选择,这次我们计划使用Provider来实现更多功能。Provider是一种非常实用的工具,它除了能提供全局共享的变量和方法外,还可以使用同步的模式来写异步操作,并且也不需要async/await。

        在前面内容中,由于内容较少,我们专门写了一个Network.js来使用Provider处理网络切换选择。随着开发内容的增加,我们需要全局共享的变量或者方法会越来越多。这些变量有些是保存在本地存储的,比如私钥、简要账户信息等;有些变量只需要保存在内存中即可,比如是否登录,当前选择的网络,当前登录的钱包等。因此,我们将Provider进行了整合,使用一个GlobalProvider.js来保存那些不需要本地存储的全局变量,使用StorageProvider.js来保存需要本地存储的内存变量。

        先删除src/contexts/Network.js,然后新建src/contexts/GlobalProvider.js,代码如下:

/**
*  本文件用来全局获取和更新不需要本地存储的全局变量(内存中)
*/
import React, { createContext, useContext, useReducer, useMemo, useCallback } from 'react'

const UPDATE='UPDATE'
const GlobalProvider = createContext()

function useGlobalContext() {
  return useContext(GlobalProvider)
}

//todo 全局变量随着开发逐渐增加
const global_init = {
    network:"homestead",
    isLogin:false,
    wallet:null
}

function reducer(state,{type,payload}) {
    switch (type) {
        case UPDATE:{
            return { ...state,...payload }
        }
        default:{
          throw Error(`Unexpected action type in GlobalContext reducer: '${type}'.`)
        }
    }
}

export default function Provider({children}) {
    const [state, dispatch] = useReducer(reducer, global_init)

    const update = useCallback( payload => {
        dispatch({ type: UPDATE, payload})
    }, [])

    return (
        <GlobalProvider.Provider value={useMemo(() => [state,{update}], [state, update])}>
          {children}
        </GlobalProvider.Provider>
     )
}

export function useGlobal() {
    const [state,] = useGlobalContext()
    return state
}

export function useUpdateGlobal() {
    const [,{update}] = useGlobalContext()
    return update
}

        接着我们再新建src/contexts/StorageProvider.js,代码如下:

/**
*  本文件用来全局获取和更新本地存储
*  存储内容随着开发逐渐增加
*/
import React, { useState,useEffect,createContext,useMemo,useContext, useCallback } from 'react'
import {reactLocalStorage} from 'reactjs-localstorage';

//需要在.env.local等文件中设置REACT_APP_APPKEY,代表本APP的key或者ID
const appKey = process.env.REACT_APP_APPKEY;

//创建上下文环境,固定用法
const StorageContext = createContext()

function useStorageContext() {
    return useContext(StorageContext)
}

//定义一个provider
export default function Provider({ children }) {
  //内存中保留一份缓存,不用每次从本地存储读取
  const [data, setData] = useState(null)
  //存储更新的同时也更新内存缓存
  const update = useCallback( _data => {
      reactLocalStorage.setObject(appKey,_data)
      setData(_data)
  },[])

  //provider返回值,注意update包装在一个对象中,直接当作数组元素返回有时会出问题
  return (
    <StorageContext.Provider value={useMemo(() => [data,{update}], [data, update])}>
      {children}
    </StorageContext.Provider>
  )
}

/**
* 获取本地存储的hook,这里先返回一个undefined,读取本地存储后再更新这个返回值
*/
export function useStorage() {
    const [data,{update}] = useStorageContext();

    useEffect(()=>{
        if(!data) {
            let _data = reactLocalStorage.getObject(appKey);
            //还未保存过数据为{}或者[]时
            if( !_data.length) {
                update([])
            }else{
                update(_data)
            }
        }
    },[data,update])

    return data
}

//更新存储的hook
export function useUpdateStorage() {
    const [,{update}] = useStorageContext();
    return update
}

        大家注意看一下这个useStorage()的用法,它是一个hook,首先返回一个未定义的值,然后再读取本地存储的值后进行更新。通常更新值都是异步的(这里恰好是同步的),值更新以后所有使用这个值的子元素都会重新渲染来使用最新的值,这对一些需要定时更新的全局共享的值非常有用。比如我们在一个交易所里,可以用它来定时更新ETH的价格等,更新后所有界面自动使用最新的价格。

        那么有的人可能会问为什么不使用async/await来直接获取最新的值,而是先返回一个值然后再更新呢。这是因为它是一个hook,虽然hook内部可以使用Promise,但是hook只能应用在函数组件最顶层,它只能用同步的方法调用,所以这里不能使用async/await

二、React中本地存储与自定义环境变量

        我们使用reactjs-localstorage这个库来实现React中的本地存储,让我们先安装它:

npm install reactjs-localstorage

        它读取或者设置本地存储时,需要提供一个appId之类的字符串,它的用法如下:

import {reactLocalStorage} from 'reactjs-localstorage';
 
reactLocalStorage.set('var', true);
reactLocalStorage.get('var', true);
reactLocalStorage.setObject('var', {'test': 'test'});
reactLocalStorage.getObject('var');
reactLocalStorage.remove('var');
reactLocalStorage.clear();

其实它保存的是K/V对(键/值对),这里的'var'相当于一个key,需要注意的是,使用setObject时保存的数据也可以是一个数组,因为在JavaScript中,数组也是对象。另外当使用getObject时,如果对应的键不存在,它返回的是一个空对象{}。注意,空对象在直接应用于逻辑判断时是返回true的。

        另外我们将有关的自定义环境变量集中设置,在项目根目录下新建.env.local,内容如下:

REACT_APP_APPKEY = 'KHWallet2019'
REACT_APP_PASSWORD_LENGTH = 1

        第一个就是本地存储的key,改成你自己的,第二个是限定密码最小长度,这里为了简单,设置成了1。

        注意,在React中,自定义的环境变量必须以REACT_APP_开头。有好几个地方可以设置自定义环境变量:

.env                # 在所有的环境中被载入
.env.local          # 在所有的环境中被载入,但会被 git 忽略
.env.[mode]         # 只在指定的模式中被载入
.env.[mode].local   # 只在指定的模式中被载入,但会被 git 忽略

        这些环境变量在React中通过代码process.env.REACT_APP_NOT_SECRET_CODE来获取,注意不要存放私钥、密码等。

三、初始化新建的Provider

        让我们修改src/index.js,加入这次新建的两个Provider的初始化,修改完成后的代码片断如下:

function AllProvider() {
    return (
        <NotistackWrapper>
            <GlobalProvider>
                <StorageProvider>
                    <Main />
                </StorageProvider>
            </GlobalProvider>
        </NotistackWrapper>
    )
}

ReactDOM.render(<AllProvider />,document.getElementById('root'));

        因为篇幅关系,这里只列出代码片断,完整代码请大家直接在文章结尾的码云链接上去看。

四、使用AES加密

        我们的钱包私钥需要加密后保存在客户端,本次开发使用了AES加密,先安装它:

npm install crypto

        然后新建src/utils/index.js,代码片断如下:

import crypto from 'crypto'

export function aesEncrypt(data,key) {
    let cipher = crypto.createCipher('aes192', key);
    let crypted = cipher.update(data, 'utf8', 'hex');
    crypted += cipher.final('hex');
    return crypted;
}

export function aesDecrypt(encrypted, key) {
    let decipher = crypto.createDecipher('aes192', key);
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
}

        上面分别对应加密和解密两个方法,key就是密码。

五、新建登录页面

        创建src/views/SignIn.js,里面的代码如下:

import React, {useState} from 'react';
import {makeStyles} from '@material-ui/core/styles';
import { Link } from "react-router-dom";
import {useSimpleSnackbar} from 'contexts/SimpleSnackbar.jsx';
import TextField from '@material-ui/core/TextField';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import LockIcon from '@material-ui/icons/LockOutlined';
import Typography from '@material-ui/core/Typography';
import FormControl from '@material-ui/core/FormControl';
import {ethers} from 'ethers';
import {useStorage} from 'contexts/StorageProvider'
import {useUpdateGlobal} from 'contexts/GlobalProvider.js'
import { withRouter } from "react-router";
import {aesDecrypt} from 'utils'

const useStyles = makeStyles(theme => ({
    avatar: {
        margin: theme.spacing(1),
        backgroundColor: theme.palette.secondary.main
    },
    form: {
        width: '100%', // Fix IE 11 issue.
        marginTop: theme.spacing(3),
        textAlign: 'center'
    },
    submit: {
        fontSize: 20,
        width: "50%",
        marginTop: theme.spacing(6)
    },
    import: {
        fontSize: 18,
        textDecoration:"none",
        color:"#f44336",
        margin: theme.spacing(6),
    },
    wallet: {
        textAlign: "center",
        marginTop: theme.spacing(3),
        fontSize: 20
    },
    container: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        margin: theme.spacing(3)
    }
}));

function SignIn({history}) {
    const classes = useStyles();
    const showSnackbar = useSimpleSnackbar()
    const [password, setPassword] = useState('')
    const storage = useStorage()
    const updateGlobal = useUpdateGlobal()

    const updatePassword = e => {
        let _password = e.target.value;
        setPassword(_password)
    };

    const onSubmit = e => {
        e.preventDefault();
        if(storage && storage.length > 0) {
            let _crypt = storage[0].crypt;
            try{
               let privateKey = aesDecrypt(_crypt,password)
               let wallet = new ethers.Wallet(privateKey)
               let options = {
                   isLogin:true,
                   wallet,
               }
               updateGlobal(options)
               history.push('/detail')
           }catch(err) {
               showSnackbar("密码错误",'error')
           }
        }
    }

    return (
        <div className={classes.container}>
            <Avatar className={classes.avatar}>
                <LockIcon/>
            </Avatar>
            <Typography  color='secondary' className={classes.wallet}>
                KHWallet,简单安全易用的
            </Typography>
            <Typography  color='secondary' className={classes.wallet}>
                以太坊钱包
            </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>
                <Button type='submit' variant="contained" color="primary" className={classes.submit}>
                    登录
                </Button>
            </form>
            <Link to="/import" className={classes.import}>重置密码/导入新账号</Link>
        </div>
    )
}

export default withRouter(SignIn);

        让我们着重来看这段代码:

const onSubmit = e => {
        e.preventDefault();
        if(storage && storage.length > 0) {
            let _crypt = storage[0].crypt;
            try{
               let privateKey = aesDecrypt(_crypt,password)
               let wallet = new ethers.Wallet(privateKey)
               let options = {
                   isLogin:true,
                   wallet,
               }
               updateGlobal(options)
               history.push('/detail')
           }catch(err) {
               showSnackbar("密码错误",'error')
           }
        }
    }

        从代码中可以看到,我们使用了ethers这个库通过解密后的私钥创建了一个钱包,并保存在内存中,钱包建立之后会跳转到钱包主界面/detail。让我们先安装它:

npm install ethers

        ethers是一个很棒的用来进行以太坊上钱包管理和各种交互的库,有了它就不需要web3.js了,它几乎能满足你关于以太坊的一切需求,所以向大家极力推荐。这里是它的文档:https://docs.ethers.io/ethers.js/html/,一定要多读几遍,多读几遍,多读几遍!

        让我们建立一个最简单的钱包主页面来完成这个逻辑,这个页面只显示钱包地址,新建src/views/WalletDetail.jsx,代码如下:

import React from 'react';
import {makeStyles} from '@material-ui/core/styles';
import {useGlobal} from 'contexts/GlobalProvider'

const useStyles = makeStyles(theme => ({
    container: {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        margin: theme.spacing(3)
    }
}));

function WalletDetail() {
    const classes = useStyles();
    let {wallet} = useGlobal();

    return (
        <div className={classes.container}>
            {wallet.address}
        </div>
    )
}

export default WalletDetail

在这里插入图片描述

六、集中处理路由

        随着开发内容的增多,我们需要集中处理路由。由于路由可以在客户端由用户直接访问,所以在路由导航前需要进行相应的判断,比如没有账号就不能使用登录界面,只能使用创建或者导入界面等。

        其实也可以不用路由而使用一个全局变量来控制显示哪个页面,但是我们主要是学习React和Material UI,所以就采用了一个复杂的方式使用路由来进行控制。

        新建src/layouts/Routes/index.js,代码如下:

import React,{Suspense,lazy} from 'react';
import {useStorage} from 'contexts/StorageProvider';
import {useGlobal} from 'contexts/GlobalProvider';
import {withRouter} from "react-router";
import { Route, Switch,Redirect } from "react-router-dom";

const ImportWallet = lazy(() => import('views/ImportWallet'));
const CreateWallet = lazy(() => import('views/CreateWallet'));
const SignInWallet = lazy(() => import('views/SignIn'));
const WalletDetail = lazy(() => import('views/WalletDetail'));

function SwitchRoute({history,path}) {
    history.push(path)
    return null
}

function Admin({history}) {
    const storage = useStorage()
    const {isLogin} = useGlobal()
    const hasAccount = storage && storage.length !== 0 ;

    if(!storage) {
        return null
    }
    return (
        <Suspense fallback ={null}>
            <Switch>
                <Route path="/import" component={ImportWallet} />
                <Route path="/create" >
                    {hasAccount ? <SwitchRoute history={history} path='/sign' /> : <CreateWallet /> }
                </Route>
                <Route path='/sign' >
                    {hasAccount ? <SignInWallet /> : <SwitchRoute history={history} path='/create' />}
                </Route>
                <Route path='/detail'>
                    {hasAccount
                        ? (isLogin ? <WalletDetail /> : <SwitchRoute history={history} path='/sign' />)
                        : <SwitchRoute history={history} path='/create' />}
                </Route>
                <Redirect from='/' to='/detail' />
            </Switch>
        </Suspense >
    )
}

export default withRouter(Admin)

可以看到,我们将页面的延迟导入和路由导航都放在这个页面进行统一处理,并且通过hasAccoutisLogin两个变量来控制路由导航。注意我们使用了一个很小的函数组件SwitchRoute来进行实际跳转。可能有人会问,为什么我们不直接将history.push(path)写在Route下面,而还要写一个新的函数组件,那是因为Route下面只能是节点,必须有渲染。

        接着我们再修改/src/views/Main.jsx,导入上面的文件:

import Routes from 'layouts/Routes';

        再修改路由导航处的代码:

<Router >
    <WalletBar/>
    <Switch>
        <Route path="/" component={Routes}/>
    </Switch>
</Router>

        该代码将所有访问转到Routes.js也就是我们上面的路由处理文件进行处理。

七、其它修改

        这次学习还有一些其它修改,比如网络选择按钮中代码的修改(因为现在没有Network.js了),这里就不再列出了。同时我们对导入钱包和新建钱包界面的密码输入框增加了个显/隐按钮,用来显示或者隐藏按钮,如下图所示:
在这里插入图片描述
注意:

        我在这里遇到了一点小问题,这里显/隐按钮的颜色是紫色,这是我自定义Theme中secondary的颜色。然而在Material UI标准theme下secondary应该显示红色才对。这里我并未使用自定义theme,并且把颜色定义往上移一层就可以正确显示了。为了突出问题,这里仍然把它显示成紫色(也顺眼一点)。这个问题有待进一步研究,有兴趣的读者可以等下看完整的代码,这里先放上代码片断。

<TextField id="key-password-input"
    label={isPrivateKey ? "私钥" : "助记词"}
    required
    type={showKey ? "text" : "password"}
    value={key}
    onChange={handleChange('key')}
    InputProps={{
        endAdornment:(
            <InputAdornment position="end">
                <IconButton
					//这里设置color颜色就是正确的
					//color='secondary'
                    aria-label="toggle key visibility"
                    onClick={handleChangeShow('showKey')}
                    onMouseDown={handleMouseDownPassword}
                >
                	//颜色在这里设置就不对
                    {showKey ? <Visibility color='secondary'/> : <VisibilityOff color='secondary'/>}
                </IconButton>
             </InputAdornment>
         )
    }
/>

        这个问题哪位读者知道症结所在,还请留言或者私信告知。

        由于篇幅限制,这里不再列出所有修改过的地方,比如钱包导入和钱包登录逻辑的实现,请大家下载源码后自己研究一下。

八、创建一个以太坊账号

        好了,我们以太坊钱包的第一阶段已经可用了。先将/src/.env.local中对应环境变量设置好,然后运行 npm start,你就会看到一个创建账号的界面了。如果提示缺少了某个库,请先npm install 库名来安装。
在这里插入图片描述
        还没有以太坊账号的小伙伴们还在等什么,马上输入你的密码创建一个吧。创建之后就会跳到还未开发的钱包主页面 -_- 。点击刷新之后,你就会看到创建页面变成登录页面了:
在这里插入图片描述
        输入你创建钱包(导入钱包)时的密码就可以登录了。

注意:

        需要注意的是:这个钱包的密钥加密保存在本地存储中,如果你清除了本地存储,也就把它清除掉了,所以你的账号就丢了,谁也无法找回了。以后我们会增加导出账号的功能,让你能对钱包私钥做一个备份,但是目前,还是别轻易清除本地存储才好。

        吐槽一下:CSDN将我们的登录信息也保存在本地存储中,如果你在写文章时为了测试钱包不小心清除了本地存储(或者其它别的原因),对不起,它会认为你没有登录。你再点击保存草稿或者发布文章都不可行,意味着你以前未保存的内容会丢失。不过,还是可以挽救的,你还可以点击导出按钮,将整个文章导出为一个MarkDown文件,这样重新登录后再点击导入,导入刚才的md文件即可。

下一次计划开发钱包主页面的部分功能。

本次学习完成后的码云地址: => https://gitee.com/TianCaoJiangLin/khwallet

退请大家留言指出错误或者提出改进意见

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
stable-diffusion-webui一个稳定的扩散网络用户界面,该项目的开发可以分为个主要步骤。 首先,我们需要搭建一个基本的Web界面框架。我们可以选择使用现成的Web开发框架,如React或Vue.js。这些框架提供了一套良好的组件化和状态管理机制,方便我们构建一个复杂而稳定的用户界面。我们需要使用HTML和CSS来设计和布局页面,并使用框架提供的组件和API来实现各种功能和交互。 接下来,我们需要连接界面与稳定的扩散网络后端。我们可以使用HTTP请求来与后端进行通信,获取和发送数据。为了保持稳定性,我们可以使用异步请求,以避免界面在等待响应时冻结。我们可以使用现有的HTTP库,如Axios或Fetch,来简化请求的处理。同时,我们还需要进行错误处理和数据验证,以确保数据的准确性和完整性。 最后,我们需要为界面添加一些功能和特性。这可能包括用户认证和授权,数据可视化和图表展示,以及与其他用户的实时交互和通信。我们可以使用现有的库和工具,如Chart.js和Socket.io,来实现这些功能。同时,我们还需要进行一些性能和安全性方面的优化,以确保界面的快速响应和数据的安全性。 总之,开发一个简单的stable-diffusion-webui需要搭建基本的Web界面框架,连接后端,并为界面添加功能和特性。通过以上步骤,我们可以实现一个稳定而功能丰富的扩散网络用户界面。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AiMateZero

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值