refactor(config): 服务地址改为 dev/prod 配置文件与单个环境开关
启动时按环回、局域网、公网顺序探测地址,换网络后会静默切到别处,排查 困难。改为代码内一个布尔开关选择环境,地址分别放在 dev_config.dart 与 prod_config.dart,不再探测;调试条改地址后直接重载,不再重新探测。 release 构建若仍是开发开关会启动即报错,避免线上包连开发机。
This commit is contained in:
1 parent
57f84c45bd
commit
a6d6b66d31
13 files changed
+101
-382
No files matched your search
+24
-153
@@ -1,173 +1,51 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:io';
|
||||
import 'dev_config.dart';
|
||||
import 'prod_config.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 客户端与后端约定的运行时配置。
|
||||
/// 客户端运行时配置。
|
||||
///
|
||||
/// 接口地址优先级:运行时设置 > `--dart-define=API_BASE_URL=...` > 平台默认值。
|
||||
/// 开发机的局域网地址会随网络变化,换网络后改 [devHostBaseUrl] 即可。
|
||||
class AppConfig {
|
||||
const AppConfig._();
|
||||
|
||||
/// 开发调试默认服务地址,当前指向本机局域网 IP。
|
||||
/// 两套环境各一个文件:
|
||||
/// - `dev_config.dart`:[DevConfig](本机联调)
|
||||
/// - `prod_config.dart`:[ProdConfig](生产)
|
||||
///
|
||||
/// 切换环境只改下面这一个布尔值,不用传任何构建参数、也不做网络探测。
|
||||
abstract final class AppConfig {
|
||||
/// **环境开关**:true 用开发环境,false 用生产环境。
|
||||
///
|
||||
/// 换网络时只改这一行,地址不要带结尾斜杠。
|
||||
static const String devHostBaseUrl = 'http://192.168.31.243:8081';
|
||||
/// 打生产包前必须改成 false;release 构建若仍为 true 会在启动时直接报错。
|
||||
static const bool useDevConfig = true;
|
||||
|
||||
/// 本机后端地址:通过 `adb reverse tcp:8081 tcp:8081` 映射到开发机,
|
||||
/// 探测可达时 App 直连本机服务;不可达时回落到公网地址。
|
||||
static const String lanBaseUrl = 'http://127.0.0.1:8081';
|
||||
|
||||
/// 公网地址(远程/移动网络访问时使用)。
|
||||
static const String publicBaseUrl = 'http://39.162.50.99:8081';
|
||||
|
||||
static const String _overrideBaseUrl = String.fromEnvironment('API_BASE_URL');
|
||||
/// 当前选中的环境地址。
|
||||
static String get baseUrl =>
|
||||
useDevConfig ? DevConfig.baseUrl : ProdConfig.baseUrl;
|
||||
|
||||
/// 调试面板手改的地址(见 `ServerEndpoint`);为空表示用上面的环境地址。
|
||||
static String _runtimeBaseUrl = '';
|
||||
|
||||
/// 启动期异步准备(iOS 无线数据权限弹窗),目录请求前等待它。
|
||||
static Future<void>? _ready;
|
||||
|
||||
/// 启动地址探测完成信号;默认已完成,避免影响既有测试与调用方。
|
||||
/// 启动准备完成信号;默认已完成,避免影响既有测试与调用方。
|
||||
static Future<void> get ready => _ready ?? Future<void>.value();
|
||||
static set ready(Future<void> value) => _ready = value;
|
||||
|
||||
static const Duration _probeTimeout = Duration(seconds: 2);
|
||||
static const List<String> _probeCandidates = [
|
||||
lanBaseUrl,
|
||||
devHostBaseUrl,
|
||||
if (devHostBaseUrl != publicBaseUrl) publicBaseUrl,
|
||||
];
|
||||
static const Duration requestTimeout = Duration(seconds: 8);
|
||||
|
||||
/// Build-time server URL supplied through `--dart-define=API_BASE_URL`.
|
||||
static String get compiledBaseUrl => _overrideBaseUrl;
|
||||
/// 实际使用的服务地址:运行时手改优先,否则用环境开关选中的地址。
|
||||
static String get apiBaseUrl =>
|
||||
_runtimeBaseUrl.isNotEmpty ? _runtimeBaseUrl : baseUrl;
|
||||
|
||||
/// Existing platform-aware fallback used when no runtime or build override exists.
|
||||
static String get defaultBaseUrl => _defaultBaseUrl;
|
||||
|
||||
/// Effective server URL, including the debug-only runtime override.
|
||||
static String get apiBaseUrl => _runtimeBaseUrl.isNotEmpty
|
||||
? _runtimeBaseUrl
|
||||
: (compiledBaseUrl.isNotEmpty ? compiledBaseUrl : defaultBaseUrl);
|
||||
|
||||
/// 生产包固定走公网地址;仅 debug 才按平台默认到环回/开发机。
|
||||
static final String _defaultBaseUrl = kReleaseMode
|
||||
? publicBaseUrl
|
||||
: (_runsOnDevMachine
|
||||
? lanBaseUrl
|
||||
: devHostBaseUrl);
|
||||
|
||||
/// 模拟器、桌面端与开发机同机,环回地址可用;iOS 真机不满足。
|
||||
/// Android 没有等价的探测方式,统一按真机处理(模拟器用
|
||||
/// `--dart-define=API_BASE_URL=http://10.0.2.2:8081` 覆盖)。
|
||||
static final bool _runsOnDevMachine =
|
||||
kIsWeb ||
|
||||
defaultTargetPlatform == TargetPlatform.macOS ||
|
||||
defaultTargetPlatform == TargetPlatform.windows ||
|
||||
defaultTargetPlatform == TargetPlatform.linux ||
|
||||
(defaultTargetPlatform == TargetPlatform.iOS &&
|
||||
Platform.environment.containsKey('SIMULATOR_DEVICE_NAME'));
|
||||
|
||||
/// Applies an already normalized runtime URL. Passing null clears it.
|
||||
/// 应用一个已经规范化的运行时地址;传 null 表示清除。
|
||||
static void applyRuntimeBaseUrl(String? value) {
|
||||
_runtimeBaseUrl = value ?? '';
|
||||
}
|
||||
|
||||
/// Clears the in-memory runtime override, mainly for tests and restarts.
|
||||
/// 清除运行时地址,主要给测试与重启流程用。
|
||||
static void clearRuntimeBaseUrl() {
|
||||
_runtimeBaseUrl = '';
|
||||
}
|
||||
|
||||
static const Duration requestTimeout = Duration(seconds: 8);
|
||||
|
||||
/// 按环回、开发机、公网顺序探测,写入首个返回 200 的地址。
|
||||
///
|
||||
/// 全部不可达时使用平台默认地址,真机保留开发机地址,避免退回公网。
|
||||
static Future<void> probeAndApplyBaseUrl({
|
||||
Future<bool> Function(String baseUrl)? probe,
|
||||
String? fallbackBaseUrl,
|
||||
|
||||
/// 编译期地址测试注入;未传时读取 `API_BASE_URL`。
|
||||
String? compiledOverride,
|
||||
}) async {
|
||||
final compiled = compiledOverride ?? compiledBaseUrl;
|
||||
if (compiled.isNotEmpty) {
|
||||
if (_runtimeBaseUrl.isEmpty) applyRuntimeBaseUrl(compiled);
|
||||
return;
|
||||
}
|
||||
// 生产包固定公网地址,不再探测环回/开发机局域网地址,避免请求到开发机。
|
||||
if (kReleaseMode) {
|
||||
if (_runtimeBaseUrl.isEmpty) applyRuntimeBaseUrl(publicBaseUrl);
|
||||
return;
|
||||
}
|
||||
final candidates = <String>[
|
||||
if (_runtimeBaseUrl.isNotEmpty) _runtimeBaseUrl,
|
||||
for (final candidate in _probeCandidates)
|
||||
if (candidate != _runtimeBaseUrl) candidate,
|
||||
];
|
||||
final results = await Future.wait([
|
||||
for (final candidate in candidates)
|
||||
probe == null
|
||||
? _probeBaseUrl(candidate)
|
||||
: _runInjectedProbe(candidate, probe),
|
||||
]);
|
||||
for (var index = 0; index < candidates.length; index++) {
|
||||
final candidate = candidates[index];
|
||||
final result = results[index];
|
||||
if (kDebugMode) {
|
||||
final status = result.ok
|
||||
? 'ok'
|
||||
: 'fail(${result.reason ?? 'request failed'})';
|
||||
debugPrint('[probe] $candidate -> $status');
|
||||
}
|
||||
if (result.ok) {
|
||||
applyRuntimeBaseUrl(candidate);
|
||||
return;
|
||||
}
|
||||
}
|
||||
applyRuntimeBaseUrl(fallbackBaseUrl ?? defaultBaseUrl);
|
||||
}
|
||||
|
||||
static Future<_ProbeResult> _runInjectedProbe(
|
||||
String baseUrl,
|
||||
Future<bool> Function(String baseUrl) probe,
|
||||
) async {
|
||||
try {
|
||||
return _ProbeResult(ok: await probe(baseUrl));
|
||||
} catch (error) {
|
||||
return _ProbeResult(ok: false, reason: error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
static Future<_ProbeResult> _probeBaseUrl(String baseUrl) async {
|
||||
final client = HttpClient()..connectionTimeout = _probeTimeout;
|
||||
try {
|
||||
return await _requestCategories(
|
||||
client,
|
||||
Uri.parse('$baseUrl/app/categories'),
|
||||
).timeout(_probeTimeout);
|
||||
} catch (error) {
|
||||
return _ProbeResult(ok: false, reason: error.toString());
|
||||
} finally {
|
||||
client.close(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<_ProbeResult> _requestCategories(
|
||||
HttpClient client,
|
||||
Uri uri,
|
||||
) async {
|
||||
final request = await client.getUrl(uri);
|
||||
final response = await request.close();
|
||||
await response.drain<void>();
|
||||
final ok = response.statusCode == 200;
|
||||
return _ProbeResult(
|
||||
ok: ok,
|
||||
reason: ok ? null : 'HTTP ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
|
||||
/// 后端返回的是相对路径(/files/...),这里补上服务地址。
|
||||
static String mediaUrl(String path) {
|
||||
if (path.isEmpty) return '';
|
||||
@@ -175,10 +53,3 @@ class AppConfig {
|
||||
return '$apiBaseUrl$path';
|
||||
}
|
||||
}
|
||||
|
||||
class _ProbeResult {
|
||||
const _ProbeResult({required this.ok, this.reason});
|
||||
|
||||
final bool ok;
|
||||
final String? reason;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
/// 开发环境配置。
|
||||
///
|
||||
/// 本机联调用:换 Wi-Fi、换网段后只改这里的 [baseUrl],地址不要带结尾斜杠。
|
||||
abstract final class DevConfig {
|
||||
/// 开发机后端地址。
|
||||
static const String baseUrl = 'http://192.168.31.243:8081';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
/// 生产环境配置。
|
||||
///
|
||||
/// 发版前确认这里的 [baseUrl],地址不要带结尾斜杠。
|
||||
abstract final class ProdConfig {
|
||||
/// 生产后端地址。
|
||||
static const String baseUrl = 'http://39.162.50.99:8081';
|
||||
}
|
||||
@@ -22,16 +22,9 @@ abstract final class ServerEndpoint {
|
||||
return value.replaceFirst(RegExp(r'/+$'), '');
|
||||
}
|
||||
|
||||
/// Resolves runtime > dart-define > default using normalized valid values.
|
||||
static String resolve({
|
||||
required String? stored,
|
||||
required String compiled,
|
||||
required String fallback,
|
||||
}) {
|
||||
return normalize(stored) ??
|
||||
normalize(compiled) ??
|
||||
normalize(fallback) ??
|
||||
fallback;
|
||||
/// 运行时手改优先,其次构建期默认地址;两者都必须先通过规范校验。
|
||||
static String resolve({required String? stored, required String fallback}) {
|
||||
return normalize(stored) ?? normalize(fallback) ?? fallback;
|
||||
}
|
||||
|
||||
/// Loads the persisted override before the app creates its ApiClient.
|
||||
@@ -46,11 +39,7 @@ abstract final class ServerEndpoint {
|
||||
final prefs = preferences ?? SharedPreferencesAsync();
|
||||
final stored = await prefs.getString(preferenceKey);
|
||||
AppConfig.applyRuntimeBaseUrl(
|
||||
resolve(
|
||||
stored: stored,
|
||||
compiled: AppConfig.compiledBaseUrl,
|
||||
fallback: AppConfig.defaultBaseUrl,
|
||||
),
|
||||
resolve(stored: stored, fallback: AppConfig.baseUrl),
|
||||
);
|
||||
} catch (_) {
|
||||
AppConfig.clearRuntimeBaseUrl();
|
||||
|
||||
Reference in new issue
Block a user