摘要:随着前端技术的飞速发展,越来越多的技术领域开始被前端工程师踏足。从NodeJs问世至今,各种前端工具脚手架、服务端框架层出不穷,“全栈工程师”对于前端开发者来说,再也不只是说说而已。在NodeJs及其衍生技术高速发展的同时,Nw和Electron的问世,更是为前端发展提速不少,依稀记得哪位前辈说过,“能用Js改写的,终将用Js改写”,这不,客户端来了!使用Electron也有一段时间了,各种简单复杂的问题,也都或多或少的遇见过,下决心整理出一套客户端模板出来,一是加深一下自己的理解,二是供小伙伴们参考指正。本文选择Electron6.x+Webpack4+vue全家桶为技术栈,一套代码可以分别打包在客户端和web端,结合webpack,支持热更新,打包为exe安装包,过程中会涉及vue全家桶、electron的常见问题、配置和优化,webpack的对应配置等。从零开始,把electron、vue、webpack统统纳入自己的知识体系!

说明:本着模拟从零开始的过程,最开始的架构或者代码设计可能不是最优解,有可能只适用于当前情况,后续会一步步完善,也可能会部分重构,关键是体会这个从零到一,再到完善的过程。

一、新建工程

  1、说好的从零开始,就从新建文件夹开始吧,新建electron-vue-template文件夹。

  2、cmd进入文件夹,执行npm init,初始化一个node项目。

  3、完善工程目录结构:

    

  项目根目录的结构大致就是上面这个样子,后续完善过程中,会在对应目录下增加相应的子目录,后面会有讲到。下面介绍一下各个目录的作用:

    app:webpack编译后的整个项目的代码,包括主进程和渲染进程,使用electron-builder打包exe安装包时,会把这部分代码打进去;

    builder:webpack打包脚本,包括打包主进程、渲染进程,打包各个环境的exe安装包,启动各个环境的devServer等;

    config:配置文件,包括环境配置、版本等;

    dist:构建出的静态文件,exe,zip等;

    src:源码目录;

      main:主进程源码;

      renderer:渲染进程源码;

  4、执行npm i electron -D,下载electron,如果7.0.0版本安装不成功的话,可尝试cnpm i electron@6.1.2 -D安装6.1.2版本,我是尝试了好多次都无法下载7.0.0版本,所以这里使用的是6.1.2。

二、窗口配置,启动一个最简单的electron应用

  1、进入src下的main文件夹,新建index.html和main.js文件;

  2、index.html文件,除了常规的结构之外,随便写点简单的内容即可,本文只在body标签内写入一下代码:

<h1>Welcome to electron-vue-template!</h1>

   3、Electron文档中说,您应当在 main.js 中创建窗口,并处理程序中可能遇到的所有系统事件。不过,随着我们的应用逐渐复杂,可能不止存在一个窗口,在main.js写入过多逻辑或者配置的话,会使我们的项目越来越难维护,所以正确的做法是,对应的窗口有自己专门的Js文件,负责这个窗口的配置和事件监听,而main.js文件只需要负责窗口的调度和系统级别的事件监听。当然,我们今天的目的是启动一个最简单的electron应用,所以直接写在了main.js文件里:

复制代码

const url = require('url');
const path = require('path');
const { app, BrowserWindow } = require('electron');
function createWindow() {
    let win = new BrowserWindow({
        width: 800,
        height: 600
    });
    // 获取index.html的file协议路径
    const indexPath = url.pathToFileURL(path.join(__dirname, 'index.html')).href;
    // 如果路径或者参数中含有中文,需要对路径进行编码处理
    win.loadURL(encodeURI(indexPath));
    // 打开开发者工具
    win.webContents.openDevTools();
    // 监听窗口的关闭事件,释放窗口对象
    win.on('closed', () => {
        win = null;
    });
}

// 创建窗口
app.on('ready', createWindow);
// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
    // 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
    // 否则绝大部分应用及其菜单栏会保持激活。
    if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
    // 在macOS上,当单击dock图标并且没有其他窗口打开时,
    // 通常在应用程序中重新创建一个窗口。
    if (!win) createWindow();
});

复制代码

   Electron apps 使用JavaScript开发,其工作原理和方法与Node.js 开发相同。Electron模块包含了Electron提供的所有API和功能,引入方法和普通Node.js模块一样:Electron模块所提供的功能都是通过命名空间暴露出来的。 比如说:Electron.app负责管理Electron 应用程序的生命周期,Electron.BrowserWindow类负责创建窗口。

  4、启动应用

  废了这么多话,应用到底该怎么启动?那还不简单,在package.json的script标签里新增一个start命令,命令内容为node ./src/main/main.js,然后运行npm start,程序不就执行了嘛!于是马上添加了start命令,启动的时候命令行就报错了。WTF??什么鬼!看了报错信息,定位到了是在app.on('ready')这一行,这也能报错??

  试着打印了一下app,undefined!!于是又加了两行代码:  

const electron = require('electron');
console.log(electron)

  打印了一下electron,竟然是个这玩意儿:E:\lh\demo\electron-vue-template\node_modules\_electron@6.1.2@electron\dist\electron.exe,不应该是个对象吗?

  又翻了翻文档,恍然大悟,Electron并不属于node应用,通过node来执行入口文件当然是不行的,要用electron来执行,正确的命令为:electron ./src/main/main.js,再次运行npm start,看着命令行输出的内容以及刚刚启动的窗口,舒服的长出了一口气。

  

第一篇的内容就写到这里了,很少系统的去总结,总感觉有些内容写不出来,暂且做个引子吧,如果希望后续的文章对某部分详细讲解的话,欢迎留言,同时,如果有不恰当的地方,也欢迎批评指正!

摘要:上篇文章说到了如何新建工程,并启动一个最简单的Electron应用。“跑起来”了Electron,那就接着把Vue“跑起来”吧。有一点需要说明的是,webpack是贯穿这个系列始终的,我也是本着学习的态度,去介绍、总结一些常用到的配置及思路,有不恰当的地方,或者待优化的地方,欢迎留言。项目完整代码:https://github.com/luohao8023/electron-vue-template

下面开始~~~

一、安装依赖

vue、webpack:不多说了

vue-loader:解析、转换.vue文件

vue-template-compiler:vue-loader的依赖包,但又独立于vue-loader,简单的说,作用就是使用这个插件将template语法转为render函数

webpack-dev-server:快速搭建本地运行环境的工具

webpack-hot-middleware:搭配webpack-dev-server使用,实现热更新

chalk:命令行输出带有颜色的内容

依赖包就介绍这么多,后面需要什么可以自行下载,这里不多赘述了。

 

二、完善工程目录

  

webpack.render.config.js:渲染进程打包配置

dev.js:本地调试脚本

views:页面代码

index.js:vue工程入口文件

index.ejs:打包生成html文件时的模板

三、配置Vue工程

1、编写入口文件,render>index.js

复制代码

import Vue from 'vue';
import index from './views/index.vue';

//取消 Vue 所有的日志与警告
Vue.config.silent = true;
new Vue({
    el: '#app',
    render: h => h(index)
});

复制代码

2、编写根组件,render>views>index.vue

复制代码

<template>
    <div class="content">
        <h1>Welcome to electron-vue-template!</h1>
    </div>
</template>

<script>
export default {}
</script>
<style></style>

复制代码

3、编写html模板文件,render>index.ejs,webpack解析、打包vue文件时,以此模板生成html文件

复制代码

