多肽项目总结
- 布局与样式
- SVG动画
- reagent & react
- shadow-cljs
- 逻辑实现
- 执行调度
- core.match简化条件判断
- 共享clj/cljs的代码
- core.async和javascript的Promise
- 优雅的开始/结束一个core.async的loop
- core.async的pub-sub
- core.async的监听多channel
- 使用taoensso.sente做websocket通讯
- 新增一个版本步骤
- 使用taoensso.timbre来记录log
- 语言的基本错误
- wirshark的使用
- electron
- windows上屏蔽menu
- 屏蔽快捷键
- 关键路径
- 补充本系统的目录内容
- 数据目录
- 安装目录
- 开发模式下
- 生产环境
- electron的版本变化和它的进程模型
- render进程的启动参数
- 指定目录
- 启动外部进程
- 打印
- 在render中通过remote对象结束应用
- electron-builder 打包
- electron的一些原生UI能力
- 应用自动升级(draft)
- osx下打包签名(draft)
- 安装/卸载界面的定制
- re-frame
- NPM体系
- Babashka脚本
- react中cljs 晦涩的错误(draft)
- 参考资料
布局与样式
系统使用material UI 4.0, 是一套reactjs的组件库, 支持灵活的风格定制.
Material的关键属性
MaterialUI支持(button, icon, tab, button-base)组件上的水波(ripple)效果. 所以我们在首页的色块上, 提供了这种观感.
所以我们可以扩展ButtonBase,来实现带有这个效果的组件.
[:> ButtonBase {:class "db w-100 h-100"
:focus-ripple true
:id (str "login" (= :logged-in login-state))
:on-click (fn []
(if (and (= :logged-in login-state)
(#{:running :breaked} @(rf/subscribe [:program-status])))
(utils/show-error "Program is running. Please abort the program first.")
(rf/dispatch [:effects.login/update-state fx])))}
[:> Box ...]]
styled functions
重点是其中的@material-ui/system提供的style functions, 可以轻松定制自己的风格化组件.
基于自适应的考虑, 我们在系统中自定义了BoxRpx这个组件, 用来处理这一情况:
(defn transform [value]
(if (number? value)
(if (<= value 1)
(str (* 100 value) "%")
(px-to-vw value))
value))
;; padding 举例
(def padding (style #js {:prop "paddingRpx"
:cssProperty "padding"
:transform transform}))
(def spacing (compose padding
padding-x
padding-y
padding-top
padding-bottom
padding-left
padding-right
margin
margin-x
margin-y
margin-top
margin-bottom
margin-left
margin-right))
(def sizing (compose width min-width max-width height min-height max-height))
(def typography (compose font-size
line-height
letter-spacing))
(def style-function (compose responsive/sizing
responsive/spacing
responsive/typography))
(def BoxRpx ((styled Box) style-function #js {:name "RpxMuiBox"}))
这样,我们就可以在系统中使用我们带有自定义props的组件了
[:> Box {:padding-rpx 32
:display "flex"
:flex-direction "column"
:justify-content "space-between"
:align-items "center"
:height 1}
[:> Box {:flex-grow 1
:width-rpx 100
:height-rpx 100
:display "flex"
:justify-content "center"
:align-items "center"}
[:img {:src icon
:class "flex-none"}]]
[:span {:class "b w-100 tl color-white font-size-body"} text]]
SVG动画
将html转成hiccup
svg源码的html语句,通过Calva语句转成hiccup,在clojure语法中可以使用. 注意:需要将转成的一部分内容属性进行修改,部分内容如下
[[":xmlns:xlink" ":xmlnsXlink"]
[":gradienttransform" ":gradientTransform"]
[":filterunits" ":filterUnits"]
[":xlink:href" ":xlinkHref"]
[":lineargradient" ":linearGradient"]
[":fegaussianblur" ":feGaussianBlur"]
[":stddeviation" ":stdDeviation"]
[":feoffset" ":feOffset"]
[":fecomposite" ":feComposite"]
[":fecolormatrix" ":feColorMatrix"]
[":radialgradient" "radialGradient"]]
svg的管线流向图及其他常用动画
管线流向图使用的属性:stroke-dashoffset
stroke-dashoffset属性: 这个属性在SVG中定义了虚线模式开始的位置. 通过改变这个值, 可以使得看起来虚线在路径上移动.- 动画过程:
from { stroke-dashoffset: 1000; }: 动画开始时,stroke-dashoffset被设置为1000. 这意味着虚线模式的开始位置被推迟了1000个单位, 使得虚线看起来在特定位置开始.to { stroke-dashoffset: 0; }: 动画结束时,stroke-dashoffset变为0. 这意味着虚线模式回到了其初始位置. 可以通过更改path的中xy的值来更改水流效果的流向问题.
降低CPU负载, 灵活展现管线图的液体流向. 动画内容可以参考下面嵌入的网页.
有关svg的实现运动的动画效果
思路:使用定时器,将图片进行切换,实现svg某一部分内容高亮旋转效果
;; 三张svg组件的传入
(defn mixing-tank-svg "配液罐单张svg, `idx` 是三张svg的index"
[idx svg]
(get svg (mod idx 3)))
;; 页面初始化的时候开启定时器,根据某一个状态进行判断render中是否开启某一高亮旋转的组件svg
(defn mixing-tank
"配液罐&激活罐"
[component s1s2s3]
(let [*s1s2s3? (rf/subscribe [:db/motors])
*stop? (r/atom false)
*idx (r/atom 0)]
(r/create-class
{:component-did-mount #(async/go-loop []
(async/<! (async/timeout 100))
(swap! *idx inc)
(when-not @*stop?
(recur)))
:reagent-render (fn []
(let [open? (= :close (get @*s1s2s3? :S1 :close))
opens2? (= :close (get @*s1s2s3? :S2 :close))
opens3? (= :close (get @*s1s2s3? :S3 :close))]
(cond
(= "s1" s1s2s3) [:g
[mixing-tank-svg (if open?
0
@*idx) component]]
(= "s2" s1s2s3) [:g
[mixing-tank-svg (if opens2?
0
@*idx) component]]
(= "s3" s1s2s3) [:g
[mixing-tank-svg (if opens3?
0
@*idx) component]]
:else nil)))
:component-will-unmount #(reset! *stop? true)})))
reagent & react
error-boundary
React 应用如果出现error, 整个应用会挂掉, 这个不是我们期待的行为. 在调试期间的error会crash整个程序, 而且不可恢复, 只能对网页进行全部载入, 相当的不友好.
幸而React16引入了error-boundary组件, 即实现了component-did-catch生命周期函数的组件的子组件的错误出现error的时候会被捕获和处理.
出现错误之后, 把错误修正, 代码会继续hotload, 并不打断开发流程.
error-boundary
(defn catch []
(defn error-boundary [comp]
(r/create-class
{:constructor (fn [this props]
(set! (.-state this) #js {:error nil}))
:component-did-catch (fn [this e info])
:get-derived-state-from-error (fn [error] #js {:error error})
:render (fn [this]
(r/as-element
(if-let [error (.. this -state -error)]
[:div
"Something went wrong."
[:button {:on-click #(.setState this #js {:error nil})} "Try again"]]
comp)))})))
error-boundary的使用:
[catch ;; 全局ui异常捕获
[root-component]]
error-boundary是个非常让人兴奋的特性, 很大提高了开发的便利, 全局增加error处理应该是它多种用法中的一个, 其他用法有待继续探索.
SetInnerHTML
dangerouslySetInnerHTML 是 React 中的一个特性, 用于在组件内部直接设置 HTML 内容.
直接操作 HTML 可能导致跨站脚本攻击(XSS)的风险, 因此 React 默认会转义所有从用户获得的内容.
dangerouslySetInnerHTML 提供了一种绕过这种默认行为的方式, 允许开发者直接在组件内插入原始 HTML.
我们使用这个特性在组件中插入svg(支持svg带动画)
[:div {:style {:height 100 :width 100}
:dangerouslySetInnerHTML {:__html "<svg >...</svg>\r\n"} }]
shadow-cljs
项目使用shadow-cljs进行(clojurescript侧)的依赖管理以及编译和打包.
编译过程中, shadow-cljs会把package.json中的依赖以及shadow-cljs中指定的cljs依赖统一编译处理
多目标编译
shadow-cljs支持多个编译目标, 在electorn开发中, 主进程和页面进程是两个目标(还有一个单元测试的目标).
:builds {:test {...}
:app {:target :browser
:compiler-options {:optimizations :simple}
:output-dir "public/compiled/cljs/"
:asset-path "/compiled/cljs"
:build-hooks [(resources.hooks/watch-resource)]
:modules {:main {:init-fn app.main/main!
:depends-on #{:resources}}
:resources {:entries [resources.helper]}}
:closure-defines {day8.re-frame.tracing.trace-enabled? true
re-frame.trace.trace-enabled? true}}
:electron {:target :node-script
:dev {:closure-defines {electron.main/dev? true
common.file-util/dev? true}}
:main electron.main/main!
:output-to "native/cljs/entry.js"}}
前端单元测试

在shadow-cljs.edn中配置test build
{:test {:target :browser-test
:compiler-options {:optimizations :simple}
:test-dir "resources/public/js/test"
:ns-regexp "-test$"
:devtools {:http-port 8021
:http-root "resources/public/js/test"}} }
在"resources/public/js/test"目录下创建index.html
<!DOCTYPE html>
<html><head><title>shadow.test.browser</title><meta charset="utf-8"></head><body><script src="/js/test.js"></script><script>shadow.test.browser.init();</script></body></html>
所有文件名符合正则表达 :ns-regexp "-test$"的文件都会认为是测试文件.
配置文件定义变量, 区分环境
在开发模式下, shadow-cljs.edn中,为main进程中的electron.main命名空间的变量dev? 进行赋值, dev? 这个变量在开发和部署模式下会有不同的值.
shadow-cljs.edn中:
{:builds {:electron {:target :node-script
:dev {:closure-defines {electron.main/dev? true}}
:main electron.main/main!
:output-to "native/cljs/entry.js"}}
main.cljs中:
(goog-define dev? false)
编译hook, 生成依赖资源
比如Less生成css
:build-hooks [(resources.hooks/watch-resource)]
逻辑实现
执行调度
系统使用core.async作为异步执行的调度的控制器.
即通过channle之间的依赖关系控制执行流程. 常见的channel有
- 普通channel即buffer为1的channel,
(chan). - 定时channel
(timeout 1000), 可以指定定时的时间. - 绑定特殊buffer的channel, 比如说互动窗口的channel
(chan (async/sliding-buffer 1)).
计时器控制某个步骤的等待时间. 我们对计时器进行了封装.
逻辑上计时器的组织
- 可以多个并行
- 可以多个串行
- 可以被打断, 恢复, 终止
- 可以组织为一个新的计时单元, 具有同样属性
单一的计时器

多个串行的计时器

多个并行的计时器

core.async对计时器的控制
单一的计时器

多个串行的计时器

多个并行的计时器

对于其他的channle, 我们也进行了响应的封装, 除了自身依赖关系之外, 也需要可以响应break和abort.
core.match简化条件判断
函数式编程中pattern是一个模式, 很大程度上能简化条件的复杂度.
下面的代码负责watch观察一个atom的变化情况, 从true->false/false->true的时候发通知.
(match [old-state new-state]
[true false] (send-emg-off-alert)
[false true] (send-emg-on-alert)
:else nil)
注意match的版本非常好的描述了这个变化, 而if的版本需要仔细读才能得到这个意思
(if (and (true? old-state) (false? new-state))
(send-emg-off-alert)
(when (and (false? old-state) (true? new-state))
(send-emg-on-alert)))
又或有多个条件判断, 但是结果有限的情况, match也直观很多.
(match [program-running? select-program? program-table?]
[true false _] [program-table]
[true true _] [function-table (name @program-name)]
[false _ true] [program-table]
[false _ false] [function-table (name @program-name)])
共享clj/cljs的代码
cljc后缀的代码供两边使用, #? 区分不同环境
(ns program-util
(:require
[clojure.edn :as edn]
#?(:cljs [common.file-util :refer [read-db-from-disk]])))
(defn get-current-speed-interval [db-file]
(let [db #?(:cljs (read-db-from-disk db-file)
:clj (edn/read-string (slurp db-file)))
speed (get-in db [:params :speed])
interval (get-in db [:params :interval])]
[(speed-limit speed) (interval-limt interval)]))
core.async和javascript的Promise
(:require
[cljs.core.async :refer [go]]
[cljs.core.async.interop :refer-macros [<p!]])
;; 下面有三个JS的promise, 我们通过<p!将之依次串行
(defn print-current-page [pdf-file-name]
(go
(let [webcontent ((.. electron -remote -getCurrentWebContents))
dialog-info (<p! (-> electron ;; promise 1: 拿到dialog的状态
(.-remote)
(.-dialog)
(.showOpenDialog
#js {:properties
#js ["openDirectory"]})))
canceled? (g/get dialog-info "canceled")
filePath (g/getValueByKeys dialog-info "filePaths" 0)
data (<p! (.printToPDF webcontent #js {}))] ;; promise 2 : 拿到要打印的数据
(when-not canceled?
(try
(prn filePath "..." canceled?) ;; promise 3: 开始打印
(<p! (.writeFile fs (path-join filePath (str pdf-file-name ".pdf")) data
(fn [error]
(if error
(js/console.log "error:" error)
(js/console.log "print successful@!!!!!")))))
(catch js/Error err (js/console.log (ex-cause err))))))))
优雅的开始/结束一个core.async的loop
(defn start-loop-query
"周期ms查询, 默认值是20"
[ms]
(reset! global-query-chan (chan))
(go-loop []
(alt!
(async/timeout ms) (do
(query-cmd-and-read-bytes)
(recur))
;;---------
@global-query-chan nil)))
(defn stop-loop-query []
(put! @global-query-chan "stop"))
core.async的pub-sub
一个pub多个sub的情况
;; publisher is just a normal channel
(def publisher (atom (chan)))
(defn renew-publisher []
(reset! publisher (chan)))
;; publication is a thing we subscribe to
(def publication
(atom (pub @publisher #(:topic %))))
(defn renew-publication []
(reset! publication (pub @publisher #(:topic %))))
(defn take-and-put-into-global-chan[publication]
(let [channel (chan)]
(sub publication :func-end-with-uuid channel)
(go-loop []
(<! channel)
(>! @global-chan " my heart will go on...")
(recur))))
(defonce func-finished-chan (chan))
(defonce func-begin-chan (chan))
(defn sub-func-end [publication]
(let [end-channel (chan)]
(sub publication :func-end-with-uuid end-channel)
(go-loop []
(put! func-finished-chan
(<! end-channel))
(recur))))
(defn sub-func-begin [publication]
(let [begin-channel (chan)]
(sub publication :func-begin-with-uuid begin-channel)
(go-loop []
(put! func-begin-chan
(<! begin-channel))
(recur))))
(defn sub-to-all []
(renew-publisher)
(renew-publication)
(sub-func-begin @publication)
(sub-func-end @publication)
(take-and-put-into-global-chan @publication))
core.async的监听多channel
(go-loop []
(let [[v ch] (alts! [func-begin-chan
func-finished-chan])]
(if (= ch func-begin-chan )
(send-step-begin v) ;; 来自begin channel
(send-step-finished v) )) ;; 来自finish channel
(recur))
使用taoensso.sente做websocket通讯
前后端无缝衔接, 非常愉快的使用体验
taoensso.sente 是一个用于 Clojure(Script) 的 WebSocket 库. 它提供了一种简单, 灵活和可靠的方式来构建基于 WebSocket 的实时应用程序, 如聊天应用, 实时协作工具等.
这部分的逻辑可以详述.
(def chsk-send! send-fn) ; ChannelSocket's send API fn
(defn send-emg-on-alert []
(debug "emergency break on")
(chsk-send! :taoensso.sente/nil-uid
[:server/emergency-stop-on nil]))
;; 这样, [:server/emergency-stop-on nil] 函数将会创建一个新的 Ring 处理器, 用于处理 WebSocket 连接.
;; :chsk/recv 选项来设置接收消息的事件处理器.
;; 需要编写消息处理器函数来处理接收到的消息. 这个函数将会接收到两个参数: 通道 ch 和消息 msg.
新增一个版本步骤
新增数据库
- 每个版本的function和基础数据内容都是通过
src/cljc/common/db中的文件进行配置 db-template中的内容在每次reset-db选择版本的时候都会重新写入db, 此处需要注意是否有其他依赖的数据库进行了同步操作, 比如本系统中的cn_dist
(defn main! []
(let [db-path (file-path (if dev? "p_server_dev.db" "p_server.db"))]
(rf/clear-subscription-cache!)
(run-query "select * from sys_db" db-path db-rows->edn db-callback)
(run-query "select * from cn_dict;" db-path rows->edn build-dict)
(rf/dispatch [:init-re-frame-pub-sub-delay])))
;; 解决方案: 在选择版本号的时候重新run-query一遍数据库
- 以870为例, 写入该版本的所有function,
db-template对象中的function即为所有function的的内容
;;functions
;;显示的函数名 {默认的参数名 默认的参数值}
{"A to RV" {:RV-Volume "100.0"
:Purge-Time "0.5"}
"B to AT" {:AT-Volume "100.0"
:Purge-Time "0.5"}
默认函数的方法处理
src/cljc/program_util.cljc文件中是关于函数名映射成方法名的主要文件,cljc文件是前后端可共用文件btn-name->func-name将初始化数据的函数名称映射成方法名称- 参数:
process-parameters处理函数名称的默认参数 - 溶液计算:
calc-reagent-consumption-inner溶液的消耗计算函数 - 时间计算:
calc-func-time-inner - 将处理之后的方法名映射到
peptide.clj中的f-mapping方法中
前后端函数的联调处理
- 主要:
taoensso.sente创建的一个websocket进行操作 - 前端通过调用封装的
:ws-event方法向后端发送一个函数, 后端调用该函数, 执行后端方法 - 后端
go-loop-recur方法, 循环监听一个channel是否产生变化, 如果产生变化, 则发送函数, 触发前端调用函数
;;前端向后端发送
(rf/reg-fx :ws-event
(fn [[msg-id event]]
(chsk-send! [msg-id event])))
(kf/reg-event-fx :open-pump
(fn [db [vs]]
:ws-event [:user/open-pump vs]))
;;后端接收
(defmethod -event-msg-handler :user/open-pump
[{:keys [event]}]
(let [[_ vs] event]
(open-pump vs)))
;;后端向前端发送
(defn pumps-chan (chan))
(defn open-pump
"泵开启"
[ps]
(put! pumps-chan [:open ps]))
;;监听
(defonce _pumps-chan
(go-loop []
(send-pumps-changes (<! pumps-chan))
(recur)))
;;发送
(defn send-pumps-changes [[open-or-close vs]]
(chsk-send! :taoensso.sente/nil-uid
[:server/send-pumps-changes [open-or-close vs]]))
这样就实现前后联调啦!
硬件指令联调
- 目前解析消息的字节序
- 发送是小端字节序
- 接收是大端字节序
接收plc传入的消息
通过go-loop函数,进行循环取plc传入的数据,此时会造成一个延迟,由于消息接收很频繁所以对系统会造成延迟.
方案:进行消息过滤,这一次与上一次数据进行比较如果无变化则不向前端下发内容
;; 1.定义一下内容对传入的信息进行解析
;; ubyte:表示无符号字节(Unsigned Byte), 它是一个 8 位无符号整数, 范围在 0 到 255 之间.
;; float32-be 表示大端(Big Endian)格式的 32 位浮点数, 即单精度浮点数. 它是一个 32 位的二进制数, 其中最高位表示符号位, 接下来的 8 位表示指数部分, 剩余的 23 位表示尾数部分.
;; 根据客户提供的plc传给上位机的内容,进行字符解析
(g/defcodec frame-synthesis
[:ubyte :ubyte :ubyte :ubyte :ubyte :ubyte :ubyte :ubyte
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be
:float32-be])
plc硬件指令下发
例如:通过某一个方法进行硬件指令下发
- 硬件指令内容为 "03 02 00 00 00 00"
- 传入的内容如果使用boolean则需要转成十六进制的字节
- gio/encode 主要生成16进制的字节数据和频率数据
- ubyte:表示了要编码的数据的格式,指定了要编码为一个无符号字节
- unit16-le:表示一个16位无符号整数
(defn inverter1-cmd
"上位机发给PLC 变频电机1控制
16#03 02 MB25 MB26 MB27
启动 := M25.0;
频率(HZ) MB26 MB27"
[open? freq]
(gio/encode [:ubyte :ubyte :ubyte :uint16-le :ubyte :ubyte]
[0x03 0x02 (flags->byte [open?]) freq]))
以上便可以实现,对传入的消息进行解析+下发硬件指令
使用taoensso.timbre来记录log
前后端都有很愉快的使用体验
语言的基本错误
还是会犯一些基础的语法错误, 嗯嗯...不丢人😂
case语句的判断条件不能是变量. 下面这个你猜结果是啥?
(let [v 2]
(case 2
1 "your input is 1"
v "your input is 2"
"not 1 or 2"
))
;;=> "not 1 or 2"
;; 补充:如果想在case匹配多个选择可以使用如下内容
(case id
(1 2 3) "1,2 or 3"
"default")
;; 如果传入的id为1或2或3则
;; => "1,2 or 3"
thread-macr使用匿名函数的时候不要忘记再外面加层括号.
因为使用但参数的函数的时候, thread-macro允许不加括号, 有的时候造成了误导.
(->> 10
inc ;; 单参数函数可以不加括号
range ;; 同上
(reduce +))
以下情况要注意, 都要加括号哟....
(->> 10
((partial + 1))
((comp range inc))
(#(filter even? %))
((fn [lst] (count lst))))
wirshark的使用
在联调过程中, 或者项目开发的过程中, 需要检查后台接口是否断联
可以使用
nmap内置的ncat如: 后台接口为
http://localhost:3000ncat localhost 3000wireshark抓包工具的使用
找到以太网链接启动的内容
过滤接口
tcp.port== 6688
electron
windows上屏蔽menu
设置BrowserWindow对象的MenuBarVisibility
(.setMenuBarVisibility win false)
屏蔽快捷键
electron包下的globalShortcut可以定制快捷键, regiseter函数的返回值是true或者false, 代表是否可以绑定这个快捷键.
(defn disable-hot-keys
"禁止打开开发者模式, 禁止使用快捷键刷新"
[]
(.register e/globalShortcut "Control+Shift+I" (fn [] #js{}))
(.register e/globalShortcut "Control+Shift+R" (fn [] #js{})))
关键路径
在桌面应用的设计中, 一般会区分数据目录和应用目录, 数据目录用来存储应用使用过程中产生的数据(数据库), 应用安装目录保存应用程序本身.
这个设计可以保证用户在升级/重装应用的时候, 数据可以得到保留.
同样的, 在我们当前的应用中, 对此也进行了区分, 但是事情的复杂性在于细节, 其中的一个细节在于应用的开发时和部署时, 这两个目录是不一样的.
还有一个细节是, 我们的应用是一个多进程应用, 即Electon主进程, Electonr的renderer进程和后台的JVM进程. 这几个进程在不同的场景中, 都要和 数据目录(中的数据库), 以及应用目录(日志)打交道. 而且各个进程中, 两个目录的获得方法也有差异.
最后还有就是Electron中是否启用asar压缩也决定了这两个目录的位置.
开发时
开发时, 数据目录和应用目录都是工程目录的根目录, 保证开发可以以最简单方便的方式进行.
部署时(windows系统)
- 用户home路径, 获得方式
(.. js/process -env -HOME), 在window对应用户的home目录, 比如C:\usrs\userX\ - 应用数据路径
(.. js/process -env -APPDATA), windows下对应用户目录下C:\usrs\userX\AppData\Roming\AppY\ - 应用程序exe路径
(.-execPath js/process), windows下对应"C:\Users\a123\AppData\Local\Programs\psi-workstation\Aurite-workstation.exe" - js代码所在的路径
__dirname, 对应入口js的所在路径, 因为renderer进程和main进程各有自己的入口js, 所有两者也有区分- main进程, 即使是asar的压缩文件中比如说
C:\Users\a123\AppData\Local\Programs\psi-workstation\resources\app.asar\native\cljs - renderer进程,
C:\Users\a123\AppData\Local\Programs\psi-workstation\resources\electron.asar\renderer
- main进程, 即使是asar的压缩文件中比如说
补充本系统的目录内容
数据目录
C:\usrs\userX\AppData\Roming\psi-workstation
安装目录
C:\usrs\userX\AppData\local\psi-workstation
开发模式下
后端
- 后端配置文件
dev-config.edn因本系统中使用多个版本及版本的不同信息在配置文件中, 添加字段:version区分不通用版本信息内容 - 后端读取文件路径及方式
a. 使用java的
System.getProperty读取的内容,如:System.getProperty("user.dir")读取的是当前
java -jar 文件名.jar所在的路径,如果是后台启动则启动的是后台所在的当前目录,可以通过emcas中的cider-connect-clj:local-host 7000链接本系统的后台,输入(clojrue.java.shell/sh "powershell" "pwd")来查看当前windows环境下后端所执行的目录,是否与getProperty获取的路径一致
System.getProperty与clojure.java.shell/sh命令获取的路径是一致的,开发环境直接使用环境中添加的sqlite命令和vl2svg命令进行更改- 注意:使用sh命令操作
powershell的时候需要powershell空格Invoke-Expression表达式
(sh "powershell" "Invoke-Expression" "'表达式'")
表达式中如果有双引号需要转义,使用三个反斜杠加双引号进行转移
生产环境
后端
- 后端配置文件
env/prod/resources/config.edn中的配置文件的内容是打包之后的后台读取的配置文件,可以看到在env/dev/resources同样有config.edn文件,但是由于开发环境中,读取dev-config.edn文件的优先级高于env/dev/resources/config.edn中的内容,所以开发环境中该文件为空对象 - System.getProperty
与clojure.java.shell/sh命令获取的路径是一致的 本系统中使用shell命令来执行powershell命令,其中vega和sqlite的exe安装包,在安装目录的resources目录下,所以执行win的生成环境时需要判断是否是win环境,添加为./resouces/sqlite3.exe和./resources/vega-cli.exe` - 打包的过程中如果需要将系统的内容打包成单独的文件或者exe模块,需要在
package.json中添加
"extraResources": [
{
"from": "./target/p-server-0.1.0-SNAPSHOT-standalone.jar",
"to": "./"
},
{
"from": "./sqlite3.exe",
"to": "sqlite3.exe"
},
{
"from": "./vega-report.exe",
"to": "vega-report.exe"
},
{
"from": "./pdfImg.png",
"to": "pdfImg.png"
},
{
"from": "./p_server_dev.db",
"to": "p_server.db"
}
]
前端
- 所在文件是当前目录,首先确认打包的时候是否是在安装目录下
C:\usrs\userX\AppData\local\psi-workstation前端读取安装目录文件需要用到(path-join js/__dirname "../../../" "temp-demo.pdf")
electron的版本变化和它的进程模型
本项目的开发从20年持续到23年, Electron版本跨度从10.x现在(23年11月)的28.x, 中间有18个大版本.
参考发布时间线
Electron 的发布周期是每 8 周发布一个新的稳定版本, 这是为了跟随 Chromium 的发布周期, Chromium 每 6 周发布一个新版本electronjs.org. 这意味着每年 Electron 会发布大约 11 个新的稳定版本.
需要注意的是, 这个周期是从 2021 年开始的, 之前的发布周期是每 12 周发布一个新的稳定版.
此外, Electron 的版本支持政策是最新的 3 个稳定版本, 但从 2021 年开始, 这个政策扩展到了最新的 4 个稳定版本electronjs.org. 这意味着 Electron 的开发者可以期待每个版本至少被支持 4 个月.
一方面, 如此剧烈的变动说明electron是个非常活跃的项目(值的拥有),另一方面又意味着对于开发者得不断适应API和参数的变化.
Electron的天赋能力是让浏览器中的js运行时可以使用nodejs的完整生态, 这是个超能力,后续的版本很多是对这个能力进行限制.
希望大家尽可能使用preload脚本来限制前端可用的api.
- contextIsolation 的变化
- remote模块在14后被完全移除掉
- nodeIntegration 的变化
render进程的启动参数
支持node的require
Browserwindow的启动参数设置
(ns electron.main
(:require
["electron" :as e]))
(e/BrowserWindow.
#js {:webPreferences #js {:nodeIntegration true}})
全屏幕
(e/BrowserWindow.
#js {:webPreferences #js {:fullscreen true
:simpleFullscreen true}})
是否保留菜单
(e/BrowserWindow.
#js {:webPreferences #js {:autoHideMenuBar (if dev? false true)}})
;; 测试环境保留menubar
禁止自动休眠/熄屏
工业系统执行过程中, 是要求不能熄屏和休眠的.
(ns electron.main
(:require
["electron" :as e]))
;; 系统禁止休眠和熄屏
(doto e/powerSaveBlocker
(.start "prevent-app-suspension" )
(.start "prevent-display-sleep" ))
指定目录
参考下面代码, 注意<p!来串联多个promise
(defonce fs (js/require "fs"))
(defonce electron (js/require "electron"))
(defn print-current-page [pdf-file-name]
(go
(let [webcontent ((.. electron -remote -getCurrentWebContents))
dialog-info (<p! (-> electron
(.-remote)
(.-dialog)
(.showOpenDialog
#js {:properties
#js ["openDirectory"]})))
_ (prn dialog-info)
_ (def _d dialog-info)
canceled? (g/get dialog-info "canceled")
filePath (g/getValueByKeys dialog-info "filePaths" 0)
data (<p! (.printToPDF webcontent #js {}))
]
(when-not canceled?
(try
(prn filePath "..." canceled?)
(<p! (.writeFile fs (path-join filePath (str pdf-file-name ".pdf")) data
(fn [error]
(if error
(js/console.log "error:" error)
(js/console.log "print successful@!!!!!")))))
(catch js/Error err (js/console.log (ex-cause err))))))))
启动外部进程
使用child-process的的exec或者spawn函数, 下面启动了根目录下的一个jar包
(ns electron.main
(:require
["child_process" :as cp]
[taoensso.timbre :as timbre :refer-macros (tracef debugf infof warnf errorf)]))
(.exec cp
(str "java -jar " "./my-server.jar" )
nil
(fn [error, stdout, stderr]
(when error
(errorf "启动发生了错误:%s" (.-stack error)))
(infof "执行结果:%s" stdout)
(when (.-lenght stderr)
(errorf "Error:%s" stderr))))
打印
打印整个web内容为pdf, 需要用到文件系统fs模块和electron模块
(defonce fs (js/require "fs"))
(defonce electron (js/require "electron"))
(defn print-current-page [pdf-file-name]
(let [webcontent ((.. electron -remote -getCurrentWebContents))]
(-> (.printToPDF webcontent #js {})
(.then (fn [data]
(.writeFile fs (str "./" pdf-file-name ".pdf") data
(fn [error]
(if error
(js/console.log error)
(js/console.log "print successful@!!!!!"))))))
(.catch (fn [error]
(js/console.log error))))))
在render中通过remote对象结束应用
(defn close-app []
( -> electron
(.-remote)
(.getCurrentWindow)
(.close)))
electron-builder 打包
electron-builder 是electron的一站式打包方案, 功能上包括了高度定制化的增加icon, 打包,自动升级等功能, 当前的使用只能算得上是浅尝辄止.
icons
在根目录下创建build目录, 放入mac的icns后缀的图标, windows平台下的ico或者png后缀图标, linux下的png图标. 打包生成的应用就有图标了.
当前windows已经支持png作为应用图表, 如下配置可以增加icon. 注意icon生成是个打包期间的行为, 目录public就是当前项目根目录下的public目录
"win": {
"target": "nsis",
"icon": "public/icon.png"
}
electron的一些原生UI能力
结合repl, 可以玩一天
(comment ;; 创建一个无边框的窗口
(let [board-less-win (e/BrowserWindow. #js { :width 800 :height 100, :frame false })]
(.show board-less-win)))
(comment
(.setFullScreen @main-window true) ;; 全屏幕
(.setSimpleFullScreen @main-window true) ;; 全屏幕
(.maximize @main-window) ;; 窗口最大化
(.minimize @main-window) ;; 最小化
(.restore @main-window) ;; 从docker恢复
(.getContentSize @main-window ) ;; 拿到content大小
(.isAlwaysOnTop @main-window ) ;; 始终最前
(.setProgressBar @main-window 0.5) ;; 设置一个docker icon上的进度条
(.setBadgeCount e/app 10 )) ;; 设置一个角标
应用自动升级(draft)
osx下打包签名(draft)
安装/卸载界面的定制
NSIS通常用作Windows平台的安装解决方案, 通常与electron-builder或其他打包工具一起使用. electron-builder提供了一个简化的接口来定义NSIS安装程序的行为和外观, 这样开发者无需手动编写复杂的NSIS脚本.
NSIS(Nullsoft Scriptable Install System)是一个开源的工具, 用于创建Windows安装器. 它最初是由Nullsoft公司开发的, 该公司也是Winamp媒体播放器的开发者. NSIS允许开发者构建具有各种定制选项和复杂逻辑的安装程序.
可以在electron-builder的配置文件中(通常位于项目的package.json)指定各种NSIS设置, 以定制安装程序的行为. 这包括定义安装和卸载程序的图标, 是否允许用户更改安装目录, 安装过程中的自定义脚本等.
re-frame
使用拦截器->interceptor在事件处理的前/后增加逻辑
拦截器截获context(re-frame中最大的数据结构, 是cofx的的爹), 可以拿到/修改db, event, cofx.
下面是个针对多有的参数修改事件记录log的例子, 在事件处理之前截获,并且记录修改的值.
(def log-change-params-event
(rf/->interceptor
:id :log-event
:before (fn [context]
(def _context context)
(let [user (get-in context [:coeffects :db :login :username])
event (get-in context [:coeffects :re-frame.std-interceptors/untrimmed-event])
id (first event)
val (second event)]
(rf/dispatch
[:write-log (str "USER_ACTION: user " user " changed parameter " (name id) " to: " val)]))
context)))
;;------------------------------------------
(run!
(fn [e]
(kf/reg-event-db
(:id e)
[log-change-params-event]
(fn [db [val]]
(assoc-in db (:path e) val)))
(rf/reg-sub (:id e)
(fn [db]
(get-in db (:path e)))))
params-meta)
reg-sub 很灵活
reg-sub的完全体:
(reg-sub
:query-id
signal-fn ;; <-- here
computation-fn)
注意:
- singal-fn <---- 不写明就是 (fn [_ _] re-frame/app-db), 返回db
- coputation-fn <---- 我们熟悉的匿名函数, 其实它非常灵活
signal-fn 返回一个vector, 接下来这个vector会传给计算函数
(fn [query-vec dynamic-vec]
[(subscribe [:a-sub])
(subscribe [:b-sub])])
计算函数的签名是这样子:
(fn [[a b] query-vec] ;; 1st argument is a seq of two values
....)
其实我们平时用的是一个特殊情况, 等价与下面:
(reg-sub
:query-id
(fn [_ _] re-frame/app-db) ;; <--- explicit input-fn
a-computation-fn) ;; has signature: (fn [db query-vec] ... ret-value)
灵活传参, 可以省掉很多不必要的reg-sub.
(rf/reg-sub :uuid-end-time
(fn [db [_ uuid]]
(get-in db [:step-uuids :end uuid])))
;;-----------
(rf/subscribe [:uuid-end-time uuid])
NPM体系
NPM上的包是非常豪放的, 某些版本的包有bug很正常
这是具体的包的作者的错, 不是NPM的错, 开发过程中遇到过
- shadow-cljs 2.10.19 不能读取目录下的shadow-cljs.edn配置文件
- material-ui/lab 4.0.0-alpha.56 的Alert控件不能正常编译
- ....
不好怀疑自己, 多多尝试吧, 现阶段前端的包测试不充分是常态.
package.json 和 package-lock.json
根据我的粗浅理解, package.json 只是定了个版本的范围, 即必须大于4.8.3
{"@material-ui/core": "^4.8.3"}
package-lock.json记录了我的实际安装版本, 是4.11.0
"@material-ui/core": {
"version": "4.11.0",
"resolved": "https://registry.npm.taobao.org/@material-ui/core/download/@material-ui/core-4.11.0.tgz",
"integrity": "sha1-tpsm5FU8nlPyv68QU+IWoK+b4Vo=",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.10.0",
"@material-ui/system": "^4.9.14",
"@material-ui/types": "^5.1.0",
"@material-ui/utils": "^4.10.2",
"@types/react-transition-group": "^4.2.0",
"clsx": "^1.0.4",
"hoist-non-react-statics": "^3.3.2",
"popper.js": "1.16.1-lts",
"prop-types": "^15.7.2",
"react-is": "^16.8.0",
"react-transition-group": "^4.4.0"
}
},
如果想明确指定某个版本, 可以在package.json中写明版本号, 注意没有^号:
"@material-ui/lab": "4.0.0-alpha.50",
MaterialUI的lab包
主要分为三个依赖包
- core
- icons
- lab
留意下会发现lab的包都是alpha, 也就都是探索版, MaterialUI中的很多新特性/控件会先放到lab包中, 稳定以后向core迁移. 这就会导致alpha中的很多内容是不稳定的.
尽可能小范围的引用
发现在开发的时候, 要编译七千多文件, 不仅编译慢, 执行也会受拖累, 打包也会大很多.
[:app] Compiling ...
[:app] Build completed. (1677 files, 0 compiled, 0 warnings, 1.32s)
Bisect一下发现某个版本上引入了整个icons包.
["@material-ui/icons" :as icons]
删掉之后
[:app] Compiling ...
[:app] Build completed. (7229 files, 15 compiled, 0 warnings, 8.09s)
[:app] Compiling ...
所以还是要用什么引什么, 不光对于icon包, 所有的包都适用.
["@material-ui/core/IconButton" :default IconButton]
["@material-ui/icons/KeyboardArrowUp" :default KeyboardArrowUp]
["@material-ui/icons/KeyboardArrowDown" :default KeyboardArroDown]
Babashka脚本
bb.edn
配置tasks跨平台/全功能的任务执行
➜ p-server git:(master) ✗ bb tasks
The following tasks are available:
clean-up kill all running backedn & plc
init-db reset db to its clean state
delete-shadow delete .shadow-cljs
compile-cljs compile cljs target app & electron
compile-css webpack compile css
welcome 欢迎
backend start running lein run with `log-file-name`
plc run plc simulator
build-ui build ui, 1. delete .shadow-cljs 2. update css 3. build app electron 4. build-package
ui-dev 以开发模式启动UI
build-package update package commit & date time
test 执行所有的UI单元测试
psi switch back to psi-station
cleavage switch to cleavage-station
etaion 驱动Electron 前端测试
- 启动/停止 electron
- 写入org-mode的测试报告
- 使用
js-execute执行js脚本, 拿到re-frame.db/app-db来进行各类判断 - 使用
npre-client控制后台服务
使用Babashka而不是clojure做脚本,更快的启动和测试.
具体的实现内容,可以查看单元测试blog 以下是一个前端的保存点击的例子
;;clojure.test 中的,为了保证每次测试的独立性 :each在每个deftest前都要执行一次传入函数
;; compose-fixtrue 谁现在前先执行谁 先执行 backend 清日志 db electron
(use-fixtures :each (compose-fixtures
(compose-fixtures
backend-fixture
db-fixture)
electron-fixture))
(report/deftest-with-rpt Program页面测试
(screenshot report/*test-name* "初始化页面")
(api/wait driver 1)
(select-versin-if-needed driver 870) (screenshot report/*test-name* "选择进入870")
(login driver)
(screenshot report/*test-name* "用户登录")
(goto-program driver)
(report/testing-with-rpt
"保存一个程序"
;;普通的打印日志
(org-insert-txt report/*test-name* "** 编写并保存program")
;; 截图 + 备注
(screenshot report/*test-name* "进入program界面")
;; 等待
(api/wait-exists driver {:id "A to RV"})
;; 双击
(api/double-click driver {:id "A to RV"})
;; 单击
(api/click driver {:id "more"})
;; 等待点击
(api/click-visible driver {:id "save"})
(api/wait-visible driver {:id "name"})
;; 模拟输入
(api/fill-human driver {:id "name"} "AtoRV")
(api/click-visible driver {:id "saveProgram"})
;;生成一个列表的assert语句 true/false + 备注
(report/is-with-rpt (boolean (get-in (get-state driver) [:programs :AtoRV])) (str "保存" "A to RV" "函数成功"))
(screenshot report/*test-name* "保存程序")
;; 按esc键
(api/fill-active driver keys/escape)
(goback driver)
(goto-sync-ctrl driver)
(api/wait-visible driver {:tag :td :fn/has-text "AtoRV"})
(api/double-click driver {:tag :td :fn/has-text "AtoRV"})
;;开始执行程序之前打开watcher
(screenshot report/*test-name* "准备执行程序")
(api/click-visible driver {:id "START"})
(api/wait-visible driver {:id "pass"})
;; 删除键
(api/fill-human driver {:id "pass"} (repeat 10 keys/backspace))
(api/fill-human driver {:id "pass"} "admin")
(api/fill-active driver keys/tab)
(api/fill-active driver keys/tab)
(api/fill-active driver keys/enter)
(org-insert-txt report/*test-name* "执行function A to RV")
(let [[func-name every-step] (log-handler "log/trace-rolling.log" 50)]
(loop []
(let [str-list (every-step)
str-log (second (string/split str-list #" - "))
operat-num (second (string/split str-log #": "))
;;FIXME:仅限某一个funtion/program还需更改
end-flag (= "Ends" (last (string/split str-log #" ")))]
(org-insert-txt report/*test-name* (str "function执行的步骤" str-list))
(report/is-with-rpt (boolean operat-num) str-log)
(api/click-visible driver {:id "Information"})
(screenshot report/*test-name* "程序执行变化步骤")
(when-not end-flag
(recur)))))
(api/click-visible driver {:id "Reagent Consumption"})
(screenshot report/*test-name* "溶液内容")
)
(report/testing-with-rpt
"第二个function的内容"
(org-insert-txt report/*test-name* "43个function点击auto的内容")
)
)