electron+react+七牛云开发笔记

Electron 和 React 双剑合璧做 markdown

使用 Electrong 、 React 和七牛云做一个在线 markdown。

环境准备

使用creact-react-app创建 react,然后在里面添加 electron 的代码和配置即可。
react 和 electron 结合,需要启动两个服务,在 electron 的窗口中嵌入 react 启动的页面。

优化启动命令

  1. 一个命令窗口启动项目
  2. 当 react 启动后再启动 electron
  3. 关闭 create-react-app 启动时自动打开浏览器的功能

一个命令窗口启动项目

因为 electron 和 react 需要分别启动,所以每次打开两个命令窗口比较麻烦,可以使用Concurrently插件的来实现这个功能。
Concurrently 的优点

  1. 跨平台(window 和 mac 对 npm 原生的多命令同时运行符号&支持不同)
  2. 控制台输出信息友好
  3. 一键停止启动的所有命令

当 react 启动后再启动 electron

上面配置完后,一个命令就把 electron 和 react 都启动了,由于是并行的,所以当 electron 启动时会先显示一个白屏,然后等 react 也启动了需要手动刷新一下,react 的页面才会显示出来。我们可以设置让 react 的服务启动后再启动 electron,这个可以使用 wait-on 插件

禁止 react 自动打开浏览器

这个是 webpac 的功能,设置自动开发浏览器的选项为 false 即可。这个create-react-app已经帮我们做了处理,只需设置环境变量即可。考虑到跨平台,使用cross-env设置环境变量。

react-markdown 编辑器

编辑器地址https://github.com/RIP21/react-simplemde-editor

设置 key

在给编辑器赋值时,如果编辑器的值是根据不同条件显示不同的内容,这时需要设置一个 key,不然即使条件发生变化,页面重新渲染,编辑器也无法读取到新的值。

扁平化处理

由于数组在操作时,操作某个元素都需要遍历查找,所以把数组扁平化处理,操作起来更方便。

检测 electron 是否是开发环境

1
2
3
npm install electron-is-dev

const isDev = require('electron-is-dev');

在 react 中使用 Node.js 的 API

前面说过,在 electron 的渲染进程中的 js 不仅可以使用前端的各种 API, 我们还可以使用 Node 的 API。

main.js 中启动 electron,并且创建窗口就是运行在主进程中,创建的各个窗口就是渲染进程,窗口中页面引用的 js 就是运行在渲染进程中的 js。

因此我们可以在页面的 js 中直接使用 node 的 api 对本地系统资源进行操作。

require 的小 bug

在网页的js中使用require关键字引入nodeapi,发现获取到的对象是空对象,在electronissues中知道解决方法,把require改成window.require

1
2
// const fs = require('fs')  // 获取不到对象
const fs = window.require("fs");

原因:

原来在写 demo 时没遇到这个问题,为什么现在遇到了呢?
因为原来写 demo 比较简单,使用最原始的方式写,运行是没问题的。但是现在的环境是使用了 react,因为我们的 react 使用了 webpack 进行了打包,webpack 会对 es6 的 import 和 require 都进行处理,webapck 对引入文件的寻找有自己的路径方式,所以当我们使用 require 的方式引入 fs 时,被 webpack 编译截胡了,所以就找不到对象了,前面加上 window,webpack 知道是原生的对象,就不做处理了。

查看 electron 的 node 版本

这里要用到 promise 版本的 fs,但是这个是 node 10 版本后推出的,所以要确保 node 的版本是 10 之后,但是这里说的 node 版本不是本地安装的版本,而是 electron 中使用的版本。
如何查看 electron 的版本呢?
这个在 electron 给的 demo 中已经显示了各个工具的版本号,在页面的 js 中调用环境变量查看

1
process.versions.node;

数据持久化

node 提供了操作本地文件的功能,我们可以使用 node 提供的 api 把内容写入到本地系统的文件中。
electron 的主进程提供的app.getPath(name)方法可以方便获取本地一些目录路径,具体参考官网文档介绍。
我们把文件写入到这些路径中。