<!DOCTYPE html>
<html lang="zh-CN">
<!--template for 2019年10月30日-->
<!--<%= new Date().getFullYear()+'/'+(new Date().getMonth()+1)+'/'+new Date().getDate()+' '+new Date().getHours()+':'+new Date().getMinutes() %>-->
<head>
    <meta charset="UTF-8">
    <title>模板文件</title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
    <meta HTTP-EQUIV="pragma" CONTENT="no-cache">
    <meta HTTP-EQUIV="Cache-Control" CONTENT="no-store, must-revalidate">
    <meta HTTP-EQUIV="expires" CONTENT="Wed, 26 Feb 1997 08:21:57 GMT">
    <meta HTTP-EQUIV="expires" CONTENT="0">
</head>
<body>
    <div id="app"></div>
</body>
</html>

复制代码

4、编写webpack配置文件,builder>webpack.render.config.js,建议按照本文这种方式,把配置文件单独抽出来,这样的话,本地调试和打包可以共用一套配置,只需要传递不同参数就可以了,不要把所有的配置和打包逻辑写在一个文件里,太长、太乱、太难维护

复制代码

/*
Name:    渲染进程配置
Author: haoluo
Date:   2019-10-30
 */
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const devMode = process.env.NODE_ENV === 'development';

module.exports = {
    mode: devMode ? 'development' : 'production',
    entry: {
        main: './src/render/index.js'
    },
    output: {
        path: path.join(__dirname, '../app/'),
        publicPath: devMode ? '/' : '',
        filename: './js/[name].[hash:8].js'
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                exclude: /node_modules/,
                loader: 'vue-loader'
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/render/index.ejs',
            filename: './index.html',
            title: 'electron-vue-template',
            inject: false,
            hash: true,
            mode: devMode
        })
    ]
}

复制代码

适当解释一下:

mode:环境参数,针对不同的环境,webpack内部有一些不同的机制,并对相应环境做相应的优化

entry:入口,webpack执行构建的第一步将从入口文件开始,递归查询并解析所有依赖的模块。配置方式有多种,可参考webpack文档,这里我们配置的路径是'./src/render/index.js',意思是src目录下,render文件夹下的index.js,而webpack配置文件是在builder文件夹下,那这个“./”的相对路径到底是相对于谁呢?这就得说一下webpack中的路径问题了,context 是 webpack 编译时的基础目录,入口起点(entry)会相对于此目录查找,那这个context又是个什么东西?webpack源码有关默认配置中有这么一句话

this.set("context", process.cwd());

这就是context的默认值,工程的根目录,那这个entry的配置就很好理解了。

output:打包的输入配置,路径建议设置为绝对路径。

module和plugins就不多说了。

5、编写本地调试脚本

复制代码

/**
* Tip:    调试渲染进程
* Author: haoluo
* Data:   2019-10-30
**/
process.env.NODE_ENV = 'development';
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackHotMiddleware = require('webpack-hot-middleware');
const chalk = require('chalk');
const http = require('http');
function devRender() {
    console.log('启动渲染进程调试......');
    const webpackDevConfig = require('./webpack.render.config.js');
    const compiler = webpack(webpackDevConfig);
    new WebpackDevServer(
        compiler, {
            contentBase: webpackDevConfig.output.path,
            publicPath: webpackDevConfig.output.publicPath,
            open: true,//打开默认浏览器
            inline: true,//刷新模式
            hot: true,//热更新
            quiet: true,//除第一次编译外,其余不显示编译信息
            progress: true,//显示打包进度
            setup(app) {
                app.use(webpackHotMiddleware(compiler));
                app.use('*', (req, res, next) => {
                    if (String(req.originalUrl).indexOf('.html') > 0) {
                        getHtml(res);
                    } else {
                        next();
                    }
                });
            }
        }
    ).listen(8099, function(err) {
        if (err) return console.log(err);
        console.log(`Listening at http://localhost:8099`);
    });
    compiler.hooks.done.tap('doneCallback', (stats) => {
        const compilation = stats.compilation;
        Object.keys(compilation.assets).forEach(key => console.log(chalk.blue(key)));
        compilation.warnings.forEach(key => console.log(chalk.yellow(key)));
        compilation.errors.forEach(key => console.log(chalk.red(`${key}:${stats.compilation.errors[key]}`)));
        console.log(chalk.green(`${chalk.white('渲染进程调试完毕\n')}time:${(stats.endTime-stats.startTime)/1000} s`));
    });
}

function getHtml(res) {
    http.get(`http://localhost:8099`, (response) => {
        response.pipe(res);
    }).on('error', (err) => {
        console.log(err);
    });
}

devRender();

复制代码

都是一些常规操作,可以阅读一下代码。

6、配置启动命令,在package.json中新增dev命令,启动本地调试(先起了再说,报错什么的,见招拆招)

  "scripts": {
    "start": "electron ./src/main/main.js",
    "dev": "node ./builder/dev.js"
  },

然后命令行运行npm run dev。。。。。。反正我这儿是报错了。。。说是找不到html-webpack-plugin模块,那就运行npm i html-webpack-plugin -D安装一下,如果步骤一没有做的话,后面可能还会遇到很多模块找不到的情况,解决方法很简单,缺什么安装什么就好了。安装完所有的模块之后,启动,还是报错了。。。。。。

ModuleNotFoundError: Module not found: Error: Can't resolve 'vue' in ...
ModuleNotFoundError: Module not found: Error: Can't resolve 'vue-loader' in ...

检查了下package.json文件和node_modules,发现我的vue-loader没有装,然后就是装一下(如果没有遇到这个步骤,可以忽略)

再次运行

  

这个报错就很友好了吗,就是vue-loader告诉你,必须安装vue-template-compiler插件,不然就不工作,那就装一下。

接着运行,就知道没那么容易成功

  

vue-loader报错说缺少了插件,让检查是否配置了VueLoaderPlugin插件,搜一下这是个什么鬼,看这里,15+版本的vue-loader需要配合VueLoaderPlugin使用,然后看了一下我使用的vue-loader版本15.7.1,那就配一下这个东西。

  

接着运行,终于没有报错了,但是页面为啥子是白的,我的h1标签呢?冷静下来分析一下问题,页面没有东西说明我打包时生成的html文件有问题(devServer会把打包出来的静态文件保存在内存里),而html文件是根据ejs模板生成的,那会不会是模板配置有问题?

  

看一下我们的模板,结构是没什么问题啊,但是,没有引用css和js文件啊,也就是我们辛辛苦苦解析vue文件,打包css和js,最后却没有引用。。。好吧,那就再配置一下ejs模板,把相应的文件引入一下

复制代码

<!DOCTYPE html>
<html lang="zh-CN">
<!--template for 2019年10月30日-->
<!--<%= new Date().getFullYear()+'/'+(new Date().getMonth()+1)+'/'+new Date().getDate()+' '+new Date().getHours()+':'+new Date().getMinutes() %>-->
<%
function getFilePath(filename,libsPath){
    let _filenameSearchIndex=filename.indexOf("?");
    let _libsPathSearchIndex=libsPath.indexOf("?");
    let _filename=filename.substr(0,_filenameSearchIndex<1?filename.length:_filenameSearchIndex);
    let _libsPath=libsPath.substr(0,_libsPathSearchIndex<1?libsPath.length:_libsPathSearchIndex);
    let htmlfilename=path.relative(_filename,_libsPath);
    return libsPath;
}
let path = require('path'),jsArr = [],cssArr = [];
let filename="./index.html";
    //修正目录结构
    for(let i=0;i<htmlWebpackPlugin.files.css.length;i++){
        let name=getFilePath(filename,String(htmlWebpackPlugin.files.css[i]));
        cssArr.push(name);
    }
    for(let i=0;i<htmlWebpackPlugin.files.js.length;i++){
        let name=getFilePath(filename,String(htmlWebpackPlugin.files.js[i]));
        jsArr.push(name);
    }
%>
<head>
    <meta charset="UTF-8">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
    <meta HTTP-EQUIV="pragma" CONTENT="no-cache">
    <meta HTTP-EQUIV="Cache-Control" CONTENT="no-store, must-revalidate">
    <meta HTTP-EQUIV="expires" CONTENT="Wed, 26 Feb 1997 08:21:57 GMT">
    <meta HTTP-EQUIV="expires" CONTENT="0">
    <% cssArr.forEach(css=>{ %><link rel="stylesheet" href="<%= css %>" />
    <% }) %>
</head>
<body>
    <div id="app"></div>
    <% jsArr.forEach(js=>{ %><script type="text/javascript" src="<%= js %>"></script>
    <% }) %>
</body>
</html>

复制代码

我们可以在ejs中拿到html-webpack-plugin插件的一些信息,比如插件配置、生成的文件等,然后拿到js和css文件,并引入进来,这里建议看一下ejs模板语法。

我们接着运行,终于出来了。

  

7、配置打包脚本

在builder文件夹下新建build.js,引入配置,直接运行webpack打包即可,不需要devServer。

复制代码

/**
* Tip:    打包
* Author: haoluo
* Data:   2019-10-30
**/
process.env.NODE_ENV = 'production';
const chalk = require("chalk");
const del = require("del");
const webpack = require('webpack');
const renderConfig = require('./webpack.render.config.js');

del(["./app/*"]); //删除历史打包数据

viewBuilder().then(data => {
    console.log("打包输出===>", data)
}).catch(err => {
    console.error("打包出错,输出===>", err);
    process.exit(1);
});

function viewBuilder() {
    return new Promise((resolve, reject) => {
        console.log("打包渲染进程......");
        const renderCompiler = webpack(renderConfig);
        renderCompiler.run((err, stats) => {
            if (err) {
                console.log("打包渲染进程遇到Error!");
                reject(chalk.red(err));
            } else {
                let log = "";
                stats.compilation.errors.forEach(key => {
                    log += chalk.red(`${key}:${stats.compilation.errors[key]}`) + "\n";
                })
                stats.compilation.warnings.forEach(key => {
                    log += chalk.yellow(key) + "\n";
                })
                Object.keys(stats.compilation.assets).forEach(key => {
                    log += chalk.blue(key) + "\n";
                })
                log += chalk.green(`time:${(stats.endTime-stats.startTime)/1000} s\n`) + "\n";
                resolve(`${log}`);
            }
        })
    })
}

复制代码

在package.json中新增打包命令

"scripts": {
    "start": "electron ./src/main/main.js",
    "dev": "node ./builder/dev.js",
    "build": "node ./builder/build.js"
  },

npm run build执行打包,这次还真是出奇的顺利啊,看一下app文件夹,已经生成了静态文件,然后直接在浏览器打开index.html文件,正常显示。

 

四、使用vuex,vue-router,axios

说好的全家桶呢,这里我们不用vue-resource了,使用axios。

1、使用vuex

安装vuex依赖,在src>render文件夹下新建store文件夹,并在store文件夹下新增:

actions.js

export default {}

index.js

复制代码

import Vue from 'vue';
import Vuex from 'vuex';
import actions from './actions.js';
import mutations from './mutations.js';
Vue.use(Vuex);
// 这里为全局的,模块内的请在模块内动态注册
const store = new Vuex.Store({
    strict: true,
    state: {
        userInfo: {
            name: 'haoluo',
            address: 'beijing'
        }
    },
    getters: {},
    mutations,
    actions
});
export default store;

复制代码

mutations.js

复制代码

export default {
    //设置用户信息
    setUserInfo(state, config) {
        if (!config) {
            state.userInfo = {};
        }
        for (var objName in config) {
            state.userInfo[objName] = config[objName];
        }
    }
}

复制代码

以上三个文件的实力代码,比官网教程还简单,可以自行研究一下文档。

文件建好之后,需要把store挂载到vue实例上,找到vue工程的入口文件,src>render>index.js

复制代码

import Vue from 'vue';
import store from './store/index.js';
import index from './views/index.vue';

//取消 Vue 所有的日志与警告
Vue.config.silent = true;
new Vue({
    el: '#app',
    store: store,
    render: h => h(index)
});

复制代码

然后我们就可以使用啦,找到根组件,src>render>views>index.vue

复制代码

<template>
    <div class="content">
        <h1>Welcome to electron-vue-template!</h1>
        <h2>name:{{userInfo.name}}</h2>
        <h2>address:{{userInfo.address}}</h2>
    </div>
</template>

<script>
import {mapState} from 'vuex';
export default {
    computed: {
        ...mapState(['userInfo'])
    }
}
</script>
<style></style>

复制代码

mapState是state的辅助函数,是个语法糖,借助mapState我们可以更方面的获取属性,而不需要写一堆啰里吧嗦的东西,通过计算属性computed接收userInfo,然后就可以使用啦,运行本地调试,发现页面上已经可以正常显示了

属性有了之后我们可以使用,但如果想要改变vuex中存储的属性呢?为了保证单向数据流以及方便对数据的追踪等一些其他原因,不建议直接修改vuex的属性,而是需要通过mutations,这里也有一个辅助函数mapMutations,用法同mapState类似,只不过需要用methods去接收,作为一个全局方法使用

复制代码

<!-- render>views>index.vue -->
<template>
    <div class="content">
        <h1>Welcome to electron-vue-template!</h1>
        <h2>name:{{userInfo.name}}</h2>
        <h2>address:{{userInfo.address}}</h2>
        <button @click="changeAddress">设置address为tianjin</button>
    </div>
</template>

<script>
import {mapState,mapMutations} from 'vuex';
export default {
    computed: {
        ...mapState(['userInfo'])
    },
    methods: {
        ...mapMutations(['setUserInfo']),
        changeAddress() {
            this.setUserInfo({
                address: 'tianjin'
            });
        }
    }
}
</script>
<style></style>

复制代码

当点击按钮的时候。userInfo中的address被修改了,页面渲染的值也相应的改变了

 

2、使用vue-router

安装vue-router依赖,在render文件夹下新增router文件夹,并在其中新增index.js

复制代码

module.exports = [
    {
        path: '/index.html',
        name: 'index',
        meta: {
            title: '首页',
            author: '--',
            parentRouter: '--'
        },
        component: (resolve) => {
            require.ensure([], () => {
                return resolve(require('../views/index.vue'))
            }, "index")
        },
        children: []
    }
];

复制代码

在入口文件render>index.js中引入并挂载

复制代码

// render>index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from './store/index.js';
import routers from './router/index.js';
import index from './views/index.vue';

Vue.use(VueRouter);

let router = new VueRouter({
    routes: routers
})

//取消 Vue 所有的日志与警告
Vue.config.silent = true;
new Vue({
    el: '#app',
    router: router,
    store: store,
    render: h => h(index)
});

复制代码

运行一下,页面可以正常显示,在地址栏输入http://localhost:8099/index.html时,也是没有问题的,现在新增加一个页面,订单页

复制代码

<template>
    <div class="content">
        <h1>order page!</h1>
    </div>
</template>

<script>
export default {}
</script>
<style></style>

复制代码

再配置下路由

复制代码

