Skip to content
On this page

支线篇-打包生成第一个桌面应用(骄傲自豪)


前言

许多小伙伴们通过阅读 环境篇-动手搭建我们的简历平台章节,将 Electron + React 搭建起来后,还没开始写几行代码,就迫不及待想打包成桌面应用,毕竟搞出自己第一个桌面应用,是一件很骄傲自豪的事情!!!为此,此章节将以 chapter-11分支为主,新增支线代码 chapter-14-app进行简易版的打包构建成桌面端应用。

⚠️ 请注意此章节为支线任务,非主线任务。只是为了快速圆大家一个桌面应用梦想。后续的章节,仍然以 chapter-11分支进行开发。

强烈在看章节内容的同时,结合仓库代码进行学习。👉 此项目代码在: chapter-14-app

electron 和 react 的关系

我们需要先搞清楚一下,electron 跟 react 的关系。

本地开发

当我们在本地开发时,运行的脚本命令是:

javascript
npm run start:main // 运行主进程
npm run start:render // 运行渲染进程

react 通过 webpack-dev-server 起了一个本地服务,我们通过 http://127.0.0.1:7001 就能访问我们的站点。Electron 是通过 BrowserWindow 创建了一个浏览器窗口,此窗口通过 loadURL 加载了我们的地址(你可以理解成 webview 形式),从而显示我们的网页。

javascript
// 创建浏览器窗口
const mainWindow = new BrowserWindow({
  width: 1200,
  height: 800,
  webPreferences: {
    devTools: true,
    nodeIntegration: true,
  },
});

if (isDev()) {
  // 开发环境
  mainWindow.loadURL(`http://127.0.0.1:7001`);
} else {
  // 生产环境
  mainWindow.loadURL(`file://${path.join(__dirname, '../dist/index.html')}`);
}

打包构建

当我们本地开发完成之后,需要打包上线,需要跑什么命令?不要想太复杂。它们也是分开打包的

javascript
npm run build:main // 打包主进程(对 app/main/electron.ts进行打包 )
npm run build:render // 打包渲染进程(也就是对 React 进行打包)

打包之后,我们 dist 目录就会存在相应的资源文件。如 index.html、electron.js 等。

关系

提问:抛开 Electron,我们常规的 React 项目,在打包之后,如何运行?

我们是通过点击 dist 下的 index.html 就能在浏览器页面中打开看效果(如果你发现没效果,请确保是以相对路径加载资源文件);但如果结合了 Electron,那么此时点击 dist 下的 index.html,在浏览器中打开,一般都会出错的。

如何理解?Electron 它内置了 Chromium 和 Node,试想一下,你能在 Chrome 浏览器的控制台中输出 process 吗?并不能,它会报错。

image.png

但如果你是通过 Electron 的 BrowserWindow 创建的浏览器窗口,去打印 process,就能显示内容

image.png

这也是为什么 Electron 项目中 dist 下的 index.html 不能直接在 Chrome 浏览器运行,因为你的代码,可能用到了 Electron API、Node API,Chrome 浏览器无法识别这是什么东西。只有通过 Electron 生成的浏览器窗口,LoadURL 加载此页面,才能正常运行。

通过前面的代码可以看到,在生产环境下,我们 LoadURL 的是 dist 下的 index.html

javascript
if (isDev()) {
  // 开发环境
  mainWindow.loadURL(`http://127.0.0.1:7001`);
} else {
  // 生产环境
  mainWindow.loadURL(`file://${path.join(__dirname, '../dist/index.html')}`);
}

打包构建

上面了解了 Electron 和 React 的关系之后,接下来我们开始打包构建,此次项目中,将以 electron-builder 进行应用打包。请记住,我们以 chapter-11 分支代码进行打包,如果你是基于其他分支,请确保 webpack 的配置是正确的。

第一步:打包 Electron 和 React

在 webpack 文件夹下,新增 webpack.main.prod.js,表示主进程在生产环境下的打包配置。

javascript
// webpack/webpack.main.prod.js
const path = require('path');
const baseConfig = require('./webpack.base.js');
const webpackMerge = require('webpack-merge');

const prodConfig = {
  entry: path.resolve(__dirname, '../app/main/electron.ts'),
  target: 'electron-main',
  output: {
    filename: 'electron.js',
    path: path.resolve(__dirname, '../dist'),
  },
  devtool: 'inline-source-map',
  // 👇 这里改成生产环境
  mode: 'production',
};

module.exports = webpackMerge.merge(baseConfig, prodConfig);

新增 webpack.render.prod.js,表示渲染进程在生产环境下的打包配置。

javascript
const path = require('path');
const webpackMerge = require('webpack-merge');
const baseConfig = require('./webpack.base.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const prodConfig = {
  // 👇 这里改成生产环境
  mode: 'production',
  entry: {
    index: path.resolve(__dirname, '../app/renderer/app.tsx'),
  },
  output: {
    filename: '[name].[hash].js',
    path: path.resolve(__dirname, '../dist'),
  },
  target: 'electron-renderer',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: path.join(__dirname, '../dist'),
    compress: true,
    host: '127.0.0.1', // webpack-dev-server启动时要指定ip,不能直接通过localhost启动,不指定会报错
    port: 7001, // 启动端口为 7001 的服务
    hot: true,
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader'],
      },
      {
        test: /\.less$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[name]__[local]__[hash:base64:5]',
              },
            },
          },
          'postcss-loader',
          'less-loader',
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../app/renderer/index.html'),
      filename: path.resolve(__dirname, '../dist/index.html'),
      chunks: ['index'],
    }),
  ],
};

module.exports = webpackMerge.merge(baseConfig, prodConfig);

紧接着,我们前往 package.json 中,添加打包命令

