使用Vue和Electron构建本地应用

Electron提供了一种方式,将 WebApp 构建成本地应用。

另一个类似的方案是NW.js

没有做细致的比较,项目时间很紧,简单的做下对比:

  • Star, Electron 有 7 万多,NW.js 是 3 万 5 千多
  • github/Atom 和 microsoft/VS Code 使用的是 Electron

粗浅的看,区别还是有的:

  • NW.js 的chromium版本很新,目前是73,Electron 当前用的是69,这是由于不同的实现架构造成的,前者更容易更新版本
  • NW.js 启动入口是 html 文件,Electron 是运行 js 程序

最简单的方式,electron-vue#

如果想一步到位的生成可以起步开发的 Vue 项目,最简单的方式是使用SimulatedGREG/electron-vue

SimulatedGREG/electron-vue 提供了模版,在vue-cli已安装的前提下,可以直接一个命令创建项目:

1
vue init simulatedgreg/electron-vue my-project

不过我因为如下理由没有选择这个方式,虽然vue-routervuex等都帮你配置好了:

  • 使用的 Electron 版本太旧了,2.x,当前 Electron 版本是4.1.x5.0将在 4 月底发布
  • 项目不活跃,最后更新是 5 个月以前的了

如果希望构建长期稳定架构的产品,这个项目风险还是很大的。当然它的做法可以借鉴学习。

直接使用 electron-forge#

基于命令,可以创建 electron 项目,创建的这个项目集成了如下命令:

  • 启动开发环境,便于开发调试,支持 hot reloading
  • 构建,打包并生成可分发的包(在 macOS 下)
  • 打包,将项目打包成当前开发系统的可执行程序包
  • 发布,发布到目标上去,比如 github

当前版本 forge 5.2.4#

当前稳定版本是5.2.4,如果想在这个版本下创建 vue 项目,大致步骤是:

  • 执行forge init --template=vue命令,创建使用 vue 模版的项目
  • 在这个空项目基础上,自己手动增加vuexvue-router的支持

不过,这个版本有如下问题:

  • 不支持npm link模块,不利于架构上的关注分离,如果临时项目这个问题不大,但产品级别的话会影响工作效率
  • hot reloading存在 bug,这个看起来只影响开发阶段,但我认为是个比较严重的问题
    • 现象是,当修改文件的时候,比如 html/js/vue,会加载 2 次文件
    • 要想监控到这个现象,需要在DevTools的 Console 界面勾选Preserve log,这样会保存上次加载的日志
    • 这个问题并不出在 Electron-forge 项目本身,而是 forge 依赖的项目,electron-compile
    • 不知道是不是这个原因,当前开发的 forge v6,已经不再使用electron-compile了,而是使用内置的plugin-webpack

因此,forge v5 版本我也不考虑了。

forge v6 beta#

这个版本的问题是,从去年 4 月 16 日的 beta1 到现在最新的 beta34,已经快 1 年了,不知道是否会很快发布(说明成熟了)。

如果要在这个版本开发 vue 项目,就没有现成的 template 可用了。需要自己加进来。好在使用通用的webpack,这个过程并不太难。

创建基本项目#

首先需要按照v6 Getting Started创建一个最基本的项目架子。这个项目太简单了:

  • 还没有 render 进程的 js 文件,自己编写的 js 文件要自己<script src=""..的方式引入进来使用
  • render 进程的 js 文件不能在开发环境的 app 自动 reloading

vanilla 项目,让 js 代码自动 reloading#

需要使用Plugin/Webpack

