介绍

这篇文章,主要的是讲述如何搭建Electron项目,这里会用到上篇文章Webpack4+node+typescript+hotReload 搭建的框架,这里,大家可以先clone下来:

1
git clone git@github.com:spcBackToLife/node-webpack4-ts-demo.git

基础框架的搭建主要有以下部分内容,基本每部分都是单独的一篇文章:

  • 基础node环境搭建: webpack4 + node + typescript
  • Electron集成: 单窗口 + 主进程、渲染进程在dev、prod环境的webpack处理。
  • React 集成
  • Mobx 集成 (由于redux比较重,dva比较麻烦,最后选择Mobx做react状态管理会更合适)
  • Electron集成: 多窗口 + 窗口渲染进程webpack处理。
  • 数据缓存、存储服务管理
  • 应用基础功能服务设计:录屏、截屏
  • 应用集成三方环境设计:Python环境
  • 应用webview注入js设计
  • 运行日志集成

Electron 集成 - 单窗口 + 主进程、渲染进程在dev、prod环境的webpack处理

关于Electron基础集成请看:Electron 入门简介,此处不再累赘,照做就行。

  1. 添加Electron依赖

    1
    ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/ yarn add electron@6.0.0 -D
  2. src下新建:main-processrender-process文件夹,将main.ts放置到main-process中。

    • main-process #主进程文件夹
    • render-process #渲染进程文件夹

    关于主进程、渲染进程相关内容,请看文章:Electron-主进程、渲染进程
    强烈建议先读完这个概念再来看这篇文章下面内容,除非你非常熟悉Electron,已经了解主进程、渲染进程。

  3. src下新建:index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>electron-sprout</title>
    <script>
    (
    function() {
    if (!process.env.HOT) { // production环境下,加载打包后的style.css,css处理在webpack里。
    const link = document.createElement('link');
    link.ref = 'stylesheet';
    link.href = '../dist/style.css';
    }
    }
    )
    </script>
    </head>
    <body>
    <div id="root">
    先写个hellowold在这,集成react的时候清掉
    </div>
    </body>
    <script>
    {
    const scripts = [];
    const port = process.env.PORT || 1212;
    // 开发环境下,加载webpack-dev-server下的js。
    // 生产环境下,加载打包后的js
    scripts.push(
    (process.env.HOT)
    ? 'http://localhost:' + port + '/dist/renderer.dev.js'
    : '../dist/renderer.prod.js'
    );
    // 将需要加载的js写入页面,进行加载。
    document.write(
    scripts
    .map(script => `<script defer src="${script}"><\/script>`)
    .join('')
    );
    }
    </script>
    </html>
  4. main-process目录下新建main-window.ts:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    import { BrowserWindow } from 'electron';

    export const createWindow = () => {
    const win = new BrowserWindow({
    show: true,
    width: 890,
    height: 556,
    webgl: true,
    frame: true,
    transparent: false,
    webPreferences: {
    webSecurity: true,
    allowRunningInsecureContent: false,
    nativeWindowOpen: false,
    nodeIntegration: true,
    },
    });
    win.webContents.openDevTools();
    win.loadURL(`file://${__dirname}/../index.html`);
    };

如上代码所见:我们使用electron提供的BrowserWindow打开了一个系统窗口,这个窗口类似于浏览器的一个tab,可以采用loadURL加载一个网页或者本地文件index.html。api具体信息可以查看Electron 官网

  1. 将main.ts的测试代码都删除,换上如下:
1
2
3
4
5
6
7
8
9

import { app } from 'electron';
import { createWindow } from './main-window';

app.on('ready', async () => {
createWindow(); // 创建一个系统应用窗口
});