网页数据持久化
文档的的内容我们通过 node api 把文件写入到了本地磁盘上,网页上的数据列表我们也需要持久化,以避免刷新一下网页数据就没了;
我们插件electron-store,其实这个插件也是把数据存储在本地磁盘,数据存储在一个 json 文件中。

electron-store 的数据存储路径是通过app.getPath('userData')获取的,根据 electron 官网,mac 对应的是~/Library/Application Support文件夹中。
进入上面的文件夹,会看到一个和你项目同名的文件夹,electron-store的数据就存储在这个文件夹中。文件默认名字叫config,可以根据参数name来更改。

mac 系统,在终端中进入包含空格的文件夹的方式

  1. 使用*代替空格
  2. 使用反斜杠转义空格
1
2
cd Application\ Support
cd Application*Support

菜单功能

菜单也是主进程提供的 api,根据 electron 提供的 Menu 和 MenuItem 来创建菜单。

实现上线文菜单功能

所谓上下文菜单,主要是右键菜单。当点击右键时,触发上下文菜单事件contextmenu事件,然后把通过 Menu 创建的菜单实例传递给要显示的窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const menu = new Menu();
menuItems.forEach(item => {
menu.append(new MenuItem(item));
});

const onOpenContextMenu = e => {
// 判断点击的元素是否在指定的元素范围内
if (document.querySelector(targetSelctor).contains(e.target)) {
clickedElemnt.current = e.target;
// 指定菜单在哪个窗口中显示
menu.popup({ window: remote.getCurrentWindow });
}
};
// 打开上下文菜单时触发
window.addEventListener("contextmenu", onOpenContextMenu);

获取菜单的数据

在点击菜单时,可以根据点击的元素来获取元素的属性,比如 id,但是我们可能需要更多的信息,我们可以自定义一些属性,然后通过节点的属性获取;这是老方法,html5 提供了更方便更规范的方法,即设置data-*属性,通过这种方式设置的属性,可以通过HTMLElement.dataset来获取他们。

1
2
3
4
5
6
7
8
9
10
<div id="user" data-id="1234567890" data-user="johndoe" data-date-of-birth>
John Doe
</div>;

var el = document.querySelector("#user");

// el.id ===> 'user'
// el.dataset.id ===> '1234567890'
// el.dataset.user ===> 'johndoe'
// el.dataset.dateOfBirth ===> ''

判断元素是否用了某个样式 class

以前用 jquery,可以很方便做各种操作,其实现在原生的也提供了很多方便的 api。
元素的 classList 就是元素所有的样式类,并且提供了多种操作方法,判断是否包含某一个,可以简单实现

1
node.classList.contains("classname"); // true | false

menu 触发的事件是在主进程中触发的,所以 menu 事件需要通过进程之间的通信方式来通信。
同一进程之间的通信使用 ipcMain 来进行,

1
2
3
4
// 触发
ipcMain.emit("事件名");
// 监听
ipcMain.on("事件名", () => {});

menuTemplate中的 menu 事件都发送到了 main 中进行处理,其实可以直接处理,然后把结果返回给渲染进程。这里是为了统一。

主进程和渲染进程之间的通信是通过 主窗口的实例(mainWindow) 和 ipcRender,在主进程中使用 mainWindow.webContents.send(),在渲染进程中使用 ipcRender;
在渲染进程中,即在页面中可以直接使用 node 的 api 进行业务处理。

应用程序打包

为缓解 Windows 下路径名过长的问题, 略微加快一下 require 的速度以及隐藏你的源代码,你可以选择把你的应用打包成 asar 档案文件,这只需要对你的源代码做一些很小的改动。
大部分用户可以毫不费力地使用这个功能,因为它在 electron-packager,、electron-forge 和 electron-builder 中都得到了支持,开箱即用。

