作业的基本信息
这个作业属于哪个课程 | FZU-CS-SE |
---|---|
这个作业要求在哪里 | https://bbs.csdn.net/forums/ssynkqtd-05?spm=1001.2014.3001.6685 |
这个作业的目标 | 完成一个具有可视化界面的计算器 |
其他参考文献 | 各大技术栈的官方网站 |
1. 作业内容
完成一个具有可视化界面的计算器
GitHub地址:Initial commit · SingAurora/electron-calculator@f35be9b (github.com)
2. 界面展示
加法
减法
乘法
除法
次方, 这应该可以算科学计算器了
3. PSP表格
PSP 阶段 | 描述 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 15 |
Estimate | 估计这个任务需要多少时间 | 20 | 20 |
Development | 开发 | 600 | 650 |
Analysis | 需求分析 (包括学习新技术) | 110 | 120 |
Design Spec | 生成设计文档 | 30 | 50 |
Design Review | 设计复审 | 30 | 40 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
Design | 具体设计 | 40 | 55 |
Coding | 具体编码 | 300 | 325 |
Code Review | 代码复审 | 30 | 45 |
Test | 测试(自我测试,修改代码,提交修改) | 60 | 60 |
4. 解题思路描述
4.1 设置开发环境
确保您的开发环境中已经安装了Node.js和npm。
安装Electron和Vite CLI作为全局依赖:npm install -g electron vite。
4.2 初始化项目
使用Vite创建一个新的React项目:vite create my-calculator --template react。
进入项目目录:cd my-calculator。
4.3 安装Tailwind CSS:
安装Tailwind CSS并配置:根据Tailwind CSS的官方文档进行安装和配置。
4.4 引入PixiJS:
使用npm或yarn安装PixiJS:npm install pixi.js。
4.5 设计UI界面:
创建React组件来设计计算器的用户界面。您可以使用Tailwind CSS来帮助样式化组件。
设计并实现计算器的按钮、显示屏幕等UI元素。
4.6 实现计算逻辑
在React组件中实现计算器的逻辑,包括数字按钮、操作符按钮、清除按钮等的点击事件处理。
创建状态管理来处理计算器的计算逻辑。
4.7 集成PixiJS:
创建一个PixiJS画布来显示一些有趣的效果,如动画、特效等。您可以将PixiJS画布嵌入到React组件中,例如作为计算器的背景或装饰。
4.8 测试
使用Vitest编写测试用例,测试计算器的各种功能。确保您的计算器在各种情况下都能正常工作。
测试UI交互、计算准确性以及PixiJS特效等。
5. 设计与实现过程
创建 react 工程
npx create-react-app my-app
安装必要的依赖
"dependencies": {
"@electron-toolkit/preload": "^2.0.0",
"@electron-toolkit/utils": "^2.0.0",
"electron-updater": "^6.1.1",
"particles-bg": "^2.5.5",
"proton-engine": "^5.4.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-mouse-particles": "^1.1.5"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.1",
"@electron-toolkit/eslint-config-prettier": "^1.0.1",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.4",
"autoprefixer": "^10.4.15",
"daisyui": "^3.7.5",
"electron": "^25.6.0",
"electron-builder": "^24.6.3",
"electron-vite": "^1.0.27",
"eslint": "^8.47.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.29",
"postcss-loader": "^7.3.3",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.9",
"vitest": "^0.34.5"
}
修改必要的命令
"scripts": {
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:win": "npm run build && electron-builder --win --config",
"build:mac": "npm run build && electron-builder --mac --config",
"build:linux": "npm run build && electron-builder --linux --config",
"test": "vitest"
},
配置tailwind
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/renderer/index.html', './src/renderer/src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {}
},
plugins: [require('daisyui'),'prettier-plugin-tailwindcss']
}
配置prettier
module.exports = {
tailwindConfig: './tailwind.config.js',
}
index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>德丽莎世界第一可爱</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
electron运行需要的入口文件
import { app, shell, BrowserWindow } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
tailwind的全局css文件
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-color: #000000;
}
@layer components {
.btn-primary {
@apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
}
}
比较复杂的样式独立在外面
/*太复杂的样式写在这里*/
.content {
/*background-color: rgb(241 245 249);*/
box-shadow: -8px -8px 16px -10px rgba(255, 255, 255, 1), 8px 8px 16px -10px rgba(0, 0, 0, .15);
}
.btn-cont button {
background: linear-gradient(135deg, rgb(222, 222, 222) 0%, rgb(255, 255, 255) 100%);
box-shadow: -4px -4px 10px -8px rgb(189, 189, 189), 4px 4px 10px -8px rgba(0, 0, 0, .3);
}
.btn-cont button:active {
box-shadow: -4px -4px 10px -8px rgba(255, 255, 255, 1) inset, 4px 4px 10px -8px rgba(0, 0, 0, .3) inset;
}
App组件
import ParticlesBg from 'particles-bg'
import List from './List'
import MouseParticles from 'react-mouse-particles'
import { useState, useEffect } from 'react'
function App() {
// 创建一个状态来存储窗口的宽度
const [windowWidth, setWindowWidth] = useState(window.innerWidth)
// 创建一个事件处理程序,用于更新窗口宽度的状态
const handleResize = () => {
setWindowWidth(window.innerWidth)
}
// 使用useEffect在组件挂载后添加窗口大小变化的事件监听器
useEffect(() => {
window.addEventListener('resize', handleResize)
// 在组件卸载时移除事件监听器,以避免内存泄漏
return () => {
window.removeEventListener('resize', handleResize)
}
}, [windowWidth]) // 空数组作为第二个参数表示只在组件挂载和卸载时运行一次
return (
<>
{/*<Calculator/>*/}
<List />
<ParticlesBg type="circle" bg={true} />
<MouseParticles g={1} color="random" cull="col,image-wrapper" life="6" />
</>
)
}
export default App
react的入口文件
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
需要的响应式数据
const [text, setText] = useState('') // 显示的结果值, 也就是第一行输入框的值
const [status, setStatus] = useState(0) // 判断用户是否已经进行求值 0: 是, 1: 否
const [value, setValue] = useState('') // 计算结果值, 也就是第二行输入框的值
const [sign, setSign] = useState('')
change方法
const change = (e) => {
if (
e.target.innerHTML === '+' ||
e.target.innerHTML === '-' ||
e.target.innerHTML === '*' ||
e.target.innerHTML === '/' ||
e.target.innerHTML === '^'
) {
setSign(e.target.innerHTML)
}
// e.target.innerHTML 获取按钮的值
let val // 定义一个变量进行更新值
if (status === 0) {
if (text === '0') {
// 判断第一次是否是0
if (
e.target.innerHTML === '+' ||
e.target.innerHTML === '-' ||
e.target.innerHTML === '*' ||
e.target.innerHTML === '/' ||
e.target.innerHTML === '.' ||
e.target.innerHTML === '^'
) {
val = text + e.target.innerHTML
} else {
val = e.target.innerHTML
}
} else if (text === '') {
if (
e.target.innerHTML === '+' ||
e.target.innerHTML === '*' ||
e.target.innerHTML === '/' ||
e.target.innerHTML === '^'
) {
val = ''
} else {
val = e.target.innerHTML
}
} else {
const arr = text.split('')
setText('')
if (arr.length > 0) {
// 获取显示的值最右一位
setText(arr[text.split('').length - 1])
}
if (
text === '+' ||
text === '-' ||
text === '*' ||
text === '/' ||
text === '.' ||
text === '^'
) {
// 判断最后一位是否是 ' + ' ' — ' ' * ' ' / '
if (
e.target.innerHTML !== '+' &&
e.target.innerHTML !== '-' &&
e.target.innerHTML !== '*' &&
e.target.innerHTML !== '/' &&
e.target.innerHTML === '^'
) {
// 判断用户再次输入的是否是数字
val = text + e.target.innerHTML
} else {
// 不是则删除之前的运算符
val = text.substring(0, text.length - 1) + e.target.innerHTML
}
} else {
val = text + e.target.innerHTML
}
}
} else {
if (
e.target.innerHTML === '+' ||
e.target.innerHTML === '-' ||
e.target.innerHTML === '*' ||
e.target.innerHTML === '/' ||
e.target.innerHTML === '^'
) {
// 等于后在该值上进行运算,则不覆盖原有的值
val = text + e.target.innerHTML
} else {
// 对等于后的值不进行计算,则进行覆盖
val = e.target.innerHTML
}
}
let valueText = '' // 处理实时计算的变量
if (val !== '') {
if (val === '.') {
valueText = ''
} else {
const lastArrs = val.split('')
let lastTexts = ''
// 获取显示的值最右一位
if (lastArrs.length > 0) {
// eslint-disable-next-line no-var
lastTexts = lastArrs[val.split('').length - 1]
}
if (
lastTexts === '+' ||
lastTexts === '-' ||
lastTexts === '*' ||
lastTexts === '/' ||
lastTexts === '^'
) {
valueText = eval(val.substring(0, val.length - 1))
} else {
if (sign === '^') {
valueText = eval(mathPow(val))
} else {
valueText = eval(val)
}
}
}
}
setText(val)
setStatus(0)
setValue(valueText)
}
equals方法
const equal = () => {
if (text !== '') {
console.log('text', text)
const equalArr = text.split('')
console.log('equalArr', equalArr)
const equalText = equalArr[text.split('').length - 1]
console.log('equalText', equalText)
if (sign === '^') {
setSign('')
setText(eval(mathPow(text)))
setStatus(1)
setValue('')
return
}
setText(eval(text))
setStatus(1)
setValue('')
}
}
clear方法
const clear = () => {
setText('')
setStatus(0)
setValue('')
}
del方法
const del = () => {
if (text !== '') {
if (typeof text === 'number') {
// 计算结果后值为数字类型 强制转成字符串类型
setText(text.toString())
}
setText(text.substring(0, text.length - 1))
setStatus(0)
}
}
mathPow方法
const mathPow = (inputString) => {
let parts = inputString.split('^')
if (parts.length === 2) {
let x = parts[0]
let y = parts[1]
let resultString = `Math.pow(${x},${y})`
return resultString
} else {
console.log('输入字符串格式不正确')
return ''
}
}
按钮的基本样式
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-red-400"
onClick={clear}
>
C
</button>
按钮组样式
<div className="grid gap-x-3 grid-cols-4 mb-5">
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-red-400"
onClick={del}
>
Del
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-red-400"
onClick={clear}
>
C
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
^
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
+
</button>
</div>
显示区域样式
<div className="mb-4 border border-gray-200 border-solid rounded-lg">
<div className="h-16 w-66 text-right leading-normal rounded-t-lg text-5xl mr-1 text-slate-500">
{text}
</div>
<div className="h-10 w-66 text-right leading-normal rounded-b-lg text-2xl mr-2 text-slate-500">
{value}
</div>
</div>
容器样式
<div className="fixed left-1/2 top-1/2 -translate-y-1/2 -translate-x-1/2">
<div className="content rounded-lg p-6 pb-3 w-96 bg-slate-100">
{/* 显示区域 */}
<div className="mb-4 border border-gray-200 border-solid rounded-lg">
<div className="h-16 w-66 text-right leading-normal rounded-t-lg text-5xl mr-1 text-slate-500">
{text}
</div>
<div className="h-10 w-66 text-right leading-normal rounded-b-lg text-2xl mr-2 text-slate-500">
{value}
</div>
</div>
{/* 键盘区 */}
<div className="btn-cont pt-4">
<div className="grid gap-x-3 grid-cols-4 mb-5">
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-red-400"
onClick={del}
>
Del
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-red-400"
onClick={clear}
>
C
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
^
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
+
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
7
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
8
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
9
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
-
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
4
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
5
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
6
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
*
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
1
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
2
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
3
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
/
</button>
</div>
<div className="grid gap-x-3 grid-cols-4 mb-3">
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
0
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={change}
>
.
</button>
<button className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:fill-sky-400 fill-slate-400 flex items-center justify-center">
<svg className="w-6 h-6" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path
className="fill-inherit"
d="M928.64 896a2.144 2.144 0 0 1-0.64 0H96a32.032 32.032 0 0 1-27.552-48.288l416-704c11.488-19.456 43.552-19.456 55.104 0l413.152 699.2A31.936 31.936 0 0 1 928.64 896zM152.064 832h719.84L512 222.912 152.064 832z"
></path>
</svg>
</button>
<button
className="hover:bg-sky-200 text-2xl m-2 w-14 h-14 rounded-full hover:text-sky-400"
onClick={equal}
>
=
</button>
</div>
</div>
</div>
</div>
6. 异常处理
在React应用中, 异常处理非常重要, 因为它有助于确保应用程序在出现错误时能够保持稳定和可靠, 在开发体验上, 异常处理可以防止应用程序崩溃或以不友好的方式失败. 它们可以提供更好的用户体验, 即使在出现问题时也能够提供友好的反馈, 而不是让用户看到错误消息或空白页面. 另外, 通过捕获和处理这些错误, 您可以确保应用程序继续运行而不崩溃, 从而提高了应用程序的稳定性.
在本次React应用中, 我使用以下三种方法来进行异常处理
6.1 错误边界
React引入了错误边界的概念, 可以在组件层次结构中捕获并处理错误, 而不会导致整个应用程序崩溃, 帮助我们减轻很多工作量
错误边界的结构如下:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新状态
return { hasError: true };
}
componentDidCatch(error, info) {
// 组件堆栈
logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// 出现错误后要渲染的UI
return this.props.fallback;
}
return this.props.children;
}
}
6.2 try-catch
try-catch是非常经典的用于异常处理和错误捕捉的方法, 各种语言都有给出示例, 大家已经很熟悉了, 但还是有一些地方需要学习一下
- 只捕获您能够处理的错误:仅仅捕获自己知道如何处理的错误. 不要捕获所有异常, 因为这可能会隐藏应用程序中的问题, 使调试更加困难.
- 错误信息的使用: 在catch块中,您可以访问捕获到的错误对象。该对象通常有两个重要的属性:message和stack。message属性包含错误的描述,stack属性包含有关错误发生位置的信息。您可以使用这些信息进行记录或调试。
- 避免空的catch块: 不要创建空的catch块,因为它们会阻止错误信息被记录,使调试问题变得困难。至少在catch块中添加一些处理错误的代码,如记录错误或显示错误信息。
部分示例代码
//
try {
// 一些可能引发错误的代码
} catch (error) {
// 避免空的catch块,至少添加一些处理错误的代码
console.error('发生错误:', error);
}
try {
// 一些可能引发错误的代码
} catch (error) {
if (error instanceof TypeError) {
// 处理类型错误
} else if (error instanceof ReferenceError) {
// 处理引用错误
} else {
// 处理其他错误
}
}
try {
// 一些可能引发错误的代码
} catch (error) {
// 处理错误
} finally {
// 无论是否发生错误,这里的代码都会执行
}
6.3 组件生命周期方法
下面这张图片是React的生命周期函数, 是学习React非常经典的图, 使用生命周期函数就可以在特定的生命周期捕获错误
React生命周期函数是用于管理组件在其生命周期内的各个阶段的方法, 这些函数可以让我们可以在不同的时刻执行代码, 以便进行初始化, 数据获取, 状态更新, DOM 操作和资源清理等操作. React 16.3之后, 生命周期函数被重新命名和重构, 他们的用途更加清晰, 不仅仅是局限于错误捕获和异常处理
7. 程序性能改进
7.1 PIXIJS改进
绘制一个计算器应用程序使用PixiJS时, 性能优化至关重要, 那当然少不了一些逐步的优化步骤
- 使用纹理图集:将所有计算器按钮和界面元素的图像合并到一个或多个纹理图集中,以减少纹理切换和减轻GPU负担。
- 使用精灵池:避免频繁地创建和销毁精灵对象,而是使用精灵池来重用它们,减少内存和性能开销。
- 图形缓存:对于静态或不经常更改的图形元素,可以将它们缓存为纹理以提高渲染性能.
- 避免频繁的渲染帧:使用requestAnimationFrame来控制渲染帧的频率,不必在每一帧都进行渲染,可以在必要时进行更新。
- 使用GPU加速:确保PixiJS中的WebGL渲染器处于启用状态,以充分利用硬件加速,特别是对于复杂的图形和动画。
- 优化交互:仅在需要时启用交互,避免为不可交互的元素添加事件监听器,减少事件的冒泡和捕获
- 减少透明度和混合模式:透明度和混合模式会增加渲染成本,所以只有在需要时才使用它们,尽量减少其使用。
- 使用帧缓冲对象:对于需要后处理或特殊效果的部分,可以使用帧缓冲对象来提高性能。
- 精简对象层次:尽量减少PixiJS对象的嵌套层次,避免过多的容器和显示对象,这可以降低渲染的复杂性.
- 监控性能:使用浏览器的性能分析工具来监控应用程序的性能,查找性能瓶颈并进行适当的优化。
- 懒加载资源:只加载和使用当前场景所需的资源,避免一次性加载所有资源,以减少初始加载时间。
- 使用Web Workers:将一些计算密集型的任务移到Web Workers中,以避免阻塞主线程
逐步执行这些优化步骤可以显著提高PixiJS绘制计算器应用程序的性能, 万一后期有什么高并发的场景下, 确保用户能够流畅地使用它. 不同应用场景可能需要不同的优化方法, 因此后面根据场景定期进行性能测试和分析也很重要。
7.2 性能测试
部分科学计算性能测试代码
const Benchmark = require('benchmark');
// 准备要测试的数学运算
function calculateSquareRoot(x) {
return calculator.sqrt(x);
}
function calculateExponential(x) {
return calculator.exp(x);
}
function calculateTrigonometric(x) {
return calculator.sin(x);
}
function calculateLogarithm(x) {
return calculator.log(x);
}
// 创建一个性能测试套件
const suite = new Benchmark.Suite;
// 添加测试用例
suite
.add('计算平方根', () => {
for (let i = 1; i <= 1000; i++) {
calculateSquareRoot(i);
}
})
.add('计算指数', () => {
for (let i = 1; i <= 1000; i++) {
calculateExponential(i);
}
})
.add('计算三角函数', () => {
for (let i = 1; i <= 1000; i++) {
calculateTrigonometric(i);
}
})
.add('计算对数', () => {
for (let i = 1; i <= 1000; i++) {
calculateLogarithm(i);
}
})
.on('cycle', event => {
console.log(String(event.target));
})
.on('complete', function() {
console.log('性能测试完成');
console.log('最快的是: ' + this.filter('fastest').map('name'));
})
.run({ async: true });
使用之前当然要先下载这个包
pnpm i benchmark
运行性能测试后,可以获得有关每个测试用例的性能数据,以及哪个测试用例是最快的
需要注意的是, 使用Benchmark.js的on(‘complete’)回调来分析性能数据,包括平均运行时间、标准差等。这可以帮助您更好地理解性能差异
.on('complete', function() {
console.log('性能测试完成');
console.log('最快的是: ' + this.filter('fastest').map('name'));
console.log('平均运行时间: ' + this.map('mean'));
console.log('标准差: ' + this.map('deviation'));
})
8. 单元测试展示
Vitest 是一个由 Vite 提供支持的极速单元测试框架. 定位在Vite的生态圈中的首选框架, 它拥有许多插件和集成框架, 已经慢慢形成一个活跃的生态社区。
测试代码
下面是部分的测试代码
import { expect, test } from 'vitest'
import { sum, minus, multiply, divide, power } from './fun'
test('sum: 1 + 2 = 3', () => {
expect(sum(1, 2)).toBe(3)
})
test('minus: 1 - 2 = -1', () => {
expect(minus(1, 2)).toBe(-1)
})
test('multiply: 1 * 2 = 2', () => {
expect(multiply(1, 2)).toBe(2)
})
test('divide: 1 / 2 = 0.5', () => {
expect(divide(1, 2)).toBe(0.5)
})
test('power: 2 ^ 3 = 8', () => {
expect(power(2, 3)).toBe(8)
})
import { sin, cos, tan, asin, acos, atan } from './fun'
test('sin: sin(0) = 0', () => {
expect(sin(0)).toBe(0)
})
test('cos: cos(0) = 1', () => {
expect(cos(0)).toBe(1)
})
test('tan: tan(0) = 0', () => {
expect(tan(0)).toBe(0)
})
test('asin: asin(0) = 0', () => {
expect(asin(0)).toBe(0)
})
test('acos: acos(0) = 1.5707963267948966', () => {
expect(acos(0)).toBe(1.5707963267948966)
})
test('atan: atan(0) = 0', () => {
expect(atan(0)).toBe(0)
})
import { dec2bin, dec2oct, dec2hex } from './fun'
test('dec2bin: dec2bin(0) = 0', () => {
expect(dec2bin(0)).toBe('0')
})
test('dec2oct: dec2oct(0) = 0', () => {
expect(dec2oct(0)).toBe('0')
})
test('dec2hex: dec2hex(0) = 0', () => {
expect(dec2hex(0)).toBe('0')
})
测试输出
8. 心路历程与收获
在开发一个基于Electron、Vite、React、Tailwind CSS、Vitest和PixiJS的计算器应用的过程中,我经历了一段有趣的心路历程,同时也积累了一些宝贵的经验和收获。在这篇文章中,我想和大家分享一下我在这个项目中遇到的一些挑战和解决方案,以及我从中学到的一些技术和技巧。
学习新技术栈:
开始这个项目之前,我对Electron、Vite、PixiJS等技术栈的了解非常有限。我只知道Electron是一个可以用Web技术开发桌面应用的框架,Vite是一个基于ESM的快速开发服务器,PixiJS是一个用于创建2D图形和动画的JavaScript库。通过这个项目,我不仅掌握了这些技术,还学到了如何将它们协同工作。例如,我学会了如何使用Vite来构建Electron应用,并且如何在Electron中使用PixiJS来渲染画布。
构建用户界面
设计和实现计算器的用户界面是一个有趣的挑战。我想要创建一个简洁而美观的UI,同时也要考虑用户的操作体验。我学会了如何使用React来构建交互式UI,并且通过Tailwind CSS可以轻松地样式化应用程序。React让我可以将UI分解为可复用的组件,并且通过状态和属性来管理数据流。Tailwind CSS让我可以使用实用类来快速地定义元素的样式,并且支持响应式设计和自定义主题。
计算逻辑的复杂性
实现计算器的计算逻辑比我想象的要复杂。处理数字、操作符、清除按钮以及运算符的顺序等各种情况需要仔细考虑和测试。这让我更好地理解了计算机科学中的算法和数据结构。为了实现计算器的功能,我使用了一个栈来存储操作符和操作数,并且使用了一个函数来判断操作符的优先级。当用户输入一个操作符时,我会比较它与栈顶元素的优先级,如果高于或等于栈顶元素,则将其压入栈中;如果低于栈顶元素,则将栈中所有优先级高于或等于该操作符的元素弹出,并进行相应的运算,然后将结果和该操作符压入栈中。当用户点击等号按钮时,我会将栈中所有元素弹出,并进行最后的运算,然后将结果显示在屏幕上。
PixiJS的创造性应用
集成PixiJS为应用程序添加了一些有趣的动画和特效。我学会了如何在React项目中嵌入PixiJS画布,并将其用于创造性的视觉效果。例如,当用户输入一个数字时,我会在画布上生成一个对应的粒子,并让它沿着一条曲线飞向屏幕上方;当用户点击清除按钮时,我会在画布上生成一个爆炸效果,并让屏幕上所有数字消失;当用户点击等号按钮时,我会在画布上生成一个彩虹效果,并让结果显示在屏幕中央。
协作和测试
使用Vitest编写测试用例非常有助于确保我的计算器应用的质量。我学会了如何编写单元测试和集成测试,以及如何在开发过程中不断进行测试和调试。Vitest是一个基于Vite的测试框架,它可以让我在浏览器中运行测试,并且支持热重载和代码覆盖率。我使用了Jest和Testing Library来编写测试用例,分别测试了计算器的逻辑和UI。我还使用了ESLint和Prettier来保证代码的规范和格式。
技术社区和文档
在开发过程中,我积极参与了相关技术的社区,查阅文档和教程。我发现了庞大的技术社区,这对于解决问题和学习新知识非常有帮助。我在Stack Overflow、GitHub、Reddit等平台上寻找答案和灵感,也在Medium、Dev.to、YouTube等平台上阅读和观看优秀的文章和视频。我还经常浏览官方文档和API,以便更深入地理解各种技术的原理和用法。
完成项目的满足感
最终完成了这个计算器应用后,我感到非常满足。它不仅是一个实用的工具,还是我技术能力的展示,同时也是对我的持续学习和努力的奖励。我很高兴能够将这个项目分享给大家,也希望能够得到大家的反馈和建议。
总的来说
这个项目是一个充满挑战和乐趣的学习过程。通过它,我不仅增加了对多种前端和后端技术的了解,还提高了问题解决和创造性编码的能力。我鼓励每个人尝试类似的项目,因为它们可以帮助您不断提升自己的技能并获得深刻的满足感。不管您的项目规模如何,每一步都是前进的一步。