基本步骤参见为:

  • 安装配置 webpack 插件
    • 安装插件,npm i -D @electron-forge/plugin-webpack
    • 这个插件将webpackHMR,只重新加载修改过的代码,因此性能很好
    • 它实际上使用了 Webpack 的 2 个关键的开发工具
      • webpack-dev-middleware
        • 在开发环境下启动 webserver,可通过它访问 Web 相关程序
        • 类似 webpack-dev-server,提供更多自定义功能的机制
        • 内部封装了webpack-dev-server
        • @electron-forge/plugin-webpack就是借助这个机制增加自己的自定义内容进去
      • webpack-hot-middleware
        • 实现自动加载 js 文件等
        • 使用的是 Webpack 的Hot Module Replacement,概念可参见Concepts/Hot Module Replacement
        • 基本思路是,当开发环境下程序变化,通过webpack-dev-middleware通知浏览器端的webpack-hot-middleware组件,后者根据改动部分,只重新加载受到影响的代码
  • 如果之前 js 是<script src=""..引入到 html 的,删除引入代码部分
  • 按照官方配置要求,在package.json文件中
    • 修改main属性:"main": "./.webpack/main",
    • 加入有关plugins的配置,注意,一定要有js:...,就是 render 的程序文件,如果没有会报错
  • 创建对应的配置文件
    • webpack.main.config.js,主进程配置文件
    • webpack.renderer.config.js,渲染进程配置文件
  • 在主进程的 js 文件中,替换为:mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
    • 需要注意,官方文档写错了,loadUrl
  • 在 render 的 js 文件中,使用 HMR api,注册模块,好让 HMR 发现此模块更新后重新加载它