module.exports = [
    {
        path: '/index.html',
        name: 'index',
        meta: {
            title: '首页',
            author: '--',
            parentRouter: '--'
        },
        component: (resolve) => {
            require.ensure([], () => {
                return resolve(require('../views/index.vue'))
            }, "index")
        },
        children: []
    },
    {
        path: '/order.html',
        name: 'order',
        meta: {
            title: '订单页',
            author: '--',
            parentRouter: '--'
        },
        component: (resolve) => {
            require.ensure([], () => {
                return resolve(require('../views/order.vue'))
            }, "order")
        },
        children: []
    }
];

复制代码

并在首页index.vue中增加跳转按钮,运行之后,发现跳不过去,尴尬~~~,路由跳转,需要有<router-view></router-view>去接收才行啊

改造一下吧,views下新增home.vue,把index.vue中的内容拷贝过去,index.vue改为下面这样

复制代码

<!-- render>views>index.vue -->
<template>
    <div>
        <router-view></router-view>
    </div>
</template>

<script>
export default {
    methods: {},
    mounted() {
        this.$router.push({
            name: 'home'
        });
    }
}
</script>
<style></style>

复制代码

router文件改为下面这样

复制代码

module.exports = [
    {
        path: '/index.html',
        name: 'index',
        meta: {
            title: '首页',
            author: '--',
            parentRouter: '--'
        },
        component: (resolve) => {
            require.ensure([], () => {
                return resolve(require('../views/index.vue'))
            }, "index")
        },
        children: [
            {
                path: '/home.html',
                name: 'home',
                meta: {
                    title: 'home页',
                    author: '--',
                    parentRouter: '--'
                },
                component: (resolve) => {
                    require.ensure([], () => {
                        return resolve(require('../views/home.vue'))
                    }, "home")
                },
                children: []
            },
            {
                path: '/order.html',
                name: 'order',
                meta: {
                    title: '订单页',
                    author: '--',
                    parentRouter: '--'
                },
                component: (resolve) => {
                    require.ensure([], () => {
                        return resolve(require('../views/order.vue'))
                    }, "order")
                },
                children: []
            }
        ]
    }
];

复制代码

再次运行,页面已经可以正常跳转了。

3、axios,这里暂时不说,后续electron和vue联调的时候会补上。

几点说明:

  1、截止目前,webpack配置以及dev和build脚本仅仅是调通大致的流程,还有很多可优化可考究的地方,后续完善项目的过程中会对打包流程逐渐润色;

  2、vue全家桶在实际项目中高级用法很多,细节也很多,这里只是最简单的用法,如若用到实际项目中,还要基于目前情况再做细化;

  3、本系列文章旨在记录、回顾整个项目框架搭建流程,把握整体结构,很多地方需要根据实际项目再做处理;

  4、如有错误或不当的地方,欢迎指出,共同进步!

摘要:前面两篇介绍了如何启动Electron和Vue项目,但到目前为止,虽然把Electron和Vue放到了一个工程里,但貌似它们之间并没有什么关系,通过不同的命令各自运行,Vue写出的页面也无法在Electron中展示,这跟我们的标题显然是不符合的。先喊个口号,从我做起,拒绝标题党!这篇就说一下,如何把Electron和Vue通过Webpack结合起来。项目完整代码:https://github.com/luohao8023/electron-vue-template

一、新建webpack.main.config.js,webpack打包electron的常规配置。

复制代码

const path=require('path');
const webpack = require('webpack');
const { dependencies } = require('../package.json');
module.exports = {
    mode: process.env.NODE_ENV,
    entry: {
        main: ['./src/main/main.js'],
    },
    output: {
        path: path.join(process.cwd(), 'app'),
        libraryTarget: 'commonjs2',
        filename: './[name].js'
    },
    node: {
        fs: 'empty',
        __dirname: false
    },
    optimization: {
        runtimeChunk: false,
        minimize: true
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            }
        ]
    },
    externals: [
        ...Object.keys(dependencies || {})
    ],
    resolve: {
        extensions: ['.js']
    },
    plugins:[
        new webpack.NoEmitOnErrorsPlugin(),
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': '"production"'
        })
    ],
    target: 'electron-main'
};

复制代码

二、改造dev.js,主体思路是打包主进程、打包渲染进程,启动electron应用。

复制代码

process.env.NODE_ENV = 'development';//开发模式
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackHotMiddleware = require('webpack-hot-middleware');
const chalk = require('chalk');
const http = require('http');
const { spawn } = require('child_process');
const electron = require('electron');
const path = require('path');

// 构建主进程
function buildMain() {
    const mainWebpackConfig = require('./webpack.main.config.js');
    return new Promise((resolve, reject) => {
        console.log('打包APP主进程......');
        let log = '';
        // 删除历史打包数据
        require('del')(['./app/main.js']);
        const mainCompiler = webpack(mainWebpackConfig);
        mainCompiler.run((err, stats) => {
            let errorInfo = '';
            if (err) {
                console.log('打包主进程遇到Error!');
                reject(chalk.red(err));
            } else {
                Object.keys(stats.compilation.assets).forEach(key => {
                    log += chalk.blue(key) + '\n';
                })
                stats.compilation.warnings.forEach(key => {
                    log += chalk.yellow(key) + '\n';
                })
                stats.compilation.errors.forEach(key => {
                    errorInfo += chalk.red(`${key}:${stats.compilation.errors[key]}`) + '\n';
                })
                log += errorInfo+ chalk.green(`time:${(stats.endTime-stats.startTime)/1000} s\n`) + "\n";
                if(errorInfo){
                    reject(errorInfo)
                }else{
                    resolve(log);
                }
                console.log('打包主进程完毕!', log);
            }
        });
    });
}

// 构建渲染进程
function devRender() {
    console.log('启动渲染进程调试......');
    const webpackDevConfig = require('./webpack.render.config.js');
    const compiler = webpack(webpackDevConfig);
    new WebpackDevServer(
        compiler, {
            contentBase: webpackDevConfig.output.path,
            publicPath: webpackDevConfig.output.publicPath,
            open: true,//打开默认浏览器
            inline: true,//刷新模式
            hot: true,//热更新
            quiet: true,//除第一次编译外,其余不显示编译信息
            progress: true,//显示打包进度
            setup(app) {
                app.use(webpackHotMiddleware(compiler));
                app.use('*', (req, res, next) => {
                    if (String(req.originalUrl).indexOf('.html') > 0) {
                        console.log(req.originalUrl)
                        getHtml(res);
                    } else {
                        next();
                    }
                });
            }
        }
    ).listen(8099, function(err) {
        if (err) return console.log(err);
        console.log(`Listening at http://localhost:8099`);
    });
    compiler.hooks.done.tap('doneCallback', (stats) => {
        const compilation = stats.compilation;
        Object.keys(compilation.assets).forEach(key => console.log(chalk.blue(key)));
        compilation.warnings.forEach(key => console.log(chalk.yellow(key)));
        compilation.errors.forEach(key => console.log(chalk.red(`${key}:${stats.compilation.errors[key]}`)));
        console.log(chalk.green(`${chalk.white('渲染进程调试完毕\n')}time:${(stats.endTime-stats.startTime)/1000} s`));
    });
}

// 启动Electron
function startElectron() {
    let electronProcess = spawn(electron, [path.join(process.cwd(), 'app/main.js')]);
    electronProcess.stdout.on('data', data => {
        // 正常输出为蓝色
        electronLog(data, 'blue');
    });
    electronProcess.stderr.on('data', data => {
        // 错误信息为红色
        electronLog(data, 'red');
    });
}

// 美化输出
function electronLog(data, color) {
    let log = '';
    data.toString().split(/\r?\n/).forEach(line => {
        log += `\n${line}`;
    });
    if (/[0-9A-z]+/.test(log)) {
        console.log(
            chalk[color].bold('┏ Electron -------------------') + 
            log + 
            chalk[color].bold('┗ ----------------------------')
        );
    }
}

function getHtml(res) {
    http.get(`http://localhost:8099`, (response) => {
        response.pipe(res);
    }).on('error', (err) => {
        console.log(err);
    });
}

// 构建
function build() {
    Promise.all([buildMain(), devRender()]).then(() => {
        startElectron();
    }).catch(err => {
        console.log(err);
        process.exit();
    });
}

build();

复制代码

运行npm run dev,迎接即将出现的各种错误吧!以下不是运行一次时出现的错误,而是每次解决完报错之后默认运行一次。

1、找不到babel-loader,这个简单,装一个就行了,npm i babel-loader -D。

2、看样子是babel-loader还需要其他依赖,按提示安装,npm i @babel/core -D。

3、直接打开了浏览器有木有?!!在dev.js中62行,把打开默认浏览器的选项设为false,即open:false。

4、再运行,electron可以正常打开了,但显示但内容却不对,无论怎么修改vue文件重新运行,都不会起作用。找到src/main/main.js,把

win.loadURL(encodeURI(indexPath));

这一行改为

win.loadURL('http://localhost:8099');

发现之前代码的一个错误,win变量是在createWindow函数中声明但,但在函数外部却有调用,修改一下。

再次运行,可以正常启动,而且显示的是vue文件中的内容,尝试修改一下页面内容,热刷新也是正常的。

至此,我们已经可以正常的进行本地开发调试了,再说一下大致的思路:

第一篇和第二篇主要是把完整的Electron项目和Vue项目融合到一个工程里,本篇主要是增加了主进程的webpack配置,并改造了dev.js,在这个脚本中主要做了下面的事:启动渲染进程的打包(这里是devServer),打包主进程,以main.js为入口,输出位置为app文件夹,之后用node的spawn模块,以electron命令执行app文件夹下的main.js,启动electron,并把electron的页面地址只想devServer地址,这就是本地调试的大致逻辑了。文字不太多,主要是理解webpack配置及调试脚本。后面会介绍到构建整个项目,并打包exe安装文件,以及其他的一些配置优化、项目结构优化。

摘要:前面几篇介绍了如何启动electron和vue项目,并进行联合调试,这篇就来给我们的Electron配置润色一下,至少看起来不那么像一个‘demo’。项目完整代码:https://github.com/luohao8023/electron-vue-template

一、’清理‘主进程文件main.js,提取窗口配置文件。

之前我们是把创建窗口以及窗口配置都放在了main.js中,这样会让我们的主进程看起来很乱,掺杂了各种配置、各方面的代码,而且一旦我们的项目稍微复杂一些,比如同时维护多个窗口,或者有很多针对某个窗口的事件监听等。这里所说的一个个窗口其实就是electron的渲染进程,不同的渲染进程由主进程来统一维护和调度。把渲染进程提取为单独的配置文件,对外只暴露方法,这样就能简化主进程代码,也让我们的项目结构更清晰、更合理,总之是好处多多。

新建文件:main>index.js

复制代码

/**
* Tip:    主进程
* Author: haoluo
* Data:   2020-02-25
**/
const {
    BrowserWindow,
    dialog
} = require("electron");
const electron = require("electron");
const process = require("process");
const url = require("url");
const path = require("path");
const cookie = require('cookie');

const devMode = process.env.NODE_ENV === "development";
let mainWindow = null;

const filter = {
    urls: ['http://*.kakayang.cn/*']
};
//创建窗口
function createWindow() {
    // 首页路径,file协议,pathToFileURL得到编码过的URL
    let filePath = url.pathToFileURL(path.join(__dirname, 'index.html')).href;
    let indexUrl = 'http://localhost:8099/';
    let winW = electron.screen.getPrimaryDisplay().workAreaSize.width,
        winH = electron.screen.getPrimaryDisplay().workAreaSize.height;
    let config = {
        title: "electron-vue-template",
        width: winW <= 1240 ? winW : 1240,
        height: winH <= 730 ? winH : 730,
        minWidth: winW <= 1240 ? winW : 1240,
        minHeight: winH <= 730 ? winH : 730,
        offscreen: true,
        show: true,
        center: true,
        frame: false,  //去掉窗口边框
        autoHideMenuBar: true, //隐藏菜单栏
        titleBarStyle: 'customButtonsOnHover',
        simpleFullscreen: true,
        resizable: process.platform === 'darwin', //可否调整大小
        movable: true, //可否移动
        minimizable: true, //可否最小化
        maximizable: true, //可否最大化
        fullscreen: false, //MAC下是否可以全屏
        skipTaskbar: false, //在任务栏中显示窗口
        acceptFirstMouse: true, //是否允许单击页面来激活窗口
        transparent: process.platform === 'darwin', //允许透明
        opacity: 1,//设置窗口初始的不透明度
        closable: true,
        backgroundColor: '#fff',
        allowRunningInsecureContent: true,//允许一个 https 页面运行 http url 里的资源
        webPreferences: {
            devTools: true, //是否打开调试模式
            webSecurity: false,//禁用安全策略
            allowDisplayingInsecureContent: true,//允许一个使用 https的界面来展示由 http URLs 传过来的资源
            allowRunningInsecureContent: true, //允许一个 https 页面运行 http url 里的资源
            nodeIntegration: true//5.x以上版本,默认无法在渲染进程引入node模块,需要这里设置为true
        }
    };
    // 增加session隔离配置
    config.webPreferences.partition = `persist:${Date.now()}${Math.random()}`;
    mainWindow = new BrowserWindow(config);
    global.windowIds.main = mainWindow.webContents.id;
    // 开发环境使用http协议,生产环境使用file协议
    mainWindow.loadURL(devMode ? encodeURI(indexUrl) : filePath);

    //监听关闭
    mainWindow.on('closed', function () {
        mainWindow = null;
    }).on('close', function (event) {
        mainWindow.send("close-window-render");
        event.preventDefault();
    }).on('ready-to-show', function () {
        mainWindow.show();
    });

    try {
        if (mainWindow.webContents.debugger.isAttached()) mainWindow.webContents.debugger.detach("1.1");
        mainWindow.webContents.debugger.attach("1.1");
        mainWindow.webContents.debugger.sendCommand("Network.enable");
    } catch (err) {
        console.log("无法启动调试", err);
        dialog.showErrorBox("get", "无法启动调试");
    }
    // 拦截请求并处理cookie
    mainWindow.webContents.session.webRequest.onBeforeSendHeaders(filter, onBeforeSendHeaders);
    mainWindow.webContents.session.webRequest.onHeadersReceived(filter, onHeadersReceived);
    return mainWindow;
}
function onBeforeSendHeaders(details, callback) {
    if (details.requestHeaders) {
        details.requestHeaders['Cookie'] = global.cookie;
        details.requestHeaders['Origin'] = details.url;
        details.requestHeaders['Referer'] = details.url;
    }
    callback({ requestHeaders: details.requestHeaders });
}
function onHeadersReceived(details, callback) {
    let cookieArr = [];
    for (let name in details.responseHeaders) {
        if (name.toLocaleLowerCase() === 'Set-Cookie'.toLocaleLowerCase()) {
            cookieArr = details.responseHeaders[name];
        }
    }
    let webCookie = "";
    cookieArr instanceof Array && cookieArr.forEach(cookieItem => {
        webCookie += cookieItem;
    });
    let webCookieObj = cookie.parse(webCookie);
    let localCookieObj = cookie.parse(global.cookie || '');
    let newCookie = Object.assign({}, localCookieObj, webCookieObj);
    let cookieStr = "";
    for (let name in newCookie) {
        cookieStr += cookie.serialize(name, newCookie[name]) + ";";
    }
    global.cookie = cookieStr;
    callback({ response: details.responseHeaders, statusLine: details.statusLine });
}
module.exports = {
    create(_callback) {
        if (mainWindow && !mainWindow.isDestroyed()) {
            mainWindow.destroy();
        }
        mainWindow = createWindow();
        if (_callback instanceof Function) _callback(mainWindow);
        return mainWindow;
    }
}

