1. 使用的脚手架
https://github.com/qdhaiqiang/shadow-electron-starter
2. 将原项目figwheel编译的浏览器版改造成electron版本
将干净的项目clone下来以后按顺序做如下改动。
2.1 移入原项目文件
- 移入
resouce
目录下的css和image文件,从resouce/public/
到resouce/public/
- 移入cljs文件
需要注意的是移入以后需要根据新的文件目录,全文替换namespace名称,无论以前前面是什么,都改成app.renderer
,这样项目的其他配置的地方就不用改了。
2.2 配置原项目依赖的版本
- 引入npm和cljs依赖
在根目录的shadow-cljs.edn文件中加入依赖
:dependencies [[re-frame "0.10.7"]
[reagent "0.8.1"]
[luminus-transit "0.1.1"]
[org.clojure/core.async "0.4.500"]
[kee-frame "0.3.3" :exclusions [metosin/reitit-core]]
[binaryage/devtools "0.9.10"]
[re-frisk "0.5.4.1"]
[metosin/reitit "0.3.9"]
[com.rpl/specter "1.1.3"]
[antizer "0.3.1"]]
2.3 使用figwheel时引入的几个cljsjs文件手动处理
其中,antizer是用antd早的cljsjs的一个轮子。
- cljsjs.antd问题:
参考:https://github.com/priornix/antizer/issues/21
自建cljsjs.antd文件,将
(ns cljsjs.antd
(:require ["antd" :as antd]))
(js/goog.exportSymbol "antd" antd)
- cljsjs.echarts问题
引入npm包
yarn add echarts
自建cljsjs.echarts文件
(ns cljsjs.echarts
(:require ["echarts" :as echarts]))
(js/goog.exportSymbol "echarts" echarts)
- moment问题
引入npm包
yarn add moment
namespace中增加
(require ["moment" :as moment])
代码替换
(.format (js/moment (js/Date.)) "YYYY-MM-DD")
# 替换成
(.format (moment (js/Date.)) "YYYY-MM-DD")
2.4 css和图片问题
- css问题
样式文件找到,删除原项目中样式的依赖,类似以下代码:
[:link {:rel "stylesheet" :href "/css/dhome.css"}]
将样式文件增加到index.html中
<link href="css/dhome.css" rel="stylesheet" type="text/css">
- 图片丢失问题
图片文件是路径问题,原来css里的
background-image: url('/img/notice.png');
改成:
background-image: url('img/notice.png');
原来cljs里的
[:img.headDCBoxImg {:src "/img/moban.svg"}]
改为:
[:img.headDCBoxImg {:src "img/moban.svg"}]
2.5 增加本来开发re-frisk显示
shadow-cljs.edn
的builds
配置中增加dev和devtools,配置时注意引入的re-frisk
的版本要和reagent的版本的对应性,可以参考re-frisk说明,最后的配置文件如下:
;; shadow-cljs configuration
{:source-paths ["src"]
:dependencies [[re-frame "0.10.7"]
[reagent "0.8.1"]
[luminus-transit "0.1.1"]
[org.clojure/core.async "0.4.500"]
[kee-frame "0.3.3" :exclusions [metosin/reitit-core]]
[binaryage/devtools "0.9.10"]
[re-frisk "0.5.4.1"]
[metosin/reitit "0.3.9"]
[com.rpl/specter "1.1.3"]
[antizer "0.3.1"]]
:dev-http {8080 "resources/public/"}
:builds {:main {:target :node-script
:output-to "resources/main.js"
:compiler-options {:optimizations :simple}
:main app.main.core/main}
:renderer {:target :browser
:output-dir "resources/public/js"
:asset-path "js"
:compiler-options {:optimizations :simple}
:devtools {:watch-dir "resources/public"
:ignore-warnings true
:preloads [re-frisk.preload]}
:dev {:closure-defines {"re_frame.trace.trace_enabled_QMARK_" true}}
:modules {:renderer {:init-fn app.renderer.core/init!}}}}}
3. 配置打包
3.1 打包命令
package.json
的scripts结构下增加:
"package-mac": "export CSC_IDENTITY_AUTO_DISCOVERY=false && npm run build && npx shadow-cljs release :main :renderer && npx electron-builder build --mac",
"package-win": "npm run build && npx shadow-cljs release :main :renderer && npx electron-builder build --win"
shadow-cljs.end
文件的builds
的:main
和:renderer
中都增加:compiler-options {:optimizations :simple}
,不用做js混淆,这样mac上打的包才能打开,否则打包后会提示如图错误
注意:mac上没有配置签名,所以在打包的命令行里要执行
export CSC_IDENTITY_AUTO_DISCOVERY=false
这个配置阻止签名。
- 打包后运行,启动打包程序,报下面的错
参考# How to refer a Google Closure function in ClojureScript?,将原代码:
(goog.string.format "%.1f" (/ (+ m 1) 10))))) (range 10))
修改为:
(:import
[goog.string format])
(format "%.1f" (/ (+ m 1) 10))))) (range 10))
这一点原因还没有搞明白,先放几个查出来相关说法的链接
https://github.com/bhauman/lein-figwheel/issues/222
3.2 搞下app图标
找个logo的png图片,最小是512x512的,命令为icon.png
,并将其在一些在线工具上转出一个icon.ico
和icon.icns
,放在项目根目录下创建出来的build
目录里。
# marvin @ 192 in ~/git/redcreation/alk-wxapi/alk-doctor/build on git:v_2.0 o [22:50:14] C:127
$ tree
.
├── icon.icns
├── icon.ico
└── icon.png
0 directories, 3 files
上面的图片也可以用这种命令自己转,话说mac还是很牛逼的。
sips -z 16 16 pic.png --out icon_16x16.png
sips -z 512 512 pic.png --out tmp.iconset/icon_512x512.png
3.3 手动检测新版本
在登录页面调用一个获取最新版本的接口,返回新版本及更新内容。然后跟当前app的版本进行比较。(注意此代码要在render进程中,在main进程中的话会提示个错误,错误和原因见:https://stackoverflow.com/questions/59789539/electronjs-referenceerror-navigator-is-not-defined)
(def current-version
(let [fs (js/require "fs")
package (.readFileSync fs "./package.json" "utf-8")]
(get (js->clj (.parse js/JSON package)) "version")))
(def mac? (->> (.-userAgent js/navigator)
(re-seq #"Mac")
(count)
zero?
not))
本地运行
完美。然而,打包以后
可能是我想简单了,文件都被打包到asar里了,目录结构也变了,打包以后不能用这种方式获取package.json
文件了。
也许有别的办法可以找到这个文件,因为毕竟electron的api能找到,说明肯定有办法,electron提供了一个 app.getVersion()的API,用这个来解决。
那么问题又来了,这个api只能在main进程中调用,怎么跟render打交道呢?在前人Tony.Yang老师的多番指导下我也总算是拼出来了,也就是消息订阅和监听机制,让main和renderer联起来。ipcMain实现从主进程到渲染进程的通信,ipcRenderer从渲染器进程到主进程的异步通信。主要实现步骤:
- 第一步:在main中注册
ipcMain
(defn main []
; CrashReporter can just be omitted
(.start crashReporter
(clj->js
{:companyName "MyAwesomeCompany"
:productName "MyAwesomeApp"
:submitURL "https://example.com/submit-url"
:autoSubmit false}))
(.on app "window-all-closed" #(when-not (= js/process.platform "darwin")
(.quit app)))
(.on app "ready" init-browser)
(.on ipcMain "app-msg" (fn [_ arg]
(js/console.log "arg-------" arg)
(case (.-cmd arg)
"get-app-version" (send-message "app-version" (.getVersion app))
"其他消息"))))
- 第二步:新建一个util,跟ipcMain的通信
;; electron render进程的内容引入
(defonce electron (js/require "electron"))
(defonce remote (.-remote electron))
(defonce web-contents (.-webContents remote))
(defonce browser-window (.-BrowserWindow remote))
(defonce ipc-render (.-ipcRenderer electron))
(defonce version (atom ""))
(defn current-version []
(.on ipc-render "app-version" (fn [_ v]
(reset! version v)))
(.send ipc-render "app-msg" (clj->js {:cmd "get-app-version"})))
- 第三步:使用
那个页面想用这个版本号,只要订阅一下这个namespace的version这个atom便可。如果发生订阅不到的情况,在路由入口处调一下current-version
也就可以了。
2021-01-12补充(没文化,真可怕)
其实没有必要这么麻烦,Tony告诉我render里也可以获取版本,于是我试了一下,果然前面有大半天的功夫在做无用功,在render里定义个函数就解决了的事:
(defonce electron (js/require "electron"))
(defonce remote (.-remote electron))
(defn get-version []
(let [version (.getVersion (.-app remote))]
(prn "render进程里获取到的当前版本:" version)
version))
4. 加入本地缓存
策略是将所有db的内容缓存到本地文件中,代码较多不全部贴了,主要是有两个错误提一下。
- 本地文件不存在
在render进程里读写文件时提示,文件或者目录不存在,这需要在main进程中调用render里的函数来初始化这个文件。需要调用的函数init-app-data
,整个file.cljs内容如下,包括文件创建、写入、读取。
(ns utils.file
(:require
[clojure.edn :refer [read-string]]))
(def fs (js/require "fs"))
(def file-exists? (.-existsSync fs))
(def mkdir (.-mkdirSync fs))
(defn read-file [file-path]
(.readFileSync fs file-path "utf-8"))
(defn write-file [file-path content]
(js/console.log "write at file-path:" file-path)
(.writeFileSync fs file-path content))
(def path (js/require "path"))
(def path-join (.-join path))
(goog-define dev? false)
(defn get-app-data-path []
(if dev?
"."
(let [platform (.-platform js/process)
home-dir (.. js/process -env -HOME)
app-data-dir (.. js/process -env -APPDATA)
app-name "eallergy-doctor"]
(case platform
"darwin" (path-join home-dir "Library" "Application Support" app-name)
"win32" (path-join app-data-dir app-name)
"linux" (path-join home-dir (str "." app-name))
(do (js/console.error "Unsupported platform!")
((.exit js/process) 1))))))
(defn get-path-for-file [f]
(path-join (get-app-data-path) f))
(defn file-path [file]
(path-join (get-app-data-path) file))
(defn app-data-exists? []
(if dev?
true
(file-exists? (get-app-data-path))))
(defn- write-init-db!
"only call on production env"
[]
(write-file (file-path "appdb.edn") (str {})))
(defn init-app-data []
(when (and (not dev?)
(not (app-data-exists?)))
(mkdir (get-app-data-path)))
(when (not (file-exists? (get-path-for-file "appdb.edn")))
(write-init-db!)))
(defn file->str [file]
(->> file
file-path
read-file
str))
(defn file->edn [file]
(->> file
file->str
read-string))
(defn read-db-from-disk [db]
(file->edn db))
文件不存在报错如下:
这需要在main里调用一下
init-app-data
即可。注意main线程是electron .
的时候调用的,所以编译完应该关闭electron窗口,重新启动。
- 另一个错误:require is not defined
在render中有如下代码:
(def fs (js/require "fs")
(def fs (js/require "electron")
运行时报错如图:
链接中乃正解,github初始化的配置里缺少了,需在main线程中创建窗口的配置中应该增加以下配置:
mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true
}
});
5. 增加自动更新
如果要实现自动更新,Tony推荐以下文章(这个项目没有实现自动更新,以后再补充):
5. 增加自动更新
如果要实现自动更新,Tony推荐以下文章(这个项目没有实现自动更新,以后再补充):
- https://www.electron.build/auto-update#private-github-update-repo
- electron-updater实现应用自动更新 手动更新过程
- Electron开发实战之记账软件14——自动更新
- electron应用自动更新
- Electron应用使用electron-builder配合electron-updater实现自动更新
自查资料: