Appearance
打包篇-应用程序生产环境构建
前言
截至目前,我们已将整个简历平台开发完毕。接下来我们实现应用程序的生产环境打包构建。相对于第十四章而言,本章节的相关配置更为全面与详细。如果你对本章节内容兴趣不大,可以快速阅读或跳过。
本章节的主要目的是:
- 实现 Electron 生产环境的构建打包
- 实现 React 生产环境的构建打包
- 通过 Electron-Builder 打包生成可执行文件
- 解决遇到的一系列问题
- 生成不同平台的可执行文件
同时此配置将会是后续 Webpack 打包优化、Electron 打包体积优化的铺垫,接下来的两章将以此配置进行优化。
搞清楚 Electron 与 React 的关系
此关系已在💥 第四章-开发前必读!Electron 与 React 的关系进行说明,大家先去搞清楚关系之后(阅读时长 3 分钟),再返回看继续阅读。
本地开发
当我们在本地开发时,运行的脚本命令是:
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
吗?并不能,它会报错。
但如果你是通过 Electron 的 BrowserWindow 创建的浏览器窗口,去打印 process
,就能显示内容
这也是为什么 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 打包即可,接下来,让我们一步步动手实践。
第一步:重新划分 Webpack 目录
回过头去看我们的 webpack
目录,存在三个文件
但这是我们开发环境下所需的配置,为此我们需要新增生产环境下打包构建的配置。
我们删掉 webpack.base.js
文件,原因是:既然划分了主进程和渲染进程的分开打包,我认为没必要将两者的通用内容划分到一块。
我们把上面的三个文件都删除,重新来配置一下 Electron 与 React 的相关打包内容。记得看注释
🐂 Electron 方面
新增三个文件
webpack.main.base.js
基础配置webpack.main.dev.base.js
开发环境下的配置webpack.main.prod.base.js
生产环境下的配置
javascript
// webpack/webpack.main.base.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: path.resolve(__dirname, '../app/main/electron.ts'),
output: {
filename: 'electron.js',
path: path.resolve(__dirname, '../dist'),
},
target: 'electron-main',
devtool: 'inline-source-map',
resolve: {
// 主进程不会存在 jsx、tsx,所以不用配置这些后缀
// 至于为什么不需要,会在下一章节优化处说明
extensions: ['.js', '.ts'],
},
module: {
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
],
},
plugins: [
// 用于打包后的主进程中正确获取__dirname
new webpack.DefinePlugin({
__dirname: '__dirname',
}),
],
};
Electron 方面的基础配置已经配好,接下来我们修改一下开发环境
和生产环境
的配置
javascript
// webpack/webpack.main.dev.js
const webpackMerge = require('webpack-merge');
const mainBaseConfig = require('./webpack.main.base.js');
const devConfig = {
mode: 'development',
};
module.exports = webpackMerge.merge(mainBaseConfig, devConfig);
javascript
// webpack/webpack.main.prod.js
const webpackMerge = require('webpack-merge');
const mainBaseConfig = require('./webpack.main.base.js');
const prodConfig = {
mode: 'production',
};
module.exports = webpackMerge.merge(mainBaseConfig, prodConfig);
🐷 React 方面
新增三个文件
webpack.render.base.js
基础配置webpack.render.dev.base.js
开发环境下的配置webpack.render.prod.base.js
生产环境下的配置
javascript
// webpack/webpack.render.base.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 多入口,这在第十七章有讲解
entry: {
index: path.resolve(__dirname, '../app/renderer/app.tsx'),
setting: path.resolve(
__dirname,
'../app/renderer/windowPages/setting/app.tsx'
),
},
output: {
filename: '[name].[hash].js',
path: path.resolve(__dirname, '../dist'),
},
resolve: {
// 这里就需要 jsx 和 tsx 了
extensions: ['.js', '.jsx', '.ts', '.tsx'],
// 别名配置,在 Electron 中并未用到别名路径,所以拆到 React 这边的配置中
alias: {
'@assets': path.join(__dirname, '../', 'assets/'),
'@src': path.join(__dirname, '../', 'app/renderer'),
'@common': path.join(__dirname, '../', 'app/renderer/common'),
},
},
target: 'electron-renderer',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.(jpg|png|jpeg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
},
},
],
},
{
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'],
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, '../app/renderer/windowPages/setting/index.html'),
filename: path.resolve(__dirname, '../dist/setting.html'),
chunks: ['setting'],
}),
],
};
React 方面的基础配置已经配好,接下来我们修改一下开发环境
和生产环境
的配置
因为我们在开发环境下,通过 webpack-dev-server 起了一个本地服务,所以我们的开发环境配置应为
javascript
// webpack/webpack.render.dev.js
const path = require('path');
const webpackMerge = require('webpack-merge');
const renderBaseConfig = require('./webpack.render.base.js');
const devConfig = {
mode: 'development',
devServer: {
contentBase: path.join(__dirname, '../dist'),
compress: true,
host: '127.0.0.1', // webpack-dev-server启动时要指定ip,不能直接通过localhost启动,不指定会报错
port: 7001, // 启动端口为 7001 的服务
hot: true,
},
};
module.exports = webpackMerge.merge(renderBaseConfig, devConfig);
javascript
// webpack/webpack.render.prod.js
const webpackMerge = require('webpack-merge');
const renderBaseConfig = require('./webpack.render.base.js');
const prodConfig = {
mode: 'production',
};
module.exports = webpackMerge.merge(renderBaseConfig, 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",
},
最终我们的 webpack
文件夹是这样的
第二步:安装 electron-builder
这边我们通过 electron-builder
进行打包,根据官方文档,我们进行安装
npm install electron-builder --save-dev
第三步:添加打包命令进行打包
由于很多属性,一一细讲并不现实,小伙伴可以前往 electron-builder 官方文档 阅读。
通过官方文档的 快速上手,只需要添加对应的一些配置即可实现打包。
首先我们在 package.json
中添加 build 属性,紧接着添加 PC 打包构建命令
json
{
"scripts": {
// 👇 新增的PC构建打包命令
// 直接生成一个安装完毕的程序,能直接运行,而不是安装包
"pack": "electron-builder --dir",
// 这个命令是生成真正的安装包
"dist": "electron-builder"
},
// 👇 新增打包相关的应用信息
"build": {
"appId": "visResumeMook.ElectronReact", // 自定义 appId
"productName": "VisResumeMook", // 打包之后的程序名
"copyright": "Copyright © 2019 ${author}",
// https://www.electron.build/configuration/contents.html#files
// 打包的 app.asar 中包含哪些文件,到时候解压出来就是哪些文件
"files": ["dist/**/*", "package.json", "node_modules/"],
// 构建的可执行文件放在 package 目录下
"directories": {
"output": "package"
}
}
}
这时候我们就可以执行打包了。一个完整的打包构建命令为:
javascript
// 1. 删除上一轮打包的 dist 目录和 package 目录
rm -rf dist package
// 2. 构建打包Elctron
npm run build:main
// 3. 构建打包 React
npm run build:render
// 4. 通过 electron-builder 构建安装包
//(如果你的静态资源没发生改变,可以直接 pack,不用再重新打包主进程和渲染进程)
npm run pack
运行一下,发现报错了
我们将 electron 放到 devDependencies 中
json
{
"dependencies": {
// 👇 将其放在 devDependencies 中
// "electron": "^11.1.1"
},
"devDependencies": {
"electron": "^11.1.1"
}
}
重新 install,再执行一下命令 npm run pack
(因为这里我的静态资源没发生改变,可以直接 pack,不用再重新打包主进程和渲染进程)
执行结果还是出错,原因在于我们的应用入口文件写错了
我们检查一下 package.json 中的 main 属性,果然有问题,进行修改
json
{
// 👇 修改成打包后的入口文件
"main": "./dist/electron.js"
}
再试一次,npm run pack
,发现这次没问题了。稳妥
进入到 package 目录下,找到 mac 目录,进入可以看到一个可执行文件
双击打开,发现好像有些问题
💥 What?问题很多
- 怎么主题配色的功能消失了?换言之,读取的
appConfig
文件好像都不存在?正因为读不到,所以没有了主题列表
- 点击进入
简历制作
模块无反应?
- 模版列表怎么都没图片了,我们从
assets/template
文件夹中读取的模版封面,怎么也不见了呢?
🌈 问题一个个定位解决
有意思,这才是进步,毕竟快速定位问题也是一种技能。
Question1: 主题列表消失
先来看第一个问题,主题配色的功能消失了?为什么没有主题列表?双击打开应用,我们进入切到开发者工具,看看控制台输出什么。
果不其然,在第十六章时讲到,我们的主题是通过读取 appConfig
目录下的 theme.config.json
文件构造出来的主题列表,appConfig
文件夹都找不到,怎么会有主题列表呢?
百思不得其解?怎么会没有 appConfig 目录?前往 dist 文件夹瞧一瞧,果然没有!
为什么会如此?要知道,Webpack 只会将你所需要的资源进行打包,换言之,凡是你通过 import xxx
的资源引入并使用,就会被打包到 dist 文件夹中。
纸上得来终觉浅,得知此事要躬行,我们来试一试。我们就在 app/renderer/app.tsx
根文件下,引入 assets/template/template1.jpg
文件(验证一下)
ts
import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router';
import store from './store';
import { Provider } from 'react-redux';
// 👇 ⚠️ 这里只是引入 template1.jpg 图片
import TestWebpackJpg from '@assets/template/template1.jpg';
function App() {
return (
<Provider store={store}>
<Router />
</Provider>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
此时我们再重新打包一下
bash
// 1. 删除上一轮打包的 dist 目录
rm -rf dist
// 2. 重新打包 React
npm run build:render
小伙伴们猜一下,此时的 dist/images/
目录下是否会出现这张图片呢?
让我来告诉你答案:并不会。你可以前往 dist 目录查看,你会发现这张照片并没有打进去。我们将代码稍作修改
ts
import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router';
import store from './store';
import { Provider } from 'react-redux';
// 👇 ⚠️ 引入 template1.jpg 图片
import TestWebpackJpg from '@assets/template/template1.jpg';
function App() {
// 👇⚠️ 打印这张图片
console.log(TestWebpackJpg);
return (
<Provider store={store}>
<Router />
</Provider>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
此时我们再重新打包一下
bash
// 1. 删除上一轮打包的 dist 目录
rm -rf dist
// 2. 重新打包 React
npm run build:render
小伙伴们猜一下,此时的 dist/images/
目录下是否会出现这张图片呢?多说无益,是骡子是马,拉出来溜一溜
普天同庆,我们是能在 dist/images/
下发现这张图片的。说明 Webpack 只会将我们使用到的 import
资源打包到 dist 下,针对 appConfig
和 assets/template
根本不会打包,因为我们是通过 NodeJS 的 fs 文件系统进行读取的,问题来了,如何让打包之后的 dist 也能有这些文件呢?
在网上搜索了一些教程,发现如脚手架创建出来的目录,会存在一个 public
文件夹存放静态资源,如果你将静态资源文件放入该 public 文件夹,webpack 将不会处理它,在你打包的时候,会将 public
文件夹直接复制一份到你构建出来的文件夹中。
所以我的解决方案就是:将所需的文件夹复制一份放在 dist
目录下,如何复制?在 webpack 官方文档中有提供一个插件,使用 copy-webpack-plugin就能实现我们的需求。
接下来让我们修改一下 webpack.render.base.js
文件
javascript
// webpack/webpack.render.base.js
/* eslint-disable @typescript-eslint/no-require-imports */
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
// 👇 通过该插件实现资源文件的拷贝
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../assets'),
to: path.resolve(__dirname, '../dist/assets'),
},
{
from: path.resolve(__dirname, '../appConfig'),
to: path.resolve(__dirname, '../dist/appConfig'),
},
],
}),
],
};
此时我们再重新打包一下
bash
// 1. 删除上一轮打包的 dist 目录
rm -rf dist
// 2. 重新打包 React
npm run build:render
可以看到这时候在 dist 目录下,已经有对应的文件夹目录。
这时候是不是重新用 electron-builder
构建一个包,就可以了呢?让我们试试。走一个完整的打包构建命令
javascript
// 1. 删除上一轮打包的 dist 目录和 package 目录
rm -rf dist package
// 2. 构建打包Elctron
npm run build:main
// 3. 构建打包 React
npm run build:render
// 4. 通过 electron-builder 构建安装包
//(如果你的静态资源没发生改变,可以直接 pack,不用再重新打包主进程和渲染进程)
npm run pack
构建完毕之后,前往查看发现还是不行,控制台报错
Uncaught (in promise) Error: ENOENT: no such file or directory, scandir '/Users/pengdaokuan/Desktop/pdk/visResumeMook/package/mac/VisResumeMook.app/Contents/Resources/assets/template'
有趣,这个路径不正确,我们所期望的是,这里的 assets/template
前面应该是带有一个 dist
目录的。让我们来看看这个安装包的相关代码,由于 electron 打包后的代码是压缩过后的,我们需要对其解压一下。我们到 package/mac/VisResumeMook.app/Contents/Resources
中找到 app.asar,对它进行解压
// 1. 全局安装
npm install -g asar
// 2. 到该目录下,正确目录自行拼写
cd package/mac/VisResumeMook.app/Contents/Resources
// 3. 解压
asar extract app.asar ./
事实证明是存在的,那到底是什么问题导致没有的呢?我们再注意看看报错信息,主要问题就是路径问题,我们通过断点来看看 appConfig
的绝对路径是什么。进入到控制台,找到 Sources
,然后对 useThemeActionHooks.ts
文件打上端点。
然后我们刷新一下页面,我们鼠标悬浮到 appPath
上,看看输出的是什么
好家伙,这路径很明显不正确,如果你还记得 getAppPath()
方法获取应用程序的路径,想必不会忘记我们是通过 app.getAppPath() 方法获取的,这个 API 得到的是在文件管理器中的应用程序路径。而我们期望得到的是带有 app.aras/dist
这样的一个路径。所以说通过这个 API 获取是不正确的,那该通过什么获取呢? 答案是 __dirname
我们在控制台打印一下 __dirname
,看看结果是什么。
这时候再去找 dist 下的 appConfig 是不是就行了?所以问题切到了,如何得到正确的路径。我们肯定不愿再改 getAppPath()
方法,所以我们只需要改主进程中,ipc 响应的数据即可。
javascript
// app/main/electron.ts
// getAppPath() 方法主要是通过 ipc 通信,得到项目路径,我们将 __dirname 的路径返回
ipcMain.on('get-root-path', (event, arg) => {
event.reply('reply-root-path', __dirname);
});
让我们走一个完整的打包构建命令
javascript
// 1. 删除上一轮打包的 dist 目录和 package 目录
rm -rf dist package
// 2. 构建打包Elctron
npm run build:main
// 3. 构建打包 React
npm run build:render
// 4. 通过 electron-builder 构建安装包
//(如果你的静态资源没发生改变,可以直接 pack,不用再重新打包主进程和渲染进程)
npm run pack
然后进入 package/mac
双击应用程序,然后看到首页就有简历列表啦~
Quesiton2: 简历模版列表没有封面
上一个问题解决了,这个问题应该也不难,我们进入到模版列表,发现还是没有封面,原因竟然是
Uncaught (in promise) Error: ENOENT, distassets/template not found in /Users/pengdaokuan/Desktop/pdk/visResumeMook/package/mac/VisResumeMook.app/Contents/Resources/app.asar
等等,这个路径有点问题,distassets
中间是不是少了个 /
,前往 app/renderer/hooks
,找到 useReadDirAssetsTemplateHooks.ts
文件进行修改
让我们走一个完整的打包构建命令
javascript
// 1. 删除上一轮打包的 dist 目录
rm -rf dist
// 2. 构建打包Elctron
npm run build:main
// 3. 构建打包 React
npm run build:render
// 4. 通过 electron-builder 构建安装包
//(如果你的静态资源没发生改变,可以直接 pack,不用再重新打包主进程和渲染进程)
npm run pack
进入 package/mac
双击应用程序,进去到模版列表页面,是没问题的。
此时再去点击简历制作,也是可行的。至此我们的打包问题已经得到解决。
第四步:Window & Mac 双管齐下
前面我们是根据文档,做了最为简单的配置,通过 electron-builder 官方文档,它声明:
Without target configuration, electron-builder builds Electron app for current platform and current architecture using default target.
也就是说,如果你什么都不配置的情况下,会根据你的系统平台,给你打一个默认的包。但我们往往是希望构建时,能生成多平台的安装包。接下来让我们配置一下吧,更多属性可自行查阅文档
json
{
"build": {
"appId": "visResumeMook.ElectronReact",
"productName": "VisResumeMook",
"copyright": "Copyright © 2019 ${author}",
// 包含的文件,这个在解压 asar 时可以看到源代码
"files": ["dist/**/*", "package.json", "node_modules/"],
// 生成的安装包输出到 package 文件夹
"directories": {
"output": "package"
},
"mac": {
"target": ["dmg", "zip"],
"category": "public.app-category.productivity"
},
"dmg": {
// 这个是安装时的图标位置
"contents": [
{
"x": 130,
"y": 220,
"type": "link",
"path": "/Applications"
},
{
"x": 410,
"y": 220,
"type": "file"
}
]
},
"win": {
"target": ["msi", "nsis"]
}
}
}
让我们走一个完整的打包构建命令
javascript
// 1. 删除上一轮打包的 dist 目录和 package 目录
rm -rf dist package
// 2. 构建打包Elctron
npm run build:main
// 3. 构建打包 React
npm run build:render
// 4. ⚠️ 这里是要执行 dist,生成真正的安装包
npm run dist
经过一段时间等待,我们可以看到打包完成
让我们看看 package
下有什么文件
有一个 zip
和 dmg
,我是 Mac 电脑,所以我双击打开 dmg 的安装包进行安装
安装之后就能愉快使用啦
总结
本章节内容特别多,构建过程也遇到了许多的问题,阿宽认为最重要的是小伙伴们能搞清楚 Electron 与 React 的构建关系,搞清楚之后再通过 electron-builder
构建成安装包即可。
小伙伴们可以再回顾一下本章节的整体内容,代码可见👉 chapter-22-build
到此,我们的应用程序的构建已结束。但事情真的如此简单吗?