前言
本篇主要记录学习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: {},//小狐狸钱包的账户addressaccount: '',//以太坊网络ID:0x5net: '',//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: ({" "}{" "}You must install Metamask, a virtual Ethereum wallet, in yourbrowser.
),};}},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: ({" "}{" "}You must install Metamask, a virtual Ethereum wallet, in yourbrowser.
),};}},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 changeconsole.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
由于项目做了改版,存在多级子路由,这里路由路径要注意的是子路由带/和不带/是有区别的。
比如:
{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.pushRouter.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
{{$t(item.navname)}}
效果:
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.父子方法调用
父页面调用子页面方法用this.$refs
父页面
......methods:{initEdit(row){this.$refs.myTempPageRef.handleUpdate(row);},},
子页面
myTempPage.vuemethods:{handleUpdate(){//TODO dosomething},},
子页面调用父页面方法用this.$emit()
父页面
......methods:{onNotifyBack(){//dosomething},},
子页面
success.vue......methods:{toBack(){this.$emit("toBack");},},
3.store的使用
页面使用语法糖获取store属性
computed: {...mapState({balance: state => state.myStore.balance,address: state => state.myStore.account,}),},
…mapState是语法糖。
取值时注意不能是state.account,因为vuex结构修改成多个模块,取值时要加上定义的模块,比如state.myStore.account、state.user.email等等
页面对store属性变更
这时这里的SET_TRADE_COUNTER方法名前不加模块名
this.$store.dispatch('SET_TRADE_COUNTER', this.tradeCounter+1);
页面调用store定义的方法
同样的方法名前不加模块名
this.$store.dispatch('connectWallet').then((res) => {//TODO});
在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位小数的数字,其他均无法输入
......methods:{handleAmountInput(value) {//大于等于0,且只能输入2位小数let val=value.replace(/^\D*([0-9]\d*\." />6.vue生成二维码引入vue-qr
npm install vue-qr --save
使用
import VueQr from 'vue-qr'...components:{VueQr,},...
7.小狐狸3d logo
下载小狐狸钱包3d logo资源
本人在github上和其他网站均找了许久,最后融合到项目整了几次,总算总结出来具体哪些文件可用,并且好用的。资源如下:
Metamask小狐狸3d Logo
代码中使用
将metamask-logo放入utils下,package.json文件中引入这2个"gl-mat4": "1.1.4","gl-vec3": "1.0.3"然后npm install
使用
......data(){return {viewer: null,}},mounted () {//加載3D小狐狸logoconst ModelViewer = require('@/utils/metamask-logo');this.viewer = ModelViewer({// Dictates whether width & height are px or multipliedpxNotRatio: 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.金额格式化(千分位)
效果是: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) " />
使用方法
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);}// 校验 URLexport 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 < 21return 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');}};
四、待继续整理