asar 的优点

  1. 缓解 Windows 下路径名过长的问题
  2. 略微加快 require 的速度
  3. 隐藏源代码

打包 View 层的代码

因为项目里面使用的 react,不能在浏览器中直接使用,所以需要使用 webpack 打包成浏览器能识别的静态资源,这和常规打包没什么区别,直接使用create-react-app提供的 build 命令即可。

1
npm run build

设置加载路径,把 electron 的加载页面路径设置成打包后文件的路径

1
2
3
const urlLocation = isDev
? "http://localhost:3000/"
: `file://${path.join(__dirname, "./build/index.html")}`;

electron-builder

我们使用 electron-builder 来打包。

  1. 配置
    electrong-builder 的配置比价简单,直接在 package.json 中进行配置。
    在根节点增加build项,在里面写相关的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    "build": {
    "appId": "electron-react-markdown",
    "productName": "七牛云文档管理",
    "copyright": "Copyright @ 2019-12 ${author}",
    // 这个默认设置了入口文件的名字,关闭会用我们自己的入口文件
    "extends": null,
    // 如果不同平台需要单独配置相关参数,可以增加相关的配置,比如下面是mac系统的配置
    "mac": {}
    }
  2. 增加 build 命令

1
2
3
4
5
"scripts": {
......
"pack": "electron-builder --dir",
"dist": "electron-builder"
},

pack 和 dist 的区别
pack 生成的是安装包安装后生成的那些文件。
dist 生成安装包

增加钩子
我们可以在执行命令的时候,指定先执行哪个命令,这个是 npm 提供的钩子;
方式是把相关命令的前面加上pre即可,比如下面是执行 pack 的时候,先执行 build

1
2
"pack": "electron-builder --dir",
"prepack":"npm run build"

自定义需要打包的文件

build 的 files 字段配置了打包时,哪些文件会被打包到程序中。它有自己的默认配置,但是一旦自定义了这个字段,默认的就不起作用了。
这里需要指定的文件是在 electron 进程中用到的文件,react 中用到的都已打包到 build 目录中,只需指定 build 目录即可。

1
2
3
4
5
6
7
8
9
10
11
12
"build":{
"files": [
"build/**/*",
"node_modules/**/*",
"settings/**/*",
"package.json",
"main.js",
"./menuTemplate.js",
"./src/AppWindow.js",
"./src/utils/QiNiuManager.js"
]
}

files 的配置说明

1
2
3
4
5
6
7
8
9
10
[
// match all files|| 匹配所有文件
"**/*",

// except for js files in the foo/ directory || 不匹配foo文件夹下js,其他文件仍能匹配
"!foo/*.js",

// unless it's foo/bar.js || 匹配foo/bar.js文件
"foo/bar.js"
]

设置路径

在 package.json 中设置

1
"homepage": "./",

打包

通过上面的配置,现在执行打包命令,因为 pack 生成的就是安装后的文件,所以可以进入文件夹直接打开使用。

1
npm run pack

查看安装包内容

我用的是 mac 系统,点击生成的安装包,右键–查看包内容,可以看到一个 Contents 文件夹,文件中包含了所有的文件。

生成安装包

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
"build": {
"appId": "electron-react-markdown",
"productName": "七牛云文档管理",
"copyright": "Copyright @ 2019-12 ${author}",
"extends": null,
"files": [
"build/**/*",
"node_modules/**/*",
"settings/**/*",
"package.json",
"main.js",
"./menuTemplate.js",
"./src/AppWindow.js",
"./src/utils/QiNiuManager.js"
],
"directories": {
// 指定静态资源的路径
"buildResources": "assets"
},
"mac": {
// mac电脑的应用程序有分类,这里指定安装到哪个分类下
"category": "public.app-category.productivity",
// 打出的安装包的名字
"artifactName": "${productName}-${version}-${arch}.${ext}"
},

"dmg": {
"background": "assets/appdmg.png",
"icon": "assets/icon.icns",
"iconSize": 100,
"contents": [
{
"x": 380,
"y": 280,
"type": "link",
"path": "/Applications"
},
{
"x": 100,
"y": 280,
"type": "file"
}
],
// 打开的安装窗口大小
"window": {
"width": 500,
"height": 500
}
},
"win": {
// 这里指定打包出两种类型的安装包
"target": [
"msi",
"nsis"
],
"icon": "assets/icon.ico",
"artifactName": "${productName}-Web-Setup-${version}.${ext}",
"publisherName": "温木"
},
"nsis": {
"allowToChangeInstallationDirectory": true,
"oneClick": false,
"perMachine": false
}
}

