React + Electron + Antd 写一个简单的计算器客户端

创建react项目

create-react-app calculator

引入electron

yarn add electron --dev

引入antd

yarn add antd

引入electron-is-dev,用来判断当前是开发环境还是生产环境

yarn add electron-is-dev

在public目录下加入electron.js, preload.js

electron.js

// Modules to control application life and create native browser window
const { app, BrowserWindow, Menu } = require("electron");
const path = require("path");
const isDev = require("electron-is-dev");

let mainWindow;

function createWindow() {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      preload: path.join(__dirname, "preload.js"),
    },
  });

  // 清除顶部菜单
  Menu.setApplicationMenu(null);

  if (isDev) {
    mainWindow.loadURL("http://localhost:3000");
    mainWindow.openDevTools();
    // mainWindow.webContents.openDevTools();
  } else {
    mainWindow.loadFile("./build/index.html");
  }

  mainWindow.on("closed", function () {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });
}

// 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(() => {
  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", function () {
  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.

preload.js

// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener("DOMContentLoaded", () => {
  const replaceText = (selector, text) => {
    const element = document.getElementById(selector);
    if (element) element.innerText = text;
  };

  for (const type of ["chrome", "node", "electron"]) {
    replaceText(`${type}-version`, process.versions[type]);
  }
});

修改package.json

{
  ...
   "main": "public/electron.js",
   "homepage": ".",
   "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "electron": "electron ."
    }
  ...
}

运行程序

# 运行react
yarn start

# 运行electron
yarn electron

每次都运行两个命令很麻烦,使用concurrently和wait-on,将两个命令放在一起运行

yarn add concurrently wait-on --dev

修改package.json

{
  ...
  "scripts": {
        "electron": "concurrently \"react-scripts start\" \"wait-on http://localhost:3000 && electron .\""
    }
  ...
}

客户端渲染使用react

计算器一般分为两块,一块显示计算结果,一块为按钮操作区,新建两个组件input-button.js, input-text.js

// # input-text.js

import React, { Component } from "react";
import { Input } from "antd";

const { TextArea } = Input;

class inText extends Component {
  render() {
    return (
      <TextArea
        type="text"
        id="content"
        autoSize={false}
        value={this.props.value}
        readOnly={true}
      />
    );
  }
}
export default inText;
// # input-button.js
import React, { Component } from "react";
import { Button } from "antd";
import PropTypes from "prop-types";

class inButton extends Component {
  static propTypes = {
    className: PropTypes.string,
  };
  static defaultProps = {
    className: "same_size",
  };
  render() {
    return (
      <Button
        type="button"
        className={this.props.className}
        value={this.props.value}
      >
        {this.props.value}
      </Button>
    );
  }
}
export default inButton;

App.js

