@flun/node-mobile-app 4.0.3 → 4.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +38 -38
- package/CHANGELOG.md +4 -14
- package/bin/cli.js +5 -5
- package/mobileAppConfig.js +18 -18
- package/package.json +1 -1
- package/scripts/copy-files.js +1 -1
- package/src/commands/build.js +1 -1
- package/src/commands/test.js +21 -15
- package/src/config.js +8 -11
- package/src/copy.js +24 -28
- package/src/fingerprint.js +5 -5
- package/src/generate.js +7 -10
- package/src/install.js +13 -13
- package/src/optimize-node-modules.js +10 -8
- package/src/patcher.js +21 -21
- package/src/platform-ios.js +7 -7
- package/src/utils.js +13 -13
- package/template/App.tsx +83 -87
- package/template/index.js +1 -1
- package/template/metro.config.js +1 -3
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +68 -68
package/src/utils.js
CHANGED
|
@@ -21,7 +21,7 @@ const ensureDir = dir => {
|
|
|
21
21
|
return false;
|
|
22
22
|
},
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* 递归复制,exclude 支持 'node_modules/'、'*.log'、'.git/'
|
|
25
25
|
*/
|
|
26
26
|
copyDir = (src, dest, exclude = []) => {
|
|
27
27
|
ensureDir(dest);
|
|
@@ -36,8 +36,8 @@ const ensureDir = dir => {
|
|
|
36
36
|
readJson = file => JSON.parse(fs.readFileSync(file, 'utf-8')),
|
|
37
37
|
writeJson = (file, obj) => fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8'),
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
* 返回 true
|
|
39
|
+
* 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
|
|
40
|
+
* 返回 true 表示写入了,false 表示内容相同跳过
|
|
41
41
|
*/
|
|
42
42
|
writeIfChanged = (file, content) => {
|
|
43
43
|
if (fs.existsSync(file)) {
|
|
@@ -49,7 +49,7 @@ const ensureDir = dir => {
|
|
|
49
49
|
return true;
|
|
50
50
|
},
|
|
51
51
|
/**
|
|
52
|
-
* 大小和 mtime
|
|
52
|
+
* 大小和 mtime 都一致才跳过复制,否则复制
|
|
53
53
|
* 用于用户项目 → nodejs-project 的增量复制
|
|
54
54
|
*/
|
|
55
55
|
copyFileIfChanged = (src, dst) => {
|
|
@@ -70,9 +70,9 @@ const ensureDir = dir => {
|
|
|
70
70
|
},
|
|
71
71
|
/**
|
|
72
72
|
* 统一的子进程启动:
|
|
73
|
-
* - Windows:.cmd / .bat 需经 shell
|
|
73
|
+
* - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
|
|
74
74
|
* 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190。
|
|
75
|
-
* - 其它平台:不用 shell
|
|
75
|
+
* - 其它平台:不用 shell,参数交由 Node 转义。
|
|
76
76
|
*/
|
|
77
77
|
spawnCmd = (bin, args, baseOpts = {}) => {
|
|
78
78
|
if (process.platform === 'win32') {
|
|
@@ -82,7 +82,7 @@ const ensureDir = dir => {
|
|
|
82
82
|
return spawnSync(bin, args, { ...baseOpts, shell: false });
|
|
83
83
|
},
|
|
84
84
|
/**
|
|
85
|
-
* 异步 spawn:参数同 spawnCmd
|
|
85
|
+
* 异步 spawn:参数同 spawnCmd,返回 ChildProcess
|
|
86
86
|
* @param {string} bin 命令
|
|
87
87
|
* @param {string[]} args 参数
|
|
88
88
|
* @param {object} [baseOpts] spawn 选项
|
|
@@ -96,7 +96,7 @@ const ensureDir = dir => {
|
|
|
96
96
|
return spawn(bin, args, { ...baseOpts, shell: false });
|
|
97
97
|
},
|
|
98
98
|
/**
|
|
99
|
-
* 执行命令:默认实时输出(inherit);quiet=true
|
|
99
|
+
* 执行命令:默认实时输出(inherit);quiet=true 时捕获输出,成功静默、失败才打印
|
|
100
100
|
*/
|
|
101
101
|
run = (bin, args, opts = {}) => {
|
|
102
102
|
const { quiet, ...restOpts } = opts;
|
|
@@ -124,7 +124,7 @@ const ensureDir = dir => {
|
|
|
124
124
|
return r;
|
|
125
125
|
},
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
127
|
+
* 执行命令:实时输出,仅首行加缩进前缀,其余行原样
|
|
128
128
|
* @param {string} bin 命令
|
|
129
129
|
* @param {string[]} args 参数
|
|
130
130
|
* @param {object} [opts] spawn 选项 + prefix
|
|
@@ -155,7 +155,7 @@ const ensureDir = dir => {
|
|
|
155
155
|
const configured = config?.android?.adbPath;
|
|
156
156
|
if (configured) {
|
|
157
157
|
if (fs.existsSync(configured)) return configured;
|
|
158
|
-
console.warn(chalk.yellow('⚠️ config.android.adbPath
|
|
158
|
+
console.warn(chalk.yellow('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured));
|
|
159
159
|
}
|
|
160
160
|
// 2. 环境变量 ANDROID_HOME / ANDROID_SDK_ROOT
|
|
161
161
|
for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
|
|
@@ -176,7 +176,7 @@ const ensureDir = dir => {
|
|
|
176
176
|
return 'adb';
|
|
177
177
|
},
|
|
178
178
|
/**
|
|
179
|
-
* 定位 hermesc(Hermes
|
|
179
|
+
* 定位 hermesc(Hermes 编译器,用于把 JS bundle 编成字节码)
|
|
180
180
|
*/
|
|
181
181
|
/**
|
|
182
182
|
* 读取安装状态标志(记录上次由本工具安装的包与变体)
|
|
@@ -221,7 +221,7 @@ const ensureDir = dir => {
|
|
|
221
221
|
ensureInstallable = (adb, appId, want, buildDir, serial) => {
|
|
222
222
|
const installed = getInstalledVariant(adb, appId, serial);
|
|
223
223
|
if (installed && installed !== want) {
|
|
224
|
-
console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want}
|
|
224
|
+
console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want} 包签名不同,先卸载`));
|
|
225
225
|
spawnCmd(adb, [...(serial ? ['-s', serial] : []), 'uninstall', appId], { encoding: 'utf-8' });
|
|
226
226
|
}
|
|
227
227
|
},
|
|
@@ -274,4 +274,4 @@ import chalk from 'chalk';
|
|
|
274
274
|
export {
|
|
275
275
|
ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, spawnCmdAsync, run, runQuiet, runStream,
|
|
276
276
|
runSilent, findAdb, readInstallState, writeInstallState, getInstalledVariant, ensureInstallable, findHermesc, sanitizeFileName, buildEslintConfigSource
|
|
277
|
-
};
|
|
277
|
+
};
|
package/template/App.tsx
CHANGED
|
@@ -9,101 +9,97 @@ import runtime from './mobileApp.runtime';
|
|
|
9
9
|
// 'system' → 跟随系统(内部用默认浅/深色)
|
|
10
10
|
// { light: '#fff', dark: '#000' } → 按系统主题切换
|
|
11
11
|
const resolveBgColor = (bg: string | { light: string; dark: string }, theme: string | null | undefined): string => {
|
|
12
|
-
if (typeof bg === 'object' && bg !== null)
|
|
12
|
+
if (typeof bg === 'object' && bg !== null)
|
|
13
13
|
return (theme === 'dark' ? bg.dark : bg.light) || bg.light || '#ffffff';
|
|
14
|
-
|
|
15
|
-
if (bg === 'system') {
|
|
16
|
-
return theme === 'dark' ? '#121212' : '#ffffff';
|
|
17
|
-
}
|
|
14
|
+
if (bg === 'system') return theme === 'dark' ? '#121212' : '#ffffff';
|
|
18
15
|
return bg || '#ffffff';
|
|
19
|
-
}
|
|
16
|
+
},
|
|
17
|
+
App = () => {
|
|
18
|
+
const systemTheme = useColorScheme(),
|
|
19
|
+
[url, setUrl] = useState<string | null>(null), [failedReason, setFailedReason] = useState<string | null>(null),
|
|
20
|
+
[showDetail, setShowDetail] = useState(false),
|
|
21
|
+
styles = StyleSheet.create({
|
|
22
|
+
fill: { flex: 1 },
|
|
23
|
+
loading: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
|
24
|
+
errorContainer: {
|
|
25
|
+
flex: 1,
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
padding: 24,
|
|
29
|
+
},
|
|
30
|
+
errorTitle: { fontSize: 20, fontWeight: '600', marginBottom: 16, color: '#c00' },
|
|
31
|
+
errorHint: { fontSize: 14, textAlign: 'center', color: '#888' },
|
|
32
|
+
detailToggle: { fontSize: 13, marginTop: 24, color: '#06c' },
|
|
33
|
+
detailText: {
|
|
34
|
+
fontSize: 12,
|
|
35
|
+
marginTop: 12,
|
|
36
|
+
paddingHorizontal: 16,
|
|
37
|
+
color: '#666',
|
|
38
|
+
fontFamily: 'monospace',
|
|
39
|
+
textAlign: 'center',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
NodeMobile.start('main.js');
|
|
20
44
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
padding: 24,
|
|
33
|
-
},
|
|
34
|
-
errorTitle: { fontSize: 20, fontWeight: '600', marginBottom: 16, color: '#c00' },
|
|
35
|
-
errorHint: { fontSize: 14, textAlign: 'center', color: '#888' },
|
|
36
|
-
detailToggle: { fontSize: 13, marginTop: 24, color: '#06c' },
|
|
37
|
-
detailText: {
|
|
38
|
-
fontSize: 12,
|
|
39
|
-
marginTop: 12,
|
|
40
|
-
paddingHorizontal: 16,
|
|
41
|
-
color: '#666',
|
|
42
|
-
fontFamily: 'monospace',
|
|
43
|
-
textAlign: 'center',
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
NodeMobile.start('main.js');
|
|
45
|
+
const listener = (msg: unknown) => {
|
|
46
|
+
if (typeof msg !== 'string') return;
|
|
47
|
+
if (msg.startsWith('Node started on ')) {
|
|
48
|
+
const target = msg.slice('Node started on '.length).trim();
|
|
49
|
+
setFailedReason(null);
|
|
50
|
+
setUrl(target);
|
|
51
|
+
} else if (msg.startsWith('Node failed: ')) {
|
|
52
|
+
const reason = msg.slice('Node failed: '.length).trim();
|
|
53
|
+
setFailedReason(reason);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const target = msg.slice('Node started on '.length).trim();
|
|
53
|
-
setFailedReason(null);
|
|
54
|
-
setUrl(target);
|
|
55
|
-
} else if (msg.startsWith('Node failed: ')) {
|
|
56
|
-
const reason = msg.slice('Node failed: '.length).trim();
|
|
57
|
-
setFailedReason(reason);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
57
|
+
NodeMobile.channel.addListener('message', listener);
|
|
58
|
+
return () => NodeMobile.channel.removeListener('message', listener);
|
|
59
|
+
}, []);
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// 失败:显示错误页(用户看提示,开发者点开看详情)
|
|
62
|
+
if (failedReason) {
|
|
63
|
+
return (
|
|
64
|
+
<View style={styles.errorContainer}>
|
|
65
|
+
<Text style={styles.errorTitle}>应用启动失败</Text>
|
|
66
|
+
<Text style={styles.errorHint}>请稍后重试,或联系应用发布者</Text>
|
|
67
|
+
<Text
|
|
68
|
+
style={styles.detailToggle}
|
|
69
|
+
onPress={() => setShowDetail((v) => !v)}
|
|
70
|
+
>
|
|
71
|
+
{showDetail ? '▼ 收起详细信息' : '▶ 查看详细信息'}
|
|
72
|
+
</Text>
|
|
73
|
+
{showDetail && (
|
|
74
|
+
<Text style={styles.detailText}>{failedReason}</Text>
|
|
75
|
+
)}
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{showDetail ? '▼ 收起详细信息' : '▶ 查看详细信息'}
|
|
76
|
-
</Text>
|
|
77
|
-
{showDetail && (
|
|
78
|
-
<Text style={styles.detailText}>{failedReason}</Text>
|
|
79
|
-
)}
|
|
80
|
-
</View>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
80
|
+
// 尚未就绪:loading
|
|
81
|
+
if (!url) {
|
|
82
|
+
if (!runtime.showLoading) return <View style={styles.fill} />;
|
|
83
|
+
return (
|
|
84
|
+
<View style={styles.loading}>
|
|
85
|
+
<ActivityIndicator size="large" />
|
|
86
|
+
<Text>{runtime.loadingText}</Text>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
83
90
|
|
|
84
|
-
// 尚未就绪:loading
|
|
85
|
-
if (!url) {
|
|
86
|
-
if (!runtime.showLoading) return <View style={styles.fill} />;
|
|
87
91
|
return (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
<WebView
|
|
93
|
+
source={{ uri: url }}
|
|
94
|
+
style={[styles.fill, { backgroundColor: resolveBgColor(runtime.webview.backgroundColor, systemTheme) }]}
|
|
95
|
+
originWhitelist={['*']}
|
|
96
|
+
javaScriptEnabled={runtime.webview.enableJavaScript}
|
|
97
|
+
domStorageEnabled={runtime.webview.enableDomStorage}
|
|
98
|
+
allowFileAccess={runtime.webview.allowFileAccess}
|
|
99
|
+
allowUniversalAccessFromFileURLs={true}
|
|
100
|
+
mixedContentMode={runtime.webview.mixedContentMode}
|
|
101
|
+
/>
|
|
92
102
|
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<WebView
|
|
97
|
-
source={{ uri: url }}
|
|
98
|
-
style={[styles.fill, { backgroundColor: resolveBgColor(runtime.webview.backgroundColor, systemTheme) }]}
|
|
99
|
-
originWhitelist={['*']}
|
|
100
|
-
javaScriptEnabled={runtime.webview.enableJavaScript}
|
|
101
|
-
domStorageEnabled={runtime.webview.enableDomStorage}
|
|
102
|
-
allowFileAccess={runtime.webview.allowFileAccess}
|
|
103
|
-
allowUniversalAccessFromFileURLs={true}
|
|
104
|
-
mixedContentMode={runtime.webview.mixedContentMode}
|
|
105
|
-
/>
|
|
106
|
-
);
|
|
107
|
-
};
|
|
103
|
+
};
|
|
108
104
|
|
|
109
105
|
export { App };
|
package/template/index.js
CHANGED
package/template/metro.config.js
CHANGED
|
@@ -9,9 +9,7 @@ const exclusionList = exclusionListModule.default,
|
|
|
9
9
|
watchFolders: [__dirname, resolve(workspaceRoot, 'node_modules')],
|
|
10
10
|
resolver: {
|
|
11
11
|
nodeModulesPaths: [resolve(__dirname, 'node_modules'), resolve(workspaceRoot, 'node_modules')],
|
|
12
|
-
blockList: exclusionList([
|
|
13
|
-
/[/\\]nodejs-assets[/\\].*/
|
|
14
|
-
])
|
|
12
|
+
blockList: exclusionList([/[/\\]nodejs-assets[/\\].*/])
|
|
15
13
|
}
|
|
16
14
|
};
|
|
17
15
|
|
|
@@ -2,32 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
本文档讲两件事:
|
|
4
4
|
|
|
5
|
-
1.
|
|
5
|
+
1. **用预编译产物**(推荐,90% 用户)—— 从仓库下载 `android-libnode.zip`,替换,跑 CLI,完事
|
|
6
6
|
2. **自己编译**(进阶)—— 用 WSL + NDK 从源码编译带 full-icu 的 v22+
|
|
7
7
|
|
|
8
|
-
**使用 `@flun/node-mobile-app` 时不需要看这份文档就能打包**——CLI 会自动处理 ABI
|
|
8
|
+
**使用 `@flun/node-mobile-app` 时不需要看这份文档就能打包**——CLI 会自动处理 ABI 配置、缓存清理、原生模块编译;本文档只在**要换 libnode.so** 时才需要;
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## 一、自定义 libnode.so
|
|
13
13
|
|
|
14
|
-
`@flun/node-mobile-app` 默认带的是官方 `nodejs-mobile` 的 **v18**
|
|
14
|
+
`@flun/node-mobile-app` 默认带的是官方 `nodejs-mobile` 的 **v18** 二进制;v18 的限制:
|
|
15
15
|
|
|
16
|
-
| 限制 | 影响
|
|
17
|
-
| ------------------------------ |
|
|
18
|
-
| `--with-intl=none`(不带 ICU) | 不能用 `\p{...}` / `\P{...}` Unicode 正则
|
|
19
|
-
| 无法用 Express 5 | `path-to-regexp@8` 内部用 `\p{ID_Start}
|
|
20
|
-
| 停更 | 官方不再更新
|
|
16
|
+
| 限制 | 影响 |
|
|
17
|
+
| ------------------------------ | ------------------------------------------------------- |
|
|
18
|
+
| `--with-intl=none`(不带 ICU) | 不能用 `\p{...}` / `\P{...}` Unicode 正则 |
|
|
19
|
+
| 无法用 Express 5 | `path-to-regexp@8` 内部用 `\p{ID_Start}`,v18 加载时崩溃 |
|
|
20
|
+
| 停更 | 官方不再更新 |
|
|
21
21
|
|
|
22
|
-
**要跑 Express 5 / 用新特性 / 用完整 Unicode
|
|
22
|
+
**要跑 Express 5 / 用新特性 / 用完整 Unicode 正则**,需替换为 **v22+ full-icu**;
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
## 二、用预编译产物(推荐)
|
|
27
27
|
|
|
28
|
-
> **注意**:从 `@flun/nodejs-mobile-react-native` 1.0.1
|
|
29
|
-
> **本节适用于**:要把自动下载的 v18 换成 **v22+ 自编译版本**(带 full-icu
|
|
30
|
-
> 若只是用默认 v18
|
|
28
|
+
> **注意**:从 `@flun/nodejs-mobile-react-native` 1.0.1 起,Android 的 `libnode.so` 由 postinstall 脚本从 Release 自动下载(默认 v18.20.4);
|
|
29
|
+
> **本节适用于**:要把自动下载的 v18 换成 **v22+ 自编译版本**(带 full-icu,能跑 Express 5)的场景;
|
|
30
|
+
> 若只是用默认 v18,你**不需要**看本节——装包后 `libnode.so` 已就位;
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
### 2.1 下载地址
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
| GitHub | [OpenFlun/nodejs-mobile](https://github.com/OpenFlun/nodejs-mobile) | [Releases](https://github.com/OpenFlun/nodejs-mobile/releases) | [android-libnode.zip](https://github.com/OpenFlun/nodejs-mobile/releases/download/v22.23.2/android-libnode.zip) |
|
|
40
40
|
| Gitee(国内) | [OpenFlun/nodejs-mobile](https://gitee.com/OpenFlun/nodejs-mobile) | [Releases](https://gitee.com/OpenFlun/nodejs-mobile/releases) | [android-libnode.zip](https://gitee.com/OpenFlun/nodejs-mobile/releases/download/v22.23.2/android-libnode.zip) |
|
|
41
41
|
|
|
42
|
-
**文件名说明**:发布名为 `android-libnode.zip`(带 `android-`
|
|
42
|
+
**文件名说明**:发布名为 `android-libnode.zip`(带 `android-` 前缀),为未来 iOS 等平台扩展预留接口(如 `ios-libnode.zip`);**内容仍是标准的 `libnode/` 目录**(`bin/` + `include/`);
|
|
43
43
|
|
|
44
44
|
### 2.2 支持矩阵
|
|
45
45
|
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
|
|
52
52
|
**当前 v22 下载入口**:
|
|
53
53
|
- 双架构(推荐):`android-libnode.zip`(约 88 MB)
|
|
54
|
-
- 单架构 zip
|
|
54
|
+
- 单架构 zip 已废弃,不再发布
|
|
55
55
|
|
|
56
|
-
**armeabi-v7a 为什么放弃**:V8 v22 在 x64 主机上无法交叉编译 32 位 ARM 目标(详见 5.1
|
|
56
|
+
**armeabi-v7a 为什么放弃**:V8 v22 在 x64 主机上无法交叉编译 32 位 ARM 目标(详见 5.1);
|
|
57
57
|
|
|
58
58
|
### 2.3 下载与替换
|
|
59
59
|
|
|
@@ -74,7 +74,7 @@ libnode/
|
|
|
74
74
|
# 进入插件的 android 目录
|
|
75
75
|
cd <你的项目>/node_modules/@flun/nodejs-mobile-react-native/android
|
|
76
76
|
|
|
77
|
-
# 备份官方 v18
|
|
77
|
+
# 备份官方 v18(可选,方便回退)
|
|
78
78
|
cp -r libnode libnode.bak-v18
|
|
79
79
|
|
|
80
80
|
# 整体替换
|
|
@@ -99,7 +99,7 @@ Move-Item "$env:TEMP\libnode-new\libnode" "$plugin\libnode"
|
|
|
99
99
|
|
|
100
100
|
### 2.4 调整 ABI 配置
|
|
101
101
|
|
|
102
|
-
**如果下载的 `android-libnode.zip` 只含一个架构**(比如只编了 arm64-v8a
|
|
102
|
+
**如果下载的 `android-libnode.zip` 只含一个架构**(比如只编了 arm64-v8a),在 `mobileAppConfig.js` 里同步:
|
|
103
103
|
|
|
104
104
|
```js
|
|
105
105
|
android: {
|
|
@@ -107,7 +107,7 @@ android: {
|
|
|
107
107
|
}
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
**如果双架构**(arm64-v8a + x86_64
|
|
110
|
+
**如果双架构**(arm64-v8a + x86_64,当前预编译产物即为双架构):
|
|
111
111
|
|
|
112
112
|
```js
|
|
113
113
|
android: {
|
|
@@ -132,17 +132,17 @@ npx node-mobile-app build # 打包 APK
|
|
|
132
132
|
| Patch `app/build.gradle` 的 `ndk.abiFilters` | 用 `mobileAppConfig.js` 的值 |
|
|
133
133
|
| Patch `android/gradle.properties` 的 `reactNativeArchitectures` | 同步 ABI 列表 |
|
|
134
134
|
| Patch 插件 `build.gradle` 的 `abiFilters` | 让插件只编指定架构 |
|
|
135
|
-
| 重编原生模块(自动检测 `binding.gyp`)
|
|
135
|
+
| 重编原生模块(自动检测 `binding.gyp`) | 见 2.6 |
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
**你不需要手动清任何缓存;**
|
|
138
138
|
|
|
139
139
|
### 2.6 重编原生模块(仅当项目含原生模块)
|
|
140
140
|
|
|
141
|
-
Node v18 → v22
|
|
141
|
+
Node v18 → v22 后,ABI 变了(`NODE_MODULE_VERSION` 从 108 → 127);**如果项目里装了带 C++ 代码的 npm 包**(如 `bcrypt`、`sqlite3`、`sharp`),换 v22 的 `libnode.so` 后需要重编才能兼容;
|
|
142
142
|
|
|
143
|
-
**默认自动处理**:CLI 会扫描 `nodejs-project/node_modules` 下的 `binding.gyp`
|
|
143
|
+
**默认自动处理**:CLI 会扫描 `nodejs-project/node_modules` 下的 `binding.gyp` 判断是否有原生模块,自动决定是否重编,**无需配置**;
|
|
144
144
|
|
|
145
|
-
**手动强制重编**(特殊场景:换过 `libnode.so`
|
|
145
|
+
**手动强制重编**(特殊场景:换过 `libnode.so` 但依赖没变,检测机制未触发):
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
148
|
# Linux / macOS
|
|
@@ -154,11 +154,11 @@ NODE_MOBILE_FORCE_REBUILD=1 npx node-mobile-app test
|
|
|
154
154
|
$env:NODE_MOBILE_FORCE_REBUILD=1; npx node-mobile-app test
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
**没有原生模块(纯 JS
|
|
157
|
+
**没有原生模块(纯 JS 依赖)**:无需关心,检测会自动跳过;
|
|
158
158
|
|
|
159
|
-
**切换架构(arm64 ↔ x86_64
|
|
159
|
+
**切换架构(arm64 ↔ x86_64)**:不需要重编;JS 依赖跨架构,只有少数原生模块分架构;
|
|
160
160
|
|
|
161
|
-
**Windows 限制**:Windows 上 nodejs-mobile
|
|
161
|
+
**Windows 限制**:Windows 上 nodejs-mobile 无法编译原生模块(上游限制);此功能仅 Linux / macOS 可用;
|
|
162
162
|
|
|
163
163
|
### 2.7 验证
|
|
164
164
|
|
|
@@ -202,13 +202,13 @@ Start-Sleep -Seconds 5
|
|
|
202
202
|
|
|
203
203
|
预期:`I NODEJS-MOBILE: === ICU check: \p{ID_Start} OK`
|
|
204
204
|
|
|
205
|
-
若显示 `ICU check failed` → `libnode.so` 不带 full-icu →
|
|
205
|
+
若显示 `ICU check failed` → `libnode.so` 不带 full-icu → 见第三章自己编,或回退到 Express 4;
|
|
206
206
|
|
|
207
207
|
---
|
|
208
208
|
|
|
209
209
|
## 三、自己编译 libnode.so
|
|
210
210
|
|
|
211
|
-
**环境要求**:WSL(或 Linux
|
|
211
|
+
**环境要求**:WSL(或 Linux),因为构建脚本依赖 `make` / `sed` / NDK;
|
|
212
212
|
|
|
213
213
|
### 3.1 准备源码与 NDK
|
|
214
214
|
|
|
@@ -225,7 +225,7 @@ unzip android-ndk-r27-linux.zip
|
|
|
225
225
|
|
|
226
226
|
### 3.2 关键:启用 full-icu
|
|
227
227
|
|
|
228
|
-
**默认 `--with-intl=none
|
|
228
|
+
**默认 `--with-intl=none`,必须改成 `full-icu`**,否则 Express 5 跑不了;
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
231
|
cd /mnt/d/nodejs-mobile-build/nodejs-mobile
|
|
@@ -254,7 +254,7 @@ rm -rf out config.gypi config.mk config.status
|
|
|
254
254
|
./android-configure $NDK 30 arm64
|
|
255
255
|
make -j4
|
|
256
256
|
|
|
257
|
-
#
|
|
257
|
+
# 剥离符号(必须,否则 .so 会大很多)
|
|
258
258
|
cp out/Release/libnode.so out/Release/libnode.so.bak
|
|
259
259
|
$STRIP out/Release/libnode.so
|
|
260
260
|
|
|
@@ -265,7 +265,7 @@ diff <(nm -D out/Release/libnode.so.bak | awk '{print $2, $3}' | sort) \
|
|
|
265
265
|
|
|
266
266
|
# 验证 full-icu
|
|
267
267
|
strings out/Release/libnode.so | grep -c "icudt78l"
|
|
268
|
-
# 应输出几千(约 4300
|
|
268
|
+
# 应输出几千(约 4300),否则没编进 ICU
|
|
269
269
|
```
|
|
270
270
|
|
|
271
271
|
### 3.4 多架构编译(推荐用脚本)
|
|
@@ -291,7 +291,7 @@ echo "✅ full-icu 已启用"
|
|
|
291
291
|
|
|
292
292
|
mkdir -p $TMP $OUT
|
|
293
293
|
|
|
294
|
-
#
|
|
294
|
+
# 逐架构编译,暂存 .so
|
|
295
295
|
for ARCH in arm64 x86_64; do
|
|
296
296
|
echo "=== 编译 $ARCH ==="
|
|
297
297
|
cd $SRC
|
|
@@ -330,10 +330,10 @@ chmod +x /mnt/d/nodejs-mobile-build/build-all-archs.sh
|
|
|
330
330
|
|
|
331
331
|
### 3.5 多架构编译注意事项
|
|
332
332
|
|
|
333
|
-
1. **每次切换架构必须清 `out
|
|
334
|
-
2. **切换架构会清空 `out`**:所以脚本编完一个架构立即把 `.so` 复制到 `libnode-tmp/<arch>/`
|
|
335
|
-
3. **每次切架构前确认 `--with-intl=full-icu` 还在**:`android_configure.py`
|
|
336
|
-
4. **验证两个架构都带 ICU**:`strings libnode.so | grep -c "icudt78l"` 应输出几千(约 4300
|
|
333
|
+
1. **每次切换架构必须清 `out`**:中间文件不能混用,否则奇怪的编译错误;脚本里已经 `rm -rf out`;
|
|
334
|
+
2. **切换架构会清空 `out`**:所以脚本编完一个架构立即把 `.so` 复制到 `libnode-tmp/<arch>/` 独立保存;
|
|
335
|
+
3. **每次切架构前确认 `--with-intl=full-icu` 还在**:`android_configure.py` 是源码,不会因切换架构被重置——但如果你从 Git 重新 clone 或覆盖过源码,修改会丢,务必再确认;
|
|
336
|
+
4. **验证两个架构都带 ICU**:`strings libnode.so | grep -c "icudt78l"` 应输出几千(约 4300),否则该架构没编进 ICU;
|
|
337
337
|
|
|
338
338
|
---
|
|
339
339
|
|
|
@@ -343,14 +343,14 @@ chmod +x /mnt/d/nodejs-mobile-build/build-all-archs.sh
|
|
|
343
343
|
|
|
344
344
|
**官方数据(strip 后)**:
|
|
345
345
|
|
|
346
|
-
| 版本
|
|
347
|
-
|
|
|
348
|
-
| v18
|
|
349
|
-
| v22
|
|
346
|
+
| 版本 | arm64-v8a | x86_64 |
|
|
347
|
+
| ---------------------- | ------------- | ------------- |
|
|
348
|
+
| v18(官方,无 ICU) | 59.6 MB | 62.3 MB |
|
|
349
|
+
| v22(自编译,full-icu) | **约 113 MB** | **约 118 MB** |
|
|
350
350
|
|
|
351
|
-
**zip 后**:`android-libnode.zip`(含两架构 `.so` + headers)约 88 MB
|
|
351
|
+
**zip 后**:`android-libnode.zip`(含两架构 `.so` + headers)约 88 MB;
|
|
352
352
|
|
|
353
|
-
体积差异约 50 MB
|
|
353
|
+
体积差异约 50 MB,来自三部分:Node v22 核心引擎 + full-icu 数据(约 25–30 MB)+ V8 版本升级;**不是编译出错**;
|
|
354
354
|
|
|
355
355
|
### 4.2 full-icu 与 Express 版本
|
|
356
356
|
|
|
@@ -374,7 +374,7 @@ chmod +x /mnt/d/nodejs-mobile-build/build-all-archs.sh
|
|
|
374
374
|
| `res.json(obj, 200)` | `res.status(200).json(obj)` |
|
|
375
375
|
| `res.send(200, body)` | `res.status(200).send(body)` |
|
|
376
376
|
|
|
377
|
-
**不要用 `@alloc/path-to-regexp` 替换原版**:它的 `exports` 只有 `import`
|
|
377
|
+
**不要用 `@alloc/path-to-regexp` 替换原版**:它的 `exports` 只有 `import` 条件,CJS 下 `require()` 报 `No "exports" main defined`;用原版 `path-to-regexp@8.4.2`;
|
|
378
378
|
|
|
379
379
|
---
|
|
380
380
|
|
|
@@ -382,27 +382,27 @@ chmod +x /mnt/d/nodejs-mobile-build/build-all-archs.sh
|
|
|
382
382
|
|
|
383
383
|
### 5.1 armeabi-v7a 为什么放弃
|
|
384
384
|
|
|
385
|
-
**结论**:v22+ **放弃 armeabi-v7a(32 位 ARM
|
|
385
|
+
**结论**:v22+ **放弃 armeabi-v7a(32 位 ARM)**;
|
|
386
386
|
|
|
387
387
|
**原因**:
|
|
388
388
|
|
|
389
|
-
1. **V8 官方限制**:x64 主机上交叉编译 32 位 arm
|
|
389
|
+
1. **V8 官方限制**:x64 主机上交叉编译 32 位 arm 目标时,`deps/v8/include/v8config.h` 第 914 行硬性拒绝:
|
|
390
390
|
|
|
391
391
|
```
|
|
392
392
|
#error Target architecture arm is only supported on arm and ia32 host
|
|
393
393
|
```
|
|
394
394
|
|
|
395
|
-
|
|
395
|
+
即使强行绕过,后续 Torque 工具仍报大量 8 字节对齐错误;
|
|
396
396
|
|
|
397
|
-
2. **实际意义有限**:Android 从 2019 年起要求新应用提供 64
|
|
397
|
+
2. **实际意义有限**:Android 从 2019 年起要求新应用提供 64 位版本,现代设备几乎全部 arm64-v8a;
|
|
398
398
|
|
|
399
|
-
**替代**:需 32 位 ARM
|
|
399
|
+
**替代**:需 32 位 ARM 支持,用 Node.js v18 或 v20;
|
|
400
400
|
|
|
401
401
|
### 5.2 x86_64 的说明
|
|
402
402
|
|
|
403
|
-
x86_64 在 x64
|
|
403
|
+
x86_64 在 x64 主机上**无交叉编译问题**,与 arm64 流程一致;`android-libnode.zip` 已含 x86_64 产物;
|
|
404
404
|
|
|
405
|
-
**用途**:Android Studio / 命令行的 x86_64 架构 AVD
|
|
405
|
+
**用途**:Android Studio / 命令行的 x86_64 架构 AVD(模拟器);
|
|
406
406
|
|
|
407
407
|
**用模拟器时**:
|
|
408
408
|
- `mobileAppConfig.js` 的 `abiFilters` 含 `x86_64`
|
|
@@ -411,29 +411,29 @@ x86_64 在 x64 主机上**无交叉编译问题**,与 arm64 流程一致。`an
|
|
|
411
411
|
|
|
412
412
|
### 5.3 自编译相关坑
|
|
413
413
|
|
|
414
|
-
| 问题
|
|
415
|
-
|
|
|
416
|
-
| `number 116 is not a function`
|
|
417
|
-
| `\p{...}` 正则报错
|
|
418
|
-
| `No "exports" main defined`
|
|
419
|
-
| `Cannot find module 'rn-bridge'`
|
|
420
|
-
| 启动即崩:`pthread_mutex_lock called on a destroyed mutex` + `Fatal signal 6 (SIGABRT)`;`require('rn-bridge')` 返回的对象无 `channel` 属性 | v22 loader 补丁按 `rn-bridge/index.js`
|
|
421
|
-
| JNI 编译报 `undefined symbol: v8::Exception::Error`
|
|
422
|
-
| `NODE_MODULE_VERSION` 不匹配
|
|
423
|
-
| Windows 下 `Unsupported operating system for nodejs-mobile native builds`
|
|
424
|
-
| Gradle 9 报 `Could not find method exec()`
|
|
414
|
+
| 问题 | 原因 | 解决 |
|
|
415
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
416
|
+
| `number 116 is not a function` | `libnode.so` 无 full-icu | 重新用 `--with-intl=full-icu` 编译,或回退 Express 4 |
|
|
417
|
+
| `\p{...}` 正则报错 | 同上 | 同上 |
|
|
418
|
+
| `No "exports" main defined` | 用了 `@alloc/path-to-regexp` | 撤销 `overrides`,清 `node_modules` + `package-lock.json` 重装 |
|
|
419
|
+
| `Cannot find module 'rn-bridge'` | Node 20+ 不再把链接绑定暴露给 `require()` | 需修改 Node.js `lib/internal/modules/cjs/loader.js`,从 `NODE_PATH` 按目录加载 `rn-bridge`(入口由该目录 `package.json` 的 `main` 决定) |
|
|
420
|
+
| 启动即崩:`pthread_mutex_lock called on a destroyed mutex` + `Fatal signal 6 (SIGABRT)`;`require('rn-bridge')` 返回的对象无 `channel` 属性 | v22 loader 补丁按 `rn-bridge/index.js` 硬编码定位,而插件入口因 `type: module` 已改名(如 `index.cjs`),匹配失败 → Node 回退到 `process._linkedBinding('rn_bridge')` 拿裸 binding | 使用按**目录** `rn-bridge` 定位的 loader 补丁(入口由该目录 `package.json` 的 `main` 字段解析) |
|
|
421
|
+
| JNI 编译报 `undefined symbol: v8::Exception::Error` | 插件旧头文件(v18 时代)与 v22 `libnode.so` ABI 不匹配 | **用 `android-libnode.zip` 整目录替换**,其 `include/node/` 是 v22 官方 headers |
|
|
422
|
+
| `NODE_MODULE_VERSION` 不匹配 | 原生模块未重编 | `NODE_MOBILE_FORCE_REBUILD=1` 后重跑 CLI |
|
|
423
|
+
| Windows 下 `Unsupported operating system for nodejs-mobile native builds` | 插件硬编码只支持 macOS/Linux | 官方 v22 产物已含 Windows 支持(`windows-x86_64` + `host_os=win32`) |
|
|
424
|
+
| Gradle 9 报 `Could not find method exec()` | Gradle 9 移除 `exec()` | 官方 v22 产物已修复(改用 `providers.exec`) |
|
|
425
425
|
|
|
426
426
|
---
|
|
427
427
|
|
|
428
428
|
## 六、注意事项
|
|
429
429
|
|
|
430
|
-
1. **插件本身可能不兼容**:`nodejs-mobile-react-native` 的 C++ 胶水代码为特定 Node.js
|
|
431
|
-
2. **依赖库版本**:部分 npm 包限制 Node.js
|
|
432
|
-
3. **非官方产物**:自编译的 `libnode.so`
|
|
433
|
-
4. **多架构建议**:优先保证 `arm64-v8a
|
|
434
|
-
5. **full-icu 是 Express 5 的硬前提**:不要图省事用 `--with-intl=none`
|
|
435
|
-
6. **`android-libnode.zip` 是唯一发布物**:与官方 nodejs-mobile 结构一致(`bin/` + `include
|
|
436
|
-
7. **CLI 会自动处理缓存**:替换 `libnode.so` 后跑 `test` / `build
|
|
430
|
+
1. **插件本身可能不兼容**:`nodejs-mobile-react-native` 的 C++ 胶水代码为特定 Node.js 版本编写;升级到 v22+ 时可能需自行修改插件源码并重编;
|
|
431
|
+
2. **依赖库版本**:部分 npm 包限制 Node.js 版本;检查 `package.json` 的 `engines`;
|
|
432
|
+
3. **非官方产物**:自编译的 `libnode.so` 未经充分测试,生产环境前充分测试;
|
|
433
|
+
4. **多架构建议**:优先保证 `arm64-v8a`,按需补 `x86_64`;`armeabi-v7a` v22+ 已放弃;
|
|
434
|
+
5. **full-icu 是 Express 5 的硬前提**:不要图省事用 `--with-intl=none` 编译;
|
|
435
|
+
6. **`android-libnode.zip` 是唯一发布物**:与官方 nodejs-mobile 结构一致(`bin/` + `include/`),整目录替换,无需手动改头文件;
|
|
436
|
+
7. **CLI 会自动处理缓存**:替换 `libnode.so` 后跑 `test` / `build`,指纹机制检测到变化自动清 `.cxx` / `android/build` / `app/build`,全量重编;**无需手动清任何缓存**;
|
|
437
437
|
|
|
438
438
|
---
|
|
439
439
|
|