在本教程中,我们将构建一个 React Calculator 应用程序。您将学习如何制作线框、设计布局、创建组件、更新状态和格式化输出。
规划
由于我们将构建一个计算器应用程序,让我们选择一个学习范围不太复杂但也不太基本的范围来涵盖创建应用程序的不同方面。
我们将实现的功能包括:
加、减、乘、除
支持十进制值
计算百分比
反转值
重置功能
格式化更大的数字
根据长度调整输出大小
首先,我们将绘制一个基本的线框来展示我们的想法。为此,您可以使用Figma【https://figma.com/】或Diagrams.net【https://diagrams.net/】等免费工具。
请注意,在此阶段,考虑颜色和样式并不重要。最重要的是您可以构建布局并确定所涉及的组件。
设计色彩
一旦我们处理了布局和组件,剩下要做的就是选择一个漂亮的配色方案来完成设计。
以下是使应用程序看起来很棒的一些准则:
包装纸应与背景形成对比
屏幕和按钮值应该易于阅读
等号按钮应该使用不同的颜色,以增加一些口音
根据上述标准,我们将使用如下所示的配色方案。
设置项目
首先,在您的项目文件夹中打开终端并使用create-react-app【https://create-react-app.dev/】创建一个样板模板。为此,请运行以下命令:
npx create-react-app calculator
这是在零配置的情况下设置完全可用的 React 应用程序的最快和最简单的方法。之后您需要做的就是cd calculator
切换到新创建的项目文件夹并npm start
在浏览器中启动您的应用程序。
如您所见,它带有一些默认样板,因此接下来我们将在项目文件夹树中进行一些清理。
找到src
应用程序逻辑所在的文件夹,并删除除App.js
创建应用程序、index.css
设置应用程序样式和index.js
在 DOM 中呈现应用程序之外的所有内容。
创建组件
由于我们已经完成了一些线框图,我们已经知道应用程序的主要构建块。这些都是Wrapper
,Screen
,ButtonBox
,和Button
。
首先在components
文件夹内创建一个文件src
夹。然后我们将为每个组件创建一个单独的.js
文件和.css
文件。
如果您不想手动创建这些文件夹和文件,您可以使用以下单行代码来快速设置:
cd src && mkdir components && cd components && touch Wrapper.js Wrapper.css Screen.js Screen.css ButtonBox.js ButtonBox.css Button.js Button.css
包装器
该Wrapper
组件将是框架,将所有子组件固定到位。它还可以让我们之后将整个应用程序居中。
包装器.js
import "./Wrapper.css";const Wrapper = ({ children }) => { return <div className="wrapper">{children}</div>;};export default Wrapper;
包装器.css
.wrapper { width: 340px; height: 540px; padding: 10px; border-radius: 10px; background-color: #485461; background-image: linear-gradient(315deg, #485461 0%, #28313b 74%);}
屏幕
该Screen
组件将是该组件的顶部子Wrapper
组件,其目的是显示计算出的值。
在功能列表中,我们包括显示输出调整长度,这意味着较长的值必须缩小尺寸。为此,我们将使用一个名为react-textfit【https://www.npmjs.com/package/react-textfit】的小型(3.4kb gzip)库。
要安装它,请运行npm i react-textfit
然后导入并使用它,如下所示。
屏幕.js
import { Textfit } from "react-textfit";import "./Screen.css";const Screen = ({ value }) => { return ( <Textfit className="screen" mode="single" max={70}> {value} </Textfit> );};export default Screen;
屏幕.css
.screen { height: 100px; width: 100%; margin-bottom: 10px; padding: 0 10px; background-color: #4357692d; border-radius: 10px; display: flex; align-items: center; justify-content: flex-end; color: white; font-weight: bold; box-sizing: border-box;}
按钮盒
的ButtonBox
组成部分,同样Wrapper
成分,将成为孩子们的框架-只是这一次的Button
部件。
按钮框.js
import "./ButtonBox.css";const ButtonBox = ({ children }) => { return <div className="buttonBox">{children}</div>;};export default ButtonBox;
按钮框.css
.buttonBox { width: 100%; height: calc(100% - 110px); display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(5, 1fr); grid-gap: 10px;}
按钮
该Button
组件将为应用程序提供交互性。每个组件都会有value
和onClick
道具。
在样式表中,我们还将包括equal
按钮的样式。稍后我们将使用Button
props 来访问类。
按钮.js
import "./Button.css";const Button = ({ className, value, onClick }) => { return ( <button className={className} onClick={onClick}> {value} </button> );};export default Button;
按钮.css
button { border: none; background-color: rgb(80, 60, 209); font-size: 24px; color: rgb(255, 255, 255); font-weight: bold; cursor: pointer; border-radius: 10px; outline: none;}button:hover { background-color: rgb(61, 43, 184);}.equals { grid-column: 3 / 5; background-color: rgb(243, 61, 29);}.equals:hover { background-color: rgb(228, 39, 15);}
渲染元素
在 React 应用程序中渲染的基本文件是index.js
. 在我们继续之前,请确保您的index.js
外观如下:
import React from "react";import ReactDOM from "react-dom";import App from "./App";import "./index.css";ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root"));
此外,让我们检查index.css
,并确保我们重新设置默认值padding
和margin
,并设置适当的规则,在视窗中居中的应用:
@import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap");* { margin: 0; padding: 0; font-family: "Montserrat", sans-serif;}body { height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #fbb034; background-image: linear-gradient(315deg, #fbb034 0%, #ffdd00 74%);}
最后,让我们打开主文件App.js
,并导入我们之前创建的所有组件:
import Wrapper from "./components/Wrapper";import Screen from "./components/Screen";import ButtonBox from "./components/ButtonBox";import Button from "./components/Button";const App = () => { return ( <Wrapper> <Screen value="0" /> <ButtonBox> <Button className="" value="0" onClick={() => { console.log("Button clicked!"); }} /> </ButtonBox> </Wrapper> );};export default App;
在上面的例子中,我们只渲染了一个Button
组件。
让我们为线框中的数据创建一个数组表示,这样我们就可以映射并渲染 中的所有按钮ButtonBox
:
import Wrapper from "./components/Wrapper";import Screen from "./components/Screen";import ButtonBox from "./components/ButtonBox";import Button from "./components/Button";const btnValues = [ ["C", "+-", "%", "/"], [7, 8, 9, "X"], [4, 5, 6, "-"], [1, 2, 3, "+"], [0, ".", "="],];const App = () => { return ( <Wrapper> <Screen value=0 /> <ButtonBox> { btnValues.flat().map((btn, i) => { return ( <Button key={i} className={btn === "=" ? "equals" : ""} value={btn} onClick={() => { console.log(`${btn} clicked!`); }} /> ); }) } </ButtonBox> </Wrapper> );};
检查您的终端并确保您的 React 应用程序仍在运行。如果没有,请运行npm start
以重新启动它。
打开浏览器。如果你继续,你当前的结果应该是这样的:
如果需要,您还可以打开浏览器的开发工具并测试每个按下按钮的日志值。
定义状态
接下来,我们将使用 ReactuseState
钩子声明状态变量。
具体来说,会有三种状态:num
,输入的值;sign
, 所选符号: 和res
, 计算值。
为了使用useState
钩子,我们必须首先将它导入App.js
:
import React, { useState } from "react";
在App
函数中,我们将使用一个对象一次设置所有状态:
import React, { useState } from "react";// ...const App = () => { let [calc, setCalc] = useState({ sign: "", num: 0, res: 0, }); return ( // ... );};
功能
我们的应用程序看起来不错,但没有任何功能。目前,它只能将按钮值输出到浏览器控制台。让我们解决这个问题!
我们将从Screen
组件开始。将以下条件逻辑设置为value
prop,以便显示输入的数字(如果输入数字)或计算结果(如果按下等于按钮)。
为此,我们将使用内置的 JS三元运算符,它基本上是if
语句的快捷方式,?
如果表达式为真,则接收一个表达式并在之后返回一个值,如果表达式为:
假,则在之后返回一个值:
<Screen value={calc.num ? calc.num : calc.res} />
现在让我们编辑Button
组件,以便它可以检测不同的按钮类型,并在按下特定按钮后执行分配的功能。使用下面的代码:
import React, { useState } from "react";// ...const App = () => { // ... return ( <Wrapper> <Screen value={calc.num ? calc.num : calc.res} /> <ButtonBox> {btnValues.flat().map((btn, i) => { return ( <Button key={i} className={btn === "=" ? "equals" : ""} value={btn} onClick={ btn === "C" ? resetClickHandler : btn === "+-" ? invertClickHandler : btn === "%" ? percentClickHandler : btn === "=" ? equalsClickHandler : btn === "/" || btn === "X" || btn === "-" || btn === "+" ? signClickHandler : btn === "." ? comaClickHandler : numClickHandler } /> ); })} </ButtonBox> </Wrapper> );};
现在我们已准备好创建所有必要的函数。
numClickHandler
numClickHandler
仅当按下任何数字按钮 (0–9) 时才会触发该功能。然后它获取 的值Button
并将其添加到当前num
值。
它还将确保:
没有整数以零开头
逗号前没有多个零
格式将为“0”。如果 ”。” 首先被按下
输入的数字最多为 16 个整数
import React, { useState } from "react";// ...const App = () => { // ... const numClickHandler = (e) => { e.preventDefault(); const value = e.target.innerHTML; if (calc.num.length < 16) { setCalc({ ...calc, num: calc.num === 0 && value === "0" ? "0" : calc.num % 1 === 0 ? Number(calc.num + value) : calc.num + value, res: !calc.sign ? 0 : calc.res, }); } }; return ( // ... );};
comaClickHandler
comaClickHandler
仅当按下昏迷值 ('.') 时才会触发该函数。它将彗差与当前num
值相加,使其成为十进制数。
它还将确保不可能有多个逗号。
// numClickHandler functionconst comaClickHandler = (e) => { e.preventDefault(); const value = e.target.innerHTML; setCalc({ ...calc, num: !calc.num.toString().includes(".") ? calc.num + value : calc.num, });};
标志点击处理程序
signClickHandler
当用户按下+、–、*或/时,该函数被触发。然后将特定值设置为对象中的当前sign
值calc
。
它还将确保对重复调用没有影响:
// comaClickHandler functionconst signClickHandler = (e) => { e.preventDefault(); const value = e.target.innerHTML; setCalc({ ...calc, sign: value, res: !calc.res && calc.num ? calc.num : calc.res, num: 0, });};
等于点击处理程序
该equalsClickHandler
函数计算按下等于按钮 ( = )时的结果。计算基于电流num
和res
值,以及sign
选定的(见math
函数)。
然后将返回值设置为新值res
以供进一步计算。
它还将确保:
对重复调用没有影响
用户不能除以 0
// signClickHandler functionconst equalsClickHandler = () => { if (calc.sign && calc.num) { const math = (a, b, sign) => sign === "+" ? a + b : sign === "-" ? a - b : sign === "X" ? a * b : a / b; setCalc({ ...calc, res: calc.num === "0" && calc.sign === "/" ? "Can't divide with 0" : math(Number(calc.res), Number(calc.num), calc.sign), sign: "", num: 0, }); }};
反转点击处理程序
该invertClickHandler
函数首先检查是否有任何输入值 ( num
) 或计算值 ( res
),然后通过乘以 -1 来反转它们:
// equalsClickHandler functionconst invertClickHandler = () => { setCalc({ ...calc, num: calc.num ? calc.num * -1 : 0, res: calc.res ? calc.res * -1 : 0, sign: "", });};
百分比点击处理程序
该percentClickHandler
函数检查是否有任何输入值 ( num
) 或计算值 ( res
),然后使用内置Math.pow
函数计算百分比,该函数将底数返回指数幂:
// invertClickHandler functionconst percentClickHandler = () => { let num = calc.num ? parseFloat(calc.num) : 0; let res = calc.res ? parseFloat(calc.res) : 0; setCalc({ ...calc, num: (num /= Math.pow(100, 1)), res: (res /= Math.pow(100, 1)), sign: "", });};
重置点击处理程序
该resetClickHandler
函数默认 的所有初始值calc
,返回calc
Calculator 应用程序首次呈现时的状态:
// percentClickHandler functionconst resetClickHandler = () => { setCalc({ ...calc, sign: "", num: 0, res: 0, });};
输入格式
在介绍中完成功能列表的最后一件事是实现值格式化。为此,我们可以使用Emissary【https://stackoverflow.com/users/1238344/emissary】发布的修改后的 Regex 字符串:
const toLocaleString = (num) => String(num).replace(/(?<!\..*)(\d)(?=(?:\d{3})+(?:\.|$))/g, "$1 ");
本质上它所做的是取一个数字,将其格式化为字符串格式并为千位标记创建空格分隔符。
如果我们反过来处理数字字符串,首先我们需要去除空格,以便稍后将其转换为数字。为此,您可以使用此功能:
const removeSpaces = (num) => num.toString().replace(/\s/g, "");
这是您应该包含这两个函数的代码:
import React, { useState } from "react";// ...const toLocaleString = (num) => String(num).replace(/(?<!\..*)(\d)(?=(?:\d{3})+(?:\.|$))/g, "$1 ");const removeSpaces = (num) => num.toString().replace(/\s/g, "");const App = () => { // ... return ( // ... );};
看看关于如何添加完整的代码下一节toLocaleString
,并removeSpaces
为处理程序功能Button
部件。
把它放在一起
如果你一直跟着,整个App.js
代码应该是这样的:
import React, { useState } from "react";import Wrapper from "./components/Wrapper";import Screen from "./components/Screen";import ButtonBox from "./components/ButtonBox";import Button from "./components/Button";const btnValues = [ ["C", "+-", "%", "/"], [7, 8, 9, "X"], [4, 5, 6, "-"], [1, 2, 3, "+"], [0, ".", "="],];const toLocaleString = (num) => String(num).replace(/(?<!\..*)(\d)(?=(?:\d{3})+(?:\.|$))/g, "$1 ");const removeSpaces = (num) => num.toString().replace(/\s/g, "");const App = () => { let [calc, setCalc] = useState({ sign: "", num: 0, res: 0, }); const numClickHandler = (e) => { e.preventDefault(); const value = e.target.innerHTML; if (removeSpaces(calc.num).length < 16) { setCalc({ ...calc, num: calc.num === 0 && value === "0" ? "0" : removeSpaces(calc.num) % 1 === 0 ? toLocaleString(Number(removeSpaces(calc.num + value))) : toLocaleString(calc.num + value), res: !calc.sign ? 0 : calc.res, }); } }; const comaClickHandler = (e) => { e.preventDefault(); const value = e.target.innerHTML; setCalc({ ...calc, num: !calc.num.toString().includes(".") ? calc.num + value : calc.num, }); }; const signClickHandler = (e) => { e.preventDefault(); const value = e.target.innerHTML; setCalc({ ...calc, sign: value, res: !calc.res && calc.num ? calc.num : calc.res, num: 0, }); }; const equalsClickHandler = () => { if (calc.sign && calc.num) { const math = (a, b, sign) => sign === "+" ? a + b : sign === "-" ? a - b : sign === "X" ? a * b : a / b; setCalc({ ...calc, res: calc.num === "0" && calc.sign === "/" ? "Can't divide with 0" : toLocaleString( math( Number(removeSpaces(calc.res)), Number(removeSpaces(calc.num)), calc.sign ) ), sign: "", num: 0, }); } }; const invertClickHandler = () => { setCalc({ ...calc, num: calc.num ? toLocaleString(removeSpaces(calc.num) * -1) : 0, res: calc.res ? toLocaleString(removeSpaces(calc.res) * -1) : 0, sign: "", }); }; const percentClickHandler = () => { let num = calc.num ? parseFloat(removeSpaces(calc.num)) : 0; let res = calc.res ? parseFloat(removeSpaces(calc.res)) : 0; setCalc({ ...calc, num: (num /= Math.pow(100, 1)), res: (res /= Math.pow(100, 1)), sign: "", }); }; const resetClickHandler = () => { setCalc({ ...calc, sign: "", num: 0, res: 0, }); }; return ( <Wrapper> <Screen value={calc.num ? calc.num : calc.res} /> <ButtonBox> {btnValues.flat().map((btn, i) => { return ( <Button key={i} className={btn === "=" ? "equals" : ""} value={btn} onClick={ btn === "C" ? resetClickHandler : btn === "+-" ? invertClickHandler : btn === "%" ? percentClickHandler : btn === "=" ? equalsClickHandler : btn === "/" || btn === "X" || btn === "-" || btn === "+" ? signClickHandler : btn === "." ? comaClickHandler : numClickHandler } /> ); })} </ButtonBox> </Wrapper> );};export default App;
总结
恭喜!您已经创建了一个功能齐全且样式齐全的应用程序。希望你在这个过程中学到了一两件事!
网友评论文明上网理性发言 已有0人参与
发表评论: