Electron是一个跨平台建立桌面应用程序的框架,容许咱们使用HTML/CSS/JS去建立跨平台桌面应用程序。随着大前端的发展,当咱们去开发Web UI时,会习惯性的使用Webpack等构建工具以及React等钱的MVVM框架去辅助开发。在开发Electron时也是同理,所以本文将介绍如何使用Webpack/React去打包构建整个Electron应用,并使用Electron-builder构建出App。其实社区提供了不少Electron Webpack的脚手架和模版,好比electron-forge
、electron-react-boilerplate
等等,但经过本身的摸索和构建(重复造轮子),能对前端打包构建体系有个更深入的理解。javascript
Electron是使用Web前端技术(HTML/CSS/JavaScript/React等)来建立原生跨平台桌面应用程序的框架,它能够认为是Chromium、Node.js、Native APIs的组合。 css
Node.js是一个 JavaScript 运行时,基于事件驱动、非阻塞I/O 模型而得以轻量和高效。在Electron中负责调用系统底层API来操做原生GUI以及主线程JavaScript代码的执行,而且 Node.js中经常使用的utils、fs等模块在 Electron 中也能够直接使用。html
Native APIs是系统提供的GUI功能,好比系统通知、系统菜单、打开系统文件夹对话框等等,Electron经过集成Native APIs来为应用提供操做系统功能支持。前端
与传统Web网站不一样,Electron基于主从进程模型,每一个Electron应用程序有且仅有一个主进程(Main Process),和一个或多个渲染进程(Renderer Process),对应多个Web页面。除此以外,还包括GUP进程、扩展进程等其余进程。 java
在安装Electron的过程当中遇到最大的问题可能就是下载Electron包时出现网络超时(万恶的墙),致使安装不成功。 node
node_modules/@electron/get/dist/cjs/artifact-utils.js
,找处处理镜像的方法
mirrorVar
function mirrorVar(name, options, defaultValue) {
// Convert camelCase to camel_case for env var reading
const lowerName = name.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}_${b}`).toLowerCase();
return (process.env[`NPM_CONFIG_ELECTRON_${lowerName.toUpperCase()}`] ||
process.env[`npm_config_electron_${lowerName}`] ||
process.env[`npm_package_config_electron_${lowerName}`] ||
process.env[`ELECTRON_${lowerName.toUpperCase()}`] ||
options[name] ||
defaultValue);
}
复制代码
以及获取下载路径getArtifactRemoteURL
方法react
async function getArtifactRemoteURL(details) {
const opts = details.mirrorOptions || {};
let base = mirrorVar('mirror', opts, BASE_URL); // ELECTRON_MIRROR 环境变量
if (details.version.includes('nightly')) {
const nightlyDeprecated = mirrorVar('nightly_mirror', opts, '');
if (nightlyDeprecated) {
base = nightlyDeprecated;
console.warn(`nightly_mirror is deprecated, please use nightlyMirror`);
}
else {
base = mirrorVar('nightlyMirror', opts, NIGHTLY_BASE_URL);
}
}
const path = mirrorVar('customDir', opts, details.version).replace('{{ version }}', details.version.replace(/^v/, '')); // ELECTRON_CUSTOM_DIR环境变量,并将{{version}}替换为当前版本
const file = mirrorVar('customFilename', opts, getArtifactFileName(details));
// Allow customized download URL resolution.
if (opts.resolveAssetURL) {
const url = await opts.resolveAssetURL(details);
return url;
}
return `${base}${path}/${file}`;
}
复制代码
能够看到能够定义挺多环境变量来指定镜像,好比ELECTRON_MIRROR、ELECTRON_CUSTOM_DIR等等,这其实在官方文档中也有标明linux
Mirror
You can use environment variables to override the base URL, the path at which to look for Electron binaries, and the binary filename. The URL used by
@electron/get
is composed as follows:webpackurl = ELECTRON_MIRROR + ELECTRON_CUSTOM_DIR + '/' + ELECTRON_CUSTOM_FILENAME 复制代码
For instance, to usethe China CDN mirror:c++
ELECTRON_MIRROR="https://cdn.npm.taobao.org/dist/electron/" ELECTRON_CUSTOM_DIR="{{ version }}" 复制代码
所以在下载Electron时只须要添加了两个环境变量便可解决网络超时(墙)的问题
ELECTRON_MIRROR="https://cdn.npm.taobao.org/dist/electron/" ELECTRON_CUSTOM_DIR="{{ version }}" npm install --save-dev electron
复制代码
安装完electron后,能够尝试写一个最简单的electron应用,项目结构以下
project
|__index.js # 主进程
|__index.html # 渲染进程
|__package.json #
复制代码
对应的主进程index.js
部分
const electron = require('electron');
const { app } = electron;
let window = null;
function createWindow() {
if (window) return;
window = new electron.BrowserWindow({
webPreferences: {
nodeIntegration: true // 容许渲染进程中使用node模块
},
backgroundColor: '#333544',
minWidth: 450,
minHeight: 350,
height: 350,
width: 450
});
window.loadFile('./index.html').catch(console.error);
window.on('close', () => window = null);
window.webContents.on('crashed', () => console.error('crash'));
}
app.on('ready', () => createWindow());
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', createWindow)
复制代码
对应的渲染进程index.html
部分
<!DOCTYPE>
<html lang="zh">
<head><title></title></head>
<style> .box {color: white;font-size: 20px;text-align: center;} </style>
<body>
<div class="box">Hello world</div>
</body>
</html>
复制代码
向package.json
中添加运行命令
{
...,
"main": "index.js",
"script": {
"start": "electron ."
},
...
}
复制代码
npm run start
运行,一个最简单的electron应用开发完成。
Electron项目一般由主进程和渲染进程组成,主进程用于实现应用后端,通常会使用C++或rust实现核心功能并以Node插件的形式加载到主进程(好比字节跳动的飞书、飞聊的主进程则是使用rust实现),其中的JavaScript部分像一层胶水,用于链接Electron和第三方插件,渲染进程则是实现Web UI的绘制以及一些UI交互逻辑。主进程和渲染进程是独立开发的,进程间使用IPC进行通讯,所以对主进程和渲染进程进行分开打包,也就是两套webpack配置,同时为区分开发环境和生产环境,也须要两套webpack配置。此外在开发electron应用时会有多窗口的需求,所以对渲染进程进行多页面打包,总体结构以下。
project
|__src
|__main # 主进程代码
|__index.ts
|__other
|__renderer # 渲染进程代码
|__index # 一个窗口/页面
|__index.tsx
|__index.scss
|__other
|__dist # webpack打包后产物
|__native # C++代码
|__release # electron-builder打包后产物
|__resources # 资源文件
|__babel.config.js # babel配置
|__tsconfig.json # typescript配置
|__webpack.base.config.js # 基础webpack配置
|__webpack.main.dev.js # 主进程开发模式webpack配置
|__webpack.main.prod.js # 主进程生产模式webpack配置
|__webpack.renderer.dev.js # 渲染进程开发模式webpack配置
|__webpack.renderer.prod.js # 渲染进程生产模式webpack配置
复制代码
打包构建流程其实比较简单,使用webpack分别打包主进程和渲染进程,最后在使用electron-builder对打包后的代码进行打包构建,最后构建出app。 多窗口的处理,在渲染进程下的每个目录表明一个窗口(页面),并在webpack entry入口中标明,打包时分别打包到
dist/${name}
目录下,主进程加载时按webpack entry标识的名称进行加载。
首先安装webpack
npm install --save-dev webpack webpack-cli webpack-merge
复制代码
安装react
npm install --save react react-dom
复制代码
安装typescript
npm install --save-dev typescript
复制代码
以及安装对应的types包
npm install --save-dev @types/node @types/react @types/react-dom @types/electron @types/webpack
复制代码
编写对应的tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "ES2018",
"module": "CommonJS",
"lib": [
"dom",
"esnext"
],
"declaration": true,
"declarationMap": true,
"jsx": "react",
"strict": true,
"pretty": true,
"sourceMap": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"moduleResolution": "Node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"allowJs": true,
"resolveJsonModule": true
},
"exclude": [
"node_modules",
"native",
"resources"
],
"include": [
"src/main",
"src/renderer"
]
}
复制代码
编写基础的webpack配置webpack.base.config.js
,主进程和渲染进程都须要用到这个webpack配置
const path = require('path');
// 基础的webpack配置
module.exports = {
module: {
rules: [
// ts,tsx,js,jsx处理
{
test: /\.[tj]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader', // babel-loader处理jsx或tsx文件
options: { cacheDirectory: true }
}
},
// C++模块 .node文件处理
{
test: /\.node$/,
exclude: /node_modules/,
use: 'node-loader' // node-loader处理.node文件,用于处理C++模块
}
]
},
resolve: {
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '.node'],
alias: {
'~native': path.resolve(__dirname, 'native'), // 别名,方便import
'~resources': path.resolve(__dirname, 'resources') // 别名,方便import
}
},
devtool: 'source-map',
plugins: []
};
复制代码
安装babel-loader处理jsx或tsx文件,node-loader处理.node文件
npm install --save-dev babel-loader node-loader
复制代码
安装相应的babel插件
npm install --save-dev @babel/core @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-optional-chaining @babel/plugin-syntax-dynamic-import @babel/plugin-transform-react-constant-elements @babel/plugin-transform-react-inline-elements
复制代码
以及安装babel预设
npm install --save-dev @babel/preset-env @babel/preset-react @babel/preset-typescript
复制代码
编写相应的babel.config.js
配置,配置中须要对开发模式和生产模式下的代码分开处理,即便用不一样的插件
const devEnvs = ['development', 'production'];
const devPlugins = []; // TODO 开发模式
const prodPlugins = [ // 生产模式
require('@babel/plugin-transform-react-constant-elements'),
require('@babel/plugin-transform-react-inline-elements'),
require('babel-plugin-transform-react-remove-prop-types')
];
module.exports = api => {
const development = api.env(devEnvs);
return {
presets: [
[require('@babel/preset-env'), {
targets: {
electron: 'v9.0.5' // babel编译目标,electron版本
}
}],
require('@babel/preset-typescript'), // typescript支持
[require('@babel/preset-react'), {development, throwIfNamespace: false}] // react支持
],
plugins: [
[require('@babel/plugin-proposal-optional-chaining'), {loose: false}], // 可选链插件
[require('@babel/plugin-proposal-decorators'), {legacy: true}], // 装饰器插件
require('@babel/plugin-syntax-dynamic-import'), // 动态导入插件
require('@babel/plugin-proposal-class-properties'), // 类属性插件
...(development ? devPlugins : prodPlugins) // 区分开发环境
]
};
};
复制代码
主进程打包时只须要将src/main
下的全部ts文件打包到dist/main
下,值得注意的是,主进程对应的是node工程,若是直接使用webpack进行打包会将node_modules
中的模块也打包进去,因此这里使用webpack-node-externals
插件去排除node_modules
模块
npm install --save-dev webpack-node-externals
复制代码
开发模式下对应的webpack配置webpack.main.dev.config.js
以下
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const webpackBaseConfig = require('./webpack.base.config');
module.exports = merge.smart(webpackBaseConfig, {
devtool: 'none',
mode: 'development', // 开发模式
target: 'node',
entry: path.join(__dirname, 'src/main/index.ts'),
output: {
path: path.join(__dirname, 'dist/main'),
filename: 'main.dev.js' // 开发模式文件名为main.dev.js
},
externals: [nodeExternals()], // 排除Node模块
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: 'development'
})
],
node: {
__dirname: false,
__filename: false
}
});
复制代码
生产模式与开发模式相似,所以对应webpack配置的webpack.main.prod.config.js
以下
const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack');
const webpackDevConfig = require('./webpack.main.dev.config');
module.exports = merge.smart(webpackDevConfig, {
devtool: 'none',
mode: 'production', // 生产模式
output: {
path: path.join(__dirname, 'dist/main'),
filename: 'main.prod.js' // 生产模式文件名为main.prod.js
},
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: 'production'
})
]
});
复制代码
渲染进程的打包就是正常前端项目的打包流程,考虑到electron项目有多窗口的需求,因此对渲染进程进行多页面打包,渲染进程打包后的结构以下
dist
|__renderer # 渲染进程
|__page1 # 页面1
|__index.html
|__index.prod.js
|__index.style.css
|__page2 # 页面2
|__index.html
|__index.prod.js
|__index.style.css
复制代码
先来看生产模式下的打包,安装相应的插件和loader,这里使用html-webpack-plugin插件去生成html模版,并且须要对每个页面生成一个.html文件
npm install --save-dev mini-css-extract-plugin html-webpack-plugin
复制代码
css-loader
、sass-loader
、style-loader
处理样式,url-loader
、file-loader
处理图片和字体,resolve-url-loader处理scss文件url()
中的相对路径问题
npm install --save-dev css-loader file-loader sass-loader style-loader url-loader resolve-url-loader
复制代码
因为使用scss编写样式,因此须要安装node-sass
包
npm install --save-dev node-sass
复制代码
安装node-sass
其实存在挺多坑的,正常安装常常会碰到下载网络超时的问题(又是墙惹的祸),通常解决就是靠镜像。
--sass-binary-site
参数,以下
npm install --save-dev node-sass --sass-binary-site=http://npm.taobao.org/mirrors/node-sass
复制代码
对应的生产模式的webpack配置webpack.renderer.prod.config.js
以下
// 渲染进程prod环境webpack配置
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.config');
const entry = {
index: path.join(__dirname, 'src/renderer/index/index.tsx'), // 页面入口
};
// 对每个入口生成一个.html文件
const htmlWebpackPlugin = Object.keys(entry).map(name => new HtmlWebpackPlugin({
inject: 'body',
scriptLoading: 'defer',
template: path.join(__dirname, 'resources/template/template.html'), // template.html是一个很简单的html模版
minify: false,
filename: `${name}/index.html`,
chunks: [name]
}));
module.exports = merge.smart(webpackBaseConfig, {
devtool: 'none',
mode: 'production',
target: 'electron-preload',
entry
output: {
path: path.join(__dirname, 'dist/renderer/'),
publicPath: '../',
filename: '[name]/index.prod.js' // 输出则是每个入口对应一个文件夹
},
module: {
rules: [ // 文件处理规则
// 处理全局.css文件
{
test: /\.global\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: { publicPath: './' }
},
{
loader: 'css-loader',
options: { sourceMap: true }
},
{loader: 'resolve-url-loader'}, // 解决样式文件中的相对路径问题
]
},
// 通常样式文件,使用css模块
{
test: /^((?!\.global).)*\.css$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
sourceMap: true
}
},
{loader: 'resolve-url-loader'},
]
},
// 处理scss全局样式
{
test: /\.global\.(scss|sass)$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: { sourceMap: true, importLoaders: 1 }
},
{loader: 'resolve-url-loader'},
{
loader: 'sass-loader',
options: { sourceMap: true }
}
]
},
// 处理通常sass样式,依然使用css模块
{
test: /^((?!\.global).)*\.(scss|sass)$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
importLoaders: 1,
sourceMap: true
}
},
{loader: 'resolve-url-loader'},
{
loader: 'sass-loader',
options: { sourceMap: true }
}
]
},
// 处理字体文件 WOFF
{
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/font-woff' }
}
},
// 处理字体文件 WOFF2
{
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/font-woff' }
}
},
// 处理字体文件 TTF
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: { limit: 10000, mimetype: 'application/octet-stream' }
}
},
// 处理字体文件 EOT
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
use: 'file-loader'
},
// 处理svg文件 SVG
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: { limit: 10000, mimetype: 'image/svg+xml' }
}
},
// 处理图片
{
test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
use: {
loader: 'url-loader',
options: { limit: 5000 }
}
}
]
},
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: 'production'
}),
new MiniCssExtractPlugin({
filename: '[name]/index.style.css',
publicPath: '../'
}),
...htmlWebpackPlugin
]
});
复制代码
到此为止,已经完成了主进程的打包配置和渲染进程生产模式打打包配置,这里能够直接测试项目生产环境的打包结果。
首先向package.json
中添加相应的运行命令,build-main
打包主进程,build-renderer
打包渲染进程,build
主进程和渲染进程并行打包,start-main
运行Electron项目
{
...
"main": "dist/main/main.prod.js",
"scripts": {
"build-main": "cross-env NODE_ENV=production webpack --config webpack.main.prod.config.js",
"build-renderer": "cross-env NODE_ENV=production webpack --config webpack.renderer.prod.config.js",
"build": "concurrently \"npm run build-main\" \"npm run build-renderer\"",
"start-main": "electron ./dist/main/main.prod.js"
},
...
}
复制代码
在编写脚本中使用到了cross-env,顾名思义,提供跨平台的环境变量支持,而concurrently用于并行运行命令,安装以下
npm install --save-dev cross-env concurrently
复制代码
能够尝试的写个小例子测试一下打包结果,主进程src/main/index.ts
import { BrowserWindow, app } from 'electron';
import path from "path";
// 加载html,目前只对生产模式进行加载
function loadHtml(window: BrowserWindow, name: string) {
if (process.env.NODE_ENV === 'production') {
window.loadFile(path.resolve(__dirname, `../renderer/${name}/index.html`)).catch(console.error);
return;
}
// TODO development
}
let mainWindow: BrowserWindow | null = null;
// 建立窗口
function createMainWindow() {
if (mainWindow) return;
mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true
},
backgroundColor: '#333544',
minWidth: 450,
minHeight: 350,
width: 450,
height: 350
});
loadHtml(mainWindow, 'index');
mainWindow.on('close', () => mainWindow = null);
mainWindow.webContents.on('crashed', () => console.error('crash'));
}
app.on('ready', () => { createMainWindow() });
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => { createMainWindow() })
复制代码
渲染进程主页面src/renderer/index/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
// @ts-ignore
import style from './index.scss'; // typescript不支持css模块,因此这么写编译器会不识别,建议加个@ts-ignore
function App() {
return (
<div className={style.app}>
<h3>Hello world</h3>
<button>+ Import</button>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('app'));
复制代码
使用build
命令并行打包主进程和渲染进程代码
npm run build
复制代码
打包后的结果以下面所示,因此主进程在加载html文件时的路径就是../renderer/${name}/index.html
使用
npm run start-main
命令运行项目。
在渲染进程开发模式下须要实现模块热加载,这里使用react-hot-loader包,另外须要起webpack服务的话,还须要安装webpack-dev-server
包。
npm install --save-dev webpack-dev-server
npm install --save react-hot-loader @hot-loader/react-dom
复制代码
修改babel配置,开发环境下添加以下插件
const devPlugins = [require('react-hot-loader/babel')];
复制代码
修改渲染进程入口文件,即在render
时判断当前环境并包裹ReactHotContainer
import { AppContainer as ReactHotContainer } from 'react-hot-loader';
const AppContainer = process.env.NODE_ENV === 'development' ? ReactHotContainer : Fragment;
ReactDOM.render(
<AppContainer>
<App/>
</AppContainer>,
document.getElementById('app')
);
复制代码
对应的开发模式的webpack配置webpack.renderer.prod.config.js
// 渲染进程dev环境下的webpack配置
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {spawn} = require('child_process');
const webpackBaseConfig = require('./webpack.base.config');
const port = process.env.PORT || 8080;
const publicPath = `http://localhost:${port}/dist`;
const hot = [
'react-hot-loader/patch',
`webpack-dev-server/client?http://localhost:${port}/`,
'webpack/hot/only-dev-server',
];
const entry = {
index: hot.concat(require.resolve('./src/renderer/index/index.tsx')),
};
// 生成html模版
const htmlWebpackPlugin = Object.keys(entry).map(name => new HtmlWebpackPlugin({
inject: 'body',
scriptLoading: 'defer',
template: path.join(__dirname, 'resources/template/template.html'),
minify: false,
filename: `${name}.html`,
chunks: [name]
}));
module.exports = merge.smart(webpackBaseConfig, {
devtool: 'inline-source-map',
mode: 'development',
target: 'electron-renderer',
entry,
resolve: {
alias: {
'react-dom': '@hot-loader/react-dom' // 开发模式下
}
},
output: { publicPath, filename: '[name].dev.js' },
module: {
rules: [
// 处理全局css样式
{
test: /\.global\.css$/,
use: [
{loader: 'style-loader'},
{
loader: 'css-loader',
options: {sourceMap: true}
},
{loader: 'resolve-url-loader'},
]
},
// 处理css样式,使用css模块
{
test: /^((?!\.global).)*\.css$/,
use: [
{loader: 'style-loader'},
{
loader: 'css-loader',
options: {
modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
sourceMap: true,
importLoaders: 1
}
},
{loader: 'resolve-url-loader'}
]
},
// 处理全局scss样式
{
test: /\.global\.(scss|sass)$/,
use: [
{loader: 'style-loader'},
{
loader: 'css-loader',
options: {sourceMap: true}
},
{loader: 'resolve-url-loader'},
{loader: 'sass-loader'}
]
},
// 处理scss样式,使用css模块
{
test: /^((?!\.global).)*\.(scss|sass)$/,
use: [
{loader: 'style-loader'},
{
loader: 'css-loader',
options: {
modules: { localIdentName: '[name]__[local]__[hash:base64:5]' },
sourceMap: true,
importLoaders: 1
}
},
{loader: 'resolve-url-loader'},
{loader: 'sass-loader'}
]
},
// 处理图片
{
test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
use: {
loader: 'url-loader',
options: { limit: 5000 }
}
},
// 处理字体 WOFF
{
test: /\.woff(\?v=\d+\.\d+\/\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 5000,
mimetype: 'application/font-woff'
}
}
},
// 处理字体 WOFF2
{
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 5000,
mimetype: 'application/font-woff'
}
}
},
// 处理字体 TTF
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 5000,
mimetype: 'application/octet-stream'
}
}
},
// 处理字体 EOT
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
use: 'file-loader'
},
// 处理SVG
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
use: {
loader: 'url-loader',
options: {
limit: 5000,
mimetype: 'image/svg+xml'
}
}
}
]
},
plugins: [
// webpack 模块热重载
new webpack.HotModuleReplacementPlugin({
multiStep: false
}),
new webpack.EnvironmentPlugin({
NODE_ENV: 'development'
}),
new webpack.LoaderOptionsPlugin({
debug: true
}),
...htmlWebpackPlugin
],
// webpack服务,打包后的页面路径为http://localhost:${port}/dist/${name}.html
devServer: {
port,
publicPath,
compress: true,
noInfo: false,
stats: 'errors-only',
inline: true,
lazy: false,
hot: true,
headers: {'Access-Control-Allow-Origin': '*'},
contentBase: path.join(__dirname, 'dist'),
watchOptions: {
aggregateTimeout: 300,
ignored: /node_modules/,
poll: 100
},
historyApiFallback: {
verbose: true,
disableDotRule: false
}
}
});
复制代码
向package.json
中添加运行命令,dev-main
开发模式下打包主进程并运行Electron项目,dev-renderer
开发模式下打包渲染进程
{
...,
"start": {
...,
"dev-main": "cross-env NODE_ENV=development webpack --config webpack.main.dev.config.js && electron ./dist/main/main.dev.js",
"dev-renderer": "cross-env NODE_ENV=development webpack-dev-server --config webpack.renderer.dev.config.js",
"dev": "npm run dev-renderer"
},
...
}
复制代码
在这里渲染进程能够经过模块热加载更新代码,但主进程不能够,而且主进程加载的.html文件须要在渲染进程打包完成后才能加载,所以修改webpack.renderer.dev.config.js
配置,添加打包完渲染进程后对主进程进行打包并运行的逻辑
...,
devServer: {
before() {
// 启动渲染进程后执行主进程打包
console.log('start main process...');
spawn('npm', ['run', 'dev-main'], { // 至关于命令行执行npm run dev-main
shell: true,
env: process.env,
stdio: 'inherit'
}).on('close', code => process.exit(code))
.on('error', spawnError => console.error(spawnError));
}
},
...
复制代码
修改主进程的loadHtml
函数,开发模式经过url
来加载对应的页面
function loadHtml(window: BrowserWindow, name: string) {
if (process.env.NODE_ENV === 'production') {
window.loadFile(path.resolve(__dirname, `../renderer/${name}/index.html`)).catch(console.error);
return;
}
// 开发模式
window.loadURL(`http://localhost:8080/dist/${name}.html`).catch(console.error);
}
复制代码
npm run dev
开发模式下运行以下
renderer
目录下新建
userInfo
目录表示用户信息窗口, 并添加到开发模式和生产模式下的配置文件中,即
webpack.renderer.dev.config.js
和
webpack.renderer.prod.config
的entry入口中。
webpack.renderer.dev.config.js
部分
...
const entry = {
index: hot.concat(require.resolve('./src/renderer/index/index.tsx')), // 主页面
userInfo: hot.concat(require.resolve('./src/renderer/userInfo/index.tsx')) // userInfo页面
};
...
复制代码
webpack.renderer.prod.config.js
部分
...
const entry = {
index: path.join(__dirname, 'src/renderer/index/index.tsx'), // 主页面
userInfo: path.join(__dirname, 'src/renderer/userInfo/index.tsx') // userInfo页面
};
...
复制代码
主进程实现对userInfo
窗口的建立逻辑
function createUserInfoWidget() {
if (userInfoWidget) return;
if (!mainWindow) return;
userInfoWidget = new BrowserWindow({
parent: mainWindow,
webPreferences: { nodeIntegration: true },
backgroundColor: '#333544',
minWidth: 250,
minHeight: 300,
height: 300,
width: 250
});
loadHtml(userInfoWidget, 'userInfo');
userInfoWidget.on('close', () => userInfoWidget = null);
userInfoWidget.webContents.on('crashed', () => console.error('crash'));
}
复制代码
主窗口渲染进程使用IPC与主进程进行通讯,发送打开用户信息窗口消息
const onOpen = () => { ipcRenderer.invoke('open-user-info-widget').catch(); };
复制代码
主进程接收渲染进程消息,并建立出userInfo
窗口
ipcMain.handle('open-user-info-widget', () => {
createUserInfoWidget();
})
复制代码
运行结果
Electron-builder能够理解为一个黑盒子,可以解决Electron项目的各个平台(Mac、Window、Linux)打包和构建而且提供自动更新支持。安装以下,须要注意electron-builder只能安装到devDependencies
下
npm install --save-dev electron-builder
复制代码
而后在package.json
中添加build字段,build字段配置参考:build字段通用配置
{
...,
"build": {
"productName": "Electron App",
"appId": "electron.app",
"files": [
"dist/",
"node_modules/",
"resources/",
"native/",
"package.json"
],
"mac": {
"category": "public.app-category.developer-tools",
"target": "dmg",
"icon": "./resources/icons/app.icns"
},
"dmg": {
"backgroundColor": "#ffffff",
"icon": "./resources/icons/app.icns",
"iconSize": 80,
"title": "Electron App"
},
"win": {
"target": [ "nsis", "msi" ]
},
"linux": {
"icon": "./resources/icons/app.png",
"target": [ "deb", "rpm", "AppImage" ],
"category": "Development"
},
"directories": {
"buildResources": "./resources/icons",
"output": "release"
}
},
...
}
复制代码
并向package.json
中添加运行命令,package
打包多个平台,package-mac
构建Mac平台包,package-win
构建window平台包,package-linux
构建linux平台包
{
...,
"script": {
"package": "npm run build && electron-builder build --publish never",
"package-win": "npm run build && electron-builder build --win --x64",
"package-linux": "npm run build && electron-builder build --linux",
"package-mac": "npm run build && electron-builder build --mac"
}
...
}
复制代码
在执行打包时,electron-builder会去下载electron包,正常下载会出现超时(墙又来惹祸了),致使打包不成功,解决方法依然是使用镜像
ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ npm run package-mac
复制代码
说到electron应用,可能会须要C++模块支持,好比部分函数使用C++实现,或者调用已有的C++库或dll文件。前面在编写webpack.base.config.js
配置时使用node-loader
去处理.node文件,但在Electron下编写C++插件时,须要注意Electron提供的V8引擎可能与本地安装的Node提供的V8引擎版本不一致,致使编译时出现版本不匹配问题,所以在开发原生C++模块时可能须要手动编译Electron模块以适应当前Node的V8版本。另外一种方法则是使用node-addon-api
包或者Nan
包去编写原生C++模块自动去适应Electron中的V8版本,关于Node C++模块能够参考文章:将C++代码加载到JavaScript中。
例如一个简单的C++加法计算模块,C++部分
#include <node_api.h>
#include <napi.h>
using namespace Napi;
Number Add(const CallbackInfo& info) {
Number a = info[0].As<Number>();
Number b = info[1].As<Number>();
double r = a.DoubleValue() + b.DoubleValue();
return Number::New(info.Env(), r);
}
Object Init(Env env, Object exports) {
exports.Set("add", Function::New(env, Add));
return exports;
}
NODE_API_MODULE(addon, Init)
复制代码
执行node-gyp rebuild
构建.node文件,主进程在加载.node文件,并注册一个IPC调用
import { add } from '~build/Release/addon.node';
ipcMain.handle('calc-value', (event, a, b) => {
return add(+a, +b);
})
复制代码
渲染进程则进行IPC调用发送calc-value
消息获得结果,并渲染到页面中
const onCalc = () => {
ipcRenderer.invoke('calc-value', input.a, input.b).then(value => {
setResult(value);
});
};
复制代码
到此为止,项目结构已经基本搭建完毕,剩下的则是添加一些基础的状态库或者路由处理库,项目中使用Redux管理状态,React-Router处理路由,安装以下
npm install --save redux react-redux react-router react-router-dom history
npm install --save-dev @types/redux @types/react-redux @types/react-router @types/react-router-dom @types/history
复制代码
使用HashRouter
做为基础的路由模式
const router = (
<HashRouter>
<Switch>
<Route path="/" exact>
<Page1/>
</Route>
<Route path="/page2">
<Page2/>
</Route>
</Switch>
</HashRouter>
);
复制代码
react-router-dom
提供了useHistory
Hooks方便获取history执行路由相关操做,好比跳转到某个路由页面
const history = useHistory();
const onNext = () => history.push('/page2');
复制代码
Redux部分则可使用useSelector
和useDispatch
Hooks,直接选择store中的state和链接dispatch,避免使用connect高阶组件形成的冗余代码问题
const count = useSelector((state: IStoreState) => state.count);
const dispatch = useDispatch();
const onAdd = () => dispatch({ type: ActionType.ADD });
const onSub = () => dispatch({ type: ActionType.SUB });
复制代码
运行结果
Devtron是一个Electorn调试工具,方便检查,监视和调试应用。能够可视化主进程和渲染进程中的包依赖、追踪和检查主进程和渲染进程互相发送的消息、显示注册的事件和监听器、检查app中可能存在的问题等。
npm install --save-dev devtron
复制代码
使用方式以下
app.whenReady().then(() => {
require('devtron').install();
});
复制代码
另外还可使用electron-devtools-installer
,用于安装Devtools扩展,好比浏览器上经常使用的Redux、React扩展等,它会自动的去Chrome应用商店下载Chrome扩展并安装,不过因为墙的缘由,大几率会下载不了(万恶的墙又来惹祸了)
npm install --save-dev electron-devtools-installer @types/electron-devtools-installer
复制代码
使用方式
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS, REACT_PERF } from 'electron-devtools-installer';
app.whenReady().then(() => {
installExtension([REACT_PERF, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]).then(() => {});
require('devtron').install();
});
复制代码
早期在接触electron时直接使用现成的react模版进行开发,但一味的使用社区模版,出现问题时难以查找,并且社区模版提供的功能也不必定符合本身的需求,虽然是重复造轮子,但在造轮子过程当中也能学到很多东西。项目借鉴了electron-react-bolierplate
的打包模式,对部分地方进行优化调整,添加了一些相应的功能。后续的TODO则是考了对渲染进程和主进程进行包拆分优化以及结构上的优化调整。
Electron构建跨平台应用Mac/Windows/Linux
项目GitHub地址: github.com/sundial-dre…