app.on('window-all-closed', () => app.quit()); // 所有窗口关闭的时候退出应用
  1. 配置webpack处理主进程、渲染进程js。
    关于主进程、渲染进程相关内容,请看文章:Electron-主进程、渲染进程

    首先咱来配置个基础的webpack,用于处理,ts\js\css的: webpack.config.base.js,关于webpack相关内容,自己去学哈,没写文章,不学就照我这个配也行,我配的也不专业,哈哈,能用。

    新建文件夹config/webpack,在下面新建文件:webpack.config.base.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const webpack = require('webpack');

    const isProd = process.env.NODE_ENV === 'production';
    const cssLoader = {
    loader: 'css-loader',
    options: {
    modules: true,
    importLoaders: 1,
    localIdentName: '[local]__[hash:6]',
    },
    };
    // const CopyWebpackPlugin = require('copy-webpack-plugin');
    module.exports = {
    target: 'electron-main', // 会在主进程和渲染进程文章中提及这个是什么,请看第6点开头的那个文章指向。
    resolve: {
    // changed from extensions: [".js", ".jsx"]
    extensions: ['.ts', '.tsx', '.js', '.jsx'], // 需要处理的文件类型
    },
    module: {
    rules: [
    {
    test: /\.scss$/, // css loader配置
    use: [
    isProd ? MiniCssExtractPlugin.loader : 'style-loader',
    cssLoader,
    'sass-loader',
    ],
    },
    {
    test: /\.tsx?$/, //tsx处理
    exclude: /node_modules/,
    use: [

    {
    loader: 'babel-loader',
    options: {
    cacheDirectory: true,
    },
    },
    // {
    // loader: 'awesome-typescript-loader',
    // },
    ],
    },
    // Common Image Formats
    {
    test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, // 图片路径处理
    use: 'url-loader',
    },
    // addition - add source-map support
    { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, // 报错的source-map
    ],
    },
    node: {
    __dirname: false, // 处理打包后dirname filename变量路径不对问题
    __filename: false,
    },
    devtool: 'source-map',
    plugins: [
    new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn/), // 使用moment组件的话,用这个减少moment库体积,这个作为基础设施的一部分吧
    new webpack.NamedModulesPlugin(), // 用于启动HMR(hot module reload)时可以显示模块的相对路径
    ],
    };

    再来处理主进程js。新建文件config/webpack/webpack.config.main.prod.babel.js(关于为什么要区分主进程、渲染进程分别处理js, 第6点提到的那个文章里有哈。),由于主进程开发环境无需做处理,直接可以执行,所以只有production的环境下需要处理js。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    const path = require('path');
    const webpack = require('webpack');
    const merge = require('webpack-merge');
    const TerserPlugin = require('terser-webpack-plugin');
    const baseConfig = require('./webpack.config.base');

    module.exports = merge.smart(baseConfig, {
    devtool: 'source-map',
    mode: 'production',
    entry: './src/main.ts',
    output: {
    path: path.join(__dirname, '../../'),
    filename: './dist/main.prod.js',
    },
    optimization: {
    minimizer: process.env.E2E_BUILD
    ? []
    : [
    new TerserPlugin({ // 压缩Javascript
    parallel: true,
    sourceMap: true,
    cache: true,
    }),
    ],
    },
    plugins: [
    new webpack.EnvironmentPlugin({
    NODE_ENV: 'production',
    }),
    ],
    });

然后,我们再来处理渲染进程的js,在开发环境下,为了改了代码能够自动刷新页面,热更新,我们用了webpack-dev-server做处理,因此处理渲染进程js的时候会分开发环境和生成环境,有不同的配置文件。首先来生成开发环境的吧。
新建文件config/webpack/webpack.config.renderer.dev.babel.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

const path = require('path');
const merge = require('webpack-merge');
const { spawn } = require('child_process');
const webpack = require('webpack');
const baseConfig = require('./webpack.config.base');

