自学Vue开发Dapp去中心化钱包(三)

前言

本篇主要记录学习Vue并实际参与完结web3门户项目的经验和走过的弯路。拖了这么久才来还债,说项目忙那是借口,还是因为个人懒!从自学到实战Vue实际中间就1周的学习熟悉时间,学习不够深就会造成基础不稳,多次推翻重来的情况,从架子搭设到实际页面功能都存在这种情况,说来真是惭愧。最终,算是圆满完工吧。


一、项目框架

1.打包方式

vue新建项目打包方式分2种(其他的方式暂未学习):

1.使用webpack工具

学习时参照了bilibili教学老师的打包方式,也就是上篇文章(自学Vue开发Dapp去中心化钱包(二))介绍的,之后按照这个新建项目开始开发web3门户。

命令如下:

vue init webpack 项目名称

项目结构如下:

2.使用vue-cli工具

命令如下:

vue create 项目名称

项目结构如下:

总结:就学习而言,webpack打包方式新手比较适合,多数参数都能接触到,然对项目而言,再经过学习和调查后发现多数快速搭建大家用的是vue-cli工具。最终web3门户这个项目我使用了vue-cli这种打包方式的项目,结构很明朗。

2.vuex组件

store的结构上篇文章(自学Vue开发Dapp去中心化钱包(二))也介绍过,这里对文章中store的模块化重新做了优化,使其更符合“模块化”这个概念。

结构如下:

这里myStore,user,settings相当于3个不同的模块,存储3组不同的信息分别对应web3相关参数、用户相关参数、系统配置项相关参数。

注:Vuex持久化插件vuex-persistedstate这里主要是为了解决刷新后数据消失的问题,持久化缓存一些全局变量。使用时注意createPersistedState里面应该是模块的参数,比如myStore.account,是myStore模块下的参数account。

myStroe.js

import * as ethers from "ethers";
import {getWethAddress} from "@/config/contracts";
import {getEth_chainId} from "@/methods/common";

const state = {
    //provider对象
    provider: {},
    //合约对象
    contracts: {},
    //签名对象
    signer: {},
    //小狐狸钱包的账户address
    account: '',
    //以太坊网络ID:0x5
    net: '',
    //gas费,后续可能要用
    gasPrice: 0,
    //钱包余额
    balance: '0.0',
    //作为是否链接登录到小狐狸钱包的标志
    isConnectWallet: false,
    //绑卡列表数据,用于下拉框
    accountList: [],
    //交易计数,用于发生交易时同步刷新交易记录列表
    tradeCounter: 0,
}
const mutations = {
    saveProviderStore: (state, provider) => {
        state.provider = provider;
    },
    saveContractsStore: (state, contracts) => {
        state.contracts = contracts;
    },

    saveAccountStore: (state, account) => {
        state.account = account;
    },

    saveBalanceStore: (state, balance) => {
        state.balance = balance;
    },

    saveNetStore: (state, net) => {
        state.net = net;
    },

    saveGasPriceStore: (state, gasPrice) =>  {
        state.gasPrice = gasPrice;
    },
    saveIsConnectWallet: (state, isConnectWallet) =>  {
        state.isConnectWallet = isConnectWallet;
    },
    saveSigner: (state, signer) =>  {
        state.signer = signer;
    },
    saveAccountList: (state, accountList) =>  {
        state.accountList = accountList;
    },
    saveTradeCounter: (state, tradeCounter) =>  {
        state.tradeCounter = tradeCounter;
    },

}