增加打包前执行 build

1
"predist":"npm run build"

打包

1
npm run dist

这就会根据你的环境生成对应的包,比如你是 mac 则生成 mac 对应的安装包。

优化安装包

上面虽然成功生成了安装包,但是体积比较大,通过生成的文件我们知道,主要有两个文件我们可以优化,一个是 app.asar,一个是 electron.asar,app.asar 中是我们业务程序的代码,electron.asar 是系统的,我们没法改变。
那能优化就是 app.asar.它包含的内容如下:

.
├── build
├── main.js
├── menuTemplate.js
├── node_modules
├── package.json
├── settings
└── src

asar extract app.asar tmp_app
使用上面的命令,把 app.asar 中的内容“解压”到 tmp_app 文件夹中
使用tree -L 1查看目录结构

我们知道node_modulesreact使用到的安装包,其实已经打包到了build文件夹中,是不需要再进行打包的。
由于electron-build不会对开发依赖项进行打包,因此可以把不需要打包的依赖项移动到开发依赖项中(开发过程中可能不能这么干,只能在打包的时候这么做)。

优化打包文件引入

1
2
3
4
5
6
7
8
9
10
"files": [
"build/**/*",
"node_modules/**/*",
"settings/**/*",
"package.json",
"main.js",
"./menuTemplate.js",
"./src/AppWindow.js",
"./src/utils/QiNiuManager.js"
],

现在配置了很多个打包文件,其实需要引入的就两类文件,一个是前端的,一个是 electron 的。前端的都已打包到 build 文件夹中。
而 electron 的 js 代码我们是一个一个单独引用的。我们也可以把他们打包到一起,这样就可以只引入一个文件了。

1.新建一个 webpack 的配置文件 webpack.config.js
webpack.config.js 是 webpack 的默认配置文件,因为前端是通过编写的代码进行打包的,所以并没有使用到这个配置文件,所以我们新建一个也不会对前端打包造成影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
const path = require("path");

module.exports = {
target: "electron-main",
entry: "./main.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "main.js",
},
node: {
__dirname: false,
},
};

注意:

  1. 配置 target 为electron-main
  2. 配置 node 设置dirname,教程说是dirname 默认返回一个斜杠,但我测试返回没问题,加不加这个配置都一样。

增加 build 的命令

1
"buildElectron": "webpack",

electron 代码打包到一起后需要修改的地方

  1. package.json 中的 build 的 files
    这是只需要设置打包后的 main.js 文件即可,但已配置了 build 目录,所以把 electron 的代码引用都删除即可。

    1
    2
    3
    4
    5
    6
    "files": [
    "build/**/*",
    "node_modules/**/*",
    "settings/**/*",
    "package.json"
    ],
  2. electron 的入口文件
    electron 默认读取的是 package.json 的 main 字段配置的文件,打包读取的也是这个,但是现在打包需要读取 build 中的 main.js,所以增加 build 的配置

    1
    2
    3
    "extraMetadata": {
    "main": "./build/main.js"
    },

    这样打包的时候就会读取这里指定的文件。

  3. 修改 npm 的钩子

1
2
"prepack": "npm run build && npm run buildElectron",
"predist": "npm run build && npm run buildElectron"
  1. 修改 main.js 文件
    因为生产环境 main.js 的路径和前端页面在同一个文件夹下,所以引用路径也发生改变
