第一章 项目框架搭建
yarn create @vitejs/app react-vite-h5 --template react
yarn
yarn run dev

为了在项目过程中不出现千奇百怪问题,这里贴出项目的包版本。

{"name": "react-vite-h5","private": true,"version": "0.0.0","scripts": {"dev": "vite","build": "vite build","preview": "vite preview"},"dependencies": {"axios": "^0.26.1","classnames": "^2.3.1","dayjs": "^1.11.0","global": "^4.4.0","lib-flexible": "0.3.2","pm2": "^5.2.0","postcss-pxtorem": "6.0.0","pushstate-server": "^3.1.0","query-string": "^7.1.1","rc-form": "^2.4.12","react": "^17.0.2","react-captcha-code": "^1.0.7","react-dom": "^17.0.2","react-router-dom": "5.2.0","zarm": "^2.9.13"},"devDependencies": {"@vitejs/plugin-react": "^1.0.7","@vitejs/plugin-react-refresh": "^1.3.6","less": "4.1.1","vite": "^2.8.0","vite-plugin-style-import": "0.10.1"}
}
  1. 引入路由插件react-router-dom
yarn add react-router-dom@5 -S
cd src
mkdir container && cd container
mkdir Index About
cd Index && touch index.jsx
import React from "react";const Index = () => {return <div><h1>Index</h1></div>
}export default Index;
cd About && touch index.jsx
import React from "react";const About = () => {return <div><h1>About</h1></div>
}export default About;
cd src && mkdir router && touch index.jsx
import Index from '../container/Index'
import About from '../container/About'const routes = [{path: "/",component: Index},{path: "/about",component: About}
];export default routes;
vim App.jsx
import React, { useState } from 'react'
import {BrowserRouter as Router,Switch,Route
} from "react-router-dom"
import routes from '../src/router'
function App() {return <Router><Switch>{routes.map(route => <Route exact key={route.path} path={route.path}><route.component /></Route>)}</Switch></Router>
}export default App

启动项目 npm run dev

  1. 引入Zarm UI组件库
yarn add zarm -S
vim App.jsx
  import React, { useState } from 'react'import {BrowserRouter as Router,Switch,Route} from "react-router-dom"+ import { ConfigProvider } from 'zarm'
+ import zhCN from 'zarm/lib/config-provider/locale/zh_CN'
+ import 'zarm/dist/zarm.css'import routes from '../src/router'function App() {return <Router>
+     <ConfigProvider locale={zhCN} primaryColor={'#007fff'}><Switch>{routes.map(route => <Route exact key={route.path} path={route.path}><route.component /></Route>)}</Switch>
+     </ConfigProvider></Router>}export default App

此时 zarm 的样式,已经全局引入了,我们先查看在 /container/Index/index.jsx 添加一个按钮是否生效:

  1. 使用less
