Electron 和 React 双剑合璧做 markdown
使用 Electrong 、 React 和七牛云做一个在线 markdown。
环境准备
使用creact-react-app
创建 react,然后在里面添加 electron 的代码和配置即可。
react 和 electron 结合,需要启动两个服务,在 electron 的窗口中嵌入 react 启动的页面。
优化启动命令
- 一个命令窗口启动项目
- 当 react 启动后再启动 electron
- 关闭 create-react-app 启动时自动打开浏览器的功能
一个命令窗口启动项目
因为 electron 和 react 需要分别启动,所以每次打开两个命令窗口比较麻烦,可以使用Concurrently
插件的来实现这个功能。
Concurrently 的优点
- 跨平台(window 和 mac 对 npm 原生的多命令同时运行符号&支持不同)
- 控制台输出信息友好
- 一键停止启动的所有命令
当 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 | npm install electron-is-dev |
在 react 中使用 Node.js 的 API
前面说过,在 electron 的渲染进程中的 js 不仅可以使用前端的各种 API, 我们还可以使用 Node 的 API。
main.js 中启动 electron,并且创建窗口就是运行在主进程中,创建的各个窗口就是渲染进程,窗口中页面引用的 js 就是运行在渲染进程中的 js。
因此我们可以在页面的 js 中直接使用 node 的 api 对本地系统资源进行操作。
require 的小 bug
在网页的js
中使用require
关键字引入node
的api
,发现获取到的对象是空对象,在electron
的issues
中知道解决方法,把require
改成window.require
1 | // const fs = 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 cd Application\ Support
cd Application*Support
菜单功能
菜单也是主进程提供的 api,根据 electron 提供的 Menu 和 MenuItem 来创建菜单。
实现上线文菜单功能
所谓上下文菜单,主要是右键菜单。当点击右键时,触发上下文菜单事件contextmenu
事件,然后把通过 Menu 创建的菜单实例传递给要显示的窗口。
1 | const menu = new Menu(); |
获取菜单的数据
在点击菜单时,可以根据点击的元素来获取元素的属性,比如 id,但是我们可能需要更多的信息,我们可以自定义一些属性,然后通过节点的属性获取;这是老方法,html5 提供了更方便更规范的方法,即设置data-*
属性,通过这种方式设置的属性,可以通过HTMLElement.dataset
来获取他们。
1 | <div id="user" data-id="1234567890" data-user="johndoe" data-date-of-birth> |
判断元素是否用了某个样式 class
以前用 jquery,可以很方便做各种操作,其实现在原生的也提供了很多方便的 api。
元素的 classList 就是元素所有的样式类,并且提供了多种操作方法,判断是否包含某一个,可以简单实现
1 | node.classList.contains("classname"); // true | false |
menu 的事件
menu 触发的事件是在主进程中触发的,所以 menu 事件需要通过进程之间的通信方式来通信。
同一进程之间的通信使用 ipcMain 来进行,
1 | // 触发 |
在menuTemplate
中的 menu 事件都发送到了 main 中进行处理,其实可以直接处理,然后把结果返回给渲染进程。这里是为了统一。
主进程和渲染进程之间的通信是通过 主窗口的实例(mainWindow) 和 ipcRender,在主进程中使用 mainWindow.webContents.send(),在渲染进程中使用 ipcRender;
在渲染进程中,即在页面中可以直接使用 node 的 api 进行业务处理。
应用程序打包
为缓解 Windows 下路径名过长的问题, 略微加快一下 require
的速度以及隐藏你的源代码,你可以选择把你的应用打包成 asar
档案文件,这只需要对你的源代码做一些很小的改动。
大部分用户可以毫不费力地使用这个功能,因为它在 electron-packager,、electron-forge 和 electron-builder 中都得到了支持,开箱即用。
asar 的优点
- 缓解 Windows 下路径名过长的问题
- 略微加快 require 的速度
- 隐藏源代码
打包 View 层的代码
因为项目里面使用的 react,不能在浏览器中直接使用,所以需要使用 webpack 打包成浏览器能识别的静态资源,这和常规打包没什么区别,直接使用create-react-app
提供的 build 命令即可。
1 | npm run build |
设置加载路径,把 electron 的加载页面路径设置成打包后文件的路径
1 | const urlLocation = isDev |
electron-builder
我们使用 electron-builder 来打包。
配置
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": {}
}增加 build 命令
1 | "scripts": { |
pack 和 dist 的区别
pack 生成的是安装包安装后生成的那些文件。
dist 生成安装包
增加钩子
我们可以在执行命令的时候,指定先执行哪个命令,这个是 npm 提供的钩子;
方式是把相关命令的前面加上pre
即可,比如下面是执行 pack 的时候,先执行 build
1 | "pack": "electron-builder --dir", |
自定义需要打包的文件
build 的 files 字段配置了打包时,哪些文件会被打包到程序中。它有自己的默认配置,但是一旦自定义了这个字段,默认的就不起作用了。
这里需要指定的文件是在 electron 进程中用到的文件,react 中用到的都已打包到 build 目录中,只需指定 build 目录即可。
1 | "build":{ |
files 的配置说明
1 | [ |
设置路径
在 package.json 中设置
1 | "homepage": "./", |
打包
通过上面的配置,现在执行打包命令,因为 pack 生成的就是安装后的文件,所以可以进入文件夹直接打开使用。
1 | npm run pack |
查看安装包内容
我用的是 mac 系统,点击生成的安装包,右键–查看包内容,可以看到一个 Contents 文件夹,文件中包含了所有的文件。
生成安装包
配置如下:
1 | "build": { |
增加打包前执行 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_modules
中react
使用到的安装包,其实已经打包到了build
文件夹中,是不需要再进行打包的。
由于electron-build
不会对开发依赖项进行打包,因此可以把不需要打包的依赖项移动到开发依赖项中(开发过程中可能不能这么干,只能在打包的时候这么做)。
优化打包文件引入
1 | "files": [ |
现在配置了很多个打包文件,其实需要引入的就两类文件,一个是前端的,一个是 electron 的。前端的都已打包到 build 文件夹中。
而 electron 的 js 代码我们是一个一个单独引用的。我们也可以把他们打包到一起,这样就可以只引入一个文件了。
1.新建一个 webpack 的配置文件 webpack.config.js
webpack.config.js 是 webpack 的默认配置文件,因为前端是通过编写的代码进行打包的,所以并没有使用到这个配置文件,所以我们新建一个也不会对前端打包造成影响。
1 | const path = require("path"); |
注意:
- 配置 target 为
electron-main
- 配置 node 设置dirname,教程说是dirname 默认返回一个斜杠,但我测试返回没问题,加不加这个配置都一样。
增加 build 的命令
1 | "buildElectron": "webpack", |
electron 代码打包到一起后需要修改的地方
package.json 中的 build 的 files
这是只需要设置打包后的 main.js 文件即可,但已配置了 build 目录,所以把 electron 的代码引用都删除即可。1
2
3
4
5
6"files": [
"build/**/*",
"node_modules/**/*",
"settings/**/*",
"package.json"
],electron 的入口文件
electron 默认读取的是 package.json 的 main 字段配置的文件,打包读取的也是这个,但是现在打包需要读取 build 中的 main.js,所以增加 build 的配置1
2
3"extraMetadata": {
"main": "./build/main.js"
},这样打包的时候就会读取这里指定的文件。
修改 npm 的钩子
1 | "prepack": "npm run build && npm run buildElectron", |
- 修改 main.js 文件
因为生产环境 main.js 的路径和前端页面在同一个文件夹下,所以引用路径也发生改变
1 | const urlLocation = isDev ? "http://localhost:3000/" : `file://${join(__dirname, "./index.html")}`; |
- 把生产环境依赖项移动到开发依赖项中
自动更新
自动更新就是当每次打开软件时,会和远程存储下载资源的版本进行对比,如果有新版本,就提示自动更新。
既然要自动更新,那首先设置资源存储的地方并自动发布。electron-builder 提供了自动发布的功能,并且支持很多平台,其中我们喜欢的 GitHub 就在之列。具体详情可在官网的Publish模块查看。
自动发布
- 指定发布使用的平台
在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"}]
} - 配置发布命令
GitHub 的下载资源都在 release 模块,electron-builder
提供了往 GitHub 的 release 模块推送静态资源的方式。
官网给出了,使用npm script
只需要增加release
命令即可。这样每次生成安装包后,自动发布到 GitHub 上的 release。
1 | "scripts": { |
上面配置完,还需要 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 | owner: codepandy |
上面内容是从正式生成的app-update.yml
中 copy 的,最后一个可以不要。
然后在程序中判断,如果是开发模式,就使用我们创建的dev-app-update.yml
配置文件。