计算器 react electron vite tailwind


作业的基本信息

这个作业属于哪个课程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计划2015
Estimate估计这个任务需要多少时间2020
Development开发600650
Analysis需求分析 (包括学习新技术)110120
Design Spec生成设计文档3050
Design Review设计复审3040
Coding Standard代码规范 (为目前的开发制定合适的规范)2020
Design具体设计4055
Coding具体编码300325
Code Review代码复审3045
Test测试(自我测试,修改代码,提交修改)6060

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是非常经典的用于异常处理和错误捕捉的方法, 各种语言都有给出示例, 大家已经很熟悉了, 但还是有一些地方需要学习一下

  1. 只捕获您能够处理的错误:仅仅捕获自己知道如何处理的错误. 不要捕获所有异常, 因为这可能会隐藏应用程序中的问题, 使调试更加困难.
  2. 错误信息的使用: 在catch块中,您可以访问捕获到的错误对象。该对象通常有两个重要的属性:message和stack。message属性包含错误的描述,stack属性包含有关错误发生位置的信息。您可以使用这些信息进行记录或调试。
  3. 避免空的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时, 性能优化至关重要, 那当然少不了一些逐步的优化步骤

  1. 使用纹理图集:将所有计算器按钮和界面元素的图像合并到一个或多个纹理图集中,以减少纹理切换和减轻GPU负担。
  2. 使用精灵池:避免频繁地创建和销毁精灵对象,而是使用精灵池来重用它们,减少内存和性能开销。
  3. 图形缓存:对于静态或不经常更改的图形元素,可以将它们缓存为纹理以提高渲染性能.
  4. 避免频繁的渲染帧:使用requestAnimationFrame来控制渲染帧的频率,不必在每一帧都进行渲染,可以在必要时进行更新。
  5. 使用GPU加速:确保PixiJS中的WebGL渲染器处于启用状态,以充分利用硬件加速,特别是对于复杂的图形和动画。
  6. 优化交互:仅在需要时启用交互,避免为不可交互的元素添加事件监听器,减少事件的冒泡和捕获
  7. 减少透明度和混合模式:透明度和混合模式会增加渲染成本,所以只有在需要时才使用它们,尽量减少其使用。
  8. 使用帧缓冲对象:对于需要后处理或特殊效果的部分,可以使用帧缓冲对象来提高性能。
  9. 精简对象层次:尽量减少PixiJS对象的嵌套层次,避免过多的容器和显示对象,这可以降低渲染的复杂性.
  10. 监控性能:使用浏览器的性能分析工具来监控应用程序的性能,查找性能瓶颈并进行适当的优化。
  11. 懒加载资源:只加载和使用当前场景所需的资源,避免一次性加载所有资源,以减少初始加载时间。
  12. 使用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,以便更深入地理解各种技术的原理和用法。

完成项目的满足感

最终完成了这个计算器应用后,我感到非常满足。它不仅是一个实用的工具,还是我技术能力的展示,同时也是对我的持续学习和努力的奖励。我很高兴能够将这个项目分享给大家,也希望能够得到大家的反馈和建议。

总的来说

这个项目是一个充满挑战和乐趣的学习过程。通过它,我不仅增加了对多种前端和后端技术的了解,还提高了问题解决和创造性编码的能力。我鼓励每个人尝试类似的项目,因为它们可以帮助您不断提升自己的技能并获得深刻的满足感。不管您的项目规模如何,每一步都是前进的一步。

​​

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值