const actions = {
    // 触发保存方法

    SET_PROVIDER: ({ commit }, payload) => {
        commit('saveProviderStore', payload);
    },
    SET_CONTRACTS: ({ commit }, payload) => {
        commit('saveContractsStore', payload);
    },
    SET_ACCOUNT: ({ commit }, payload) => {
        commit('saveAccountStore', payload);
    },
    SET_BALANCE: ({ commit }, payload) => {
        commit('saveBalanceStore', payload);
    },
    SET_NET: ({ commit }, payload) => {
        commit('saveNetStore', payload);
    },
    SET_GAS_PRICE: ({ commit }, payload) => {
        commit('saveGasPriceStore', payload);
    },

    SET_IS_CONNECT_WALLET: ({ commit }, payload) => {
        commit('saveIsConnectWallet', payload);
    },

    SET_SIGNER: ({ commit }, payload) => {
        commit('saveSigner', payload);
    },

    SET_ACCOUNT_LIST: ({ commit }, payload) => {
        commit('saveAccountList', payload);
    },
    SET_TRADE_COUNTER: ({ commit }, payload) => {
        commit('saveTradeCounter', payload);
    },

    async connectWallet({ dispatch }) {
        let web3Provider;
        if (window.ethereum) {
            web3Provider = window.ethereum;
            try {

                //通过
                const addressArray = await web3Provider.request({
                    method: "eth_requestAccounts",
                });

                let address = addressArray[0];
                const obj = {
                    status: "👆🏽 Write a message in the text-field above.",
                    address: address,
                };
                let chainId = await getEth_chainId();
                dispatch("setProvider",{address,chainId});
                dispatch("addWalletListener");
                return obj;
            } catch (err) {
                return {
                    address: "",
                    status: "😥 " + err.message,
                };
            }
        } else {
            return {
                address: "",
                status: (
                    <span>
          <p>
            {" "}
              🦊{" "}
              <a target="_blank" href={`https://metamask.io/download.html`}>
              You must install Metamask, a virtual Ethereum wallet, in your
              browser.
            </a>
          </p>
        </span>
                ),
            };
        }
    },

    async getCurrentWalletConnected ({ dispatch }) {
        let web3Provider;
        if (window.ethereum) {
            web3Provider = window.ethereum;
            try {
                const addressArray = await web3Provider.request({
                    method: "eth_accounts",
                });
                if (addressArray.length > 0) {
                    let address = addressArray[0];
                    //请求chain写在这里,防止beforeEach时参数还未放入store中
                    let chainId = await getEth_chainId();
                    //vuex dispatch多个参数时使用object对象传递
                    dispatch("setProvider",{address,chainId});
                    dispatch("addWalletListener");

                    return {
                        address: addressArray[0],
                        status: "👆🏽 Write a message in the text-field above.",
                    };
                } else {
                    return {
                        address: "",
                        status: "🦊 Connect to Metamask using the top right button.",
                    };
                }
            } catch (err) {
                return {
                    address: "",
                    status: "😥 " + err.message,
                };
            }
        } else {
            return {
                address: "",
                status: (
                    <span>
          <p>
            {" "}
              🦊{" "}
              <a target="_blank" href={`https://metamask.io/download.html`}>
              You must install Metamask, a virtual Ethereum wallet, in your
              browser.
            </a>
          </p>
        </span>
                ),
            };
        }
    },
    setProvider({commit},data) {
        let web3Provider;
        if (window.ethereum) {
            web3Provider = window.ethereum;
            const provider = new ethers.providers.Web3Provider(web3Provider);
            const signer = provider.getSigner();
            const contractABI = require("@/config/constants/contract-abi.json");
            const wethAddress = getWethAddress();
            const daiContract = new ethers.Contract(wethAddress, contractABI, provider);

            //先改变isConnectWallet值,后改变account值
            commit('saveNetStore', data.chainId);
            commit('saveIsConnectWallet', true);
            commit('saveAccountStore', data.address);
            commit('saveProviderStore', provider);
            commit('saveContractsStore', daiContract);
            commit('saveSigner', signer);
            //监听区块
            /*provider.on("block", (blockNumber) => {
              // Emitted on every block change
              console.log("blockNumber: " + blockNumber);
            })*/
        }
    },
    addWalletListener({commit}) {
        let web3Provider;
        if (window.ethereum) {
            web3Provider = window.ethereum;
            web3Provider.on('accountsChanged', accounts => {
                //断开链接后,初始化一些值
                if(accounts.length===0){
                    commit('saveIsConnectWallet', false);
                    commit('saveProviderStore', {});
                    commit('saveContractsStore', {});
                    commit('saveSigner', {});
                    commit('saveBalanceStore', '0.0');
                    commit('saveAccountList', []);
                }else{
                    //先改变isConnectWallet值,后改变account值
                    commit('saveIsConnectWallet', true);
                }

                commit('saveAccountStore', accounts[0]);
            });

            web3Provider.on('chainChanged', (chainId) => {
                commit('saveNetStore', chainId);
            });
        }
    },


}

export default {
    state,
    mutations,
    actions
}


getter.js