json
"scripts": {
  // 本地开发
  "start:main": "webpack --config ./webpack/webpack.main.dev.js && electron ./dist/electron.js",
  "start:render": "webpack-dev-server --config ./webpack/webpack.render.dev.js",
  // 👇 新增的生产打包命令
  "build:main": "webpack --config ./webpack/webpack.main.prod.js",
  "build:render": "webpack --config ./webpack/webpack.render.prod.js",
},

我们去执行一下 npm run build:mainnpm run build:render

image.png

看看打包之后的 dist 文件,发现打包之后的 electron.js 不见了。

image.png

原因在于我们使用了 clean-webpack-plugin 插件,该插件在每次打包之前,都会将 dist 删除再新增。所以先打包的文件跟着被删掉了。我们来修改一下 webpack.base.js

javascript
// webpack/webpack.base.js
const path = require('path');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  // 其他代码省略...
  // 👇 注释此插件
  // plugins: [new CleanWebpackPlugin()],
};

我们删除 dist 目录,再次打包试试

rm -rf dist
npm run build:main
npm run build:render

image.png

这次我们就发现,dist 目录下的内容都是正确的了。为此我们 Electron 和 React 均打包成功。

第二步:安装 electron-builder

根据官方文档,我们进行安装

npm install electron-builder --save-dev

image.png

第三步:添加打包命令进行打包

通过 electron-builder 官方文档的 快速上手,我们可以发现,只需要添加对应的一些配置即可实现打包。

配置属性很多,就不一一列举,小伙伴们们可以自行前往阅读一下~

根据教程,我们在 package.json 添加 build 属性

json
{
  "build": {
    "appId": "visResumeMook.ElectronReact", // 自定义 appId
    "productName": "VisResumeMook", // 打包之后的程序名
    "copyright": "Copyright © 2019 ${author}",
    "files": ["dist/**/*", "package.json", "node_modules/"]
  }
}

接着添加脚本命令

javascript
"scripts": {
  "pack": "electron-builder --dir",
  "dist": "electron-builder"
}

这时候我们就可以执行打包了。一个完整的打包构建命令为:

javascript
rm -rf dist
npm run build:main
npm run build:render

// 如果你的静态资源没发生改变,可以直接 pack,不用再重新打包主进程和渲染进程
npm run pack

运行一下,发现报错了

image.png

我们将 electron 放到 devDependencies 中,然后重新 install,再执行一下命令

npm run pack

执行结果还是出错,原因在于我们的应用入口文件写错了

image.png

我们检查一下 package.json 中的 main 属性,果然有问题,进行修改

json
{
  // 👇 修改成打包后的入口文件
  "main": "./dist/electron.js"
}

再执行一下命令,看看会不会打包成功

npm run pack

image.png

没问题,我们再去 dist 下面看看

image.png

我们双击打开,发现白屏,来看看报什么错

image.png

加载的 dist/index.html 有问题,通过控制台查看报错信息,我们发现这个 file:// 链接有误

javascript
if (isDev()) {
  // 开发环境
  mainWindow.loadURL(`http://127.0.0.1:7001`);
} else {
  // 生产环境
  mainWindow.loadURL(`file://${path.join(__dirname, '../dist/index.html')}`);
}

很明显,这个 __dirname 好像不起作用。我们可以通过 webpack.definePlugin 添加此全局变量

修改 webpack.main.prod.js,我们为其添加此全局变量

javascript
// webpack/webpack.main.prod.js

plugins: [
  // 👇 添加这个,用于打包后的主进程中正确获取__dirname
  new webpack.DefinePlugin({
    __dirname: '__dirname',
  }),
];

然后重新打包一下

rm -rf dist
npm run build:main
npm run build:render
npm run pack

顺利打包之后,进入 dist ,双击点击我们的程序包,这时候就能正常运行了

第三步:生成多平台的安装包

当我们不配置任何平台相关信息时,将会根据你当前的设备,打包成对应的安装包。如果你想了解默认行为可点击这里,我们接下来进行 mac 和 win 的配置,配置参数太多,这里就直接上配置项了,大家可以去官网了解相关配置

json
"build": {
    "appId": "visResumeMook.ElectronReact",
    "productName": "VisResumeMook",
    "copyright": "Copyright © 2019 ${author}",
    "extends": null,
    "files": [
      "dist/**/*",
      "package.json",
      "node_modules/"
    ],
    // 这是资源文件
    "directories": {
      "buildResources": "assets",
      "output": "distAssets" // 这是打包之后安装包所在的文件夹
    },
    "mac": {
      "target": [
        "dmg",
        "zip"
      ]
    },
    "dmg": {
      "contents": [
        {
          "x": 130,
          "y": 220
        },
        {
          "x": 410,
          "y": 220,
          "type": "link",
          "path": "/Applications"
        }
      ]
    },
    "win": {
      "target": [
        "nsis",
        "msi"
      ]
    }
  },

之后我们再去执行一下打包。

npm run dist

企业微信截图_5353b118-156c-45a0-b1a6-e79d39dad901.png

我们进入到 distAssets 文件夹下,然后就会发现 dmg 安装包,进行安装即可

image.png

安装完后,我们就能愉快的使用此程序啦~

image.png

问题?

有小伙伴们可能会遇到一些问题,比如打包之后图片怎么没了,一些功能怎么消失了?不要慌,因为本章节知识支线任务,只是让小伙伴们体验一下构建成安装包的快乐,完整的构建打包请看此章节:打包篇-应用程序生产环境构建,看完之后,你遇到的打包问题都能迎刃而解~

最后

此章节以”粗暴“的方案实现,代码未经过优化和抽离,同时打包出来的程序体积很大,并未进行优化。这些操作都在主线任务的打包篇-Electron 打包体积优化去实现。