️ perf(dev): 优化 Wails 开发启动与 CI 构建耗时

- 新增 Wails 快速开发启动脚本,跳过非必要构建与绑定生成
- 优化前端依赖安装状态判断,减少重复 npm install
- 固定 CI Wails CLI 版本并增加 node_modules 缓存
- 更新开发文档中的快速启动说明
This commit is contained in:
Syngnat
2026-05-16 11:02:43 +08:00
parent 959f32327d
commit a5be4cc3ae
8 changed files with 240 additions and 8 deletions

View File

@@ -86,7 +86,6 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: '1.24'
check-latest: true
- name: Setup Node
uses: actions/setup-node@v4
@@ -95,6 +94,12 @@ jobs:
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Cache frontend node_modules
uses: actions/cache@v4
with:
path: frontend/node_modules
key: ${{ runner.os }}-node20-frontend-${{ hashFiles('frontend/package-lock.json') }}
- name: Install UPX (Windows)
if: contains(matrix.platform, 'windows')
shell: pwsh
@@ -155,7 +160,7 @@ jobs:
fi
- name: Install Wails
run: go install -v github.com/wailsapp/wails/v2/cmd/wails@latest
run: go install -v github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
- name: Setup MSYS2 Toolchain For DuckDB (Windows AMD64)
id: msys2_duckdb

View File

@@ -84,7 +84,6 @@ jobs:
uses: actions/setup-go@v5
with:
go-version: '1.24'
check-latest: true
- name: Setup Node
uses: actions/setup-node@v4
@@ -93,6 +92,12 @@ jobs:
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Cache frontend node_modules
uses: actions/cache@v4
with:
path: frontend/node_modules
key: ${{ runner.os }}-node20-frontend-${{ hashFiles('frontend/package-lock.json') }}
- name: Install UPX (Windows)
if: contains(matrix.platform, 'windows')
shell: pwsh
@@ -157,7 +162,7 @@ jobs:
fi
- name: Install Wails
run: go install -v github.com/wailsapp/wails/v2/cmd/wails@latest
run: go install -v github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
- name: Setup MSYS2 Toolchain For DuckDB (Windows AMD64)
id: msys2_duckdb

View File