package.json 改动部分大体是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"config": {
"forge": {
"plugins": [
[
"@electron-forge/plugin-webpack",
{
"mainConfig": "./webpack.main.config.js",
"renderer": {
"config": "./webpack.renderer.config.js",
"entryPoints": [
{
"html": "./src/index.html",
"js": "./src/ui.js",
"name": "main_window"
}
]

webpack.main.config.js很简单,只需要定义入口文件

1
2
3
module.exports = {
entry: "./src/index.js"
};

webpack.renderer.config.js更简单,不需要设置,空的就好了:

1
module.exports = {};

在渲染进程的 js 文件中,需要加入:

1
2
3
if (module.hot) {
module.hot.accept();
}

也许不会在实际项目中这样 HMR 普通 js 文件,但这样可以很好的了解 HMR 的基本机制。

注意:module.hot.accept()只需在入口 js 文件中使用

  • 这样的话,如果引用的 js 文件改变了,HMR 会冒泡到入口文件重新加载
  • 如果引用的 js 文件也加入了module.hot.accept(),那么 HMR 将只加载该文件,而不会加载入口文件
  • 若想合理的实现 HMR,还需要考虑程序架构,静态的/全局的可能不会被重载,而实例级别的就能在重新加载时生效

Vue 项目的配置步骤#

这部分的工作参考了How to create a Vue.js app using Single-File Components, without the CLI.

库的安装#

首先是安装运行依赖包:

1
npm i vue

开发依赖包:

1
npm i vue-loader vue-template-compiler webpack webpack-cli babel-loader @babel/core @babel/preset-env css-loader vue-style-loader html-webpack-plugin rimraf -D

需要注意的是,我们不使用webpack-dev-server,这是和How to create a Vue.js app using Single-File Components, without the CLI.不一样的。我们使用 electron-forge v6 自带的 webpack 插件。

创建相关文件#

列出创建好文件的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── .babelrc.js //新增,babel编译配置文件
├── .gitignore
├── package-lock.json
├── package.json
├── src
│   ├── App.vue //新增.vue
│   ├── index.html
│   ├── index.js
│   └── ui.js
├── webpack.main.config.js
└── webpack.renderer.config.js //需要编辑,增加配置内容
.babelrc.js#
1
2
3
module.exports = {
presets: ["@babel/preset-env"]
};
App.vue,单页面组件#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div id="app">{{ message }}</div>
</template>

<script>
export default {
data() {
return {
message: "Hello World!!!!"
};
}
};
</script>

<style>
#app {
font-size: 18px;
font-family: "Roboto", sans-serif;
color: red;
}
</style>
webpack.renderer.config.js#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
module: {
rules: [
{ test: /\.js$/, use: "babel-loader" },
{ test: /\.vue$/, use: "vue-loader" },
{ test: /\.css$/, use: ["vue-style-loader", "css-loader"] }
]
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new VueLoaderPlugin()
]
};
运行和相关测试#

在开发环境中运行,还是用npm start即可。需要说明的是:

  • electron-forge 方式,不需要考虑切换developementproduction
  • npm start将使用webpack-plugin,有自己定义的部分 webpack 配置,再增加我们的配置文件(webpack.renderer.config.js)的配置
    • 因此在webpack.renderer.config.js中不必再设置entry了,设置了反而不能正确加载
    • 不要按照 vue 的一般配置,设置webpack-dev-server,这个功能被webpack-plugin内部的相关功能接管了

目前的设置,有一些需要注意的,也是我重点测试的:

  • ./src/index.html是不支持 hmr 的,因此如果修改了这个文件,改动不会生效,这很好理解,vue 开发中,index.html 只是个程序的外壳,一般需要改变
  • ./src/ui.js,vue 的入口,这个文件改变了并不会自动生效
    • 我觉得可以接受,因为架构一旦确定也不会频繁改动
    • 如果也需要 hmr,可手动加入if (module.hot) {..部分代码
  • App.vue,无论改动<template><script><style>,都会触发 hmr
  • App.vue引用的hello.js,改动后,会触发App.vue hmr,在此基础上,hello.js引用的其他 js 文件如果改动了,也会触发App.vuehmr
  • 使用npm package打包的 macOS 可执行文件包,可正常启动运行,没有问题

基本上可以说,这样的配置,是可以让 Vue 正常工作的。

集成 Vue-router 及测试#

首先安装:

1
npm i vue-router

主要是测试在npm start开发模式下,以及在npm run package生成的可执行文件,是否都能正确使用vue-router

因为,electron-forge 的 webpack 插件

  • 在开发模式下,使用 webpack-dev-server 服务器模式
  • 在 electron 正式可执行文件,是加载本地文件

看来是不必担心,vue 默认使用#的方式,而不是用浏览器History API

代码编写过程略。route 之间的切换,可以:

1
<router-link to="/about">About</router-link>

这样等同于:

1
<a href="#/about">About</a>

集成 Vuex 及测试#

安装包:

1
npm i vuex

代码编写过程略。

需要注意的是,因为store.js一般都是在ui.js中引用:

1
2
3
4
5
6
7
8
9
10
11
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

new Vue({
el: "#app",
router,
store,
render: h => h(App)
});

因此也不能默认 hmr,需要写代码,见vuex/Hot Reloading

目前的问题是:

  • state 是无法 hmr 的,原因待查,目前考虑到是遇到这样的情况重启 electron app,就是在npm start后,在终端输入rs命令,就可以重启 app
  • mutations 等,是可以 hmr 的

测试本地库是否可正常使用,sqlite3#

安装库:

1
npm i sqlite3

在主进程(./src/index.js)和渲染进程(./src/ui.js)分别加入:

1
require("sqlite3").verbose();

webpack配置文件中加入:

1
2
3
4
5
module.exports = {
..
externals: {
sqlite3: "commonjs sqlite3"
}

目的是,sqlite3 不通过 webpack 绑定。

做了这个设置,在开发环境下应该可以正常运行了。但是不能packagemake

需要将sqlite3的相关依赖库复制到打包文件目录: ./out/your-app-name-plateform-arch/your-app-name.app/Contents/Resources/app/node_modules下,这个分发包才能加载到本地库sqlite3.

为了省事,我是用copy-node-modules将运行时依赖库都复制过去了,包括Vue等,会浪费一些空间。

开发环境下 npm link 的使用#

创建一个 nodejs 库项目,比如名为simple-lib

在项目目录下执行:

1
npm link

simple-lib目录连接加入到全局的node_modules里。

在 electron 的项目里,执行:

1
npm link simple-lib

将从全局的node_modules建立一个连接,在当前项目的node_modules目录下

如果想让simple-lib支持 HMR,需要在simple-lib的入口代码中加:

1
2
3
if (module.hot) {
module.hot.accept();
}

而且也支持打包(package 或者 make)后的使用。

使用 electron-vue-boilerplate#

我根据上述问题,编写了electron-vue-boilerplate,可以直接使用。

也不需要自己复制本地库到 package 目录里去,在这个项目里,使用 forge 的 hook 自动做了。

具体做法见该项目的 README。