关于ts + react + koa + electron-builder三端打包参考(全栈)

之前一直想做一个桌面app,在某些机缘巧合之下找到了一个值得一做的项目,又在某些机缘巧合之下集齐了linux/max/windows(win7)三大系统,于是开始了一个桌面应用的开发之旅。(代码较多,仅供参考)

项目技术栈

前端:ts + react + modern-js + ant-design/(ant-design/pro-components)

后端:ts + koa + lowdb + winston + webpack5

APP打包:electron + electron-builder

因为开发的是平台类的APP,所以前端使用antd作为组件库,后端更加倾向于轻量级的框架,koa和lowdb都是轻量级的库,都比较适合打包到app内。打包app采用的是electron-builder,主要是文档比较齐全,资料好找一些。

参考文档

Modern.js 介绍

ant-design组件总览

ant-design/pro-components组件总览

koa.js

electron-builder configuration

一文打尽:Electron将项目打包到各操作系统

前后端打包

1. 前端打包:

shell 复制代码
modern build

前端路由使用hash路由:

typescript 复制代码
import { HashRouter } from '@modern-js/runtime/router';

理由:将前端代码打包到后端,使用hash路由可以避免路由冲突问题;使用elctron.loadFile加载index.html静态文件时,使用hash路由不会出现链接跳转后找不到页面的情况,减少了一些额外的配置。

modern.config.ts配置:

typescript 复制代码
import appTools, { defineConfig } from '@modern-js/app-tools';

// https://modernjs.dev/docs/apis/app/config
export default defineConfig({
  runtime: {
    router: true,
    state: true,
  },
  plugins: [appTools()],
  output: {
    polyfill: 'ua',
    assetPrefix: './dist',
    cleanDistPath: true,
    distPath: {
      root: '<后端项目>/static/dist', // 将前端文件直接打包到后端的static/dist目录下
    },
    assetsRetry: {
      max: 3,
      crossOrigin: true,
    },
    convertToRem: true,
    disableSourceMap: true,
  },
});

这里convertToRem设置为true,是考虑到窗口大小的变化,使用rem更加合适些,配置assetsRetry可以避免资源加载出错的情况,增加资源加载容错率,当然在本地加载这个配置可以忽略不计。

config目录下添加favicon.ico或者favicon.png,为网站设置图标。

2. 后端打包:

设置静态目录

typescript 复制代码
import * as koaStatic from 'koa-static';
...

 app.use(
    koaStatic('./static', {
      gzip: true,
      maxage: 180000,
      index: 'index.html',
    }),
  );

webpack.config.js配置:

javascript 复制代码
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

function resolve(dir) {
  return path.resolve(__dirname, dir);
}

module.exports = {
  mode: 'production',
  entry: {
    main: './src/app.ts',
  },
  output: {
    filename: 'app.js',
    path: resolve('dist'),
    libraryTarget: 'commonjs2',
  },
  // externals: [/^(?!\.|\/).+/i], //node 打包可去除一些警告
  target: 'node', // 服务端打包
  resolve: {
    extensions: ['.ts', '.js'],
    extensionAlias: {
      '.js': ['.js', '.ts'], 
    },
    modules: ['node_modules', 'src'], // 将依赖一起打包进去
    alias: {
      '@': resolve('src'), // 将 '@' 映射到 'src' 目录
    },
    alias: {
      '@loadable': resolve('node_modules/@loadable'),
    },
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        include: [resolve('src')],
      },
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/transform-runtime'], // 辅助代码从这里引用
          },
        },
      },
    ],
  },
  plugins: [new CleanWebpackPlugin()],
};

APP打包

1. 项目准备

将后端static目录和app.js放入build/release目录下。

准备APP图标,icon.png:

shell 复制代码
npm i -D electron-icon-builder
electron-icon-builder --input=./icon.png --output=build --flatten

这里的electron-icon-builder只在生成图标的时候使用到,可以考虑全局安装。

build目录下将会生成icons目录:

txt 复制代码
|- build
    |- icons
        -1024x1024.png  
        -128x128.png  
        -16x16.png  
        -24x24.png  
        -256x256.png  
        -32x32.png  
        -48x48.png  
        -512x512.png  
        -64x64.png  
        -icon.icns  
        -icon.ico

其中build/icons用于linux图标配置,icon.icns用于mac图标配置,icon.ico用于windows图标配置。

2. 项目配置

.npmrc配置

ini 复制代码
strict-peer-dependencies=false

registry=https://registry.npm.taobao.org
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
phantomjs_cdnurl=http://npm.taobao.org/mirrors/phantomjs
electron_mirror=http://npm.taobao.org/mirrors/electron/

添加app.desktop,用于linux桌面配置

desktop 复制代码
[Desktop Entry]
Name=demo
Name[zh_CN]=app例子
Comment=app例子
Terminal=false
Exec=/opt/demo/bin/demo.sh %U
Icon=demo
Type=Application;IDE;
Categories=Utility;
Keywords=demo;

其中demo.sh用来启动app,这里参考微信linux版的启动脚本:

bash 复制代码
#!/bin/bash

TMPFILE=/tmp/demo.list.tmp

function store_pidlist() {
	ps -ef | grep demo | grep -v grep | grep -v demo.sh | awk '{print $2}' >${TMPFILE}
}

# 返回 0 表示app未运行;返回 1 表示app正在运行
function is_running() {
	# 1. 获取当前app的进程数量,非零走判断
	# 2. 如果有tmp文件,获取当前的进程list,比对判断,以tmp为准,tmp中的进程缺失则认定为退出
	# 3. 如果没有tmp文件,非零就退
	DEMO_NUMS=$(ps -ef | grep demo | grep -v grep | grep -v demo.sh | wc -l)
	# 能检索到demo进程的情况下才做判断
	if [ 0 -ne $DEMO_NUMS ]; then
		if [ -f ${TMPFILE} ]; then
			DEMOPIDList=($(ps -ef | grep demo | grep -v grep | grep -v demo.sh | awk '{print $2}'))
			for item in $(cat ${TMPFILE}); do
				# 如果有pid不在当前进程中,则返回0
				[[ ${DEMOPIDList[@]/${item}/} != ${DEMOPIDList[@]} ]]
				if [ $? -eq 1 ]; then
					return 0
				fi
			done
			return 1
		else
			# 如果文件丢失,直接认为demo未运行
			return 0
		fi
	fi

	return 0
}

is_running

if [ $? -eq 1 ]; then
	echo "demo is already running..."
else
	killall demo
	nohup /opt/demo/demo >/dev/null 2>&1 &
	sleep 7
	store_pidlist
fi

package.json添加必要的字段:

json 复制代码
{
    "name": "demo",
    "version": "1.0.0",
    "description": "app例子 桌面版",
    "main": "main.js",
    "author": "xxx.xx",
    "homepage": ".",
    "productName": "app例子",
    "email": "xxx@qq.com",
    "scripts": {
        "start": "electron .",
        "build:deb": "electron-builder build --linux deb",
        "build:dmg": "electron-builder build --mac dmg",
        "build:nsis": "electron-builder build --win nsis",
        "electron:generate-icons": "electron-icon-builder --input=./icon.png --output=build --flatten"
      },
    ...
}

electron-builder.json配置:

json 复制代码
{
  "appId": "com.demo.app",
  "productName": "app例子",
  "copyright": "xxx.xx Copyright © 2023",
  "files": ["main.js", "build/**/*"],
  "extraFiles": ["bin/**/*", "app.desktop"],
  "directories": {
    "output": "./release-built"
  },
  "linux": {
    "target": ["deb", "rpm"],
    "maintainer": "xxxx.xx",
    "icon": "build/icons/",
    "category": "Utility",
    "description": "app例子",
    "desktop": "app.desktop",
    "packageCategory": "GNOME;GTK;"
  },
  "win": {
    "icon": "build/icons/icon.ico",
    "artifactName": "${productName}-${platform}-${arch}-${version}.${ext}",
    "requestedExecutionLevel": "highestAvailable",
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      },
      {
        "target": "zip",
        "arch": ["x64", "ia32"]
      }
    ]
  },
  "mac": {
    "category": "public.app-category.utilities",
    "icon": "build/icons/icon.icns",
    "target": ["dmg", "zip"],
    "artifactName": "${productName}-${platform}-${arch}-${version}.${ext}"
  },
  "nsis": {
    "oneClick": false,
    "allowElevation": true,
    "allowToChangeInstallationDirectory": true,
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true,
    "artifactName": "${name}-${version}-setup.${ext}",
    "shortcutName": "app例子",
    "uninstallDisplayName": "app例子",
    "uninstallerIcon": "build/icons/icon.ico",
    "perMachine": true,
    "deleteAppDataOnUninstall": true,
    "installerIcon": "build/icons/icon.ico",
    "installerHeaderIcon": "build/icons/icon.ico"
  },
  "dmg": {
    "contents": [
      {
        "x": 410,
        "y": 190,
        "type": "link",
        "path": "/Applications"
      },
      {
        "x": 130,
        "y": 190,
        "type": "file"
      }
    ],
    "window": {
      "height": 380,
      "width": 540
    }
  }
}

其中nsis和dmg分别是windows和mac下的安装包格式,这两个需要单独做配置。

3. main.js编写

typescript 复制代码
const { app, BrowserWindow, Menu, globalShortcut } = require('electron');
const path = require('path');
const fs = require('fs');
const server = require('./build/release/app');

process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true';
// app.commandLine.appendSwitch('remote-debugging-port', '9222'); // 在主进程中添加这段代码

// 忽略访问得是https证书问题
app.commandLine.appendSwitch('--disable-http-cache');
app.commandLine.appendSwitch('ignore-certificate-errors', true);

function checkEnv() {
  let envPath = path.join(__dirname, './.env');
  if (fs.existsSync(envPath)) {
    return path.join(__dirname, './');
  }
  const root = app.getPath('userData');
  envPath = path.join(root, '.env');

  if (!fs.existsSync(root)) {
    fs.mkdirSync(root);
  }

  if (!fs.existsSync(envPath)) {
    const list = [];
    list.push(`PORT=3000`);
    list.push(`ROOT=${path.join(root, 'release')}`);
    list.push(`PATH_LOG=${app.getPath('logs')}`);
    // list.push(`PATH_STATIC=${path.join(__dirname, './build/release/static')}`);
    fs.writeFileSync(envPath, list.join('\n'), 'utf-8');
  }

  return root;
}

app.whenReady().then(() => {
  try {
    const p = checkEnv();
    server.default(p);
  } catch (e) {
    console.error(e);
  }

  // 创建一个web窗口
  const mainWin = new BrowserWindow({
    width: 1400,
    minWidth: 1200,
    minHeight: 700,
    height: 900,
    icon: path.join(__dirname, './build/icons/256x256.png'),
    webPreferences: {
      worldSafeExecuteJavaScript: false,
      // nodeIntegrationInWorker: true,
      webSecurity: false,
      // nodeIntegration: true,
      // preload: './preload.js',
    },
  });

  // 引入页面要展示的文件
  // mainWin.loadFile(path.join(__dirname, './build/release/static/index.html'));
  mainWin.loadURL('http://127.0.0.1:3000', {});

  // 监听当前窗口关闭要做的事情
  mainWin.on('close', () => {
    console.log('close');
  });

  // 去掉默认菜单
  Menu.setApplicationMenu(null);

  // 注册快捷键
  globalShortcut.register('Alt+CommandOrControl+Shift+D', () => {
    mainWin.webContents.openDevTools({ mode: 'detach' }); //开启开发者工具
  });
});

// 监听所有窗口关闭要做的事情
app.on('window-all-closed', () => {
  console.log('window-all-closed');
  app.quit(); // 窗口关闭api
});