yarn add less -D
vim vite.config.js
{plugins: [...]css: {modules: {localsConvention: 'dashesOnly'},preprocessorOptions: {less: {// 支持内联 JavaScriptjavascriptEnabled: true,}}},
}
  1. 移动端项目适配rem
yarn add lib-flexible@0.3.2 -S
vim main.jsx
  import React from 'react'import ReactDOM from 'react-dom'
+ import 'lib-flexible/flexible'import './index.css'import App from './App'ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'))
yarn add postcss-pxtorem@6.0.0
cd / && vim postcss.config.js
// postcss.config.js
// 用 vite 创建项目,配置 postcss 需要使用 post.config.js,之前使用的 .postcssrc.js 已经被抛弃
// 具体配置可以去 postcss-pxtorem 仓库看看文档
module.exports = {"plugins": [require("postcss-pxtorem")({rootValue: 37.5,propList: ['*'],selectorBlackList: ['.norem'] // 过滤掉.norem-开头的class,不进行rem转换})]
}
  1. 按需引入css
yarn add vite-plugin-style-import@0.10.1 -D
vim vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import styleImport from 'vite-plugin-style-import'// https://vitejs.dev/config/
export default defineConfig({plugins: [react(),styleImport({libs:[{libraryName:'zarm',esModule:true,resolveStyle:(name)=>{return `zarm/es/${name}/style/css`;}}]})],css: {modules: {localsConvention: 'dashesOnly'},preprocessorOptions: {less: {// 支持内联 JavaScriptjavascriptEnabled: true,}}}
})
yarn run build

  1. 二次封装axios
后端接口:http://dualseason.com:7001
yarn add axios -S
cd src && mkdir utils && vim axios.js
import axios from 'axios'
import { Toast } from 'zarm'const MODE = import.meta.env.MODE // 环境变量axios.defaults.baseURL = 'http://dualseason.com:7001'
axios.defaults.withCredentials = true
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
axios.defaults.headers['Authorization'] = `${localStorage.getItem('token') || null}`
axios.defaults.headers.post['Content-Type'] = 'application/json'axios.interceptors.response.use(res => {if (typeof res.data !== 'object') {Toast.show('服务端异常!')return Promise.reject(res)}if (res.data.code != 200) {if (res.data.msg) Toast.show(res.data.msg)if (res.data.code == 401) {window.location.href = '/login'}return Promise.reject(res.data)}return res.data
})export default axios;

代码解释:

const MODE = import.meta.env.MODE

MODE 是一个环境变量,通过 Vite 构建的项目中,环境变量在项目中,可以通过 import.meta.env.MODE 获取,环境变量的作用就是判断当前代码运行在开发环境还是生产环境。

axios.defaults.baseURL = 'http://dualseason.com:7001'

baseURLaxios 的配置项,它的作用就是设置请求的基础路径,后续我们会在项目实战中有所体现。配置基础路径的好处就是,当请求地址修改的时候,可以在此统一配置。

axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
axios.defaults.headers['Authorization'] = `${localStorage.getItem('token') || null}`
axios.defaults.headers.post['Content-Type'] = 'application/json'

上述三个配置是用于请求头的设置,Authorization 是我们在服务端鉴权的时候用到的,我们在前端设置好 token,服务端通过获取请求头中的 token 去验证每一次请求是否合法。最后一行是配置 post 请求是,使用的请求体,这里默认设置成 application/json 的形式。

axios.interceptors.response.use(res => {if (typeof res.data !== 'object') {Toast.show('服务端异常!')return Promise.reject(res)}if (res.data.code != 200) {if (res.data.msg) Toast.show(res.data.msg)if (res.data.code == 401) {window.location.href = '/login'}return Promise.reject(res.data)}return res.data
})

interceptors 为拦截器,拦截器的作用是帮你拦截每一次请求,你可以在回调函数中做一些“手脚”,再将数据 return 回去。上述代码就是拦截了响应内容,统一判断请求内容,如果非 200,则提示错误信息,401 的话,就是没有登录的用户,默认跳到 /login 页面。如果是正常的响应,则 retrun res.data

最后我们将这个 axios 抛出,供页面组件请求使用。

cd src && mkdir utils && vim index.js
import axios from './axios'export const get = axios.get
export const post = axios.post
  1. 代理配置(项目不需要代理,后端已处理跨域)
vim vite.config.js
server: {proxy: {'/api': {// 当遇到 /api 路径时,将其转换成 target 的值target: 'http://api.chennick.wang/api/',changeOrigin: true,rewrite: path => path.replace(/^\/api/, '') // 将 /api 重写为空}}
}
  1. resolve.alias别名设置
vim vite.config.js
  import { defineConfig } from 'vite'import react from '@vitejs/plugin-react'import styleImport from 'vite-plugin-style-import'
+ import path from 'path'// https://vitejs.dev/config/export default defineConfig({plugins: [react(),styleImport({libs:[{libraryName:'zarm',esModule:true,resolveStyle:(name)=>{return `zarm/es/${name}/style/css`;}}]})],css: {modules: {localsConvention: 'dashesOnly'},preprocessorOptions: {less: {// 支持内联 JavaScriptjavascriptEnabled: true,}}},
+   resolve: {+     alias: {+       '@': path.resolve(__dirname, 'src'), // src 路径
+       'utils': path.resolve(__dirname, 'src/utils') // src 路径
+     }
+   }})

此时我们便可以修改之前的代码如下:

vim router/index.jsx
import Index from '@/container/Index'
import About from '@/container/About'
vim App.jsx
import routes from '@/router'

行文至此,我们的基础开发环境已经搭建完毕,涉及构建工具、前端框架、UI 组件库、HTTP 请求库、CSS 预加载器、跨域代理、移动端分辨率适配,这些知识都是一个合格的前端工程师应该具备的,所以请大家加油,将他们都通通拿下,下载代码。

第二章 底部导航栏实现
  1. 预期效果

cd src && mkdir components && cd components && mkdir NavBar && cd NavBar && touch index.jsx style.module.less

zarm文档这里要做底部导航栏最适合的就是TabBar(众安科技移动端组件库例子)

我们从架构的层面来看,这三个按钮分别对应跳转三个页面,分别是homedata以及user,所以我们应该先创建三个路由,不过创建路由前需要创建对应的组件,用于跳转。

cd src/container && mkdir Home && cd Home && touch index.jsx style.module.less
vim src/container/Data/index.jsx
import React from "react";const Data = () => {return <div><h1>Data</h1></div>
}export default Data;
cd src/container && mkdir Data && cd Data && touch index.jsx style.module.less
vim src/container/Home/index.jsx
import React from "react";const Home = () => {return <div><h1>Home</h1></div>
}export default Home;
cd src/container && mkdir User && cd User && touch index.jsx style.module.less
vim src/container/User/index.jsx
import React from "react";const User = () => {return <div><h1>User</h1></div>
}export default User;
vim src/router/index.js
- import Index from "@/container/Index";import About from "@/container/About";import Home from "@/container/Home";import Data from "@/container/Data";import User from "@/container/User";const router = [
-     {-         path: '/',
-         component: Index,
-     }, {path: '/about',component: About,}, {path: '/user',component: User,}, {path: '/data',component: Data,}, {path: '/',component: Home,}];export default router;

确保路由能够正常跳转后再进行其他操作,接下来我们对NavBar组件进行编写,编写前我们查看文档,先确保能够显示静态再做动态。

vim NarBar/index.jsx
import React from "react";
import { TabBar } from "zarm";const NavBar = () => {return <TabBar><TabBar.Item itemKey={'/'} title={'账单'}/><TabBar.Item itemKey={'/data'} title={'统计'}/><TabBar.Item itemKey={'/user'} title={'我的'}/></TabBar>
}export default NavBar;
vim App.jsx
  import React from "react";import { BrowserRouter as Router, Route, Switch } from "react-router-dom";import router from "@/router";import { ConfigProvider } from 'zarm'import zhCN from 'zarm/lib/config-provider/locale/zh_CN'import 'zarm/dist/zarm.css'
+ import NavBar from "@/components/NavBar";const App = () => {return <Router><ConfigProvider primaryColor={'#007fff'} locale={zhCN}><Switch>{router.map(route => <Route exact key={route.path} path={route.path}><route.component /></Route>)}</Switch></ConfigProvider>
+     <NavBar/></Router>;}export default App;

到此为止我们已经可以使用组件进行代码编写了,可以写一个静态页面了,代码我已经放在了gitee上,跑不起来这些代码的可以去获取,直接下载下来运行即可。

接下来我们要实现点击NavBar进行路由跳转,只需要在上一步基础上加上如下代码,如果你不能出现跳转可能是环境没配置好,我们要明确目标,环境只是为了写代码的,我们不必在环境上花太多时间,所以我建议直接下载代码直接使用即可,接下来每一步我都会把对应代码放上来,出现问题先不要管,删掉自己的代码然后用我提供的代码进行替换即可,后面有基础后再自己搞。

vim NavBar/index.jsx
  import React from "react";import { TabBar } from "zarm";import { useHistory } from 'react-router-dom'const NavBar = () => {+     const history = useHistory();+     const changeTab = (path) => [
+         history.push(path)
+     ]+     return <TabBar onChange={changeTab}><TabBar.Item itemKey={'/'} title={'账单'} /><TabBar.Item itemKey={'/data'} title={'统计'} /><TabBar.Item itemKey={'/user'} title={'我的'} /></TabBar>}export default NavBar;

useHistory()react-router-dom提供的一个路由组件,可以使用push()方法进行路由插入history中。

接下来我们实现功能:当点击NavBar时,我们的蓝色active会标亮选中。

  import React, { useState } from "react";import { TabBar } from "zarm";import { useHistory } from 'react-router-dom'const NavBar = () => {+     const [activeKey, setActiveKey] = useState("/");const history = useHistory();const changeTab = (path) => {+         setActiveKey(path)history.push(path)}return <TabBar activeKey={activeKey} onChange={changeTab}><TabBar.Item itemKey={'/'} title={'账单'} /><TabBar.Item itemKey={'/data'} title={'统计'} /><TabBar.Item itemKey={'/user'} title={'我的'} /></TabBar>}export default NavBar;
  1. 添加底部导航栏图标

cd src/components && mkdir CustomIcon && touch index.jsx
import { Icon } from "zarm";
// 官网:https://www.iconfont.cn/home/index
export default Icon.createFromIconfont('//at.alicdn.com/t/font_2236655_w1mpqp7n1ni.js');
  import React, { useState } from "react";import { TabBar } from "zarm";import { useHistory } from 'react-router-dom'
+ import CustomIcon from "@/components/CustomIcon";const NavBar = () => {const [activeKey, setActiveKey] = useState("/");const history = useHistory();const changeTab = (path) => {setActiveKey(path)history.push(path)}return <TabBar activeKey={activeKey} onChange={changeTab}>
+         <TabBar.Item itemKey={'/'} title={'账单'} icon={<CustomIcon type="zhangdan"/>}/>
+         <TabBar.Item itemKey={'/data'} title={'统计'} icon={<CustomIcon type="tongji"/>}/>
+         <TabBar.Item itemKey={'/user'} title={'我的'} icon={<CustomIcon type="wode"/>}/></TabBar>}export default NavBar;
  1. 底部导航栏的显示与隐藏

我们要做到动态隐藏显示NavBar可以使用文档里面提到的visible属性。

我们要实现的功能:当用户强行在地址栏输入我们不存在的路由时,我们不仅不显示页面的内容,还要把底部的NavBar给屏蔽掉。

如图,我们就没有/detail路径,也就是说当用户来到这个路径时会产生bug,只显示底部导航栏但没内容,这是不行的,而且以后我们还要做登陆页面也是没有NavBar的,改!

vim App.jsx
import React, { useState, useEffect } from 'react'
import {BrowserRouter as Router,Switch,Route,useLocation
} from "react-router-dom"import { ConfigProvider } from 'zarm'import routes from '@/router'
import NavBar from '@/components/NavBar'
const App = () => {const location = useLocation() // 拿到 location 实例const { pathname } = location // 获取当前路径const needNav = ['/', '/data', '/user'] // 需要底部导航栏的路径const [showNav, setShowNav] = useState(false) // 是否展示 NavuseEffect(() => {setShowNav(needNav.includes(pathname))}, [pathname]) // [] 内的参数若是变化,便会执行上述回调函数return <Router><ConfigProvider primaryColor={'#007fff'}><Switch>{routes.map(route => <Route exact key={route.path} path={route.path}><route.component /></Route>)}</Switch></ConfigProvider><NavBar showNav={showNav}/></Router>
}export default App

我们按照修改思路进行编写代码,结果发现报错了,那问题出在哪呢?

我们来看报错信息Cannot read properties of undefined (reading 'location')

这是因为想要在函数组件内执行 useLocation,该组件必须被 Router 高阶组件包裹,我们做如下改动,将 App.jsxRouter 组件,前移到 main.jsx 内。

vim App.jsx
import React, { useState, useEffect } from 'react'
import {Switch,Route,useLocation
} from "react-router-dom"import { ConfigProvider } from 'zarm'import routes from '@/router'
import NavBar from '@/components/NavBar'
const App = () => {const location = useLocation() // 拿到 location 实例const { pathname } = location // 获取当前路径const needNav = ['/', '/data', '/user'] // 需要底部导航栏的路径const [showNav, setShowNav] = useState(false) // 是否展示 NavuseEffect(() => {setShowNav(needNav.includes(pathname))}, [pathname]) // [] 内的参数若是变化,便会执行上述回调函数return <><ConfigProvider primaryColor={'#007fff'}><Switch>{routes.map(route => <Route exact key={route.path} path={route.path}><route.component /></Route>)}</Switch></ConfigProvider><NavBar showNav={showNav}/></>
}export default App
vim main.jsx
import React from 'react'
import ReactDOM from 'react-dom'
import 'lib-flexible/flexible'
import './index.css'
import App from './App'
import { BrowserRouter as Router } from 'react-router-dom'ReactDOM.render(<React.StrictMode><Router><App /></Router></React.StrictMode>,document.getElementById('root')
)
vim NavBar/index.jsx
import React, { useState } from "react";
import { TabBar } from "zarm";
import { useHistory } from 'react-router-dom'
import CustomIcon from "@/components/CustomIcon";
const NavBar = ({showNav}) => {const [activeKey, setActiveKey] = useState("/");const history = useHistory();const changeTab = (path) => {setActiveKey(path)history.push(path)}return <TabBar activeKey={activeKey} onChange={changeTab} visible={showNav}><TabBar.Item itemKey={'/'} title={'账单'} icon={<CustomIcon type="zhangdan"/>}/><TabBar.Item itemKey={'/data'} title={'统计'} icon={<CustomIcon type="tongji"/>}/><TabBar.Item itemKey={'/user'} title={'我的'} icon={<CustomIcon type="wode"/>}/></TabBar>
}export default NavBar;

这里说明一下传入参数showNav为什么要加一个{},这是ES6语法中的结构,App.jsx<NavBar showNav={showNav}/>这里的showNav={}传给NavBar组件的其实是一个props,所以原本的写法是:

const NavBar = (props) = >{const {showNav} = props...
}

这里我们使用简写方式,能偷懒就不多写任何东西。

导航栏可以用在很多地方,映射到 PC 网页就是左侧侧边导航,道理都是相通的。移动端放在下面控制,PC 端放在左边或者右边控制罢了。所以再次强调不要学完了一个知识点,就思维定势地认为只能用在某一个需求上,能做到融会贯通,才是判断一个好程序员的标准,下载代码。

第三章 登陆注册页面

cd src/container && mkdir Login && cd Login && touch index.jsx style.module.less
vim Login/index.jsx
import React from 'react';const Login = () => {return <div><h1>Login</h1></div>
}export default Login;
vim router/index.js
  import Data from '@/container/Data'import Home from '@/container/Home'import User from '@/container/User'
+ import Login from '@/container/Login'const routes = [{path: "/",component: Home},{path: "/user",component: User},{path: "/data",component: Data},
+   {+     path:'/login',
+     component: Login
+   }];export default routes

通过上面的学习我们可以得出一个开发顺序的思路:先路由后页面,先静态后动态。首先要确保这个页面可以正常跳转了,能够出来基础页面了,最后我们才去对页面进行丰富以及美化,否则做了一堆结果页面没出来,一切都是无效的。

vim Login/index.jsx
import React from 'react'
import { Button, Cell, Checkbox, Input } from 'zarm';
import CustomIcon from '@/components/CustomIcon';const Login = () => {return <div><div /><div><span>注册</span></div><div><Cell icon={<CustomIcon type={'zhanghao'} />}><Input clearable type='text' placeholder='账号'/></Cell><Cell icon={<CustomIcon type={'mima'} />}><Input clearable type='text' placeholder='密码'/></Cell><Cell icon={<CustomIcon type={'mima'} />}><Input clearable type='text' placeholder='验证码'/></Cell></div><div><div><Checkbox /><label>阅读并同意 <a>《掘掘记账本条款》</a> </label></div><Button block theme='primary'>注册</Button></div></div>
}export default Login;

上述代码中,关键部分是账号输入、密码输入、验证码输入,这三个输入框是需要获取数据作为接口的参数提交上去的。

很多时候,服务端没有开发好接口的时候,我们前端要做的任务就是先还原 UI 稿,把该切的页面都切出来,并且预留好需要给接口提交的数据交互,比如上述三个输入框。

接下来我们来让页面变好看:

vim ./style.module.less                              #以后样式部分的这行我就省略了
.auth {min-height: 100vh;background-image: linear-gradient(217deg, #6fb9f8, #3daaf85e, #49d3fc1a, #3fd3ff00);.head {height: 200px;background: url('//s.yezgea02.com/1616032174786/cryptocurrency.png') no-repeat center;background-size: 120%;border-bottom-left-radius: 12px;border-bottom-right-radius: 12px;img {width: 34px;margin: 15px 0 0 15px;}}.tab {color: #597fe7;padding: 30px 24px 10px 24px;> span {margin-right: 10px;font-size: 14px;font-weight: bold;&.avtive {font-size: 20px;border-bottom: 2PX solid #597fe7;padding-bottom: 6px;}}}.form {padding: 0 6px;:global {.za-cell {background-color: transparent;&::after {border-top: none;}}}}.operation {padding: 10px 24px 0 24px;.agree {display: flex;align-items: center;margin-bottom: 10px;label {margin-left: 10px;font-size: 14px;}}}
}

由于我们采用的是 CSS Module 的形式进行开发,也就是你在页面中声明的类名都会根据当前页面,打一个唯一的 hash 值,比如我们页面中声明的 className={s.form},最终在浏览器中显示的是这样的:

_form_1h5us_30是已经被编译过的样式,这样做的目的是避免和别的页面的样式重名,这是目前样式管理的一个诟病,当多人参与项目开发的时候,很难做到不污染全局样式名称,除非很小心的命名样式名称。

所以经过编译之后,想要修改 .form 下的 .za-cell,如下写法,将无法修改成功:

.form {.za-cell {color: red;}
}

原因是,上述写法,.za-cell 会被编译加上 hash,组件库 Zarm 内的 dom 类名还是叫 za-cell,如上图所示。所以为了不加 hash,就需要这样操作:

.form {:global {.za-cell {color: red;}}
}

这样 .za-cell 就不会被加上 hash,如上图的.za-cell所示。

接下来我们添加验证码:

yarn add react-captcha-code -S

由于代码太长加的又少,就贴图了。

我们给页面加上相应的逻辑,首先是账号、密码、验证码:

vim Login/index.jsx
import React, { useState, useCallback } from 'react'
import { Button, Cell, Checkbox, Input } from 'zarm';
import CustomIcon from '@/components/CustomIcon';
import Captcha from "react-captcha-code"
import s from './style.module.less'const Login = () => {const [captcha, setCaptcha] = useState(''); // 验证码变化后存储值const [username, setUsername] = useState(''); // 账号const [password, setPassword] = useState(''); // 密码const [verify, setVerify] = useState(''); // 验证码//  验证码变化,回调方法const handleChange = useCallback((captcha) => {setCaptcha(captcha)}, []);return <div className={s.auth}><div className={s.head} /><div className={s.tab}><span>登陆</span><span>注册</span></div><div className={s.form}><Cell icon={<CustomIcon type="zhanghao" />}><Inputclearabletype="text"placeholder="请输入账号"onChange={(value) => setUsername(value)}/></Cell><Cell icon={<CustomIcon type="mima" />}><Inputclearabletype="password"placeholder="请输入密码"onChange={(value) => setPassword(value)}/></Cell><Cell icon={<CustomIcon type="mima" />}><Inputclearabletype="text"placeholder="请输入验证码"onChange={(value) => setVerify(value)}/><Captcha charNum={4} onChange={handleChange} /></Cell></div><div className={s.operation}><div className={s.agree}><Checkbox /><label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label></div><Button block theme="primary">{'登录'}</Button></div></div>
}
export default Login;

到此为止,我们可以获取的参数有用户输入的账号、密码、验证码以及验证码图片上的验证码。

接下来我们该写一下注册方法了,前端只需要把参数传给后端即可注册:

import React, { useState, useCallback } from 'react'
import { Button, Cell, Checkbox, Input, Toast } from 'zarm';
import CustomIcon from '@/components/CustomIcon';
import Captcha from "react-captcha-code"
import s from './style.module.less'
import { post } from '@/utils'const Login = () => {const [captcha, setCaptcha] = useState(''); // 验证码变化后存储值const [username, setUsername] = useState(''); // 账号const [password, setPassword] = useState(''); // 密码const [verify, setVerify] = useState(''); // 验证码//  验证码变化,回调方法const handleChange = useCallback((captcha) => {setCaptcha(captcha)}, []);const onSubmit = async () => {if (!username) {Toast.show('请输入账号')return}if (!password) {Toast.show('请输入密码')return}if (!verify) {Toast.show('请输入验证码')return};if (verify != captcha) {Toast.show('验证码错误')return};try {const { data } = await post('/api/user/register', {username,password});Toast.show('注册成功');} catch (error) {Toast.show('系统错误');console.log(error);}};return <div className={s.auth}><div className={s.head} /><div className={s.tab}><span>登陆</span><span>注册</span></div><div className={s.form}><Cell icon={<CustomIcon type="zhanghao" />}><Inputclearabletype="text"placeholder="请输入账号"onChange={(value) => setUsername(value)}/></Cell><Cell icon={<CustomIcon type="mima" />}><Inputclearabletype="password"placeholder="请输入密码"onChange={(value) => setPassword(value)}/></Cell><Cell icon={<CustomIcon type="mima" />}><Inputclearabletype="text"placeholder="请输入验证码"onChange={(value) => setVerify(value)}/><Captcha charNum={4} onChange={handleChange} /></Cell></div><div className={s.operation}><div className={s.agree}><Checkbox /><label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label></div><Button block theme="primary" onClick={onSubmit}>{'登录'}</Button></div></div>
}
export default Login;

记得要修改两个地方:

vim src/utils/axios.js
import axios from 'axios'
import { Toast } from 'zarm'const MODE = import.meta.env.MODE // 环境变量axios.defaults.baseURL = MODE == 'development' ? '/api' : 'http://dualseason.com:7001'
axios.defaults.withCredentials = true
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
axios.defaults.headers['Authorization'] = `${localStorage.getItem('token') || null}`
axios.defaults.headers.post['Content-Type'] = 'application/json'axios.interceptors.response.use(res => {if (typeof res.data !== 'object') {Toast.show('服务端异常!')return Promise.reject(res)}if (res.data.code != 200) {if (res.data.msg) Toast.show(res.data.msg)if (res.data.code == 401) {window.location.href = '/login'}return Promise.reject(res.data)}return res.data
})export default axios
vim vite.config.js
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import styleImport from 'vite-plugin-style-import'
import path from 'path'// https://vitejs.dev/config/
export default defineConfig({plugins: [reactRefresh(),styleImport({libs: [{libraryName: 'zarm',esModule: true,resolveStyle: (name) => {return `zarm/es/${name}/style/css`;}}]})],css: {modules: {localsConvention: 'dashesOnly'},preprocessorOptions: {less: {// 支持内联 JavaScriptjavascriptEnabled: true,}}},resolve: {alias: {'@': path.resolve(__dirname, 'src'), // src 路径'utils': path.resolve(__dirname, 'src/utils') // src 路径}},server: {proxy: {'/api': {// 当遇到 /api 路径时,将其转换成 target 的值target: 'http://dualseason.com:7001',changeOrigin: true,rewrite: path => path.replace(/^\/api/, '') // 将 /api 重写为空}}}
})

上述代码中,因为我们使用的是 async await 做异步处理,所以需要通过 try catch 来捕获异步处理过程中出现的错误,如果使用 Promise 的回调函数,则无需使用 try catch,改动如下:

post('/api/user/register', {username,password
}).then(res => {// do something
})

我们可以使用&&来替代三元表达式中的选择显示功能:

{type === 'register' &&<div className={s.agree}><Checkbox /><label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label></div>
}
{type === 'register' ?<div className={s.agree}><Checkbox /><label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label></div>:null
}

接下来把所有代码补上去:

import React, { useState, useCallback } from 'react'
import { Button, Cell, Checkbox, Input, Toast } from 'zarm';
import CustomIcon from '@/components/CustomIcon';
import Captcha from "react-captcha-code"
import s from './style.module.less'
import { post } from '@/utils'
import cx from 'classnames'const Login = () => {const [captcha, setCaptcha] = useState(''); // 验证码变化后存储值const [username, setUsername] = useState(''); // 账号const [password, setPassword] = useState(''); // 密码const [verify, setVerify] = useState(''); // 验证码const [type, setType] = useState('login'); // 登录注册类型//  验证码变化,回调方法const handleChange = useCallback((captcha) => {setCaptcha(captcha)}, []);const onSubmit = async () => {if (!username) {Toast.show('请输入账号')return}if (!password) {Toast.show('请输入密码')return}try {// 判断是否是登录状态if (type == 'login') {// 执行登录接口,获取 tokenconst { data } = await post('/api/user/login', {username,password});// 将 token 写入 localStoragelocalStorage.setItem('token', data.token);} else {if (!verify) {Toast.show('请输入验证码')return};if (verify != captcha) {Toast.show('验证码错误')return};const { data } = await post('/api/user/register', {username,password});Toast.show('注册成功');// 注册成功,自动将 tab 切换到 login 状态setType('login');}} catch (error) {Toast.show('系统错误');}};return <div className={s.auth}><div className={s.head} /><div className={s.tab}><span className={cx({ [s.avtive]: type == 'login' })} onClick={() => setType('login')}>登录</span><span className={cx({ [s.avtive]: type == 'register' })} onClick={() => setType('register')}>注册</span></div><div className={s.form}><Cell icon={<CustomIcon type="zhanghao" />}><Inputclearabletype="text"placeholder="请输入账号"onChange={(value) => setUsername(value)}/></Cell><Cell icon={<CustomIcon type="mima" />}><Inputclearabletype="password"placeholder="请输入密码"onChange={(value) => setPassword(value)}/></Cell>{type === 'register' &&<Cell icon={<CustomIcon type="mima" />}><Inputclearabletype="text"placeholder="请输入验证码"onChange={(value) => setVerify(value)}/><Captcha charNum={4} onChange={handleChange} /></Cell>}</div><div className={s.operation}>{type === 'register' &&<div className={s.agree}><Checkbox /><label className="text-light">阅读并同意<a>《掘掘手札条款》</a></label></div>}<Button block theme="primary" onClick={onSubmit}>{type === 'register' ? '注册' : '登陆'}</Button></div></div>
}
export default Login;

登陆成功则会填入tokenlocalStorage里面。

useEffect(() => {document.title = type == 'login' ? '登录' : '注册';
}, [type])

我们再完善一下代码,让它更像项目一点,我们让登录成功后跳转到首页。

import {useHistory} from 'react-router-dom'
const history = useHistory();
history.push('/')
// 或者
window.location.href = '/';

⏬代码

第四章 账单列表页

上一章节介绍的登录注册是整个项目的根基,没有拿到 token,将无法进行后续的各种操作,如账单的增删改查。所以务必将上一章节好好地阅读与揣摩,为后面的学习做好铺垫。我们直接进入本次前端实战项目的主题,账单的增删改查之列表页。

  1. 列表页编写

按照正常的开发流程,我们先将静态页面切出来,再填入数据使其动态化。在此之前,我们已经新建好了 Home 目录,该目录便是用于放置账单列表,所以我们直接在 Home/index.jsx新增代码。

头部统计实现

import React from 'react'
import { Icon } from 'zarm'
import s from './style.module.less'const Home = () => {return <div className={s.home}><div className={s.header}><div className={s.dataWrap}><span className={s.expense}>总支出:<b>¥ 200</b></span><span className={s.income}>总收入:<b>¥ 500</b></span></div><div className={s.typeWrap}><div className={s.left}><span className={s.title}>类型 <Icon className={s.arrow} type="arrow-bottom" /></span></div><div className={s.right}><span className={s.time}>2022-06<Icon className={s.arrow} type="arrow-bottom" /></span></div></div></div></div>
}export default Home;
.home {height: 100%;display: flex;flex-direction: column;padding-top: 80px;.header {position: fixed;top: 0;left: 0;display: flex;flex-direction: column;justify-content: space-between;width: 100%;height: 80px;background-color: #007fff;color: #fff;font-size: 14px;z-index: 100;padding: 10px;.data-wrap {font-size: 14px;>span {font-size: 12px;>b {font-size: 26px;font-family: DINCondensed-Bold, DINCondensed;margin-left: 4px;}}.income {margin-left: 10px;}}.type-wrap {display: flex;justify-content: flex-end;align-items: flex-end;>div {align-self: flex-start;background: rgba(0, 0, 0, 0.1);border-radius: 30px;padding: 3px 8px;font-size: 12px;}.left {margin-right: 6px;}.arrow {font-size: 12px;margin-left: 4px;}}}
}

本次项目全程采用的是 Flex 弹性布局,这种布局形式在当下的开发生产环境已经非常成熟,同学们如果还有不熟悉的,请实现对 Flex 布局做一个简单的学习,这边推荐一个学习网站。

列表页面实现

列表页面会用到 Zarm 组件库为我们提供的 Pull 组件,来实现下拉刷新以及无限滚动,我们先来将基础布局实现,如下所示:

import React,{useState} from 'react'
import { Icon } from 'zarm'
import s from './style.module.less'const Home = () => {const [list, setList] = useState([{bills: [{amount: "25.00",date: "1623390740000",id: 911,pay_type: 1,remark: "",type_id: 1,type_name: "餐饮"}],date: '2021-06-11'}]); // 账单列表return <div className={s.home}><div className={s.header}><div className={s.dataWrap}><span className={s.expense}>总支出:<b>¥ 200</b></span><span className={s.income}>总收入:<b>¥ 500</b></span></div><div className={s.typeWrap}><div className={s.left}><span className={s.title}>类型 <Icon className={s.arrow} type="arrow-bottom" /></span></div><div className={s.right}><span className={s.time}>2022-06<Icon className={s.arrow} type="arrow-bottom" /></span></div></div></div><div className={s.contentWrap}>{list.map((item, index) => <div>BillItem</div>)}</div></div>
}export default Home;

我们看到BillItem已经被渲染到页面上了,接下来我们来对BillItem进行渲染即可,我们来回顾一下效果。

{bills: [{amount: "25.00",date: "1623390740000",id: 911,pay_type: 1,remark: "",type_id: 1,type_name: "餐饮"}],date: '2021-06-11'
}

这是一个BillItem的数据结构,上面代码使用变量list来存储,也就是说list.map(item => console.log(item))就会打印出这样的结构出来:

import React, { useState } from 'react'
import { Cell, Icon } from 'zarm'
import s from './style.module.less'const Home = () => {const [list, setList] = useState([{bills: [{amount: "25.00",date: "1623390740000",id: 911,pay_type: 1,remark: "wod",type_id: 1,type_name: "餐饮"},{amount: "26.00",date: "1623390740000",id: 912,pay_type: 1,remark: "",type_id: 1,type_name: "餐饮"}],date: '2021-06-11'},{bills: [{amount: "25.00",date: "1623390740000",id: 913,pay_type: 1,remark: "",type_id: 1,type_name: "餐饮"},{amount: "26.00",date: "1623390740000",id: 914,pay_type: 2,remark: "",type_id: 1,type_name: "餐饮"}],date: '2021-06-12'}]); // 账单列表return <div className={s.home}><div className={s.header}><div className={s.dataWrap}><span className={s.expense}>总支出:<b>¥ 200</b></span><span className={s.income}>总收入:<b>¥ 500</b></span></div><div className={s.typeWrap}><div className={s.left}><span className={s.title}>类型 <Icon className={s.arrow} type="arrow-bottom" /></span></div><div className={s.right}><span className={s.time}>2022-06<Icon className={s.arrow} type="arrow-bottom" /></span></div></div></div><div className={s.contentWrap}>{list.map((item, index) => <div key={item.date}><div className={s.headerDate}><div className={s.date}>{item.date}</div><div className={s.money}><span><img src="//s.yezgea02.com/1615953405599/zhi%402x.png" alt='支' /><span>¥1</span></span><span><img src="//s.yezgea02.com/1615953405599/shou%402x.png" alt="收" /><span>¥2</span></span></div></div>{item.bills?.map(bill => <CellclassName={s.bill}key={bill.id}description={<span style={{ color: bill.pay_type == 2 ? 'red' : '#39be77' }}>{`${bill.pay_type == 1 ? '-' : '+'}${bill.amount}`}</span>}></Cell>)}</div>)}</div></div>
}export default Home;

我们现在要把每个账单项的时间显示出来,我们用到dayjs这个库。

yarn add dayjs -S
{item.bills?.map(bill =><Cell className={s.item} key={bill.id}description={<span style={{ color: bill.pay_type == 2 ? 'red' : '#39be77' }}>{`${bill.pay_type == 1 ? '-' : '+'}${bill.amount}`}</span>}help={<div>{dayjs(Number(bill.date)).format('HH:mm')}{bill.remark ? `| ${bill.remark}` : ''}</div>}></Cell>)
}

加上上面这段代码就是这个效果了,接下来我们把项目标题给补上。

vim utils/index.js
export const typeMap = {1: {icon: 'canyin'},2: {icon: 'fushi'},3: {icon: 'jiaotong'},4: {icon: 'riyong'},5: {icon: 'gouwu'},6: {icon: 'xuexi'},7: {icon: 'yiliao'},8: {icon: 'lvxing'},9: {icon: 'renqing'},10: {icon: 'qita'},11: {icon: 'gongzi'},12: {icon: 'jiangjin'},13: {icon: 'zhuanzhang'},14: {icon: 'licai'},15: {icon: 'tuikuang'},16: {icon: 'qita'}
}
title={<><CustomIconclassName={s.itemIcon}type={item.type_id ? typeMap[item.type_id].icon : 1}/><span>{ item.type_name }</span></>}

现在没差了,只需要补上一点样式即可很漂亮哒啦,不过先不急,我们给他抽离出来形成独立的组件先。

【掘掘记账本】前端React Hook,一步步详细版相关推荐

  1. 渗透测试中前端加密如何爆破(详细版)

    大佬轻喷,跟我一样菜鸟推荐看,如有其他交流心得或者带本人QQ827775983 文章目录 一.前端加密函数在哪? 二.搭建我们的加密字典 1.下载jsEncrypter压缩包 2.爆破加密弱口令 总结 ...

  2. Nginx部署VUE前端页面(图文解说详细版)

    文章目录 第一步,打包前端文件 第二步,上传打包好的文件到Nginx的安装目录下的html 第三步,自定义配置 第四步,配置Nginx代理 第五步,启动Nginx 第六步,访问你服务器的地址 如果你还 ...

  3. 微信小程序wx.login()获取openid,附:前端+后端代码(超详细版)

    微信小程序开放了微信登录的api,无论是个人还是企业申请的小程序均可使用. 首先创建一个项目,把这些代码都清空,我们自己写! 然后,开始写了! 首先index.wxml,写一个button用于发起登录 ...

  4. React Hook + Typescript,实现高颜值在线记账本

    React 已经是 JavaScript 生态系统中最受欢迎的前端框架之一.尽管人们已经对它赞不绝口,但 React 团队仍然在努力让它变得更好. 在 2018 ReactConf 大会上,React ...

  5. 源码解析 React Hook 构建过程

    2018 年的 React Conf 上 Dan Abramov 正式对外介绍了React Hook,这是一种让函数组件支持状态和其他 React 特性的全新方式,并被官方解读为这是下一个 5 年 R ...

  6. slqite3库查询数据处理方式_绝活!十一个优质React Hook库, 收藏备用

    本文字数:6539字 预计阅读时间:18分钟 建议阅读方式:收藏备用 温馨提示:最近全国大幅降温,注意防寒保暖,开心跨年 长按识别,后台回复 "电子书" 即可领取<JavaS ...

  7. 2021前端react面试题汇总

    2021前端react面试题汇总 React视频讲解 点击学习 全部视频:点击学习 (1)共同点 为了解决状态管理混乱,无法有效同步的问题统一维护管理应用状态; 某一状态只有一个可信数据来源(通常命名 ...

  8. 如何解决react hook的闭包陷阱以及避开闭包陷阱做优化

    前端框架应用hook一度成为趋势. 推出hook的框架,首当其冲就是大名鼎鼎的react. 但是很多时候hook的不正确使用,总会不自觉地掉入闭包陷阱. 首先我们了解一下hook的闭包陷阱是什么? 首 ...

  9. 【笔记-node】《Egg.js框架入门与实战》、《用 React+React Hook+Egg 造轮子 全栈开发旅游电商应用》

    20210226-20210227:<Egg.js框架入门与实战> 课程地址:https://www.imooc.com/learn/1185 第一章 课程导学 01-01 课程介绍 一. ...

最新文章

  1. 学习在Unity中创建一个动作RPG游戏
  2. 科研与爱情选谁?中科院教授教你平衡!
  3. #中regex的命名空间_Python空间分析||geopandas安装与基本使用
  4. LeetCode算法题14:递归和回溯2
  5. window下建立vue.js项目
  6. 4.6 大数据集-机器学习笔记-斯坦福吴恩达教授
  7. cmake 生成vc 项目文件
  8. python range函数for_Python的range函数与for循环语句
  9. spring Boot 学习(七、Spring Boot与开发热部署)
  10. selenium.common.exceptions.WebDriverException: Message: ‘chromedriver’解决
  11. 类似collect2: ld returned 1 exit status的错误
  12. textcnn文本词向量_文本分类模型之TextCNN
  13. 水滴石穿C语言之编译器引出的问题
  14. Mybatis解析mapper
  15. nodejs 图片处理模块 rotate_如何针对数据不平衡做处理?
  16. asp.net 页面缓存、数据缓存
  17. 自旋锁,偏向锁,轻量级锁 和 重量级锁
  18. 【docker】docker学习
  19. python绘制社会关系网络图_python画社交网络图
  20. 2009年的MACBOOK苹果电脑重装MAC OS 10.8.5系统

热门文章

  1. 开源代码分析技巧之四——国外技术社区提问
  2. 去黑头最正确的做法,肯定有效的哦
  3. NETWORK笔记7:思科命令实验
  4. 众创美业微信引流系统使用说明
  5. erdas空间建模_ERDAS空间建模工具介绍.ppt
  6. 如何快速提高英飞凌单片机编译器 TASKING TriCore Eclipse IDE 编译速度
  7. Linux打印一个文字logo
  8. 《python编程从入门到实践》读书笔记1
  9. 【提交】commit
  10. 《关爱码农成长计划》第一期报告