Electronhttps://www.electronjs.org/docs
electron-builder:打包工具

项目github地址:
https://github.com/electron/electron
https://github.com/electron/electron-quick-start

Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。

electron 桌面开发工具 与用户操作系统和node的集成方法文档
https://www.electronjs.org/zh/docs/latest/tutorial/examples

一些开发经验:

创建 BrowserWindow 窗口

const {app, BrowserWindow} = require('electron')

let remoteURL, debug

debug = !app.isPackaged
if (debug) {
  remoteURL = 'https://www.xxx.com/notifyDeskApp/login.html';
  //忽略证书的检测
  app.commandLine.appendSwitch('ignore-certificate-errors')
} else {
  remoteURL = 'https://www.xxx.com/notifyDeskApp/login.html';
}

let trayIcon = path.join(__dirname, "icon", "notify-logo.ico");

let mainWindow, logined, quit

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 400,
    height: 300,
    fullscreenable: false,
    maximizable: false,
    icon: trayIcon,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  mainWindow.setAppDetails({
    appIconPath: trayIcon,
  })

  // and load the index.html of the app.
  mainWindow.loadURL(remoteURL)
  // mainWindow.loadFile('index.html')

  mainWindow.on('minimize',(e)=>{
    if (logined) {
      mainWindow.hide();
    }
  })

  mainWindow.on('close',(e)=>{
    if (logined && !quit) {
      e.preventDefault()
      mainWindow.hide();
    } else {
      app.quit()
    }
  })

}

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()
  })

})

显示隐藏窗口

  mainWindow.hide();
  mainWindow.restore()