复制代码

修改main.js:

复制代码

const { app, BrowserWindow } = require("electron");
let mainWindow = require("./index.js");

//注册全局变量
// 页面跟路径配置,优先使用此配置,考虑到小版本更新时,版本之间的切换
global.wwwroot = {
    path: __dirname
};
global.cookie = "";
//主窗口id,在创建主窗口的js中获取并修改此处
global.windowIds = {
    main: 0
};

app.on('ready', () => {
    mainWindow.create();
});
app.on('window-all-closed', function() {
    setTimeout(() => {
        let allwindow = BrowserWindow.getAllWindows();
        if (allwindow.length === 0 ) app.exit(1);
    }, 500);
});

复制代码

二、单实例检查,只允许启动一个客户端。

新建文件:main->libs->runCheck.js:

复制代码

const {
    app,
    BrowserWindow
} = require("electron");
module.exports=()=>{
    // 单实例检查
    const gotTheLock = app.requestSingleInstanceLock();
    if (!gotTheLock) return app.quit();
    app.on('second-instance', () => {
        let myWindows = BrowserWindow.getAllWindows();
        myWindows.forEach(win => {
            if (win && !win.isDestroyed()) {
                if (win.isMinimized()) win.restore();
                win.focus();
            }
        });
    });
}

复制代码

在main.js中引入并执行check函数:

require("./libs/runCheck.js")(); //禁止打开多份

三、注册快捷键打开控制台:

细心的话可以发现,我们已经把控制台关掉了。以往的做法是在代码里打开控制台,打包发布时再把代码注释掉,某个环境的包出问题了,又要放开限制再打对应环境的包,相当的麻烦。这里的解决方案是:

通过注册快捷键的方式来操作控制台,而不是频繁的注释、取消注释代码。

新建文件:main->libs->shortcut.js:

复制代码

const {
    app,
    BrowserWindow
} = require("electron");
const globalShortcut = require("electron").globalShortcut;
class Shortcut{
    register(keys='Command+Control+Alt+F4'){
        globalShortcut.register(keys, function () {
            let allWindow = BrowserWindow.getAllWindows();
            for(let index =0;index < allWindow.length ;index++){
                let win=allWindow[index]
                if(win.webContents && !win.webContents.isDevToolsOpened()){
                    win.webContents.openDevTools({mode: 'detach'});
                }
            }
        })
    }
    
}
app.on('will-quit', function () {
    globalShortcut.unregisterAll()
});
module.exports=new Shortcut();

复制代码

然后在主进程中引用并执行:

复制代码

const shortcut = require("./libs/shortcut.js"); //注册快捷键

app.on('ready', () => {
    //注册快捷键打开控制台事件
    shortcut.register('Command+Control+Alt+F5');
    mainWindow.create();
});

复制代码

四、配置devServer:

写死端口可不是个好主意,得能配置才行,不然万一哪个端口被占用,要修改所有引用的地方,很是麻烦。

新建文件:config->devServerConfig.js:

复制代码

/**
* Tip:    devServer的配置
* Author: haoluo
* Data:   2020-02-25
* Tips:   使用以下命令启动各环境配置,npm run dev [dev|test|release]
**/

let envList = ['dev', 'test', 'release'];
let currentEnv = 'release';
let envArg = process.argv[2];

if (envArg && envList.includes(envArg)) {
    currentEnv = envArg;
}
//导出服务配置
module.exports = {
    url: '127.0.0.1',
    port: 8098,
    // 运行环境
    currentEnv: currentEnv,
    // 调试完打开浏览器
    devComplateOpened: true
};

复制代码

可以看到我们增加了启动参数,用来调试不同环境,这个参数可以用来标记不同环境的后端服务,对于后端接口地址我们也可以提取配置文件,跟这个环境参数相对应,这里就不多说了。

修改index.js:

const devServerConfig = require('@config/devServerConfig.js');

let indexUrl = `http://${devServerConfig.url}:${devServerConfig.port}/`;

修改dev.js中使用到端口信息的地方。

摘要:到目前为止,我们的项目已经具备了PC客户端该有的一些基础功能和调试环境,但是总感觉缺了灵魂,那就是结合实际项目、实际业务的细节处理,缺着吧。。。这篇文章就介绍一下预加载和自动更新,文字功底有限,如有介绍的不清楚的地方,欢迎留言指正,或者跳过文字,直接去看代码,项目完整代码:https://github.com/luohao8023/electron-vue-template,随博客更新。

一、预加载

1、什么是预加载?什么场景能用到? 

preload String (可选) -在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径。 当 node integration 关闭时, 预加载的脚本将从全局范围重新引入node的全局引用标志。

摘自electron官网的一段介绍,https://www.electronjs.org/docs/api/browser-window

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')
        }
    });

复制代码

理解应该差不多了,但什么场景能用到这玩意儿呢?按正常的逻辑来想,主进程启动后启动渲染进程,渲染进程加载页面就完事儿了,哪会用到这个preolad呢?

想一下,如果我们有以下场景:

a、如果我们启动了一个窗口(渲染进程),加载了一个线上的页面,本地没有页面文件,但要做一些错误处理,比如网络错误,页面加载失败,然后在页面空白但时候插入一些元素;

b、如果我们的一套代码部署在web端和客户端,需要用一个变量判断是在web端还是客户端;

...........

感觉举的例子好勉强啊,不要见怪,就是大概这么个意思,没准哪天就遇到了非preload解决不了的问题呢,毕竟这玩意儿还是有它的特殊之处的;

上面两个场景如果用preload来解决的话,思路是利用prelaod中能访问window对象的特点,比如b,代码中可以用window.isClient来判断是否在客户端,默认为false,然后在preload中把window.isClient设置为true,而对于部署在web端的代码来说,这个值就是false。

2、怎么用?

上面说了怎么引用preload脚本,现在说一下怎么写,下面开始xxoo乱写乱画了:

复制代码

// 访问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单文件打包,注意targer要"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"
}

复制代码

我相信,总会遇到使用preload就能迎刃而解的问题。

二、自动更新

我们都知道,electron其实是封了个chrome内核,抛开壳子不说,里面运行的其实就是我们的h5页面,而就算我们跑了个空项目,没有任何内容,打包后的安装包也得30M左右,我们希望自己的程序有自动更新功能,但是更新机制是怎样的呢?

如果我们只改动了页面某一处的文本,却要用户更新整个安装包,那显然太不合理了,一是体验不好,二是我们的流量啊......

基于这种考虑,加上electron主进程和渲染进程的划分,那我们可以考虑如下更新机制:

主进程有改动时,那没的说,用户需要更新整个客户端(当然有精力有条件的可以做动态更新,官方好像是说支持,主要是我不会);渲染进程有改动时,我们只需要把h5包下载到本地然后加载就行了,当然这需要我们打包的时候能把h5包区分出来,在更新后能打开对应版本的h5包。

这里我们称主进程的更新为大版本更新,渲染进程的更新为小版本更新。

