因为项目需要用到客户端,了解了electron和nw.js,最后决定用electron。
先安装node.js,创建项目文件夹,就不一一阐述。
最后项目的结构如下
里面我使用了electron-builder打包+electron-updater全局升级+压缩包局部更新。
因为需要局部更新,所以我关闭了asar模式。
并且我的项目是需要关联pdf文件,所以添加了后缀是.pdf的打开方式。并且监听了使用pdf打开客户端,客户端获取pdf的路径
packeage.json配置如下
{
"name": "你的项目名称",
"version": "1.0.0",
"description": "Sample to demonstrate integrating WebViewer into an Electron App",
"main": "src/index.js",
"scripts": {
"build": "electron-builder",
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"postinstall": ""
},
"author": "lifan",
"license": "ISC",
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "pdftron_reader"
}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
]
}
},
"build": {
"productName": "项目中文名称",
"appId": "",
"asar": false,
"directories": {
"output": "build"
},
"fileAssociations": {
"ext": [
".pdf"
],
"name": "pdf",
"role": "Editor"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "icon.ico",
"uninstallerIcon": "icon.ico",
"installerHeaderIcon": "/icon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"runAfterFinish": true,
"shortcutName": "nisi的项目名称"
},
"publish": [
{
"provider": "generic",
"url": "你的项目更新路径"
}
],
"dmg": {
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
"mac": {
"icon": "icon.png",
"extendInfo": {
"LSMultipleInstancesProhibited": true
}
},
"win": {
"icon": "icon.ico",
"artifactName": "${productName}_setup_${version}.${ext}",
"target": [
{
"target": "nsis",
"arch": [
"ia32"
]
}
]
}
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.58",
"@electron-forge/maker-deb": "^6.0.0-beta.54",
"@electron-forge/maker-rpm": "^6.0.0-beta.54",
"@electron-forge/maker-squirrel": "^6.0.0-beta.54",
"@electron-forge/maker-zip": "^6.0.0-beta.54",
"electron": "11.0.3",
"electron-builder": "^22.11.7"
},
"dependencies": {
"adm-zip": "^0.5.6",
"axios": "^0.21.4",
"crypto-js": "4.1.1",
"electron-squirrel-startup": "^1.0.0",
"electron-store": "^8.0.0",
"electron-updater": "^4.3.9",
"element-ui": "^2.15.6",
"fs-extra": "^7.0.1",
"request": "^2.88.2",
"vue": "^2.6.14",
"vue-router": "^3.5.2"
}
}
下面是入口文件index.js,写一下重要的几点
1.版本号设置,判断是否需要更新
2.使用electron-store判断用户登录状态,从而进入不同页面
3.获取用户打开pdf调起客户端时的路径
const { app, BrowserWindow, globalShortcut, ipcMain, Menu, shell} = require("electron");
const path = require("path");
const { autoUpdater } = require("electron-updater");
const Store = require('electron-store');
Store.initRenderer()
const fs = require('fs-extra');
const axios = require('axios');
//自定义版本号,跟服务器请求版本号对比
global.version = 1.000;
//服务器版本号
global.newversion = 0;
var isWinReady = false;
var initOpenFileQueue = [];
let mainWindow
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require("electron-squirrel-startup")) {
// eslint-disable-line global-require
app.quit();
}
//这是设置菜单栏的,我没用到所以注释了
// const Menus = [
// {
// label:'Files',
// submenu:[
// {
// label: '网页版',
// role: 'help',
// submenu: [{
// label: '网页版',
// click: function () {
// shell.openExternal('https://www.jianshu.com/u/1699a0673cfe')
// }
// }]
// },
// {
// label: '帮助',
// role: 'help',
// submenu: [{
// label: '帮助文档',
// click: function () {
// shell.openExternal('https://www.jianshu.com/u/1699a0673cfe')
// }
// }]
// }
// ]
// }
// ];
const createWindow = () => {
// Create the browser window.
//我设置了打开默认最大化
mainWindow = new BrowserWindow({
show: false,
minWidth: 650,
minHeight: 500,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false
}
});
axios.get('你的服务器版本号,例:1.0001').then(res => {
global.newversion = parseFloat(res.data);
//如果当前版本号大于上面配置的global.version,那么就更新
if (global.newversion && global.newversion > version) {
mainWindow.loadFile(path.join(__dirname, "start.html"));
//检测版本更新
updateHandle();
}else{
//这边是用了electron-store存储用户数据,判断进入页面
const store = new Store();
if(store.get('user_info') && store.get('scane_login') && store.get('user_register') && store.get('user_setting')){
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, "list.html"));
}else{
mainWindow.loadFile(path.join(__dirname, "login.html"));
}
}
}).catch(error => { console.log(error) })
// Open the DevTools.
// mainWindow.webContents.openDevTools();
mainWindow.on('ready-to-show', () => {
isWinReady = true;
})
mainWindow.webContents.on('dom-ready', () => {
sendFileList(initOpenFileQueue)
})
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
const gotTheLock = app.requestSingleInstanceLock();
if (gotTheLock) {
app.on("second-instance", (event, commandLine) => {
// 监听是否有第二个实例,向渲染进程发送第二个实例的本地路径
sendFileList(`${commandLine[commandLine.length - 1]}`);
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
app.on("ready", async () => {
// Register a 'CommandOrControl+Y' shortcut listener.
// globalShortcut.register('CommandOrControl+R', () => {
// return false;
// })
// const mainMenu = Menu.buildFromTemplate(Menus);
// Menu.setApplicationMenu(mainMenu);
createWindow();
mainWindow.maximize()
mainWindow.show()
});
} else {
app.quit();
}
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
// On OS X 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();
}
});
app.on('will-finish-launching', () => {
if (process.platform == 'win32') {
const argv = process.argv
if (argv) {
argv.forEach(filePath => {
if (filePath.indexOf('.pdf') >= 0) {
initOpenFileQueue.push(filePath);
}
})
}
} else {
// Event fired When someone drags files onto the icon while your app is running
// for macOs https://www.electronjs.org/docs/api/app#%E4%BA%8B%E4%BB%B6-open-file-macos
app.on("open-file", (event, file) => {
if (!isWinReady) {
initOpenFileQueue.push(file);
} else {
sendFileList(file)
};
event.preventDefault();
});
}
});
function sendFileList(fileList) {
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send("open-file-list", fileList)
})
}
function updateHandle() {
var name = app.getName();
var feedUrl = 'https://xxx.xxx.com/download/';
let message = {
error: '检查更新出错',
checking: '正在检查更新……',
updateAva: '检测到新版本,正在下载……',
updateNotAva: '现在使用的就是最新版本,不用更新',
};
//设置更新包的地址
autoUpdater.setFeedURL(feedUrl);
//监听升级失败事件
autoUpdater.on('error', function (error) {
sendUpdateMessage({
cmd: 'error',
message: error
})
});
//监听开始检测更新事件
autoUpdater.on('checking-for-update', function (message) {
const updatePendingPath = require('path').join(autoUpdater.app.baseCachePath, name+'-updater')
fs.emptyDir(updatePendingPath)
sendUpdateMessage({
cmd: 'checking-for-update',
message: message
})
});
//监听发现可用更新事件
autoUpdater.on('update-available', function (message) {
sendUpdateMessage({
cmd: 'update-available',
message: message
})
});
//监听没有可用更新事件
autoUpdater.on('update-not-available', function (message) {
sendUpdateMessage({
cmd: 'update-not-available',
message: message
})
});
// 更新下载进度事件
autoUpdater.on('download-progress', function (progressObj) {
sendUpdateMessage({
cmd: 'download-progress',
message: progressObj
})
});
//监听下载完成事件
autoUpdater.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
//some code here to handle event
autoUpdater.quitAndInstall();
sendUpdateMessage({
cmd: 'update-downloaded',
message: {
releaseNotes,
releaseName,
releaseDate,
updateUrl
}
})
});
//接收渲染进程消息,开始检查更新
ipcMain.on("checkForUpdate", (e, arg) => {
//执行自动更新检查
// sendUpdateMessage({cmd:'checkForUpdate',message:arg})
autoUpdater.checkForUpdates();
})
ipcMain.on('update-relaunch',function() {
app.relaunch();
app.quit();
})
}
ipcMain.on('new-window', (e, arg) => {
if (arg) {
var page = arg;
} else {
var page = 'list.html';
}
mainWindow.loadFile(path.join(__dirname, page));
})
//给渲染进程发送消息
function sendUpdateMessage(text) {
mainWindow.webContents.send('message', text)
}
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
贴一下src文件下的所有文件
先上更新页面代码,老规矩重要的几点说一下
1.我判断了大更新和小更新的区别,1.000-1.999是小更新,2是大更新,比如我现在版本是1.000我发布了新的版本1.001,那么就会执行小更新。但是如果我发布了2.000,那么就会执行批量更新。
2局部更新就是把src下的所有文件压缩成zip,上传到服务器或者oss,下载下来之后用adm-zip解压替换掉现有的src,就完成了局部更新,不需要更新几百M的安装包了
3.全局更新的地址在package.json和index.js里面都要设置,为什么我也不知道,我也是百度的。地址只要目录就行了,不需要对应包的路径。
4.贴一下oss的需要内容
5.src.zip的内容 ,不是压缩src,是压缩src下面的内容
6.记得在根目录新建一个update文件夹,并且里面放入一个src.zip。不然因为权限问题,mac无法更新成功。
下面贴start.html的代码
<html>
<head>
<link rel="stylesheet" href="index.css">
<link rel="stylesheet" href="../node_modules/element-ui/lib/theme-chalk/index.css">
</head>
<body >
<div id="app">
<el-progress type="circle" :percentage="percentage"></el-progress>
</div>
</body>
<script>
const { ipcRenderer } = require('electron');
const remote = require('electron').remote;
const fs = require('fs-extra');
const adm_zip = require('adm-zip');
const path = require("path");
var request = require('request');
var newversion = remote.getGlobal('newversion');
var version = remote.getGlobal('version');
var Vue = require('vue/dist/vue.min.js');
var ElementUI = require('element-ui');
Vue.use(ElementUI);
new Vue({
el:"#app",
data() {
return {
percentage: 0,
fileUrl: "https://xx.xx.com/download/src.zip",
baseUrl: path.join(__dirname, '../update', 'src.zip'),
localUrl: path.join(__dirname, '../src/'),
};
},
created () {
this.checkVersion()
},
methods: {
goIndex(){
ipcRenderer.send('new-window','login.html');
},
checkVersion(){
var newversion_z = parseInt(newversion);
var version_z = parseInt(version);
//小更新
if (version_z == newversion_z) {
this.downLoad();
}else{
ipcRenderer.send('checkForUpdate');
ipcRenderer.on('message', (event, arg) => {
console.log(arg)
if ("update-available" == arg.cmd) {
//显示升级对话框
console.log('需要升级');
} else if ('update-not-available' == arg.cmd) {
this.goIndex();
}else if ("download-progress" == arg.cmd) {
console.log(arg.message.percent);
this.percentage = Math.round(parseFloat(arg.message.percent));
} else if ("error" == arg.cmd) {
console.log('升级失败');
setTimeout(()=>{
this.goIndex();
}, 2000 )
}
})
}
},
downLoad(){
// Save variable to know progress
var received_bytes = 0;
var total_bytes = 0;
var req = request({
method: 'GET', uri: this.fileUrl
});
var out = fs.createWriteStream(this.baseUrl);
req.pipe(out);
req.on('response', function ( data ) {
// Change the total bytes value to get progress later.
total_bytes = parseInt(data.headers['content-length']);
});
var that = this;
req.on('data', function(chunk) {
// Update the received bytes
received_bytes += chunk.length;
that.showProgress(received_bytes, total_bytes);
});
req.on('end', function() {
const unzip = new adm_zip(that.baseUrl); //下载压缩更新包
unzip.extractAllTo(that.localUrl, /*overwrite*/true); //解压替换本地文件
ipcRenderer.send('update-relaunch');
})
},
showProgress(received, total){
this.percentage = Math.round((received * 100) / total);
}
}
})
</script>
</html>
最后右键pdf选择打开方式,打开我的客户端之后的获取路径贴一下
//在你的页面js下面引入
const { ipcRenderer } = require('electron');
const Stores = require('electron-store');
const store = new Stores();
ipcRenderer.on('open-file-list', (event, args) => {
console.log(args)
if (typeof(args)=='string') {
if (args.indexOf("pdf") != -1) {
store.set('filelist', args);
}
} else {
if (args[0]) {
store.set('filelist', args[0]);
}
}
console.log(store.get('filelist'))
})