const port = process.env.PORT || 1212;
const publicPath = `http://localhost:${port}/dist`;
module.exports = merge.smart(baseConfig, {
devtool: 'inline-source-map',
mode: 'development',
target: 'electron-renderer',
entry: {
renderer: [
require.resolve('../../src/render-process/index.tsx'),
// 'react-hot-loader/patch', // 集成react的时候再开
`webpack-dev-server/client?http://localhost:${port}/`,
'webpack/hot/only-dev-server',
],
},
output: {
publicPath: `http://localhost:${port}/dist`,
filename: '[name].dev.js',
},
plugins: [
new webpack.NamedModulesPlugin(), // 用于启动HMR时可以显示模块的相对路径
new webpack.HotModuleReplacementPlugin(), // hot module replacement 启动模块热替换的插件
new webpack.NoEmitOnErrorsPlugin(),
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
}),
new webpack.LoaderOptionsPlugin({
debug: true,
}),
],
devServer: {
port,
publicPath,
compress: true,
noInfo: true,
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,
},
before() {
if (process.env.START_HOT) {
console.log('Starting Main Process...');
spawn('npm', ['run', 'start-main-dev'], { // 在运行这个server之前会运行package.json里的`start-main-dev`指令,文章下面会配置。
shell: true,
env: process.env,
stdio: 'inherit',
})
.on('close', code => process.exit(code))
.on('error', spawnError => console.error(spawnError));
}
},
},
});

最后贴一下渲染进程的production环境的webpack吧:
新建文件config/webpack/webpack.config.renderer.prod.babel.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const merge = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');
const baseConfig = require('./webpack.config.base');

module.exports = merge.smart(baseConfig, {
devtool: 'source-map',
mode: 'production',
target: 'electron-renderer',
entry: {
renderer: require.resolve('../../src/render-process/index.tsx'),
},
output: {
path: path.join(__dirname, '..', '../dist'),
publicPath: './dist/',
filename: '[name].prod.js',
},
optimization: {
minimizer: process.env.E2E_BUILD
? []
: [
new TerserPlugin({
parallel: true,
sourceMap: true,
cache: true,
}),
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
map: {
inline: false,
annotation: true,
},
},
}),
],
},
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
}),
new MiniCssExtractPlugin({
filename: 'style.css',
}),
new BundleAnalyzerPlugin({
analyzerMode: process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
openAnalyzer: process.env.OPEN_ANALYZER === 'true',
}),
],
});
  1. 还记得咱再运行那个demo的时候使用了ts-node ./src/main-process/main.ts运行可以让我们直接用node环境ts。可是目前还没有ts-electron让我们直接用electron运行ts。因此我们需要用babel把我们的运行入口处理下。我们只需要config目录下新建:babel-entry-config.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    module.exports = {
    extensions: ['.js', '.ts', '.tsx'],
    presets: [
    [
    '@babel/preset-env',
    {
    targets: { electron: require('electron/package.json').version },
    useBuiltIns: 'usage',
    modules: 'commonjs',
    },
    ],
    '@babel/preset-typescript',
    ],
    plugins: [
    ['@babel/plugin-proposal-decorators', { legacy: true }],
    ],
    };

然后src下新建文件entry.dev.js

1
2
require('@babel/register')(require('../config/babel-entry-config'));
require('./main-process/main');

最后咱修改下package.json的运行命令:

1
2
3
4
...
"start-main-dev": "cross-env HOT=1 NODE_ENV=development electron -r @babel/register ./src/entry.dev.js",
"dev": "cross-env START_HOT=1 webpack-dev-server --config ./config/webpack/webpack.config.renderer.dev.babel.js"
...
  1. 咱还差最后一步就可以运行了,在render-process下新建一个index.tsx。关于render-process新建的tsx是什么意思,请看上面说到的主进程和渲染进程文章:Electron-主进程、渲染进程
    1
    console.log('hello 秋泽雨');

搞了半天,咱们终于可以yarn dev 启动一下了,如果猜的没错,咱肯定运行散架,因为还有些上面用到的依赖没装,哈哈:

1
yarn add cross-env webpack-dev-server webpack-merge mini-css-extract-plugin @babel/register @babel/plugin-proposal-decorators @babel/preset-typescript -D

最后终于可以看到窗口以及窗口加载的html,以及html加载的js输出的console.log: ‘hello 秋泽雨’。

整体从yarn dev之后的运行流程全解图如下:

此篇demo: https://github.com/spcBackToLife/electron-sprout/tree/release-191017

下一篇会讲:react+mobx集成

有问题欢迎加群沟通哦: