[转自]从零开始搭建Electron+Vue+Webpack项目框架(五)预加载和Electron自动更新
什么是预加载
来看看electron 官网的介绍: https://www.electronjs.org/docs/api/browser-window:
preload String (optional) - Specifies a script that will be loaded before other scripts run in the page. This script will always have access to node APIs no matter whether node integration is turned on or off. The value should be the absolute file path to the script. When node integration is turned off, the preload script can reintroduce Node global symbols back to the global scope.
翻译过来如下:
preload字符串(可选)-指定在页面中其他脚本运行之前被加载的脚本。 无论打开还是关闭 integratioin,此脚本始终可以访问node API。 该值应该是脚本的绝对文件路径。 关闭node integration后,预加载脚本将从全局局限重新引入node的全局引用标志。
- preload是BrowserWindow类的参数webPreferences的一个可选设置项,我们解读一下官网的先容:在页面运行其他脚本之前预先加载的指定的脚本:首先是个js文件没错了,再看加载时机,在页面运行其他脚本之前预先加载,这个页面不是通俗的某个h5页面,而是指某个渲染进程(需要预加载js的渲染进程,由于渲染进程可能有多个,每个就是一个窗口),我们new一个BrowserWindow,打开了一个窗口,就是启动了一个渲染进程,若是我们不给这个窗口指定页面,那它就是空缺的,若是指定了页面,那么窗口就会加载这个页面:
const win = new BrowserWindow({
width: 800,
height: 600
});
win.loadURL('https://www.baidu.com');
- 如上面代码,我们建立了一个窗口,然后加载百度首页,而preload脚本的加载时机就是窗口建立后,百度首页加载之前。若是有人问,若是不挪用loadURL方式,不加载页面,preload剧本会加载吗?谜底是会,但有什么用呢?你起个壳子不给人家看页面是什么鬼?不管这些,主要的是我们明白这个加载时机就好了;
无论页面是否集成Node,此脚本都可以调用所有Node API:首先要说明的一点是,Electron5.x以上版本,默认无法在渲染进程中调用Node API,如需使用,需要预先设置:
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
- 然后还要清楚一点,preload脚本是运行在渲染进程中的。再有一点就是,preload脚本中可以调用window工具(渲染进程其实就是起了个浏览器壳子),preload脚本运行在渲染进程,提前于页面和其他所有js的加载,又能调用Node API;
脚本文件路径为绝对路径,当node integration关闭时,预加载的脚本将从全局范围重新引入node的全局引用标志:联系前面两点明白就好了。
那么,到底什么是预加载?
某一个渲染进程,在页面加载之前加载一个本地脚本,这个脚本能调用所有Node API、能调用window工具。用法如下:
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
怎么用preload
明白应该差不多了,但什么场景能用到这玩意儿呢?按正常的逻辑来想,主进程启动后启动渲染历程,渲染进程加载页面就完事儿了,哪会用到这个preolad呢?
想一下,若是我们有以下场景:
a、若是我们启动了一个窗口(渲染进程),加载了一个线上的页面,本地没有页面文件,但要做一些错误处置,好比网络错误,页面加载失败,然后在页面空缺但时刻插入一些元素;
b、若是我们的一套代码部署在web端和客户端,需要用一个变量判断是在web端还是客户端;
………..
上面两个场景若是用preload来解决的话,思路是利用prelaod中能调用window工具的特点,好比b,代码中可以用window.isClient来判断是否在客户端,默以为false,然后在preload中把window.isClient设置为true,而对于部署在web端的代码来说,这个值就是false。
上面所说的场景b的preload 例子如下:
// 引入electron工具
const {
remote,
ipcRenderer
} = require('electron');
// 引入node模块
const fs = require('fs');
const path = require('path');
// 引入window工具
window.isClient = true;
window.sayHello = function() {
console.log('hello');
};
// 操作dom
const div = document.createElement('div');
div.innerText = 'I am a div';
document.body.appendChild(div);
// ...
如果preoad逻辑复杂,可以用webpack打包一下,单独拎出来打包就行了,webpack单文件打包注意target要”electron-renderer”:
/*
Tip: preload 打包设置
*/
const path=require('path');
const { dependencies } = require('../package.json');
module.exports = {
mode:process.env.NODE_ENV,
entry: {
preload:['./src/preload/index.js']
},
output: {
path: path.join(__dirname, '../app/'),
libraryTarget: 'commonjs2',
filename: './[name].js'
},
optimization: {
runtimeChunk: false,
minimize: true
},
node: {
fs: 'empty',
__dirname:false
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
externals: [
...Object.keys(dependencies || {})
],
resolve: {
extensions: ['.js'],
alias: {
'@': path.resolve(__dirname, "../src"),
'@public': path.resolve(__dirname, "../public")
}
},
plugins:[],
target:"electron-renderer"
}
自动更新
我们都知道,electron其实是封了个chrome内核,抛开壳子不说,里面运行的就是我们的h5页面,而就算我们跑了个空项目,没有任何内容,打包后的安装包也得30M左右,我们希望自己的程序自动更新,那么更新机制是怎样的呢?
若是我们只改动了页面某一处的脚本,却要用户更新整个安装包,那显然太不合理了,一是体验不好,二是浪费流量……
基于这种度量,加上electron主进程和渲染进程的划分,那我们可以思考如下更新机制:
主进程有改动时,用户需要更新整个客户端(可以做动态更新,官方好像是说支持);渲染进程有改动时,我们只需要把h5包下载到本地然后加载就行了,这需要我们打包时能把h5包区分出来,在更新后能打开对应版本的h5包。
这里我们称主进程的更新为大版本更新,渲染进程的更新为小版本更新。
1、打包设置修改
由于牵扯到小版本的更新,那我们打包的时刻就得把这个“小版本”给打出来。这里只讲一下怎么把小版本的压缩包给打出来。
修改build.js,使用webpack打包主进程、打包preload、打包渲染进程,获得可执行文件目录app,然后引入electrin-builder对app目录进行打包,build一个安装包,然后把渲染进程的文件压缩并生成版本号。将渲染进程打包和压缩小版本文件拆分出来,因为分模块封装的好处,各个进程的打包逻辑拆出来,能随意组合还能复用。
详细代码就不贴出来了,太占篇幅,也没什么用,可以到https://github.com/luohao8023/electron-vue-template看完整代码。
2、增加启动页,启动页显示欢迎语等,在这里检查更新
这里我们暂且叫它检查更新页,这个检查更新页是渲染进程,用户打开程序时首先显示检查更新窗口,然而这个窗口也不一定显示检查更新字样,偷偷的检查就行了,有新版本就提醒更新,没有新版本就显示欢迎语。
这儿的逻辑是单独拆分出来的,不是自动更新的时刻把自动更新逻辑自己也给更新了,容易乱套。
修改主进程代码,程序启动时首先启动自动更新窗口:
<pre style="box-sizing: border-box; margin: 0px; padding: 0px; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; line-height: 1.42857; color: rgb(51, 51, 51); word-break: break-all; overflow-wrap: break-word; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">app.on('ready', () => { //注册快捷键打开控制台事宜
shortcut.register('Command+Control+Alt+F5');
mainWindow = updateWin.create();
});</pre>
然后注册监听事件,由于自动更新窗口逻辑完成之后需要唤醒主窗口,需要主进程来协调:
<pre style="box-sizing: border-box; margin: 0px; padding: 0px; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; line-height: 1.42857; color: rgb(51, 51, 51); word-break: break-all; overflow-wrap: break-word; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">//启动主窗体
ipcMain.on('create-main',(event,arg) => { // h5页面指向指定版本
// global.wwwroot.path = arg.newVersionPath ? arg.newVersionPath : __dirname;
// if (arg.version) setVal('version','smallVersion', arg.version);
indexWin.create();
mainWindow.destroy();
});</pre>
自动更新窗口只需专注于更新逻辑就行了,逻辑竣事后呼起主窗口:
<pre style="box-sizing: border-box; margin: 0px; padding: 0px; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; line-height: 1.42857; color: rgb(51, 51, 51); word-break: break-all; overflow-wrap: break-word; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> // 更新逻辑看下面伪代码
const v1 = getOnlineVersion();
const v2 = getLocalVersion();
const needUpdate = checkVersion(v1, v2); if (needUpdate) {
downloadVersion();
} this.runMain();</pre>
在呼起主窗口的同时给主窗口通报参数,并通知主窗口有没有更新版本,以及主窗口需要加载哪个小版本的包,而主窗口在loadURL时也要做下改动:
<pre style="box-sizing: border-box; margin: 0px; padding: 0px; overflow: auto; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; font-size: 13px; display: block; line-height: 1.42857; color: rgb(51, 51, 51); word-break: break-all; overflow-wrap: break-word; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> let wwwroot = global.wwwroot.path ? global.wwwroot.path : __dirname;
let filePath = url.pathToFileURL(path.join(wwwroot, 'index.html')).href;</pre>
而wwwrot就是当前小版本包的根路径,由主历程来维护,自动更新小版本后会修改这个值,以告诉主进程加载哪个版本。
好了,烦琐了一大堆,很多多少地方没贴代码,感受贴了代码的话,篇幅就不受控制了,照样去github看完整项目吧,自动更新这一块是伪代码,只实现了渲染历程的切换(即自动更新窗口呼起主窗口),详细的更新逻辑实现起来的话还要拿线上版本去对照,这个照样留给人人在现实项目中去调试吧。
好啦,有什么问题可以留言交流,也可以直接去看代码https://github.com/luohao8023/electron-vue-template。