1、打包配置修改

为什么突然扯到打包配置修改了呢,因为牵扯到小版本的更新,那我们打包的时候就得把这个“小版本”给打出来,不然更新个🔨。因为下面还有一篇文章是专门介绍这个Electron-vue项目的打包,所以这里呢就只讲一下怎么把小版本的压缩包给打出来。

修改build.js,思路是:使用webpack打包主进程、打包preload、打包渲染进程,得到可执行文件目录app,然后引入electrin-builder对app目录进行打包,产生一个安装包,然后把渲染进程的文件压缩并标记版本号。这里我们只拣和本节相关的说,就是打包渲染进程和压缩小版本文件,为什么能拆出来说呢,当然是分模块封装的好处啦,各个进程的打包逻辑封装一下拆出来,能随意组合还能复用,否则一个又臭又长的打包脚本文件,很难维护。

具体代码就不贴出来了,太占篇幅,也没什么用,可以到https://github.com/luohao8023/electron-vue-template看完整代码。

2、增加启动页,启动页显示欢迎语等,在这里检查更新

这里我们暂且叫它检查更新页,这个检查更新页是个独立的渲染进程,用户打开程序时首先显示检查更新窗口,但是这个窗口也不一定显示检查更新字样,偷偷的检查就行了,有新版本就提示更新,没有新版本就显示欢迎语。

这儿的逻辑是单独拆分出来的,不能是自动更新的时候把自动更新逻辑本身也给更新了,容易乱套。

修改主进程代码,程序启动时首先启动自动更新窗口:

app.on('ready', () => {
    //注册快捷键打开控制台事件
    shortcut.register('Command+Control+Alt+F5');
    mainWindow = updateWin.create();
});

然后注册监听事件,因为自动更新窗口逻辑完成之后需要呼起主窗口,需要主进程来调度:

复制代码

//启动主窗体
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();
});

复制代码

自动更新窗口只需专注于更新逻辑就行了,逻辑结束后呼起主窗口:

复制代码

        // 更新逻辑看下面伪代码
        const v1 = getOnlineVersion();
        const v2 = getLocalVersion();
        const needUpdate = checkVersion(v1, v2);
        if (needUpdate) {
            downloadVersion();
        }
        
        this.runMain();

复制代码

在呼起主窗口的同时给主窗口传递参数,并告知主窗口有没有更新版本,以及主窗口需要加载哪个小版本的包,而主窗口在loadURL时也要做下改动:

    let wwwroot = global.wwwroot.path ? global.wwwroot.path : __dirname;
    let filePath = url.pathToFileURL(path.join(wwwroot, 'index.html')).href;

而wwwrot就是当前小版本包的根路径,由主进程来维护,自动更新小版本后会修改这个值,以告诉主进程加载哪个版本。

好了,啰嗦了一大堆,好多地方没贴代码,感觉贴了代码的话,篇幅就不受控制了,还是去github看完整项目吧,自动更新这一块是伪代码,只实现了渲染进程的切换(即自动更新窗口呼起主窗口),具体的更新逻辑实现起来的话还要拿线上版本去比较,这个还是留给大家在实际项目中去调试吧,就是上面这个思路。

好啦,有什么问题可以留言交流,也可以直接去看代码https://github.com/luohao8023/electron-vue-template

 

摘要:整个项目就剩最后一哆嗦了,但仅仅是当作demo模版来说,实际项目的话,还有很多需要细化的地方。项目完整代码:https://github.com/luohao8023/electron-vue-template,随博客更新。

一、打包客户端

首先是要改一下build.js,把上篇文章没做的事儿给做了。

上篇文章已经构建出了可执行文件目录app,这次我们要做的就是使用electron-builder把app目录打包为安装包。

在之前的基础上引入electron-builder,然后对app目录进行打包:

复制代码

const builder = require('electron-builder');

// 在所有的打包逻辑执行完成之后,确认已经正确生成了app目录
builder.build().then(() => {
    del(['./pack/*.yaml', './pack/*.blockmap']);
    // 为了方便,打包完成之后我们打开文件管理器
    openFileManager();
});

function openFileManager() {
    // 打开文件管理器
    let dirPath = path.join(__dirname, '..', 'pack');
    if (process.platform === 'darwin') {
        spawn('open', [dirPath]);
    } else if (process.platform === 'win32') {
        spawn('explorer', [dirPath]);
    } else if (process.platform === 'linux') {
        spawn('nautilus', [dirPath]);
    }
}

复制代码

然后自信满满的开始打包。。。。

报错了,说是什么描述缺失,icon没有设置啥的,就是打包的时候没有配置呗,去看下package.json文件,果然是少了build字段,package.json文件中的build字段就是有关打包的配置,一些必要的配置项还是要填的。

在package.json中增加build字段:

复制代码

"build": {
    "asar": true,
    "productName": "Electron+vue+webpack模板",
    "appId": "com.electron.template",
    "copyright": "Copyright © template",
    "directories": {
        "output": "pack"
    },
    "files": [
        "app/**"
    ],
    "mac": {
        "identity": "com.electron.templat",
        "target": [
            "dmg"
        ],
        "artifactName": "${productName}.${ext}",
        "icon": "main/favicon/favicon.icns"
    },
    "dmg": {
        "title": "${productName}",
        "artifactName": "${productName}.${ext}",
        "icon": "main/favicon/favicon.icns"
    },
    "win": {
        "legalTrademarks": "Copyright © template",
        "publisherName": "electron",
        "requestedExecutionLevel": "highestAvailable",
        "target": [
            {
                "target": "nsis",
                "arch": [
                    "ia32"
                ]
            }
        ],
        "artifactName": "${productName}.${ext}",
        "icon": "main/favicon/favicon.ico"
    },
    "nsis": {
        "oneClick": false,
        "allowToChangeInstallationDirectory": true,
        "perMachine": true,
        "allowElevation": true,
        "artifactName": "${productName}-安装包-V${version}.${ext}",
        "runAfterFinish": true,
        "shortcutName": "Electron+vue+webpack-template"
    }
  },

复制代码

现在我们来挨个解读一下各个配置项都是什么意思,当然还有很多其他配置,这里不再额外介绍了。

asar:是否打包为asar文件,设置为true的话,相当于给你的代码加密了一下,直接就是个.asar的文件,具体内容需要解密了之后才能看到;设置为false的话,不对你的代码进行加密处理,也就是用户安装你的程序之后,找到安装目录,就能直接看到源码,目录结构跟你开发的时候是一样的,不太安全,建议设置为true;

productName:你的应用名称,比如会显示在安装程序的标题处,以及安装完成后的应用程序目录里;

appId:你程序的唯一id,比如绑定到某第三方平台或应用市场,一般会需要这个,我是没有,随便填的;

copyright:按照网站的copyright来理解就好啦,如果你的程序不需要发不到各大市场的话,这个内容可以忽略;

directories:它下面还有其他属性,这里我们只填了ouptut选项,就是打包输出目录,我们这里填了pack文件夹;

files:需要打包哪些内容,就是你的源代码,我们这里填的"app/*",就是app目录下的所有内容;

 

上面都是一些基础的内容,下面介绍一下针对不同平台的配置:

mac:

identity:这个我不是特别清楚,看名字应该是表明开发者或者软件身份的东西;

target:你要打包成什么格式的安装包,这里填的是dmg,可以填多个;

artifactName:生成的可执行文件的名称;

icon:应用图标,显示在桌面快捷方式或者系统托盘;

针对dmg的单独配置这里就不说了,因为mac选项的target属性可以多填,我们填了dmg,就对dmg做了单独的配置,也可以忽略;

