我正在参与CSDN《新程序员》有奖征文,活动链接:https://marketing.csdn.net/p/52c37904f6e1b69dc392234fff425442
上一节 使用Electron构建跨平台的桌面应用程序(一)快速入门 简单的熟悉了一下Electron,这一节,主要是如何构建一个桌面应用,鉴于只使用 Electron
来构架工具开发效率较慢的原因,所以本文使用React【Umijs框架】
+ Electron
+ Antd
来实现一个简单的计算器功能。
需要创建 Reat
环境,如果已有环境可忽略。
mkdir my-first-tool-app && cd my-first-tool-app
yarn create @umijs/umi-app
执行结果如图所示:
yarn
yarn start
如图所示,访问 http://localhost:8000 :
默认的脚手架内置了 @umijs/preset-react
,包含布局、权限、国际化、dva、简易数据流等常用功能。比如想要 ant-design-pro
的布局,编辑 .umirc.ts
配置 layout: {}
,并且需要安装 @ant-design/pro-layout
。
先安装 @ant-design/pro-layout
:
yarn add @ant-design/pro-layout
再修改文件:
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
layout: {},
routes: [
{ path: '/', component: '@/pages/index' },
],
fastRefresh: {},
});
直接访问 http://localhost:8000 即刻看到效果,效果如图所示:
更详细的使用说明 可以去看 Umijs文档。
先进入项目根目录,如果已在根目录则忽略:
cd my-tool-app/
引入electron使用命令:
yarn add electron --dev
使用命令:
yarn add antd
此时项目目录文件如下:
为了便于项目文件管理,在pages
目录下新建 simple-calculator
目录。
此时,我们如果想通过入口的按钮入口使用计算器,那就需要路由
进行管理。
路由支持:
在 .umirc.ts
文件中加入路由,文件内容为:
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
layout: {},
routes: [
{ path: '/', component: '@/pages/index',name:'首页' },
{ path: '/simple-calculator', component: '@/pages/simple-calculator/',name:'计算器' },
],
fastRefresh: {},
});
计算器展示区可分为两块,一块显示计算结果区,一块为按钮操作区,现在新建两个组件 input-button.tsx
, input-text.tsx
,入口文件 index.tsx
以及 样式文件simple-calculator.less
。
代码参考地址:https://www.jianshu.com/p/ab31487779c9
/*
* @Author: your name
* @Date: 2021-06-26 15:41:34
* @LastEditTime: 2021-06-29 17:35:20
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /my-tool-app/src/pages/input-button.js
*/
// # input-button.js
import React, { Component,MouseEvent } from "react";
import { Button } from "antd";
import PropTypes from "prop-types";
interface IProps {
onClick (event: MouseEvent<HTMLDivElement>): void,
}
class inButton extends Component <any,any> {
static propTypes = {
className: PropTypes.string,
};
static defaultProps = {
className: "same_size",
};
render() {
return (
<Button
className={this.props.className}
value={this.props.value}
>
{this.props.value}
</Button>
);
}
}
export default inButton;
/*
* @Author: your name
* @Date: 2021-06-26 15:39:21
* @LastEditTime: 2021-06-29 16:15:01
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /my-tool-app/src/pages/simple-calculator/input-text.js
*/
import React, { Component } from "react";
import { Input } from "antd";
const { TextArea } = Input;
class inText extends Component <any,any> {
render() {
return (
<TextArea
id="content"
autoSize={false}
value={this.props.value}
readOnly={true}
/>
);
}
}
export default inText;
代码如下:
/*
* @Author: your name
* @Date: 2021-06-29 13:57:46
* @LastEditTime: 2021-06-29 17:42:55
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /my-first-tool-app/src/pages/simple-calculator/index.tsx
*/
import './simple-calculator.less';
import React, { Component } from 'react';
import Button from './input-button'
import Text from './input-text'
type onClick = (value: string ) => (e: React.MouseEvent) => void;
class SimpleCalculator extends Component<any, any> {
constructor(props : any) {
super(props);
this.state = {
string: "",
};
//const handleButton = (text: any) => this.handleButton.bind(this);
this.handleButton = this.handleButton.bind(this);
// const onClick = (text: any)=> this.handleButton.bind(this);
}
public handleButton(e: any | { target: { value: string }} ) {
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: string[] | number[] = [];
let i = 0;
while (i < arr.length) {
if (arr[i] === "") {
i++;
} else if (arr[i] === "+") {
ans.push(arr[i + 1] as never);
i += 2;
} else if (arr[i] === "-") {
ans.push(-arr[i + 1] as never);
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;
}
if(b!==undefined){
b=parseFloat(b.toString());
ans.push( b * a as never);
}
} 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;
}
if(b!==undefined){
b=parseFloat(b.toString());
ans.push( (b / a) as never);
}
} else {
ans.push(arr[i] as never);
i++;
}
}
let fin_ans = parseFloat(ans[0].toString());
for (i = 1; i < ans.length; i++) {
fin_ans += parseFloat(ans[i].toString());
}
content = fin_ans.toString();
} 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 SimpleCalculator;
代码如下:
@import '~antd/dist/antd.less';
*{
margin: 0px;
padding: 0px;
}
.content{
width: 170px;
margin:100px 0 0 100px;
}
#main{
margin: 0 auto;
width: 50%;
height: 90vh;
}
#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;
}
#components-layout-demo-top-side .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top-side .logo {
float: right;
margin: 16px 0 16px 24px;
}
.site-layout-background {
background: #fff;
}
.site-layout-content {
min-height: 280px;
padding: 24px;
background: #fff;
}
#components-layout-demo-top .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top .logo {
float: right;
margin: 16px 0 16px 24px;
}
根据上一节:使用Electron构建跨平台的桌面应用程序(一)快速入门。先在
根目录创建main.js
和 preload.js
文件
注意,地址端口和 上一节 内容不同,需新增部分:
mainWindow.loadURL("http://localhost:8000");
mainWindow.openDevTools();
文件整体内容如下:
/*
* @Author: your name
* @Date: 2021-06-26 13:52:47
* @LastEditTime: 2021-06-30 11:36:49
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /my-tool-app/public/electron.js
*/
// electron.js
// Modules to control application life and create native browser window
// 导入作为公共JS模块:
const { app, BrowserWindow } = require('electron')
const path = require('path')
let mainWindow;
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1000,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// and load the index.html of the app.
mainWindow.loadFile('index.html')
// Open the DevTools.
// mainWindow.webContents.openDevTools()
mainWindow.loadURL("http://localhost:8000");
mainWindow.openDevTools();
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.
// 部分 API 在 ready 事件触发后才能使用。
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. 也可以拆分成几个文件,然后用 require 导入。
该文件内容不变,用上一节的即可。
/*
* @Author: your name
* @Date: 2021-06-26 14:08:33
* @LastEditTime: 2021-06-26 14:08:34
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: /my-tool-app/public/preload.js
*/
// 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 dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
在package.json中配置程序主入口main.js
,并且添加运行Electron的脚本命令,如图:
文件整体代码如下:
{
"name": "my-first-tool-app",
"version": "0.1.0",
"main": "main.js",
"description": "my-first-tool-app",
"author": "sunct",
"private": true,
"scripts": {
"start": "umi dev",
"build": "umi build",
"postinstall": "umi generate tmp",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"test": "umi-test",
"test:coverage": "umi-test --coverage",
"electron": "electron ."
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,less,md,json}": [
"prettier --write"
],
"*.ts?(x)": [
"prettier --parser=typescript --write"
]
},
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@ant-design/pro-layout": "^6.20.0",
"@umijs/preset-react": "1.x",
"antd": "^4.16.6",
"umi": "^3.4.25"
},
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@umijs/test": "^3.4.25",
"electron": "^13.1.4",
"lint-staged": "^10.0.7",
"prettier": "^2.2.0",
"react": "17.x",
"react-dom": "17.x",
"typescript": "^4.1.2",
"yorkie": "^2.0.0"
}
}
依次执行:
npm start
npm run electron
执行后会弹出一个程序窗口,结果如图:
为了简化执行,可以使concurrently
库同步运行多命令,同时运行electron和react。使用wait-on库等待8000端口启动完成之后运行electron。具体的脚本命令如下:
yarn add concurrently wait-on --dev
修改 package.json
文件:
"electron": "concurrently \"umi dev start\" \"wait-on http://localhost:8000 && electron .\""
然后先关掉前面执行的npm start
和npm run electron
命令,再次重新执行:
npm run electron
这是完成程序包前的最后一步,既是最重要的一步,也是相对麻烦的一步。
基于Umi搭建Electron App,其实可以理解为通过Electron将使用Umi搭建的Web应用包装成桌面应用。这就需要对Web应用和Elecrton分别打包。
①、 web应用打包:使用webpack打包
② 、桌面应用程序打包:使用electron-builder打包
③ 、将Web应用打包后的文件(build文件夹中的所有文件)复制到桌面应用程序中(通过electron-builder中的files配置。
Electron App的打包方式 上一节 使用的是electron-forge
,那这次来使用electron-builder
。
参考electron-builder官网 或 代码仓库,执行以下命令安装electron-builder作为开发依赖。
命令如下:
yarn add electron-builder --dev
在package.json文件后面加入以下配置:
"build": {
"appId": "my-first-tool-app",
"files":[
"build/**/*"
],
"productName":"my-first-tool-app",
"directories":{
"output":"dist"
},
"mac": {
"category": "your.app.category.type",
"icon": "icon.icns"
}
}
其他配置项请参考 electron-builder官方配置文档。
.umirc.ts
中修改或增加配置: publicPath: './',
outputPath:'build',
hash: true,
history: {
type: 'hash',
},
① 代码修改
const isPro = process.env.NODE_ENV !="development";
if (isPro) {
mainWindow.loadFile("./build/index.html");
} else {
mainWindow.loadURL("http://localhost:8000");
mainWindow.openDevTools();
}
在生产环境和开发环境下加载渲染进程的方式不同,所以需要判断当前的环境然后进行分别处理。
环境变量的配置可以看一下 cross-env 这个库,在使用Umi创建工程时,cross-env是默认安装的。
如果没有则可以手动安装,命令:npm install --save-dev cross-env
或yarn add cross-env --save-dev
② 位置变动
之前在编写Electron主进程文件的时候,暂时将·main.js·放在了根目录,由于main.js与其他模块之间没有任何引用关系,所以webpcak打包时无法把main.js打进去。生产环境下main.js中加载index.html就找不到这个文件,执行打包文件会出现白屏的情况。
解决办法:
把main.js放到public文件夹下,因为webpack打包时public文件夹下的文件都会原封不动地复制到build文件夹中(如果有自己写的依赖,也顺便移到public目录下。
将package.json
中配置项"main"的值由"main.js
“改为”build/main.js
"。
"main": "build/main.js",
图标部分也改成:
"build": {
...
"mac": {
"category": "your.app.category.type",
"icon": "build/icon.icns"
}
}
如果没有public 目录,则手动创建一个,其中包含文件如下:
通过在package.json
中配置脚本命令的方式来设置当前的环境,如下:
"electron-start":"yarn build && cross-env NODE_ENV=production electron build/main.js",
"electron-dev":"concurrently \"cross-env BROWSER=none yarn start \" \"wait-on http://localhost:8000 && cross-env NODE_ENV=development electron public/main.js\" "
图中第16行是生产环境下electron启动命令,通过cross-env NODE_ENV=production
来设置环境变量为“production
”表明是生产环境,生产环境下electron主进程是直接执行的打包生成的build文件夹下的main.js
文件。
图中第17行是开发环境下electron启动命令,设置环境变量的方式与生产环境类似,但是开发环境下electron主进程是执行的public文件下的main.js
。
在package.json
里我只配置了windows和mac两种平台的打包命令(它们的共同点是都要先执行yarn build对react进行打包),因为我的电脑是mac,我只尝试了mac下的打包。
配置如下:
"dis-mac":"yarn build && electron-builder --mac",
"dis-win32":"yarn build && electron-builder --win --ia32",
"dis-win64":"yarn build && electron-builder --win --x64"
npm run dis-mac
执行结果如下:
$ npm run dis-mac
Debugger attached.
> [email protected] dis-mac
> yarn build && electron-builder --mac
Debugger attached.
yarn run v1.15.2
$ umi build
Debugger attached.
✔ Webpack
Compiled successfully in 30.69s
Browserslist: caniuse-lite is outdated. Please run:
npx browserslist@latest --update-db
Why you should do it regularly:
https://github.com/browserslist/browserslist#browsers-data-updating
Debugger attached.
Debugger attached.
Debugger attached.
Waiting for the debugger to disconnect...
Waiting for the debugger to disconnect...
Waiting for the debugger to disconnect...
DONE Compiled successfully in 30689ms 下午3:00:28
File Size Gzipped
build/umi.d1347c73.js 933.1 KB 298.8 KB
build/main.js 624.0 B 395.0 B
build/preload.js 204.0 B 180.0 B
build/umi.53ebdfcc.css 514.8 KB 65.3 KB
Images and other types of assets omitted.
Waiting for the debugger to disconnect...
✨ Done in 33.60s.
Waiting for the debugger to disconnect...
Debugger attached.
• electron-builder version=22.11.7 os=20.3.0
• loaded configuration file=package.json ("build" field)
• writing effective config file=dist/builder-effective-config.yaml
• rebuilding native dependencies dependencies=[email protected] platform=darwin arch=x64
• packaging platform=darwin arch=x64 electron=13.1.4 appOutDir=dist/mac
• skipped macOS application code signing reason=cannot find valid "Developer ID Application" identity or custom non-Apple code signing certificate, see https://electron.build/code-signing allIdentities= 0 identities found
Valid identities only
0 valid identities found
• building target=macOS zip arch=x64 file=dist/my-first-tool-app-0.1.0-mac.zip
• building target=DMG arch=x64 file=dist/my-first-tool-app-0.1.0.dmg
• building block map blockMapFile=dist/my-first-tool-app-0.1.0.dmg.blockmap
目录如下:
如有问题请在下方留言。
或关注我的公众号“孙三苗”,输入“联系方式”。获得进一步帮助。