import React, { Component } from "react";
import Button from "./input-button";
import Text from "./input-text";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      string: "",
    };
    this.handleButton = this.handleButton.bind(this);
  }

  handleButton(e) {
    if (e.target.value !== undefined) {
      let instring = e.target.value;
      let prvcontent = this.state.string;
      let content = "";
      if (
        instring === "+" ||
        instring === "-" ||
        instring === "*" ||
        instring === "/"
      ) {
        content = prvcontent + " " + instring + " ";
      } else if (instring === "附加") {
        content = "";
      } else if (instring === "C") {
        content = "";
      } else if (instring === "Back") {
        if (prvcontent) {
          let newcontent = String(prvcontent);
          if (
            newcontent[newcontent.length - 1] === " " &&
            newcontent[newcontent.length - 3] === " "
          ) {
            prvcontent = newcontent.slice(0, newcontent.length - 3);
          } else {
            prvcontent = newcontent.slice(0, newcontent.length - 1);
          }
        }
        content = prvcontent;
      } else if (instring === "=") {
        if (prvcontent) {
          if (prvcontent.indexOf(" ") !== -1) {
            let arr = prvcontent.split(" ");
            let ans = [];
            let i = 0;
            while (i < arr.length) {
              if (arr[i] === "") {
                i++;
              } else if (arr[i] === "+") {
                ans.push(arr[i + 1]);
                i += 2;
              } else if (arr[i] === "-") {
                ans.push(-arr[i + 1]);
                i += 2;
              } else if (arr[i] === "*") {
                let a;
                let b = ans.pop();
                if (arr[i + 1] === "-") {
                  a = -arr[i + 2];
                  i += 3;
                } else {
                  a = arr[i + 1];
                  i += 2;
                }
                ans.push(b * a);
              } else if (arr[i] === "/") {
                let a;
                let b = ans.pop();
                if (arr[i + 1] === "0") {
                  content = "ERROR!";
                  return;
                } else if (arr[i + 1] === "-") {
                  a = -arr[i + 2];
                  i += 3;
                } else {
                  a = arr[i + 1];
                  i += 2;
                }
                ans.push(b / a);
              } else {
                ans.push(arr[i]);
                i++;
              }
            }
            let fin_ans = parseFloat(ans[0]);
            for (i = 1; i < ans.length; i++) {
              fin_ans += parseFloat(ans[i]);
            }
            content = fin_ans;
          } else {
            content = prvcontent;
          }
        } else {
          content = "";
        }
      } else {
        if (prvcontent && parseInt(prvcontent) !== 0) {
          content = prvcontent + instring;
        } else {
          content = instring;
        }
      }
      this.setState({
        string: content,
      });
    }
  }

  render() {
    return (
      <div id="main">
        <div id="input_text">
          <Text value={this.state.string} />
        </div>
        <div id="input_button" onClick={this.handleButton}>
          <Button value={1} />
          <Button value={2} />
          <Button value={3} />
          <Button value={"Back"} />
          <Button value={"C"} />
          <Button value={4} />
          <Button value={5} />
          <Button value={6} />
          <Button value={"+"} />
          <Button value={"-"} />
          <Button value={7} />
          <Button value={8} />
          <Button value={9} />
          <Button value={"*"} />
          <Button value={"/"} />
          <Button value={"附加"} />
          <Button value={0} />
          <Button value={"."} />
          <Button value={"="} className={"equal_size"} />
        </div>
      </div>
    );
  }
}

export default App;

index.js

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./utils/reportWebVitals";

ReactDOM.render(<App />, document.getElementById("root"));

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

index.css

@import '~antd/dist/antd.css';

#main{
  margin: 0 auto;
  width: 100%;
  height: 100vh;
}

#input_text{
  padding: 3%;
  height: 25%;
  display: flex;
}
#input_button{
  /*border: 1px solid black;*/
  margin: 0 2%;
  width: 96%;
  height: 70%;
  display: flex;
  flex-wrap: wrap;
}

#input_text #content{
  flex: auto;
  resize: none;
  margin: 0;
  width: 100%;
  padding: 10px;
  font-size: 2vw;
  border: 1px solid #d9d9d9;
}
#input_button .same_size{
  flex: auto;
  margin: 1%;
  width: 18%;
  height: 20%;
  font-size: 2vw;
}
#input_button .equal_size{
  flex: auto;
  margin: 1%;
  width: 38%;
  height: 20%;
  font-size: 1.5vw;
}

运行项目可以看到客户端

yarn electron
image.png

打包

引入electron-builder

yarn add electron-builder --dev

修改package.json打包配置

"build": {
    "productName": "react electron antd",
    "appId": "com.charming",
    "asar": true,
    "files": [
      "build/**/*"
    ],
    "dmg": {
      "artifactName": "react_electron_antd.dmg",
      "contents": [
        {
          "type": "link",
          "path": "/Applications",
          "x": 410,
          "y": 150
        },
        {
          "type": "file",
          "x": 130,
          "y": 150
        }
      ]
    },
    "nsis": {
      "oneClick": false, // 一键安装(安装在C盘)
      "allowToChangeInstallationDirectory": true,   // 允许自定义安装
      "shortcutName": "react-electron-antd"
    },
    "mac": {
      "target": "dmg",
      "icon": "icon.ico"
    },
    "win": {
      "target": "nsis",
      "icon": "icon.ico"
    },
    "directories": {
      "output": "dist/" // 打包输出路径
    }
  },

加入打包命令

{
  ...
  "scripts": {
        "electron": "concurrently \"react-scripts start\" \"wait-on http://localhost:3000 && electron .\"",
        "build-win32": "react-scripts build && electron-builder --win --ia32",
        "build-win64": "react-scripts build && electron-builder --win --x64",
        "build-mac": "react-scripts build && electron-builder --mac",
    }
  ...
}

运行打包

# 64位windows程序
yarn build-win64  

运行完成以后在 dist目录下会生成一个.exe安装文件

码云地址:https://gitee.com/charming-cheng/calculator.git

推荐阅读更多精彩内容