// 获取最终的状态信息
const getters = {
    provider: state => state.myStore.provider,
    contracts: state => state.myStore.contracts,
    signer: state => state.myStore.signer,
    account: state => state.myStore.account,
    net: state => state.myStore.net,
    gasPrice: state => state.myStore.gasPrice,
    isConnectWallet: state => state.myStore.isConnectWallet,
    accountList: state => state.myStore.accountList,
    tradeCounter: state => state.myStore.tradeCounter,
    token: state => state.user.token,
    avatar: state => state.user.avatar,
    name: state => state.user.name,
    mrspFlag: state => state.user.mrspFlag,
    roles: state => state.user.roles,
    permissions: state => state.user.permissions,
    defaultDecimalPalces: state => state.settings.defaultDecimalPalces,
    tokenName: state => state.settings.tokenName,
    legalTender: state => state.settings.legalTender,
    legalDecimalPalces: state => state.settings.legalDecimalPalces,
}
export default getters

index.js

import Vue from 'vue';
import Vuex from 'vuex';
import myStore from '@/store/modules/myStore';
import user from "@/store/modules/user";
import settings from '@/store/modules/settings';
import getters from '@/store/getters';
import createPersistedState from 'vuex-persistedstate';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    myStore,user,settings,
  },
  getters,
  plugins: [
    createPersistedState({
      paths: ['myStore.isConnectWallet', 'myStore.account', 'myStore.net']
    }),
  ],
});

export default store

二、实战经验

1.router

  1. 由于项目做了改版,存在多级子路由,这里路由路径要注意的是子路由带/和不带/是有区别的。

比如:

{
    path:'/home',
    meta: {authRequired: true},
    component: Home,
    children: [
        {path:'/', redirect: 'wallet'},
        {
            path:'wallet',
            component: Wallet,
            children: [
            {path:'/', redirect: 'balances'},
            {
                path:'balances',
                component:Balances

            },{
                path:'transfer',
                component: Transfer
            },{
                path:'swap',
                component: Swap
            },{
                path:'receive',
                component: Receive
            }]
        },
    ]
  }

如果这里的path:'balances'改为path:'/balances',子路由前面加/ ,加上/就不会拼接上父级路由的path路径,地址则为http://localhost:8080/#/balances,这样就造成点击菜单时没法联动,点击父菜单子菜单也不会切换。

完整的index.js

import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login'
import Home from '@/components/Home'
import Wallet from '@/components/pages/Wallet'
import Balances from "@/views/wallet/Balances";
import Transfer from "@/views/wallet/Transfer";
import Receive from "@/views/wallet/Receive";
import Swap from "@/views/wallet/Swap";
import Bridge from '@/components/pages/Bridge'
import Deposit from "@/views/bridge/Deposit";
import Withdraw from "@/views/bridge/Withdraw";
import Card from '@/components/pages/Card'
import Bongloy from "@/views/card/Bongloy";

Vue.use(Router);


const originalPush = Router.prototype.push
Router.prototype.push = function push (location) {
  return originalPush.call(this, location).catch(err => err)
}

let routes =[
  {path: "*", redirect: "/"},
  {
    path:'/',
    name:"login",
    component: Login,
  },
  {
    path:'/home',
    meta: {authRequired: true},
    component: Home,
    children: [
        {path:'/', redirect: 'wallet'},
        {
            path:'wallet',
            component: Wallet,
            children: [
            {path:'/', redirect: 'balances'},
            {
                path:'balances',
                component:Balances

            },{
                path:'transfer',
                component: Transfer
            },{
                path:'swap',
                component: Swap
            },{
                path:'receive',
                component: Receive
            }]
        },{
            path:'bridge',
            component: Bridge,
            children: [
            {path:'/', redirect: 'deposit'},
            {
                path:'deposit',
                component:Deposit

            },{
                path:'withdraw',
                component: Withdraw
            }]
        },{
            path:'card',
            component: Card,
            children: [
            {path:'/', redirect: 'bongloy'},
            {
                path:'bongloy',
                component:Bongloy

            }]
        }
    ]
  },
];

export default new Router({
    mode: 'history', // 去掉url中的#
    routes:routes
})

菜单跳转时path

<router-link to="balances">{{$t(item.navname)}}</router-link>

效果:

  1. router里面的meta: {authRequired: true} 这个authRequired参数是做拦截路由的,当请求的路由时,验证是否需要登录认证。

需要再main.js里增加如下代码:

//拦截路由,当请求的路由时,验证是否需要登录认证,并验证当前是否已连接小狐狸且网络是0x5通道,如果不是则进入登录页面;
//authRequired是router中自定义的参数
router.beforeEach((to, from, next) => {
  if (to.matched.some(res => res.meta.authRequired)) { // 验证是否需要登陆
    if (store.getters.account&&store.getters.net===getChainId()) { // 查询本地存储信息是否已经登陆且通道正确
      next();
    } else {
      //未登录则跳转至login页面
      next({path: '/', });
    }
  } else {
    next();
  }
})