渲染进程与主进程通信
创建窗口的时候配置 preload.js, main.js

  mainWindow = new BrowserWindow({
    width: 400,
    height: 300,
    fullscreenable: false,
    maximizable: false,
    icon: trayIcon,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

窗口渲染页面与preload通过window事件,来传递事件消息
窗口加载的网页中的js事件

            //客户端登录事件:字段名称说明是否可选类型默认值detail表示该事件中需要被传递的数据,在 EventListener 获取。可选Anynullbubbles表示该事件是否冒泡。可选Booleanfalsecancelable表示该事件能否被取消。可选Booleanfalse
            let eventInit = {bubbles: false, cancelable: false, detail: {token: token, trueName: trueName}}
            let loginedEvent = new CustomEvent('logined', eventInit)
            window.dispatchEvent(loginedEvent)

preload.js

const {ipcRenderer} = require('electron');

window.addEventListener('logined', (e) => {
    ipcRenderer.send('logined', e.detail);
})

window.addEventListener('logout', (e) => {
    ipcRenderer.send('logout', e.detail);
})

window.addEventListener('open-url', (e) => {
    ipcRenderer.send('open-url', e.detail);
})

window.addEventListener('exit', (e) => {
    ipcRenderer.send('exit', e.detail);
})

main.js

const {app,shell} = require('electron')
ipcMain.on('logined', (e, data) => {
  logined = true
  mainWindow.hide();
})

ipcMain.on('open-url', (e, data) => {
  console.log(data)
  shell.openExternal(data.url);
})

ipcMain.on('logout', (e, data) => {
  logined = false
  mainWindow.restore()
})

ipcMain.on('exit', (e, data) => {
  logined = false
  quit = true
  app.quit()
})

托盘

const {app, BrowserWindow, Menu, Tray , ipcMain, shell} = require('electron')

let tray = null

const showMainWindow = (menuItem, browserWindow, event) => {
  if (mainWindow) {
    mainWindow.restore()
    mainWindow.show()
    mainWindow.focus()
  }
}

const appExit = (menuItem, browserWindow, event) => {
  quit = true
  app.quit()
}

const openDevTools = (menuItem, browserWindow, event) => {
  // Open the DevTools.
  if (mainWindow)  mainWindow.webContents.openDevTools({ mode: 'detach' })
}

let firstrun = process.argv[1] == '--squirrel-firstrun'

const setAutoOpen = function () {
  let autoOpenSet = app.getLoginItemSettings()

  let option = {
    openAtLogin: !autoOpenSet.openAtLogin, // Boolean 在登录时启动应用
    openAsHidden: !autoOpenSet.openAsHidden, // Boolean (可选) mac 表示以隐藏的方式启动应用。~~~~
  }
  if (!debug) app.setLoginItemSettings(option)
  tray.displayBalloon({iconType: 'info', title: '操作提示', content: '开机自启动: ' + (option.openAtLogin ? '已开启' : '已关闭')})
}


app.whenReady().then(() => {

  //托盘
  tray = new Tray(trayIcon)
  const contextMenu = Menu.buildFromTemplate([
    { label: '显示主窗口', type: 'normal', click: showMainWindow },
    { label: '开机自启动', type: 'normal', click: setAutoOpen, visible: !debug },
    { label: '调试窗口', type: 'normal', click: openDevTools, visible: debug },
    { label: '退出客户端', type: 'normal', click: appExit },
  ])
  tray.setToolTip(app.name)
  tray.setContextMenu(contextMenu)

})

开机自启动

app.setLoginItemSettings({
    openAtLogin: true, // Boolean 在登录时启动应用
    openAsHidden: true, // Boolean (可选) mac 表示以隐藏的方式启动应用。~~~~
  })

开发与正式环境判断: 打包后 app.isPackaged 为 true

debug = !app.isPackaged

使用系统默认的浏览器打开一个网页,而不是客户端新开一个窗口

const {shell} = require('electron')
  shell.openExternal(data.url);

只允许开一个程序

const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  return app.quit()
}

修改桌面通知时的程序标题

app.setAppUserModelId('myapp name')

electron-builder:配置参考 (electron-vue, package.json)

{
  ...
  "build": {  // electron-builder配置
    "productName":"xxxx",//项目名 这也是生成的exe文件的前缀名
    "appId": "xxxxx",//包名  
    "copyright":"xxxx",//版权  信息
    "compression": "store", // "store" | "normal"| "maximum" 打包压缩情况(store 相对较快),store 39749kb, maximum 39186kb
    "directories": {
        "output": "build" // 输出文件夹
    }, 
    "asar": false, // asar打包
    "extraResources":  { // 拷贝dll等静态文件到指定位置
        "from": "./app-update.yml",
        "to": "./b.txt"
    },
    "win": {  
        "icon": "xxx/icon.ico"//图标路径,
        "extraResources":  { // 拷贝dll等静态文件到指定位置(用于某个系统配置)
            "from": "./app-update.yml",
            "to": "./b.txt"
        }
    },
    "nsis": {
        "oneClick": false, // 一键安装
        "guid": "xxxx", //注册表名字,不推荐修改
        "perMachine": true, // 是否开启安装时权限限制(此电脑或当前用户)
        "allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
        "allowToChangeInstallationDirectory": true, // 允许修改安装目录
        "installerIcon": "./build/icons/aaa.ico", // 安装图标
        "uninstallerIcon": "./build/icons/bbb.ico", //卸载图标
        "installerHeaderIcon": "./build/icons/aaa.ico", // 安装时头部图标
        "createDesktopShortcut": true, // 创建桌面图标
        "createStartMenuShortcut": true, // 创建开始菜单图标
        "shortcutName": "xxxx" // 图标名称
    }
  }
  ...
}

https://github.com/joewalnes/websocketd

这个工具可以把普通命令行命令转成ws协议,web客户端可以通过websocket连接,互换数据;
当有连接过来的时候,会启动进程执行指定的命令,如果命令结束或者连接断开,则连接关闭,进程结束;
连接后,将客户端send过来的数据,作为STDIN标准输入,传入进程,进程的标准输出STDOUT会作为message消息发送给客户端(\n结束作为一条消息);
启动服务:

cat startNotify.sh

#!/usr/bin/env bash
websocketd --port=1234 ./demp.sh

./startNotify.sh
#或者后台运行
nohup ./startNotify.sh > /dev/null 2>&1 &

一个发什么回什么的服务端脚本代码大概这样子:demp.sh

#! /pathto/php
<?hp

while (!feof(STDIN)) {
    $line = fgets(STDIN);
    echo $line;
}

?>

建立websocket连接,web网页

<script>
        var ws = new WebSocket('ws://127.0.0.1:1234')
        ws.onopen = function () {
            console.log('ws connected')
            ws.onclose = function () {
                console.log('ws closed');
            }
            ws.onmessage = function (res) { console.log(res.data) }

            var count = 0;
            
            setInterval(function () {
                ws.send(count++)
            }, 2000)
        }
        ws.onerror = function (e) {console.log(e)}
</script>

ssl网页,要用wss协议连接,通过nginx反代吧

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream notify_ws {
    server 127.0.0.1:1234; # appserver_ip:ws_port
}

server
{

location / {

         proxy_pass http://notify_ws;
         proxy_read_timeout 300s;
         proxy_send_timeout 300s;
         
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection $connection_upgrade;
         
}

}

关键在于这两行:
最重要的就是在反向代理的配置中增加了如下两行,其它的部分和普通的HTTP反向代理没有任何差别。

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

这里面的关键部分在于HTTP的请求中多了如下头部:

Upgrade: websocket
Connection: Upgrade

这两个字段表示请求服务器升级协议为WebSocket。服务器处理完请求后,响应如下报文:

# 状态码为101
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: upgrade

告诉客户端已成功切换协议,升级为Websocket协议。握手成功之后,服务器端和客户端便角色对等,就像普通的Socket一样,能够双向通信。 不再进行HTTP的交互,而是开始WebSocket的数据帧协议实现数据交换。

这里使用map指令可以将变量组合成为新的变量,会根据客户端传来的连接中是否带有Upgrade头来决定是否给源站传递Connection头, 这样做的方法比直接全部传递upgrade更加优雅。

默认情况下,连接将会在无数据传输60秒后关闭,proxy_read_timeout参数可以延长这个时间。源站通过定期发送ping帧以保持连接并确认连接是否还在使用。

两个超时参数

proxy_read_timeout

语法 proxy_read_timeout time 默认值 60s 上下文 http server location 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。 这个时间不是获得整个response的时间,而是两次reading操作的时间。

proxy_send_timeout

语法 proxy_send_timeout time 默认值 60s 上下文 http server location 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。 如果超时后,upstream没有收到新的数据,nginx会关闭连接

多次代理转发
工作中遇见过一种情况,就是某个域名在移动网络下面访问不了,这样的话我需要通过一个前段代理服务器做转发,这样就涉及到两次代理。

比如访问的websocket服务URL为:

wss://test.enzhico.net

这个在腾讯云公网IP上面,所有网络都能访问。另外一个域名board.xncoding.com解析到电信网络,部署在网关中心,这个域名腾讯云可以访问到。

在腾讯云主机上面:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
     server_name test.enzhico.net;
     location / {
         proxy_pass http://board.xncoding.com;
         proxy_read_timeout 300s;
         proxy_send_timeout 300s;
         #proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection $connection_upgrade;
    }
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/test.enzhico.net/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/test.enzhico.net/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

上面唯一要注意的是忙,把proxy_set_header Host $host;这一行注释掉了。

而在网关中心主机上面:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream websocket {
    server localhost:8282; # appserver_ip:ws_port
}

server {
    listen 80;
    server_name board.xncoding.com;
    location / {
        proxy_pass http://websocket;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

只需要最外层使用wss协议,里面的交互都使用ws协议,所以监听80端口即可。

参考文章:
https://www.xncoding.com/2018/03/12/fullstack/nginx-websocket.html
https://www.hi-linux.com/posts/42176.html

centos8安装docker 和 docker-compose

cd /etc/yum.repos.d/
wget     https://download.docker.com/linux/centos/docker-ce.repo
yum install docker-ce docker-ce-cli containerd.io
docker --version

systemctl status docker
systemctl enable docker
systemctl start docker


cd ~/
wget -O /usr/bin/docker-compose https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64
chmod a+x /usr/bin/docker-compose

登录自己喜欢的clash服务平台,下载clash配置文件

局域网使用别忘记把config.yaml中的这个开关打开

allow-lan: true

docker-compose.yaml

version: '3'
services:
  clash:
    image: dreamacro/clash
    volumes:
      - ./config.yaml:/root/.config/clash/config.yaml
    ports:
      - "9090:9090"
      - "7890:7890"
      - "7891:7891"
      # If you need external controller, you can export this port.
      # - "8080:8080"
    restart: always
    # When your system is Linux, you can use `network_mode: "host"` directly.
    # network_mode: "bridge"
    container_name: clash
  clashui:
    image: haishanh/yacd
    ports:
      - "1234:80"
    restart: always
    # network_mode: "bridge"
    container_name: classui

config.yaml和docker-compose.yml文件放在一个目录中,执行如入命令

sudo docker-compose up -d

修改了配置文件,重启容器

sudo docker-compose restart

访问UI面板(局域网中,12是虚拟机IP)
http://192.168.10.12:1234
添加接口地址 192.168.10.12:9090
选择好节点后,浏览器用192.168.10.12:7890 即可上网

文档
https://kkfileview.keking.cn/zh-cn/docs/home.html

应用例子:

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/js-base64@3.6.0/base64.min.js"></script>

var previewUrl = 'http://127.0.0.1:8080/file/test.txt'; //要预览文件的访问地址
window.open('http://127.0.0.1:8012/onlinePreview?url='+encodeURIComponent(Base64.encode(previewUrl)));

部署运行(摘自官方文档) #
1). 物理机或虚拟机上运行 #
从 码云发行版本 下载最新版发行包
解压kkFileView-x.x.x文件(Windows用.zip包,Linux/MacOS用.tar.gz包)
打开解压后文件夹的bin目录,运行startup脚本(Windows下以管理员身份运行startup.bat,Linux以root用户运行startup.sh)
浏览器访问本机8012端口 http://127.0.0.1:8012 即可看到项目演示用首页
2). Docker容器环境环境运行 #
拉取镜像
docker pull keking/kkfileview
运行
docker run -it -p 8012:8012 keking/kkfileview
浏览器访问容器8012端口 http://xxx.xxx.xxx.xxx:8012 即可看到项目演示用首页

实践的时候通过在虚拟机上采用1方式没法有成功运行起来,推荐采用2方式,在docker环境中使用
创建容器命令

docker run -dit -p 8012:8012 -v /www/kkfileivew/fonts:/usr/share/fonts/ -e KK_OFFICE_PREVIEW_TYPE="pdf" -e BASE_URL="https://www.xxx.com/preview" -e KK_CONTEXT_PATH="/preview/" --restart=always --name kkfileview 172.19.0.14:5000/keking/kkfileview:latest

用了私有仓库,8012端口映射,出现乱码或者中文问题(常见问题中有说明),映射了/usr/share/fonts/目录到母机上,方便管理字体,使用了默认的pdf预览方式,应用了nginx反向代理功能

nginx反向代理配置的时候,需要配置一下内容替换,前台是https访问,后端是走的http反代,所以页面中会有http的链接,并且进了端口,所以替换之

location ~ /preview/ {
    proxy_pass http://172.19.0.14:8012;
    proxy_set_header Host www.xxx.com;
    
    sub_filter "http://www.xxx.com:80" "https://www.xxx.com";
    sub_filter_once off;
}

项目中就是这个地址可以预览文件了
https://www.xxx.com/preview/onlinePreview?url='+encodeURIComponent(Base64.encode(previewUrl)));

实际线上项目需要考虑一些安全问题,应该限制一些文件类别,限制文件来源等等。

常见问题:https://kkfileview.keking.cn/zh-cn/docs/faq.html

配置文档:https://kkfileview.keking.cn/zh-cn/docs/config.html
默认配置文件(4.0版)

#######################################不可动态配置,需要重启生效#######################################
server.port = ${KK_SERVER_PORT:8012}
server.servlet.context-path= ${KK_CONTEXT_PATH:/}
server.servlet.encoding.charset = utf-8
#文件上传限制
spring.servlet.multipart.max-file-size=500MB
spring.servlet.multipart.max-request-size=500MB
## Freemarker 配置
spring.freemarker.template-loader-path = classpath:/web/
spring.freemarker.cache = false
spring.freemarker.charset = UTF-8
spring.freemarker.check-template-location = true
spring.freemarker.content-type = text/html
spring.freemarker.expose-request-attributes = true
spring.freemarker.expose-session-attributes = true
spring.freemarker.request-context-attribute = request
spring.freemarker.suffix = .ftl

# office-plugin
## office转换服务的进程数,默认开启两个进程
office.plugin.server.ports = 2001,2002
## office 转换服务 task 超时时间,默认五分钟
office.plugin.task.timeout = 5m

#文件资源路径(默认为打包根路径下的file目录下)
#file.dir = D:\\kkFileview\\
file.dir = ${KK_FILE_DIR:default}
#openoffice home路径
#office.home = C:\\Program Files (x86)\\OpenOffice 4
office.home = ${KK_OFFICE_HOME:default}

#缓存实现类型,不配默认为内嵌RocksDB(type = default)实现,可配置为redis(type = redis)实现(需要配置spring.redisson.address等参数)和 JDK 内置对象实现(type = jdk),
cache.type =  ${KK_CACHE_TYPE:jdk}
#redis连接,只有当cache.type = redis时才有用
spring.redisson.address = ${KK_SPRING_REDISSON_ADDRESS:127.0.0.1:6379}
spring.redisson.password = ${KK_SPRING_REDISSON_PASSWORD:}
#缓存是否自动清理 true 为开启,注释掉或其他值都为关闭
cache.clean.enabled = ${KK_CACHE_CLEAN_ENABLED:true}
#缓存自动清理时间,cache.clean.enabled = true时才有用,cron表达式,基于Quartz cron
cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?}

