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
Chromium由Google開源,至關於Chrome瀏覽器的精簡版,在Electron中負責Web UI的渲染。Chromium可讓開發者在不考慮瀏覽器兼容性的狀況下去編寫Web UI代碼。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
主進程負責窗口的建立、進程間通訊的協調、事件的註冊和分發等。渲染進程負責UI頁面的渲染、交互邏輯的實現等。但在這種進程模型下容易產生單點故障問題,即主進程崩潰或者阻塞將會致使整個應用沒法響應。在安裝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
複製代碼
構建完成後,能夠在release目錄下看到打包出來的結果,Mac下爲.dmg文件
在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…