win:

legalTrademarks:合法商标。。。。。。

publisherName:发行商类似的意思;

requestedExecutionLevel:应用程序需要的权限,我们这里填的是highestAvailable,就是当前用户允许的最高权限,你如果是管理员用户在使用,那就是管理员权限,如果是普通用户在使用那就是普通管理员权限。设置为最高权限可以解决一些问题,比如对c盘的一些文件进行操作等。但是请注意一点,如果你的程序是以管理员身份运行的,但是你又想实现从桌面往应用程序中拖动文件的功能,这是不行的,因为文件管理器的权限是低于管理员的,windows上无法从低权限处往高权限处拖动文件,这点还是要注意一下;

target:目标平台,我们选32位,并且使用nsis打包;

artifactName:可执行文件名称;

icon:应用图标;

nsis:

因为electron-builder是基于nsis打包的(有兴趣的可以了解一下nsis),所以这里提供了一些基础配置:

oneClick:不是点击一次,也不是单例什么的,这里是一键安装的意思,设置为true的话,只要双击打开安装包,程序会自动安装并运行;建议设置为false,让用户点击下一步、下一步来安装;

allowToChangeInstallationDirectory:是否允许修改安装目录,默认为false;

perMachine:每台机器是否只允许安装一个程序,如果已安装,再次安装的时候,会要求用户先删除之前的程序;

allowElevation:允许请求提升(权限),如果设置为false,用户必须重启程序才能安装提升了权限的安装程序;

artifactName:安装包名称;

runAfterFinish:安装完成是否运行程序;

shortcutName:快捷方式名称

这是模版里用到的所有属性,解释的也不一定对。当然还有很多其他的配置项,感兴趣的可以搜一下了解了解,说不定某个小小的配置就能解决你一个大问题呢。

 

好了,说了这么多,现在接着运行打包命令吧,看看啥情况:

(node:96470) UnhandledPromiseRejectionWarning: Error: Application entry file "index.js" in the "/Volumes/SHARE/projects/github/electron-vue-template/pack/mac/Electron+vue+webpack模板.app/Contents/Resources/app.asar" does not exist. Seems like a wrong configuration.

还是有错啊,说的很详细,说是程序入口文件index.js不存在,我们看一下:

复制代码

  "name": "electron-vue-template",
  "version": "1.0.0",
  "description": "electron-vue-template",
  "main": "index.js",
  "scripts": {
    "dev": "node ./builder/dev.js",
    "build": "node ./builder/build.js"
  },

复制代码

main字段就是程序入口,我们写的是index.js,看下代码目录,我们的主进程入口是main.js,那就改一下吧,把index.js改为main.js,接着运行打包命令:

还是出错呦,入口文件找不到,这个问题还真想来好大一会儿,感觉没有错啊,名称也修改来,就是main.js啊,又瞅了眼代码目录才恍然大悟,这不阴沟里翻船嘛,通常情况下main.js是在工程根目录的,但是我们规划完工程目录之后,把main.js给打包到app目录下了,所以入口字段应该填"app/main.js",接着运行打包命令,这次终于成功了,看下pack文件夹中生成的文件:

第一个dmg文件就是mac的安装包,第二个yml文件记录了程序的一些基本信息,mac文件夹下是一个免安装的可执行程序,最后一个就是我们压缩出来的小版本,windows下跟这个目录不一样。

先不着急安装,打开mac文件夹下的可执行程序,可以直接打开我们的程序,打开之后懵了,一片空白啊,啥东西也没有,赶紧找找原因。

打开app目录发现,没有生成update.html,经排查发现,上次提交的代码有个地方写错了,拼错了个单词:

复制代码

Promise.all([buildPreload(), buildRender()]).then(resolve => {
    resolve.forEach(log => {
        console.log('打包输出===>', log);
    });
    const outpath = path.join(__dirname, '../pack/');
    try {
        fs.mkdirSync(outpath);
    } catch(e) {
        console.log('已创建pack文件夹', e);
    }
    console.log('打包渲染进程完毕!压缩小版本!');
    const zipPath = renderConfig.output.path;
    const fileName = setup.versionType + '-' + setup.version.join('.');
    const filePath = path.join(zipPath, `../pack/${fileName}.zip`);
    compress(zipPath, filePath, 7 , (type,msg) => {
        if (type === 'error'){
            Promise.reject('压缩文件时出错:' + msg);
        } else {
            console.log(`压缩包大小为:${(msg / 1024 / 1024).toFixed(2)}MB`);
        }
    });
    Promise.all([buildMain(), buildUpdate()]).then(resplve => {
        resolve.forEach(log => {
            console.log('打包输出===>', log)
        });
        builder.build().then(() => {
            del(['./pack/*.yaml', './pack/*.blockmap']);
            openFileManager();
        });
    }).catch(err => {
        console.error('打包【main】-【update】错误输出===>', err);
        process.exit(2);
    });
}).catch(err => {
    console.error('打包【preload】-【render】出错,输出===>', err);
    process.exit(1);
});

复制代码

看一下,第二个Promise.all.then中,参数写成了resplve,而在打印log的时候用的是resolve,偏偏上面有resovle,所以也没报错,但是第二次promise的log就全被吃了,赶紧改回来,再跑一下,果然有个错误:

打包输出===> ModuleNotFoundError: Module not found: Error: Can't resolve 'css-loader' in '/Volumes/SHARE/projects/github/electron-vue-template':undefined

没有css-loader,那就装一个:

打包输出===> ModuleNotFoundError: Module not found: Error: Can't resolve 'less-loader' in '/Volumes/SHARE/projects/github/electron-vue-template':undefined

又说没有less-loader,再装一个,运行命令,看到app目录下生成了update.html,这下应该没问题了吧。

打开mac文件夹下的免安装文件,程序启动后跟我们本地调试的效果是一样的,再使用安装包安装一下,安装完成打开后也是正常的。

好啦,打包客户端就说到这儿了,下面说一下怎么使用同一套代码打包web端。

二、打包web端

这里建议把打包web端的逻辑单独拆出来,网站代码是同一套,但是打包逻辑是两套

dev的逻辑就是起个devServer返回html文件就行了,不再多说。

而打包的话是针对单页面的,只会生成一个html文件,如果相针对每个路由都生成一个html文件,这里提供下思路:

引入路由文件,遍历路由,拿到路径,针对每个路径,实例化一个HtmlWebpackPlugin,即可生成一个html文件:

复制代码

webpackDevConfig.plugins.push(new HtmlWebpackPlugin({
    template: './src/index.ejs',
    filename: `.${routerPah}`,
    title: "加载中...",
    inject: false,
    hash: true,
    minify: false,
    cache: false
}))

复制代码

在package.js中增加启动命令:

  "scripts": {
    "dev": "node ./buildClient/dev.js",
    "devweb": "node ./buildWeb/dev.js",
    "build": "node ./buildClient/build.js",
    "buildweb": "node ./buildWeb/build.js"
  }

分别调试和打包客户端、web端。

这篇文章端内容就到这里了,具体的逻辑还是要去看代码的。针对这套逻辑我们其实有已经上线了的产品的,很多细化的东西也有,但是不便拿出来说,也不好做成demo。模板中可能会有些冗余代码,就是之前的逻辑没有删除干净,自行优化就好了。

有什么问题欢迎留言讨论。项目完整代码:https://github.com/luohao8023/electron-vue-template

作者:罗知晏 出处: https://www.cnblogs.com/kakayang/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】。

Logo

前往低代码交流专区

更多推荐