Appearance
打包篇-生产环境疑难杂症的解决
前言
上一章节,我们将应用程序打包构建整完了,好像看起来没啥问题了,并非如此,还是存在许多的问题,接下来我们需要继续填坑了。如果你对本章节内容兴趣不大,可以快速阅读或跳过。
🔨 坑一:开发环境凉了
我们以 chapter-22-build 分支代码,继续往下开发,让我们先去开发环境试试吧
javascript
// 👇 记住,在进入开发环境时,先将 dist 目录删除
// 因为我们常规开发时,是不会去 build dist 目录的
rm -rf dist
npm run start:main
npm run start:render
此时看看会有什么问题
直接报错了,这是为什么呢?在本地开发时,我们通过 webpack-dev-server
起了一个本地服务
javascript
devServer: {
contentBase: path.join(__dirname, '../dist'),
compress: true,
host: '127.0.0.1',
port: 7001, // 启动端口为 7001 的服务
hot: true,
}
在终端中我们也能看到输出的一些相关信息
webpack-dev-server
主要是启动了一个 express 的 HTTP 服务器,当原始文件发生改变之后,webpack-dev-server
会实时编译,但请注意,⚠️ 启动了 webpack-dev-server 后,dist 目录是看不到编译后的文件,实时编译后的文件都保存到了内存当中。所以你跑去 dist 目录下找,是找不到编译后的文件的。
按道理来讲,我们访问 dist
目录下的 assets/template
、appConfig
都应该能读到数据的。那为什么会说找不到文件夹呢?
第一时间想到的就是文件夹拷贝问题,我们检查一下 webpack/webpack.render.base.js
,该配置下,生产是没问题的,但在开发环境 dev-server
中存在问题。有没有可能是该插件,不支持 dev-server
,我们前往官网找一下有没有相关 issues,还真找到一个 👉 Does not copy files to actual output folder when webpack-dev-server is used
一圈扫荡下来,大部分的回答是 2016 年、2017 年,还有说需要降版本的。这方案肯定不得行,回想一下,好像没打包构建之前,都能正常,这是为什么?因为获取应用路径被我们修改。
ts
// 原来开发环境能正常的应用路径
const ROOT_PATH = path.join(app.getAppPath(), '../');
ipcMain.on('get-root-path', (event, arg) => {
event.reply('reply-root-path', ROOT_PATH);
});
// 打包构建时,我们将应用路径改成这样
ipcMain.on('get-root-path', (event, arg) => {
event.reply('reply-root-path', __dirname);
});
上面的路径我们能在开发环境下正常,生产环境出现问题。下面的路径在开发环境上出现问题,生产环境下正常。
那就两者结合,各负责各的,我们将代码改造一下,前往 app/main/electron.ts
进行修改
ts
// app/main/electron.ts
const ROOT_PATH = path.join(app.getAppPath(), '../');
ipcMain.on('get-root-path', (event, arg) => {
event.reply('reply-root-path', isDev() ? ROOT_PATH : __dirname);
});
让我们重新跑一下开发环境的命令吧
rm -rf dist
npm run start:main
npm run start:render
这时候是正常无问题的。那生产环境打包构建会不会有问题呢?走一个完整的打包构建命令
javascript
// 1. 删除上一轮打包的 dist 目录和 package 目录
rm -rf dist package
// 2. 构建打包Elctron
npm run build:main
// 3. 构建打包 React
npm run build:render
// 4. 通过 electron-builder 构建安装包
npm run dist
安装一下,经过验证,开发环境和生产环境均无问题!
🔨 坑二:切换主题,数据无法写入 theme.config.json
继上述打包构建之后,安装应用,此时选择切换主题,按道理来讲,应当存入 theme.config.json
文件,却得到异常报错,如下图
大家猜一猜,是什么原因?很简单,electron 不允许你对打包之后的代码进行修改,仔细想想,这也正常,你说像微信、QQ 这种应用安装包,它会允许你修改源代码文件?同样的,我们对打包构建之后的 dist/appConfig
目录进行增删改查操作,那肯定是无权限、不被允许的。
这里我给大家挖了一个坑,解铃还须系铃人,接下来让我们来填填坑~ 既然对于打包构建之后的文件都不能做增删改查操作,但我们确实有此需求,该如何实现?在 Electron 中,可以通过 app.getPath 去存储数据,我们来看看文档如何说的
也就是说,通过 app.getPath()
API,我们可以得到用户在本设备上的一些路径,比如 userData
应用程序设置文件的文件夹路径,从而进行数据的存储,这是一次大改动,鉴于此次改动量有点大,一定要认真看
- 第一步:在
app/main
目录下添加userData
,下面看代码注释
ts
import { app, ipcMain } from 'electron';
import path from 'path';
import fileAction from '@common/utils/file';
// 👇 1. 得到应用程序设置文件的文件夹,然后查看 appConfig 目录
const appConfigPath = path.resolve(app.getPath('userData'), 'appConfig');
// 👇 2 appConfig 文件夹是否可读
fileAction
.canRead(appConfigPath)
.then(() => {
// 👇 2.1 appConfig 可读情况下,判断是否存在 theme.config.json
fileAction.hasFile(`${appConfigPath}/theme.config.json`).catch(() => {
// 2.1.1 不存在则默认创建
createThemeConfigJson();
});
// 👇 2.2 appConfig 可读情况下,判断是否存在 global.config.json
fileAction.hasFile(`${appConfigPath}/global.config.json`).catch(() => {
// 2.2.1 不存在则默认创建
createGlobalConfigJson();
});
})
.catch(() => {
// 👇 2.3 appConfig 文件夹不可读,说明不存在此文件夹,则新增文件夹
fileAction.mkdirDir(appConfigPath).then(() => {
// 2.3.1 并默认创建文件
createThemeConfigJson();
createGlobalConfigJson();
});
});
// 创建默认 theme.config.json
const createThemeConfigJson = () => {
fileAction?.write(
`${appConfigPath}/theme.config.json`,
{
name: '主题配置表',
currentTheme: {
id: 'green',
fontColor: '#ffffff',
backgroundColor: '#416f5b',
},
themeList: [
{ id: 'dark', fontColor: '#ffffff', backgroundColor: '#27292c' },
{ id: 'blue', fontColor: '#ffffff', backgroundColor: '#35495e' },
{ id: 'green', fontColor: '#ffffff', backgroundColor: '#416f5b' },
{ id: 'purple', fontColor: '#ffffff', backgroundColor: '#54546c' },
{ id: 'princess', fontColor: '#ffffff', backgroundColor: '#945454' },
],
},
'utf8'
);
};
// 创建默认 global.config.json
const createGlobalConfigJson = () => {
fileAction?.write(
`${appConfigPath}/global.config.json`,
{ name: '全局配置表', resumeSavePath: '' },
'utf8'
);
};
// 👇 响应渲染进程想得到的 userData 路径,因为 app 模块只能在主进程中使用
ipcMain.on('Electron:get-userData-path', (event, arg) => {
event.reply('Electron:reply-userData-path', app.getPath('userData'));
});
由于此文件中我们用到了 @common/utils/file
别名路径,所有需要在主进程的 webpack 做一些别名配置,我们修改 webpack/webpack.main.base.js
,部分代码省略
javascript
module.exports = {
resolve: {
alias: {
'@common': path.join(__dirname, '../', 'app/renderer/common'),
},
},
};
- 第二步:引入
userData
文件
我们在主进程中引入此文件,修改 app/main/electron.ts
ts
// 将此文件引入
import './userData';
- 第三步:前往
app/renderer/common/utils/appPath.ts
下,添加新的方法
添加新的获取 userData 路径方法
ts
/**
* @description 获取应用 useData 路径
* @returns {Promise<string>}
*/
export function getUserStoreDataPath(): Promise<string> {
return new Promise(
(resolve: (value: string) => void, reject: (value: Error) => void) => {
ipcRenderer.send('Electron:get-userData-path', '');
ipcRenderer.on('Electron:reply-userData-path', (event, arg: string) => {
if (arg) {
resolve(arg);
} else {
reject(new Error('项目路径错误'));
}
});
}
);
}
- 第四步:将涉及到 appConfig 的文件都进行修改
我们去 app/renderer/hooks
下,找到 useThemeActionHooks.ts
文件修改,下边为伪代码
ts
// app/renderer/hooks/useThemeActionHooks.ts
// 👇 修改成获取 userData 路径的方法
import { getUserStoreDataPath } from '@common/utils/appPath';
/**
* @description 读取配置文件的内容
*/
function useReadAppConfigThemeFile() {
return () => {
return new Promise((resolve: (values: { [key: string]: any }) => void, reject: (value: Error) => void) => {
// 👇 这里改一下方法名
getUserStoreDataPath().then((appPath: string) => {
// ...
});
});
};
}
/**
* @description 更新配置表中的用户设置信息
* @param {string} updateKey 键
* @param {any} updateValues 值
* @param {function} callback 回调函数
*/
function useUpdateAppConfigThemeFile() {
const readAppConfigThemeFile = useReadAppConfigThemeFile();
return (updateKey: string, updateValues: any, callback?: () => void) => {
// 👇 这里改一下方法名
getUserStoreDataPath().then((appPath: string) => {
// ...
});
};
}
我们再去 app/renderer/hooks
下,找到 useGlobalConfigActionHooks.ts
文件修改,下边为伪代码
ts
// app/renderer/hooks/useGlobalConfigActionHooks.ts
// 👇 修改成获取 userData 路径的方法
import { getUserStoreDataPath } from '@common/utils/appPath';
/**
* @description 读取全局配置文件的内容
*/
export function useReadGlobalConfigFile() {
return () => {
return new Promise((resolve: (values: { [key: string]: any }) => void, reject: (value: Error) => void) => {
// 👇 这里改一下方法名
getUserStoreDataPath().then((appPath: string) => {
// ...
});
});
};
}
/**
* @description 读取配置文件的内容
* @param {string} updateKey 键
* @param {any} updateValues 值
* @param {function} callback 回调函数
*/
export function useUpdateGlobalConfigFile() {
const readGlobalConfigFile = useReadGlobalConfigFile();
return (updateKey: string, updateValues: any, callback?: () => void) => {
// 👇 这里改一下方法名
getUserStoreDataPath().then((appPath: string) => {
// ...
});
};
}
这边还需要对应用设置窗口初始化存储路径做一下修改,前往 renderer/windowPages/setting
,修改一下 index.tsx 代码,部分代码省略
ts
// app/renderer/windowPages/setting/index.tsx
// 👇 修改成获取 userData 路径的方法
import { getUserStoreDataPath } from '@common/utils/appPath';
function Setting() {
useEffect(() => {
// 👇 读取配置文件内容
readGlobalConfigFile().then((value: { [key: string]: any }) => {
// 如果存在应用存储路径,则采用
if (value?.resumeSavePath) {
setResumeSavePath(value?.resumeSavePath);
} else {
// 否则获取 userData 路径,以 userData 路径为准
// 更新 global.config.json 中的应用存储路径字段
getUserStoreDataPath().then((appPath: string) => {
setResumeSavePath(`${appPath}/resumeCache`);
updateGlobalConfigFile('resumeSavePath', `${appPath}/resumeCache`);
});
}
});
}, []);
}
我们前面讲到,在导出 PDF 时,是通过读取 global.config.json
中的 resumeSavePath
字段,以此路径,将我们的简历数据文件写入该路径下的文件夹。
那么我们也需要对应做下修改,前往 renderer/container/resume/ResumeAction
,修改一下 index.tsx 代码,部分代码省略
ts
// app/renderer/container/resume/ResumeAction/index.tsx
// 👇 修改成获取 userData 路径的方法
import { getUserStoreDataPath } from '@common/utils/appPath';
// 导出PDF
const exportPdf = () => {
toPrintPdf(`${base?.username}+${base?.school}+${work?.job}`);
setComponentVisible(false);
readGlobalConfigFile().then((value: { [key: string]: any }) => {
// 如果存在,以此路径进行简历数据文件的写入
if (value?.resumeSavePath) {
saveResumeJson(value?.resumeSavePath);
} else {
// 不存在默认路径(可能都没打开过应用设置窗口)
// 则设置默认路径并更新文件内容
getUserStoreDataPath().then((appPath: string) => {
updateGlobalConfigFile('resumeSavePath', `${appPath}/resumeCache`);
saveResumeJson(`${appPath}/resumeCache`);
});
}
});
};
// 存储数据json
const saveResumeJson = (resumeSavePath: string) => {
const date = intToDateString(new Date().valueOf(), '_');
const prefix = `${date}_${base?.username}_${base?.school}_${work?.job}_${createUID()}.json`;
// 如果路径中不存在 resumeCache 文件夹,则默认创建此文件夹
if (resumeSavePath && resumeSavePath.search('resumeCache') > -1) {
fileAction
.canRead(resumeSavePath)
.then(() => {
fileAction?.write(`${resumeSavePath}/${prefix}`, resume, 'utf8');
})
.catch(() => {
fileAction
.mkdirDir(resumeSavePath)
.then(() => {
fileAction?.write(`${resumeSavePath}/${prefix}`, resume, 'utf8');
})
.catch(() => {
console.log('创建文件夹失败');
});
});
} else {
fileAction
.mkdirDir(`${resumeSavePath}/resumeCache`)
.then(() => {
fileAction?.write(
`${resumeSavePath}/resumeCache/${prefix}`,
resume,
'utf8'
);
})
.catch(() => {
console.log('创建文件夹失败');
});
}
};
- 第五步:删除
appConfig
文件夹
因为我们现在的 appConfig 文件夹是存储在应用程序设置文件中,我们可以将其删除。
- 第六步:修改拷贝文件夹的代码
因为本地没有了 appConfig 文件夹,所以 copy-webpack-plugin
配置也需要做下修改。我们前往 webpack/webpack.render.base.js
做下更改
javascript
// webpack/webpack.render.base.js
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'),
// },
],
}),
],
};
我们重新跑一下开发环境的命令,然后进行到制定的 userData 文件夹下,找到 Electron
文件夹,看看应用启动之后,会不会生成对应的 appConfig
,并携带 theme.config.json
和 global.config.json
文件
rm -rf dist
npm run start:main
npm run start:render
经过验证,是没问题的,小伙伴们记住了,这是 userData 的路径,根据你的平台设备,进入对应的路径
appData
每个用户的应用程序数据目录,默认情况下指向:%APPDATA%
Windows 中$XDG_CONFIG_HOME
or~/.config
Linux 中~/Library/Application Support
macOS 中
生产环境打包构建会不会有问题呢?走一个完整的打包构建命令
javascript
// 1. 删除上一轮打包的 dist 目录和 package 目录
rm -rf dist package
// 2. 构建打包Elctron
npm run build:main
// 3. 构建打包 React
npm run build:render
// 4. 通过 electron-builder 构建安装包
npm run dist
打包之后,安装一下,打开应用,在应用启动之后,我们进入到 userData 路径,请注意,此时打包后的应用程序名称是 visResumeMook
,我们进去看看
开发环境和生产环境均无问题!
这时候坑一自然而然也就没了,项目中的
appConfig
都删了,哪还会有拷贝失败的问题呢?
🔨 坑三:切换主题,下次进入居然不是上一轮的
每次打开应用,你会发现主题色都是黑色,并不是我所期望的上一轮配色。那为什么会是这样呢?我们来看看 app/renderer/hooks
下的 /useThemeActionHooks.ts
文件代码。
ts
function useSelectTheme() {
const dispatch = useDispatch();
return (themeConfigValues: any) => {
// 👇 在 theme.config.json 存储到是 currentTheme 对象,而不是一个 id,需要改成这样
// const prevTheme: string = themeConfigValues?.currentTheme || '';
const prevTheme: TSTheme.Item = themeConfigValues?.currentTheme;
let nextTheme: TSTheme.Item;
if (themeConfigValues?.themeList.length > 0) {
// 👇 并不是通过 id 去找,而是直接使用当前主题,需要改成这样
// if (prevTheme) nextTheme = _.find(themeConfigValues?.themeList, { id: prevTheme }) || initTheme;
if (prevTheme) nextTheme = prevTheme || initTheme;
} else {
nextTheme = initTheme;
}
};
}
在开发环境下验证一波,确实无误,打个包看看,走一个构建流程
javascript
// 1. 删除上一轮打包的 dist 目录和 package 目录
rm -rf dist package
// 2. 构建打包Elctron
npm run build:main
// 3. 构建打包 React
npm run build:render
// 4. 通过 electron-builder 构建安装包
npm run dist
🌈 无问题,稳妥
优化 4:生产环境下禁止拉伸窗口与进入开发者模式
我们进入到主进程 app/main/electron.ts
中,修改一下代码
ts
// 👇 将这个方法导出,因为我们在 customMenu 用到
export function isDev() {
return process.env.NODE_ENV === 'development';
}
const mainWindow: MyBrowserWindow = new BrowserWindow({
width: 1200,
height: 800,
resizable: isDev(), // 根据环境进行判断
webPreferences: {
devTools: isDev(), // 根据环境进行判断
nodeIntegration: true,
},
});
// 创建应用设置窗口
const settingWindow: MyBrowserWindow = new BrowserWindow({
width: 720,
height: 240,
resizable: isDev(), // 根据环境进行判断
show: false,
frame: false,
webPreferences: {
devTools: isDev(), // 根据环境进行判断
nodeIntegration: true,
},
});
对应的,我们需要将菜单栏中的功能根据环境不同进行处理。
ts
import _ from 'lodash';
import { MyBrowserWindow, isDev } from './electron';
import { MenuItemConstructorOptions, shell, app, MenuItem, BrowserWindow } from 'electron';
const customMenu: (MenuItemConstructorOptions | MenuItem)[] = [
// ...
// ...
{
label: '视图',
submenu: [
// ...
// 这里把 `切换开发者工具` 一栏,通过环境动态配置
],
},
];
if (isDev()) {
(customMenu[2]?.submenu as any).push({
label: '切换开发者工具',
role: 'toggleDevTools',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
} else {
return 'Ctrl+Shift+I';
}
})(),
click: (item: any, focusedWindow: MyBrowserWindow) => {
if (focusedWindow) {
focusedWindow.webContents.openDevTools();
}
},
});
}
export default customMenu;
这时候在开发环境和生产环境,效果截然不同~
最后
构建打包属实不易,这过程出现很多问题,但要感谢问题的出现,通过解决问题,我们才有进步。至此我们的打包构建告一段落, 👉 此章节的相关代码在此,建议配合线上代码,结合小册内容,动手实践。