效果如下

2.父子方法调用

  1. 父页面调用子页面方法用this.$refs

父页面

...
<--引入的子页面-->
<my-temp-page ref="myTempPageRef" >
...

methods:{
    initEdit(row){
      this.$refs.myTempPageRef.handleUpdate(row);
    },
},

子页面

myTempPage.vue

methods:{
    handleUpdate(){
      //TODO dosomething
    },
  },

  1. 子页面调用父页面方法用this.$emit()

父页面

...
<Success  @toBack="onNotifyBack"/>
...
methods:{
    onNotifyBack(){
      //dosomething
    },
  },

子页面

success.vue

...
<button @click="toBack" class="reset-button" variant="outlined" data-testid="transaction-receipt-reset">{{ $t('lang.swap41') }}</button>
...
methods:{
    toBack(){
      this.$emit("toBack");
    },
  },

3.store的使用

  1. 页面使用语法糖获取store属性

computed: {
      ...mapState({
        balance: state => state.myStore.balance,
        address: state => state.myStore.account,
      }),
    },

...mapState是语法糖。

取值时注意不能是state.account,因为vuex结构修改成多个模块,取值时要加上定义的模块,比如state.myStore.account、state.user.email等等

  1. 页面对store属性变更

这时这里的SET_TRADE_COUNTER方法名前不加模块名

this.$store.dispatch('SET_TRADE_COUNTER', this.tradeCounter+1);
  1. 页面调用store定义的方法

同样的方法名前不加模块名

this.$store.dispatch('connectWallet').then((res) => {
  //TODO
});
  1. 在user(其他)模块中使用另外一个模块myStore里的方法

使用dispatch,参数中增加{root: true}

user.js
...
methodName({ dispatch }) {
    ...
    commit('SET_EMAIL', res.data.email)//调用自己模块更新属性方法
    dispatch('SET_ACCOUNT', 参数值,{root: true});//调用myStore里的更新account属性的方法
}

4.监听数据变化

vue监听某个值变化使用watch。

如下是监听store某个属性的变化,需是有变化时才会监听到。

computed: {
    storeTradeCounter(){
      return this.$store.getters.tradeCounter;//获取属性
    }
  },

...

watch:{
    //监听有交易发生时,刷新列表
    storeTradeCounter (newValue,oldValue) {

      //交易发送时试试修改store里的绑卡余额及钱包余额
      //dosomething
    },

  },

5.input框监听

监听输入框只能输入2位小数的数字,其他均无法输入

...
<input v-model="formData.amount" type="text" name="amount" placeholder="0.00"
                 @input="handleAmountInput(formData.amount)">
...

methods:{
    handleAmountInput(value) {
        //大于等于0,且只能输入2位小数
        let val=value.replace(/^\D*([0-9]\d*\.?\d{0,6})?.*$/,'$1');
        if(val==null||val==undefined||val==''){
          val=''
        }
        this.formData.amount = val;
      },
}

6.vue生成二维码

  1. 引入vue-qr

npm install vue-qr --save
  1. 使用

import VueQr from 'vue-qr'
...
components:{
    VueQr,
  },
...
<vue-qr
          :text="this.account"
          :size="148"
          logoSrc=""
          :logoScale="0.2">
      </vue-qr>

7.小狐狸3d logo

  1. 下载小狐狸钱包3d logo资源

本人在github上和其他网站均找了许久,最后融合到项目整了几次,总算总结出来具体哪些文件可用,并且好用的。资源如下:

Metamask小狐狸3d Logo

  1. 代码中使用

将metamask-logo放入utils下,
package.json文件中引入这2个

"gl-mat4": "1.1.4",
"gl-vec3": "1.0.3"

然后npm install

使用

...
<div id="logo-container" class="meta-mask-fox mr-2 h-10 w-auto md:h-16" ></div>
...

data(){
    return {
      viewer: null,
    }
  },

mounted () {
    //加載3D小狐狸logo
    const ModelViewer = require('@/utils/metamask-logo');
    this.viewer = ModelViewer({
      // Dictates whether width & height are px or multiplied
      pxNotRatio: true,
      width: 60,
      height: 60,
      // To make the face follow the mouse.
      followMouse: true,
      // head should slowly drift (overrides lookAt)
      slowDrift: false,
    });

    var container = document.getElementById('logo-container');
    container.appendChild(this.viewer.container);

  },