#######################################可在运行时动态配置#######################################
#提供预览服务的地址,默认从请求url读,如果使用nginx等反向代理,需要手动设置
#base.url = https://file.keking.cn
base.url = ${KK_BASE_URL:default}

#信任站点,多个用','隔开,设置了之后,会限制只能预览来自信任站点列表的文件,默认不限制
#trust.host = file.keking.cn,kkfileview.keking.cn
trust.host = ${KK_TRUST_HOST:default}

#是否启用缓存
cache.enabled = ${KK_CACHE_ENABLED:true}

#文本类型,默认如下,可自定义添加
simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}
#多媒体类型,默认如下,可自定义添加
media = ${KK_MEDIA:mp3,wav,mp4,flv}
#是否开启多媒体类型转视频格式转换,目前可转换视频格式有:avi,mov,wmv,3gp,rm
#请谨慎开启此功能,建议异步调用添加到处理队列,并且增加任务队列处理线程,防止视频转换占用完线程资源,转换比较耗费时间,并且控制了只能串行处理转换任务
media.convert.disable = ${KK_MEDIA_CONVERT_DISABLE:false}
#支持转换的视频类型
convertMedias = ${KK_CONVERTMEDIAS:avi,mov,wmv,mkv,3gp,rm}
#office类型文档(word ppt)样式,默认为图片(image),可配置为pdf(预览时也有按钮切换)
office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
#是否关闭office预览切换开关,默认为false,可配置为true关闭
office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:false}