4. win7打包兼容问题

  • win7安装高版本node

我的电脑右击进入属性->高级系统设置->环境变量->系统变量中添加NODE_SKIP_PLATFORM_CHECK 值:1

我安装的是node-v18.14.2

  • 打包安装exe文件,运行时出现:无法定位程序输入点GetPackageFamiliName于动态链接库KERNEL32.dll上

最终发现是electron版本太高,无法在win7上运行。最终降低版本 electron :21.4.4 , electron-builder: 22.1.0

  • 将项目打包到win7上面运行

export_win.sh

bash 复制代码
#!/bin/bash

CUR_PATH=$(pwd)
echo $CUR_PATH

TARGET_PATH="./release-built/output"
APP_PATH="./demo_app"

if [ ! -d "../release-built" ]; then
    mkdir ../release-built
fi

if [ -d "$TARGET_PATH" ]; then
    rm -rf $TARGET_PATH
fi

cp -r $APP_PATH $TARGET_PATH
rm -rf $TARGET_PATH/node_modules
rm $TARGET_PATH/package-lock.json

# 低版本兼容
# "electron": "^23.3.12", => 21.4.4
# "electron-builder": "^24.6.3" => 22.1.0

# 判断是否是mac
if [ $(uname) == "Darwin" ]; then
    sed -i '' 's/"electron": ".*"/"electron": "^21.4.4"/g' $TARGET_PATH/package.json
    sed -i '' 's/"electron-builder": ".*"/"electron-builder": "^22.1.0"/' $TARGET_PATH/package.json
else
    sed -i 's/"electron": ".*"/"electron": "^21.4.4"/g' $TARGET_PATH/package.json
    sed -i 's/"electron-builder": ".*"/"electron-builder": "^22.1.0"/g' $TARGET_PATH/package.json
fi

cd ./release-built

if [ -d "elease-demo.zip" ]; then
    rm elease-demo.zip
fi

zip -r release-demo.zip output/
rm -rf output/

然后项目复制到windows上面,解压,打开powershell,cd到项目目录,执行:

shell 复制代码
# npm install
npm i --verbose --electron_mirror=http://npm.taobao.org/mirrors/electron/
npm run build:nsis

5. app打包

linux平台运行:

shell 复制代码
npm run build:deb

mac平台运行:

shell 复制代码
npm run build:dmg

windows平台运行:

shell 复制代码
npm run build:nsis

结语

这里吐槽一声,现在找资料真的太难了,那些博客写的东西千篇一律,无非就是东抄西,西抄东,想找到自己正在需要的信息真的很少。目前最好的方式还是github学习别人的代码,以及使用chatGPT或者其他AI工具,另外查看官网也是不错的选择。

目前还有一些没有完善的地方,这里做一个TODO-list:

  • 登录逻辑:窗口切换问题
  • 应用更新:数据库更新问题
  • 数据同步:局域网数据同步
相关推荐
用户3157476081357 小时前
成为程序员的必经之路” Git “,你学会了吗?
面试·github·全栈
ZJ_.21 小时前
Electron 沙盒模式与预加载脚本:保障桌面应用安全的关键机制
开发语言·前端·javascript·vue.js·安全·electron·node.js
fanxbl95721 小时前
Electron 项目实现下载文件监听
javascript·electron·状态模式
怕冷的火焰(~杰)2 天前
创建vue+electron项目流程
vue.js·electron
new出一个对象2 天前
vue3+vite搭建脚手架项目本地运行electron桌面应用
前端·javascript·electron
明辉光焱2 天前
使用yarn,如何编译打包electron?
前端·javascript·electron·node.js
云只上2 天前
Electron + Vue3 开发桌面应用+附源码
前端·javascript·electron
fanxbl9573 天前
Electron 项目中杀掉进程的不同方式
前端·javascript·electron
Liigo4 天前
初次体验Tauri和Sycamore(1)
rust·electron·gui·tauri·wasm·sycamore
yqcoder4 天前
vite-plugin-electron 库作用
服务器·前端·electron