@@ -133,7 +133,7 @@ GoNavi is designed for developers and DBAs who need a unified desktop experience
- [Go](https://go.dev/dl/) 1.21+
- [Node.js](https://nodejs.org/) 18+
- [Wails CLI](https://wails.io/docs/gettingstarted/installation):
`go install github.com/wailsapp/wails/v2/cmd/wails@latest`
`go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0`
### Development Mode
@@ -144,6 +144,12 @@ cd GoNavi
# Start development with hot reload
wails dev
# Faster local startup when exported Go method signatures are unchanged
node tools/wails-fast-dev.mjs
# Refresh Wails JS bindings after changing exported Go method signatures
node tools/wails-fast-dev.mjs --refresh-bindings
```
### Build

View File

@@ -127,7 +127,7 @@ GoNavi 面向开发者与 DBA核心目标是让数据库操作在桌面端做
- [Go](https://go.dev/dl/) 1.21+
- [Node.js](https://nodejs.org/) 18+
- [Wails CLI](https://wails.io/docs/gettingstarted/installation):
`go install github.com/wailsapp/wails/v2/cmd/wails@latest`
`go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0`
### 开发模式
@@ -138,6 +138,12 @@ cd GoNavi
# 启动开发(热重载)
wails dev
# 本地快速启动:未修改 Go 导出方法签名时使用
node tools/wails-fast-dev.mjs
# 修改 Go 导出方法签名后刷新 Wails JS 绑定
node tools/wails-fast-dev.mjs --refresh-bindings
```
### 编译构建

View File

@@ -1 +1 @@
0295a42fd931778d85157816d79d29e5
d0464f9da25e9356e61652e638c99ffe

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import path from 'node:path';
const frontendDir = process.cwd();
const packageJsonPath = path.join(frontendDir, 'package.json');
const packageLockPath = path.join(frontendDir, 'package-lock.json');
const nodeModulesPath = path.join(frontendDir, 'node_modules');
const npmHiddenLockPath = path.join(nodeModulesPath, '.package-lock.json');
const installStatePath = path.join(nodeModulesPath, '.gonavi-install-state.json');
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const commonArgs = [
'--prefer-offline',
'--no-audit',
'--fund=false',
'--fetch-retries=5',
'--fetch-retry-mintimeout=20000',
'--fetch-retry-maxtimeout=120000',
];
const hashFile = (filePath) => {
const hash = createHash('sha256');
hash.update(readFileSync(filePath));
return hash.digest('hex');
};
const currentState = () => ({
packageJson: hashFile(packageJsonPath),
packageLock: existsSync(packageLockPath) ? hashFile(packageLockPath) : '',
});
const readInstalledState = () => {
if (!existsSync(installStatePath)) return null;
try {
return JSON.parse(readFileSync(installStatePath, 'utf8'));
} catch {
return null;
}
};
const writeInstalledState = (state) => {
writeFileSync(installStatePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
};
const packageInputsAreOlderThanNpmLock = () => {
if (!existsSync(npmHiddenLockPath)) return false;
const markerTime = statSync(npmHiddenLockPath).mtimeMs;
return [packageJsonPath, packageLockPath]
.filter(existsSync)
.every((filePath) => statSync(filePath).mtimeMs <= markerTime);
};
const runNpm = (subcommand) => {
const result = spawnSync(npmCommand, [subcommand, ...commonArgs], {
cwd: frontendDir,
env: process.env,
stdio: 'inherit',
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
};
const state = currentState();
const installedState = readInstalledState();
const forceInstall = process.env.GONAVI_FORCE_FRONTEND_INSTALL === '1';
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
if (!forceInstall && existsSync(nodeModulesPath)) {
if (
installedState?.packageJson === state.packageJson &&
installedState?.packageLock === state.packageLock
) {
console.log('Frontend dependencies are up to date; skipping npm install.');
process.exit(0);
}
if (!installedState && isCI && existsSync(npmHiddenLockPath)) {
writeInstalledState(state);
console.log('Frontend dependencies are up to date from CI cache; recorded install state.');
process.exit(0);
}
if (!installedState && packageInputsAreOlderThanNpmLock()) {
writeInstalledState(state);
console.log('Frontend dependencies are up to date; recorded install state.');
process.exit(0);
}
}
runNpm(isCI ? 'ci' : 'install');
writeInstalledState(state);

115
tools/wails-fast-dev.mjs Normal file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env node
import { spawn, spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(scriptDir, '..');
const frontendDir = path.join(projectRoot, 'frontend');
const wailsConfigPath = path.join(projectRoot, 'wails.json');
const nodeCommand = process.execPath;
const wailsCommand = process.platform === 'win32' ? 'wails.exe' : 'wails';
const usage = `Usage:
node tools/wails-fast-dev.mjs [--refresh-bindings] [--no-install] [--dry-run] [wails dev flags...]
Fast path:
- skips npm install when frontend dependencies are unchanged
- runs wails dev with -m -s -nosyncgomod -skipembedcreate
- skips Wails binding generation unless --refresh-bindings is passed
Use --refresh-bindings after changing exported Go method signatures.`;
const rawArgs = process.argv.slice(2);
if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
console.log(usage);
process.exit(0);
}
const readWailsConfig = () => {
try {
return JSON.parse(readFileSync(wailsConfigPath, 'utf8'));
} catch (error) {
console.error(`Failed to read wails.json: ${error.message}`);
process.exit(1);
}
};
const runFrontendInstall = () => {
const result = spawnSync(nodeCommand, ['scripts/wails-frontend-install.mjs'], {
cwd: frontendDir,
env: process.env,
stdio: 'inherit',
});
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
};
const hasFlag = (args, names) =>
args.some((arg) => names.some((name) => arg === name || arg.startsWith(`${name}=`)));
const wailsConfig = readWailsConfig();
const dryRun = rawArgs.includes('--dry-run');
const refreshBindings = rawArgs.includes('--refresh-bindings') || process.env.GONAVI_REFRESH_WAILS_BINDINGS === '1';
const skipInstall = rawArgs.includes('--no-install') || process.env.GONAVI_FAST_DEV_SKIP_INSTALL === '1';
const passThroughArgs = rawArgs.filter((arg) => arg !== '--refresh-bindings' && arg !== '--no-install' && arg !== '--dry-run');
const devServerUrl = process.env.GONAVI_FRONTEND_DEV_SERVER_URL || wailsConfig['frontend:dev:serverUrl'] || 'http://localhost:5173';
const wailsjsRoot = path.resolve(projectRoot, wailsConfig.wailsjsdir || './frontend', 'wailsjs');
const skipBindings = !refreshBindings && existsSync(wailsjsRoot);
const fastArgs = ['dev'];
if (!skipInstall && !dryRun) {
runFrontendInstall();
}
if (!hasFlag(passThroughArgs, ['-m'])) {
fastArgs.push('-m');
}
if (!hasFlag(passThroughArgs, ['-s'])) {
fastArgs.push('-s');
}
if (!hasFlag(passThroughArgs, ['-nosyncgomod'])) {
fastArgs.push('-nosyncgomod');
}
if (!hasFlag(passThroughArgs, ['-skipembedcreate'])) {
fastArgs.push('-skipembedcreate');
}
if (skipBindings && !hasFlag(passThroughArgs, ['-skipbindings'])) {
fastArgs.push('-skipbindings');
}
if (!hasFlag(passThroughArgs, ['-frontenddevserverurl'])) {
fastArgs.push('-frontenddevserverurl', devServerUrl);
}
if (!skipBindings && !refreshBindings) {
console.warn('frontend/wailsjs not found; generating Wails bindings this run.');
}
if (dryRun) {
const quoteArg = (arg) => (/\s/.test(arg) ? JSON.stringify(arg) : arg);
console.log(`Would run: ${[wailsCommand, ...fastArgs, ...passThroughArgs].map(quoteArg).join(' ')}`);
process.exit(0);
}
const child = spawn(wailsCommand, [...fastArgs, ...passThroughArgs], {
cwd: projectRoot,
env: process.env,
stdio: 'inherit',
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
child.on('error', (error) => {
console.error(`Failed to start Wails CLI: ${error.message}`);
process.exit(1);
});

View File

@@ -1,7 +1,7 @@
{
"name": "GoNavi",
"outputfilename": "GoNavi",
"frontend:install": "npm ci --prefer-offline --no-audit --fund=false --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000",
"frontend:install": "node scripts/wails-frontend-install.mjs",
"frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev",
"frontend:dev:serverUrl": "http://localhost:5173",