destroyed() {
    if(this.viewer!==null){
      this.viewer.setFollowMouse(true);
      this.viewer.stopAnimation();
    }
  },
  1. 效果

三、记录用到的方

1.金额格式化(千分位)

效果是:9775格式化为9,775.500000

/**
 * @description 格式化金额
 * @param number:要格式化的数字
 * @param decimals:保留几位小数 默认0位
 * @param decPoint:小数点符号 默认.
 * @param thousandsSep:千分位符号 默认为,
 */
export const formatMoney = (number, decimals = 0, decPoint = '.', thousandsSep = ',') => {
    number = (number + '').replace(/[^0-9+-Ee.]/g, '')
    let n = !isFinite(+number) ? 0 : +number
    let prec = !isFinite(+decimals) ? 0 : Math.abs(decimals)
    let sep = (typeof thousandsSep === 'undefined') ? ',' : thousandsSep
    let dec = (typeof decPoint === 'undefined') ? '.' : decPoint
    let s = ''
    let toFixedFix = function (n, prec) {
        let k = Math.pow(10, prec)
        return '' + Math.ceil(n * k) / k
    }
    s = (prec ? toFixedFix(n, prec) : '' + Math.round(n)).split('.')
    let re = /(-?\d+)(\d{3})/
    while (re.test(s[0])) {
        s[0] = s[0].replace(re, '$1' + sep + '$2')
    }
    if ((s[1] || '').length < prec) {
        s[1] = s[1] || ''
        s[1] += new Array(prec - s[1].length + 1).join('0')
    }
    return s.join(dec)
}

//去除千分位中的‘,'
export const delcommafy = function (num) {
    if (!num) return num;
    num = num.toString();
    num = num.replace(/,/gi, "");
    return Number(num);
};

使用方法

import {formatMoney} from "@/utils/fixednumber";
...
formatMoney(‘9775’, 6);//格式化成小数点6位带千分位的货币金额9,775.500000
...

2.校验

// utils.js

// 全局函数
export function validateMobile(str) {
    // 检查手机号码格式
    return /^((13[0-9])|(14[5-9])|(15([0-3]|[5-9]))|(16[6-7])|(17[1-8])|(18[0-9])|(19[1|3])|(19[5|6])|(19[8|9]))\d{8}$/.test(
        str,
    );
}

export function validateEmail(str) {
    // 检查邮箱格式
    return /^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/.test(str);
}

export function validateMoney(str) {
    // 检查金额格式
    return /^([1-9]\d*(\.\d{1,2})?|([0](\.([0][1-9]|[1-9]\d{0,1}))))$/.test(str);
}

export function validateBonMoney(str) {
    // 检查金额格式
    return /^([1-9]\d*(\.\d{1,6})?|([0](\.([0][1-9]|[1-9]\d{0,1}))))$/.test(str);
}

export function validatePhone(str) {
    // 检查电话格式
    return /^(0\d{2,4}-)?\d{8}$/.test(str);
}

export function validateQQ(str) {
    // 检查QQ格式
    return /^[1-9][0-9]{4,}$/.test(str);
}

// 检查验证码格式
export function validateSmsCode(str) {
    return /^\d4$/.test(str);
}
// 校验 URL
export function validURL(url) {
    const reg =
        /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
    return reg.test(url)
}

// 校验特殊字符
export function specialCharacter(str) {
    const reg = new RegExp(
        // eslint-disable-next-line quotes
        "[`~!@#$^&*()=|{}':;',\\[\\]<>《》/?~!@#¥……&*()——|{}【】‘;:”“'。,、? ]"
    )
    return reg.test(str)
}

/**
 * @param value
 * 测试密码是否满足条件,包括四种类型
 * 密码6-20位,必须包含大写字母,小写字母,数字及特殊字符
 */
export function validPassword(value) {
    const num = /^.*[0-9]+.*/
    const low = /^.*[a-z]+.*/
    const up = /^.*[A-Z]+.*/
    const spe = /^.*[^a-zA-Z0-9]+.*/
    const passLength = value.length > 5 && value.length < 21
    return num.test(value) && low.test(value) && up.test(value) && spe.test(value) && passLength
}

3.复制到粘贴板

export function copyToClipboard(content) {
    if (window.clipboardData) {
        window.clipboardData.setData('text', content);
    } else {
        (function (content) {
            document.oncopy = function (e) {
                e.clipboardData.setData('text', content);
                e.preventDefault();
                document.oncopy = null;
            }
        })(content);
        document.execCommand('Copy');
    }
};

四、待继续整理

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值