1
const urlLocation = isDev ? "http://localhost:3000/" : `file://${join(__dirname, "./index.html")}`;
  1. 把生产环境依赖项移动到开发依赖项中

自动更新

自动更新就是当每次打开软件时,会和远程存储下载资源的版本进行对比,如果有新版本,就提示自动更新。
既然要自动更新,那首先设置资源存储的地方并自动发布。electron-builder 提供了自动发布的功能,并且支持很多平台,其中我们喜欢的 GitHub 就在之列。具体详情可在官网的Publish模块查看。

自动发布

  1. 指定发布使用的平台
    electron-builder的配置项中增加publish配置。由官网可看出,publish的值可以是String | Object | Array<Object | String>,其实如果设置了 GitHub 的 token(GH_TOKEN),默认使用的就是 GitHub 平台。可以同时指定多个平台。
    在 package.json 中增加
    1
    2
    3
    4
    5
    "build":{
    "publish": ["github", "bintray"],
    // 或者如下
    "publish": [{provider: "github"}, {provider: "bintray"}]
    }
  2. 配置发布命令
    GitHub 的下载资源都在 release 模块,electron-builder提供了往 GitHub 的 release 模块推送静态资源的方式。
    官网给出了,使用npm script只需要增加release命令即可。这样每次生成安装包后,自动发布到 GitHub 上的 release。
1
2
3
4
5
"scripts": {
......
"release": "electron-builder",
"prerelease": "npm run build && npm run buildElectron"
},

上面配置完,还需要 github 的 token,不然 push 不上去。获取 token 的地址
点击头像->设置->Developer settings->Personal access tokens
然后生成一个 token 传给 electron-builder 即可

1
"release": "cross-env GH_TOKEN=d728e4bd5fefe3ba74881c0171284b85bb95cd6d electron-builder",

到目前为止,所有的配置就完成了。

1
npm run release

执行后,不仅重新生成了安装包,并且上传到了 GitHub 上,在项目的 release 模块可看到,生成的安装包文件都被上传了。
这时,上传的文件还是 draft 状态,也就是草稿状态,可以点击编辑,编写一些描述,然后点击发布,这时就成了发布状态;也可以点击保存成 draft。

自动更新

electron 本身也提供了程序更新的方式,但是官网也说了,如果使用 electron-builder 进行打包,可以使用 electron-updater 模块,它不依赖任何服务器并且可以从 S3, GitHub 或者任何其它静态文件存储更新. 这避开了 Electron 内置的更新机制,

安装 electron-updater

1
npm i electron-udpater --save-dev

然后在主进程中添加检查更新的代码,重新打包,然后点击生成的安装后文件中的程序,就可以看到效果。注意 GitHub 上的项目需要设置成公共的,私有的是获取不到的。

开发模式下测试自动更新

上面的配置都是在开发完后,打包成功后,点击安装包,然后查看效果,比较费劲,出了问题还的盲改后,再打包来看效果,效率比较低。
如果能在开发过程中就查看效果,那就很方便了。
上面说过,查看打包后自动更新,需要使用 release 命令,使用 dist 是会报错的,因为更新需要一个叫 app-update.yml 的文件告诉它一些更新需要的信息,比如用户名,仓库名等等。。
开发模式下,我们可以手动创建这个文件,名字前加上 dev,叫dev-app-update.yml

1
2
3
4
owner: codepandy
repo: electron_react_markdown
provider: github
updaterCacheDirName: electron-react-markdown-updater

上面内容是从正式生成的app-update.yml中 copy 的,最后一个可以不要。
然后在程序中判断,如果是开发模式,就使用我们创建的dev-app-update.yml配置文件。

文章作者: wenmu
文章链接: http://blog.wangpengpeng.site/2020/01/09/electron+react+%E4%B8%83%E7%89%9B%E4%BA%91%E5%BC%80%E5%8F%91%E7%AC%94%E8%AE%B0/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 温木的博客
微信打赏