#是否禁止下载转换生成的pdf文件
pdf.download.disable = ${KK_PDF_DOWNLOAD_DISABLE:true}
#是否禁用首页文件上传
file.upload.disable = ${KK_FILE_UPLOAD_ENABLED:false}

#预览源为FTP时 FTP用户名,可在ftp url后面加参数ftp.username=ftpuser指定,不指定默认用配置的
ftp.username = ${KK_FTP_USERNAME:ftpuser}
#预览源为FTP时 FTP密码,可在ftp url后面加参数ftp.password=123456指定,不指定默认用配置的
ftp.password = ${KK_FTP_PASSWORD:123456}
#预览源为FTP时, FTP连接默认ControlEncoding(根据FTP服务器操作系统选择,Linux一般为UTF-8,Windows一般为GBK),可在ftp url后面加参数ftp.control.encoding=UTF-8指定,不指定默认用配置的
ftp.control.encoding = ${KK_FTP_CONTROL_ENCODING:UTF-8}

#水印内容
#例:watermark.txt = ${WATERMARK_TXT:凯京科技内部文件,严禁外泄}
#如需取消水印,内容设置为空即可,例:watermark.txt = ${WATERMARK_TXT:}
watermark.txt = ${WATERMARK_TXT:}
#水印x轴间隔
watermark.x.space = ${WATERMARK_X_SPACE:10}
#水印y轴间隔
watermark.y.space = ${WATERMARK_Y_SPACE:10}
#水印字体
watermark.font = ${WATERMARK_FONT:微软雅黑}
#水印字体大小
watermark.fontsize = ${WATERMARK_FONTSIZE:18px}
#水印字体颜色
watermark.color = ${WATERMARK_COLOR:black}
#水印透明度,要求设置在大于等于0.005,小于1
watermark.alpha = ${WATERMARK_ALPHA:0.2}
#水印宽度
watermark.width = ${WATERMARK_WIDTH:180}
#水印高度
watermark.height = ${WATERMARK_HEIGHT:80}
#水印倾斜度数,要求设置在大于等于0,小于90
watermark.angle = ${WATERMARK_ANGLE:10}