xhs-minitool-creator 1.0.0
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/LICENSE +21 -0
- package/README.md +89 -0
- package/bin/create.mjs +321 -0
- package/package.json +39 -0
- package/src/index.mjs +9 -0
- package/src/pack.mjs +188 -0
- package/src/skill.mjs +188 -0
- package/src/validate.mjs +347 -0
- package/src/vite-preset.mjs +46 -0
- package/src/zip.mjs +190 -0
- package/template/.agents/skills/minitool-zip-builder/SKILL.md +47 -0
- package/template/.agents/skills/minitool-zip-builder/references/cross-platform-h5.md +69 -0
- package/template/.agents/skills/minitool-zip-builder/references/css-compatibility.md +171 -0
- package/template/.agents/skills/minitool-zip-builder/references/device-capabilities.md +169 -0
- package/template/.agents/skills/minitool-zip-builder/references/js-compatibility.md +61 -0
- package/template/.agents/skills/minitool-zip-builder/references/jsbridge-api.md +192 -0
- package/template/.agents/skills/minitool-zip-builder/references/performance-budget.md +131 -0
- package/template/.agents/skills/minitool-zip-builder/references/zip-artifact-spec.md +206 -0
- package/template/.agents/skills/minitool-zip-builder/scripts/audit_artifact.mjs +95 -0
- package/template/.agents/skills/minitool-zip-builder/scripts/audit_artifact.py +109 -0
- package/template/.agents/skills/minitool-zip-builder/skill-package.json +5 -0
- package/template/_gitignore +7 -0
- package/template/index.html +47 -0
- package/template/package.json +22 -0
- package/template/public/icons/icon-192.svg +4 -0
- package/template/public/icons/icon-512.svg +4 -0
- package/template/src/lib/storage.js +54 -0
- package/template/src/main.js +81 -0
- package/template/src/styles/app.css +229 -0
- package/template/vite.config.js +9 -0
package/src/zip.mjs
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 纯 Node ZIP 写入器(zero dependency, cross-platform)
|
|
3
|
+
*
|
|
4
|
+
* 为什么自己实现:原脚手架依赖系统的 `zip` / `zipinfo` 二进制,
|
|
5
|
+
* 在 Windows 或无 zip 工具的环境直接不可用。这里用 zlib 实现最小可用的
|
|
6
|
+
* ZIP(local file header + central directory + EOCD),只做 deflate 与 store
|
|
7
|
+
* 两种压缩方式,满足小工具交付包需求。
|
|
8
|
+
*/
|
|
9
|
+
import { deflateRawSync, crc32 } from 'node:zlib';
|
|
10
|
+
|
|
11
|
+
const LOCAL_SIG = 0x04034b50;
|
|
12
|
+
const CENTRAL_SIG = 0x02014b50;
|
|
13
|
+
const EOCD_SIG = 0x06054b50;
|
|
14
|
+
|
|
15
|
+
const STORE = 0;
|
|
16
|
+
const DEFLATE = 8;
|
|
17
|
+
|
|
18
|
+
/** create_system:3 = Unix,见 createZip 中关于文件名编码的说明 */
|
|
19
|
+
const UNIX_SYSTEM = 3;
|
|
20
|
+
|
|
21
|
+
/** 体积小于该阈值时不压缩(deflate 反而变大) */
|
|
22
|
+
const DEFLATE_MIN_SIZE = 64;
|
|
23
|
+
|
|
24
|
+
/** 已本身就是压缩格式,deflate 收益极低,直接 store */
|
|
25
|
+
const NO_COMPRESS_EXT = new Set([
|
|
26
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.zip',
|
|
27
|
+
'.gz', '.br', '.woff', '.woff2', '.mp3', '.mp4', '.webm',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function toDosDateTime(date) {
|
|
31
|
+
const year = Math.max(date.getFullYear(), 1980);
|
|
32
|
+
const time =
|
|
33
|
+
((date.getHours() & 0x1f) << 11) |
|
|
34
|
+
((date.getMinutes() & 0x3f) << 5) |
|
|
35
|
+
((date.getSeconds() / 2) & 0x1f);
|
|
36
|
+
const day =
|
|
37
|
+
(((year - 1980) & 0x7f) << 9) |
|
|
38
|
+
(((date.getMonth() + 1) & 0x0f) << 5) |
|
|
39
|
+
(date.getDate() & 0x1f);
|
|
40
|
+
return { time, day };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function crc32Of(buf) {
|
|
44
|
+
// Node 22+ 内置 zlib.crc32;低版本回退到查表实现
|
|
45
|
+
if (typeof crc32 === 'function') return crc32(buf) >>> 0;
|
|
46
|
+
return crc32Table(buf);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let TABLE = null;
|
|
50
|
+
function crc32Table(buf) {
|
|
51
|
+
if (!TABLE) {
|
|
52
|
+
TABLE = new Int32Array(256);
|
|
53
|
+
for (let n = 0; n < 256; n += 1) {
|
|
54
|
+
let c = n;
|
|
55
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
56
|
+
TABLE[n] = c;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let crc = -1;
|
|
60
|
+
for (let i = 0; i < buf.length; i += 1) {
|
|
61
|
+
crc = (crc >>> 8) ^ TABLE[(crc ^ buf[i]) & 0xff];
|
|
62
|
+
}
|
|
63
|
+
return (crc ^ -1) >>> 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 创建 zip 二进制内容
|
|
68
|
+
* @param {{ name: string, data: Buffer, date?: Date }[]} entries
|
|
69
|
+
* @returns {Buffer}
|
|
70
|
+
*/
|
|
71
|
+
export function createZip(entries) {
|
|
72
|
+
const chunks = [];
|
|
73
|
+
const central = [];
|
|
74
|
+
let offset = 0;
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
// 统一为 POSIX 路径分隔符
|
|
78
|
+
const name = entry.name.split('\\').join('/');
|
|
79
|
+
const nameBuf = Buffer.from(name, 'utf8');
|
|
80
|
+
const raw = entry.data;
|
|
81
|
+
const date = entry.date || new Date();
|
|
82
|
+
const { time, day } = toDosDateTime(date);
|
|
83
|
+
const crc = crc32Of(raw);
|
|
84
|
+
|
|
85
|
+
const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
|
|
86
|
+
|
|
87
|
+
// 文件名含非 ASCII(如中文)时必须置 UTF-8 标志位(bit 11),
|
|
88
|
+
// 否则解压方按本地编码解析会得到乱码甚至创建失败。
|
|
89
|
+
const isUtf8Name = /[^\x00-\x7F]/.test(name);
|
|
90
|
+
const flags = isUtf8Name ? 0x0800 : 0;
|
|
91
|
+
|
|
92
|
+
const shouldDeflate =
|
|
93
|
+
raw.length >= DEFLATE_MIN_SIZE && !NO_COMPRESS_EXT.has(ext);
|
|
94
|
+
|
|
95
|
+
let method;
|
|
96
|
+
let payload;
|
|
97
|
+
if (shouldDeflate) {
|
|
98
|
+
const compressed = deflateRawSync(raw, { level: 9 });
|
|
99
|
+
if (compressed.length < raw.length) {
|
|
100
|
+
method = DEFLATE;
|
|
101
|
+
payload = compressed;
|
|
102
|
+
} else {
|
|
103
|
+
method = STORE;
|
|
104
|
+
payload = raw;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
method = STORE;
|
|
108
|
+
payload = raw;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const local = Buffer.alloc(30);
|
|
112
|
+
local.writeUInt32LE(LOCAL_SIG, 0);
|
|
113
|
+
local.writeUInt16LE(20, 4); // version needed
|
|
114
|
+
local.writeUInt16LE(flags, 6);
|
|
115
|
+
local.writeUInt16LE(method, 8);
|
|
116
|
+
local.writeUInt16LE(time, 10);
|
|
117
|
+
local.writeUInt16LE(day, 12);
|
|
118
|
+
local.writeUInt32LE(crc, 14);
|
|
119
|
+
local.writeUInt32LE(payload.length, 18);
|
|
120
|
+
local.writeUInt32LE(raw.length, 22);
|
|
121
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
122
|
+
local.writeUInt16LE(0, 28); // extra len
|
|
123
|
+
|
|
124
|
+
chunks.push(local, nameBuf, payload);
|
|
125
|
+
|
|
126
|
+
const cd = Buffer.alloc(46);
|
|
127
|
+
cd.writeUInt32LE(CENTRAL_SIG, 0);
|
|
128
|
+
// version made by 高字节为 create_system:3 = Unix。
|
|
129
|
+
// 必须写成 Unix 而非 0(MS-DOS/FAT),否则 BSD/macOS unzip 会按
|
|
130
|
+
// 本地代码页解析文件名,即使 UTF-8 标志位已置也会乱码或创建失败。
|
|
131
|
+
cd.writeUInt16LE((UNIX_SYSTEM << 8) | 20, 4);
|
|
132
|
+
cd.writeUInt16LE(20, 6); // version needed
|
|
133
|
+
cd.writeUInt16LE(flags, 8);
|
|
134
|
+
cd.writeUInt16LE(method, 10);
|
|
135
|
+
cd.writeUInt16LE(time, 12);
|
|
136
|
+
cd.writeUInt16LE(day, 14);
|
|
137
|
+
cd.writeUInt32LE(crc, 16);
|
|
138
|
+
cd.writeUInt32LE(payload.length, 20);
|
|
139
|
+
cd.writeUInt32LE(raw.length, 24);
|
|
140
|
+
cd.writeUInt16LE(nameBuf.length, 28);
|
|
141
|
+
cd.writeUInt16LE(0, 30); // extra len
|
|
142
|
+
cd.writeUInt16LE(0, 32); // comment len
|
|
143
|
+
cd.writeUInt16LE(0, 34); // disk number
|
|
144
|
+
cd.writeUInt16LE(0, 36); // internal attrs
|
|
145
|
+
// 外部属性:Unix 权限位(0o100644 << 16),缺少时部分解压工具行为异常。
|
|
146
|
+
// 注意 JS 位运算产生有符号 32 位,需 >>> 0 转回无符号。
|
|
147
|
+
cd.writeUInt32LE(((0o100644 << 16) >>> 0), 38);
|
|
148
|
+
cd.writeUInt32LE(offset, 42);
|
|
149
|
+
|
|
150
|
+
central.push(Buffer.concat([cd, nameBuf]));
|
|
151
|
+
offset += local.length + nameBuf.length + payload.length;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const centralBuf = Buffer.concat(central);
|
|
155
|
+
|
|
156
|
+
const eocd = Buffer.alloc(22);
|
|
157
|
+
eocd.writeUInt32LE(EOCD_SIG, 0);
|
|
158
|
+
eocd.writeUInt16LE(0, 4); // disk number
|
|
159
|
+
eocd.writeUInt16LE(0, 6); // disk with central dir
|
|
160
|
+
eocd.writeUInt16LE(entries.length, 8);
|
|
161
|
+
eocd.writeUInt16LE(entries.length, 10);
|
|
162
|
+
eocd.writeUInt32LE(centralBuf.length, 12);
|
|
163
|
+
eocd.writeUInt32LE(offset, 16);
|
|
164
|
+
eocd.writeUInt16LE(0, 20); // comment len
|
|
165
|
+
|
|
166
|
+
return Buffer.concat([...chunks, centralBuf, eocd]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 读取 zip 内的文件名清单(用于自检)
|
|
171
|
+
* @param {Buffer} zipBuf
|
|
172
|
+
* @returns {string[]}
|
|
173
|
+
*/
|
|
174
|
+
export function listZipEntries(zipBuf) {
|
|
175
|
+
const names = [];
|
|
176
|
+
let pos = 0;
|
|
177
|
+
// 扫描 local file header
|
|
178
|
+
while (pos + 30 <= zipBuf.length) {
|
|
179
|
+
const sig = zipBuf.readUInt32LE(pos);
|
|
180
|
+
if (sig !== LOCAL_SIG) break;
|
|
181
|
+
const method = zipBuf.readUInt16LE(pos + 8);
|
|
182
|
+
const compSize = zipBuf.readUInt32LE(pos + 18);
|
|
183
|
+
const nameLen = zipBuf.readUInt16LE(pos + 26);
|
|
184
|
+
const extraLen = zipBuf.readUInt16LE(pos + 28);
|
|
185
|
+
const name = zipBuf.slice(pos + 30, pos + 30 + nameLen).toString('utf8');
|
|
186
|
+
names.push({ name, method, size: compSize });
|
|
187
|
+
pos += 30 + nameLen + extraLen + compSize;
|
|
188
|
+
}
|
|
189
|
+
return names;
|
|
190
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: minitool-zip-builder
|
|
3
|
+
description: >-
|
|
4
|
+
小红书小工具构建开发指南:把 H5 页面打包成符合容器规范的离线 zip。
|
|
5
|
+
新建或改写小工具 / H5 页面、打包小工具 zip、处理端能力限制与容器 CSP 约束时使用。
|
|
6
|
+
metadata:
|
|
7
|
+
version: "1.6.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# 小工具 ZIP 构建指南
|
|
11
|
+
|
|
12
|
+
**小工具是一种基于离线 H5 实现的 app 形式**:你写一套标准网页(以 `index.html` 为入口),打包成 `.zip`,由容器(PC 模拟器 / 真机 WebView)加载运行。它本质就是 Web,HTML/CSS/JS 经验直接适用——只是运行在受控容器里:**纯本地、不联网,所有资源须打包在内**,且部分 Web 能力被收紧。
|
|
13
|
+
|
|
14
|
+
目标产物:可直接上传的 **`.zip` 静态包**,在 PC 模拟器与真机行为一致。
|
|
15
|
+
|
|
16
|
+
## 何时使用
|
|
17
|
+
|
|
18
|
+
- 从零新建小工具页面并打包成 `.zip`
|
|
19
|
+
- 将已有纯 H5 页面改写为小工具规范并打包
|
|
20
|
+
|
|
21
|
+
## 工作流程
|
|
22
|
+
|
|
23
|
+
每一步**动手前必须先读对应 reference 并严格遵守其全部约束**,不要凭记忆产出:
|
|
24
|
+
|
|
25
|
+
1. **编写 / 适配 HTML** — 先读 [zip-artifact-spec.md](references/zip-artifact-spec.md):目录结构、`index.html` 模板、路径与资源引用规则,按其编写
|
|
26
|
+
2. **端能力合规** — 先读 [device-capabilities.md](references/device-capabilities.md):对照「不可用能力 / 行为」逐项核对,命中项移除或改用其给出的替代写法
|
|
27
|
+
3. **Native 能力(JSBridge)** — 需要发笔记、存相册、跳原生页等能力时,先读 [jsbridge-api.md](references/jsbridge-api.md):仅使用文档列出的 `window.xhs.miniTool.*` API,参数与必填项以该文档为准,未列出的字段不要传
|
|
28
|
+
4. **JS 兼容性** — 先读 [js-compatibility.md](references/js-compatibility.md):以 Android 8.1 出场 Chrome / WebView 61 为最低基线;直接交付的 JS 可使用 ES2017,更新语法须由已有构建链转译,Web API 须做能力检测
|
|
29
|
+
5. **CSS 兼容性** — 先读 [css-compatibility.md](references/css-compatibility.md):采用“Chrome 61 基线层 + 能力检测增强层”;只为实际使用的新能力提供局部回退,不维护两套完整 CSS
|
|
30
|
+
6. **跨端适配** — 先读 [cross-platform-h5.md](references/cross-platform-h5.md):触摸、滚动、安全区、PC vs 真机差异
|
|
31
|
+
7. **性能设计** — 先读 [performance-budget.md](references/performance-budget.md):控制源码 / 静态数据 / Base64 / 媒体体积;使用 WebGL 时必须控制 GPU 资源,并提供运行时降级
|
|
32
|
+
8. **正确性自查** — 静态核对页面能正常运行、无违规能力(被禁 API 无调用 / 残留、脚本加载顺序、引用资源都在 zip 内、改写时未误改业务逻辑),见 [zip-artifact-spec.md](references/zip-artifact-spec.md) 自检清单
|
|
33
|
+
9. **审计并打包** — 按 [performance-budget.md](references/performance-budget.md) 的环境分支选择 Node、Python 或人工审计;修复全部错误,逐条核对各 reference 末尾的自检清单后再交付。审计脚本是辅助工具,不得因运行时缺失跳过门禁
|
|
34
|
+
|
|
35
|
+
> **产出前提**:交付的 zip 必须同时满足 `zip-artifact-spec.md`、`device-capabilities.md`、`js-compatibility.md`、`css-compatibility.md`、`performance-budget.md` 与(若使用 JSBridge)`jsbridge-api.md` 的全部约束。任何约束以 reference 为准。
|
|
36
|
+
|
|
37
|
+
## Reference
|
|
38
|
+
|
|
39
|
+
| 文档 | 何时读 |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| [zip-artifact-spec.md](references/zip-artifact-spec.md) | 写 HTML / 打包时:目录结构、`index.html` 模板、路径与资源引用规则、打包自检 |
|
|
42
|
+
| [device-capabilities.md](references/device-capabilities.md) | 处理端能力时:哪些 Web 能力可用 / 不可用及替代写法、如何实现常见交互(手势、拍照、选图等) |
|
|
43
|
+
| [jsbridge-api.md](references/jsbridge-api.md) | 调用 Native 能力时:`window.xhs.miniTool.*` 全量 API、参数约束、示例与常见组合 |
|
|
44
|
+
| [js-compatibility.md](references/js-compatibility.md) | 写 JS / 选择构建产物时:Android 8.1 出场 Chrome / WebView 61 最低基线、Web API 检测与局部降级 |
|
|
45
|
+
| [css-compatibility.md](references/css-compatibility.md) | 写 CSS / 选择构建产物时:Chrome 61 基线、能力检测、现代 CSS 增强与局部回退 |
|
|
46
|
+
| [cross-platform-h5.md](references/cross-platform-h5.md) | 适配多端时:触摸、滚动、安全区、PC 模拟器与真机差异 |
|
|
47
|
+
| [performance-budget.md](references/performance-budget.md) | 开发和交付前:包体、静态数据、Base64、媒体、长列表与 WebGL 资源控制和降级 |
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 跨端 H5 适配
|
|
2
|
+
|
|
3
|
+
> 小工具同一份 H5 同时跑在 PC 模拟器与真机 WebView。以下是保证两端一致体验的适配要点。
|
|
4
|
+
|
|
5
|
+
CSS 的最低语法与布局能力以 [css-compatibility.md](./css-compatibility.md) 为准;本文件只说明触摸、滚动、安全区和设备形态差异。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 触摸
|
|
10
|
+
|
|
11
|
+
```css
|
|
12
|
+
body { -webkit-touch-callout: none; }
|
|
13
|
+
.touchable:active { opacity: 0.7; }
|
|
14
|
+
html { touch-action: manipulation; }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
交互优先用 Pointer Events(`pointerdown/move/up`)统一处理鼠标与触摸;纯触摸场景用 `touchstart/touchmove/touchend`。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. 滚动
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
.scroll-container {
|
|
25
|
+
overflow-y: auto;
|
|
26
|
+
-webkit-overflow-scrolling: touch;
|
|
27
|
+
overscroll-behavior-y: contain;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
纵向回弹由容器控制,HTML 无需额外配置。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 3. 安全区
|
|
36
|
+
|
|
37
|
+
```css
|
|
38
|
+
.custom-nav { padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px)); }
|
|
39
|
+
.bottom-bar { padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)); }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
需配合 `<meta name="viewport" ... viewport-fit=cover>`。PC 模拟器不产生真实 `env()`,而是注入 `--safe-area-inset-*` 变量模拟安全区;真机 `env()` 为真实值。用 `var(--safe-area-inset-*, env(...))` 组合,两端都生效。
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 4. 布局与媒体
|
|
47
|
+
|
|
48
|
+
- 页面级容器用 `%` / `flex` / `vw`,勿写死 `width: 375px`
|
|
49
|
+
- 图片 `max-width: 100%`
|
|
50
|
+
- 用系统字体栈,避免非必要 `.woff2`
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 5. PC 模拟器 vs 真机
|
|
55
|
+
|
|
56
|
+
| 特性 | PC 模拟器 | 真机 | 建议 |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| 触摸 | 鼠标 → touch 模拟 | 原生 touch | 用 pointer events 统一 |
|
|
59
|
+
| 安全区 | 注入 `--safe-area-inset-*` 变量模拟 | `env()` 真实值 | 用 `var(--safe-area-inset-*, env(safe-area-inset-*, 0px))` 组合 |
|
|
60
|
+
| 软键盘 | 无 | 遮挡输入框 | 监听 `visualViewport` 处理 |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 6. 自检
|
|
65
|
+
|
|
66
|
+
- [ ] 交互用 pointer / touch events,未依赖鼠标 hover 才能触发的关键操作
|
|
67
|
+
- [ ] 布局自适应,无写死像素宽度
|
|
68
|
+
- [ ] 安全区用 `var(--safe-area-inset-*, env(safe-area-inset-*, 0px))` 组合,配合 `viewport-fit=cover`
|
|
69
|
+
- [ ] 图片自适应且体积受控
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# CSS 兼容性规范
|
|
2
|
+
|
|
3
|
+
> 小工具 CSS 的最低兼容基线是 **Android 8.1 出场 Chrome / WebView 61**。这不等于只能写旧 CSS:交付物采用“Chrome 61 可用的基线层 + 能力检测后的增强层”,在新内核上使用更合适的 CSS 能力。
|
|
4
|
+
|
|
5
|
+
## 1. 基线原则
|
|
6
|
+
|
|
7
|
+
- 基线层保证布局、文字、按钮和核心交互在 Chrome 61 可用;增强层可以使用新 CSS 改善布局、视觉或交互。
|
|
8
|
+
- 按功能点提供回退与增强,不维护两套完整页面或两份完整样式表,避免双份样式逐渐漂移。
|
|
9
|
+
- 根据特性选择检测方法:能用声明级回退的按层叠覆盖,能准确查询的使用 `@supports`,语法检测无法证明实际布局行为时使用 JS 做最小行为检测。
|
|
10
|
+
- 浏览器会静默丢弃无法解析的选择器、声明或整条 at-rule。不能像 JS 一样靠异常发现 CSS 不兼容,必须检查最终产物和实际布局。
|
|
11
|
+
- 兼容性以最终 zip 内的 CSS 为准。源码使用了构建工具,不代表产物已经兼容。
|
|
12
|
+
|
|
13
|
+
## 2. Chrome 61 不可作为唯一实现的能力
|
|
14
|
+
|
|
15
|
+
以下能力晚于 Chrome 61,不能作为唯一实现。先提供基线路径,再用适合该能力的检测方式启用增强。
|
|
16
|
+
|
|
17
|
+
| 能力 | 基线写法 / 降级方式 |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Flexbox `gap` / `row-gap` / `column-gap` | 基线用子项单边 `margin`;增强用 JS 实际测量 Flex 布局后切换 class,不能只检测 `gap` 属性语法 |
|
|
20
|
+
| `aspect-ratio` | 固定媒体尺寸,或用百分比 `padding-top` 比例盒;内容绝对定位 |
|
|
21
|
+
| `min()` / `max()` / `clamp()` | 先写固定值、百分比或 `calc()`,再用媒体查询分档 |
|
|
22
|
+
| `inset`、`margin-inline`、`padding-block` 等逻辑属性 / 简写 | 使用 `top/right/bottom/left` 与 `margin-left/right`、`padding-top/bottom` 等物理属性 |
|
|
23
|
+
| `overflow: clip` | 使用 `overflow: hidden` |
|
|
24
|
+
| `:focus-visible` | 先提供 `:focus` 样式;增强规则不能让键盘焦点在基线端消失 |
|
|
25
|
+
| `:has()` | 由 JS 在父元素上切换状态 class |
|
|
26
|
+
| Container Queries(`@container`) | 使用 viewport 媒体查询,或由 JS 按容器尺寸切换 class |
|
|
27
|
+
| Subgrid | 使用普通 Grid、Flex 或显式轨道尺寸 |
|
|
28
|
+
| CSS Nesting、Cascade Layers(`@layer`)、`@property` | 已有构建链能可靠展开时才在源码使用;最终产物不得保留为核心规则 |
|
|
29
|
+
| `dvh` / `svh` / `lvh` | 先用 `%` / `100vh`;受软键盘影响的全屏高度用 JS 维护 CSS 变量并保留静态兜底 |
|
|
30
|
+
| `color-mix()`、`oklab()`、`oklch()` 等现代颜色 | 先写 `#hex`、`rgb()`、`rgba()` 或 `hsl()` 颜色 |
|
|
31
|
+
| `backdrop-filter` | 先给不依赖模糊的实色 / 半透明背景;模糊仅作增强,并同时考虑 `-webkit-backdrop-filter` |
|
|
32
|
+
| `text-wrap: balance` 等现代排版属性 | 保留普通换行;不要让其决定关键区域高度 |
|
|
33
|
+
|
|
34
|
+
## 3. 布局与前缀
|
|
35
|
+
|
|
36
|
+
Chrome 61 可使用 Flexbox、基础 Grid、媒体查询、CSS Variables、`calc()`、transform、transition 和 animation。仍须处理以下跨端差异:
|
|
37
|
+
|
|
38
|
+
- Flex 子项内有长文本、图片或滚动区域时,按方向显式设置 `min-width: 0` 或 `min-height: 0`,避免内容撑破容器。
|
|
39
|
+
- Flex 间距先用子项 `margin` 形成基线;需要时通过行为检测启用 `gap` 并清除 margin。动态增删子项时确认首尾间距仍正确。
|
|
40
|
+
- Grid 只使用基础显式 / 隐式轨道;间距使用 Chrome 61 可解析的 `grid-gap`。不要使用 Subgrid、Masonry 或依赖新语法的自动布局。
|
|
41
|
+
- 需要隐藏文本时使用 `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`。多行截断使用 `display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: <行数>; overflow: hidden`,并保证截断失效时页面仍可用。
|
|
42
|
+
- `user-select`、`appearance`、文字截断和毛玻璃等 WebKit 相关能力按需同时写 `-webkit-` 前缀与标准声明。不要机械给所有属性加前缀。
|
|
43
|
+
- 关键操作不能只在 `:hover` 出现;触摸端默认可见,并可用 `@media (hover: hover)` 增加鼠标悬停效果。
|
|
44
|
+
|
|
45
|
+
## 4. 基线层与增强层
|
|
46
|
+
|
|
47
|
+
### 声明回退和 `@supports`
|
|
48
|
+
|
|
49
|
+
先写 Chrome 61 可用的完整基线,再覆盖增强:
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
.panel {
|
|
53
|
+
background: rgba(255, 255, 255, 0.96);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
|
|
57
|
+
.panel {
|
|
58
|
+
background: rgba(255, 255, 255, 0.72);
|
|
59
|
+
-webkit-backdrop-filter: blur(12px);
|
|
60
|
+
backdrop-filter: blur(12px);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
同一属性使用“旧值在前、新值在后”的回退顺序:
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
.page {
|
|
69
|
+
min-height: 100vh;
|
|
70
|
+
min-height: var(--app-height, 100vh);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
安全区须先保留普通值,再使用容器注入变量与 `env()`:
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
.bottom-bar {
|
|
78
|
+
padding-bottom: 0;
|
|
79
|
+
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`@supports` 适合检测 `aspect-ratio`、`backdrop-filter`、动态视口单位等能由单个声明准确表达的能力。需要由 JS 切换 class 时,可以使用 `CSS.supports()` 检测同类声明。任何检测路径都必须保留基础样式。
|
|
84
|
+
|
|
85
|
+
### Flex gap 必须检测布局行为
|
|
86
|
+
|
|
87
|
+
`CSS.supports('gap', '1px')` 和 `@supports (gap: 1px)` 只能证明浏览器认识该属性和值,不能证明 `gap` 在 Flexbox 中生效;支持 Grid gap、但不支持 Flex gap 的内核也可能通过语法检测。需要启用 Flex gap 时,实际创建一次 Flex 容器并测量:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
function supportsFlexGap() {
|
|
91
|
+
var flex = document.createElement('div');
|
|
92
|
+
flex.style.position = 'absolute';
|
|
93
|
+
flex.style.visibility = 'hidden';
|
|
94
|
+
flex.style.display = 'flex';
|
|
95
|
+
flex.style.flexDirection = 'column';
|
|
96
|
+
flex.style.rowGap = '1px';
|
|
97
|
+
flex.appendChild(document.createElement('div'));
|
|
98
|
+
flex.appendChild(document.createElement('div'));
|
|
99
|
+
document.body.appendChild(flex);
|
|
100
|
+
var supported = flex.scrollHeight === 1;
|
|
101
|
+
flex.parentNode.removeChild(flex);
|
|
102
|
+
return supported;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (supportsFlexGap()) {
|
|
106
|
+
document.documentElement.classList.add('supports-flex-gap');
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
对应 CSS 只维护一套组件规则,其中 margin 是基线,gap 是增强:
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
.actions {
|
|
114
|
+
display: flex;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.actions > * + * {
|
|
118
|
+
margin-left: 12px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.supports-flex-gap .actions {
|
|
122
|
+
column-gap: 12px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.supports-flex-gap .actions > * + * {
|
|
126
|
+
margin-left: 0;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
行为检测放在包内经典脚本中,并在 `document.body` 存在后、核心页面渲染前执行。检测一次即可,不要在 resize 或渲染循环中反复测量。
|
|
131
|
+
|
|
132
|
+
不要用 UA、Android 版本或机型字符串决定样式能力。能力检测决定是否启用增强,Chrome 61 基线层始终保留。
|
|
133
|
+
|
|
134
|
+
## 5. 有构建链与无构建链
|
|
135
|
+
|
|
136
|
+
### 直接交付静态文件
|
|
137
|
+
|
|
138
|
+
没有现成构建链时,不为 CSS 兼容性临时引入 PostCSS、Autoprefixer 或新的 npm 依赖;直接按 Chrome 61 基线编写。
|
|
139
|
+
|
|
140
|
+
### 项目已有构建链
|
|
141
|
+
|
|
142
|
+
将目标浏览器至少设置为:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
Chrome >= 61
|
|
146
|
+
ios_saf >= 18.4
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- 可使用已有的 Autoprefixer 补必要前缀;若源码使用 nesting 等新语法,须确认已有插件会把它转换成普通 CSS。
|
|
150
|
+
- Autoprefixer 只补前缀,不会把 Flex `gap`、`aspect-ratio`、`:has()`、Container Queries 或动态视口单位自动改写成等价的旧布局。
|
|
151
|
+
- 压缩器也须使用相同浏览器目标,避免把兼容写法重新合并成 Chrome 61 无法解析的现代语法。
|
|
152
|
+
- 交付前检查构建后的 CSS;zip 中只保留最终静态产物,不带 source map 和构建配置。
|
|
153
|
+
|
|
154
|
+
## 6. 视口、安全区与实测
|
|
155
|
+
|
|
156
|
+
- 页面宽度使用 `%`、Flex 或基础 Grid,不写死 `375px` 等单一机型宽度。
|
|
157
|
+
- `100vh` 在移动端地址栏、容器高度变化和软键盘出现时可能不等于可视高度。关键全屏区域优先使用父容器百分比;必须跟随可视高度时,由 JS 监听尺寸变化并维护 `--app-height`,同时保留 `100vh` 回退。
|
|
158
|
+
- 安全区规则须配合 `viewport-fit=cover`,具体组合见 [cross-platform-h5.md](./cross-platform-h5.md)。
|
|
159
|
+
- 现代桌面浏览器或 PC 模拟器通过不等于 Chrome 61 通过。能够运行旧内核时,至少检查首屏、滚动区、弹层、表单、横竖屏 / 尺寸变化和核心交互;无法运行时在交付说明中标记“Chrome 61 CSS 兼容性未实测”。
|
|
160
|
+
|
|
161
|
+
## 7. 交付检查
|
|
162
|
+
|
|
163
|
+
- [ ] Chrome 61 基线层能够独立完成核心布局和交互,新内核通过能力检测启用增强层
|
|
164
|
+
- [ ] 未维护两套完整 CSS;只为实际使用的现代能力提供局部回退与增强
|
|
165
|
+
- [ ] Flex gap 使用布局行为检测,或仅使用 margin 基线;未把 `@supports (gap: ...)` / `CSS.supports('gap', ...)` 当作 Flex gap 检测
|
|
166
|
+
- [ ] `aspect-ratio`、`clamp()`、逻辑属性、`:has()`、Container Queries、Subgrid、CSS Nesting、动态视口单位等实际使用项均有合适的回退与检测 / 构建转换
|
|
167
|
+
- [ ] Flex 子项已检查溢出与滚动,必要处有 `min-width: 0` / `min-height: 0`
|
|
168
|
+
- [ ] 关键操作在触摸端可见,不依赖 `:hover`;焦点样式不只依赖 `:focus-visible`
|
|
169
|
+
- [ ] 安全区、全屏高度、软键盘、长文本和多行截断均有基线回退
|
|
170
|
+
- [ ] 使用构建链时已检查最终 CSS,而不是只检查源码或 Autoprefixer 是否运行
|
|
171
|
+
- [ ] 未进行 Chrome 61 / Android 8.1 实际验证时明确标记“CSS 兼容性未实测”
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# 小工具能力清单
|
|
2
|
+
|
|
3
|
+
> 小工具运行在受限容器中:**纯本地、不联网**,把它当作一个能力受限的浏览器页面。
|
|
4
|
+
> **以本文为基线**:命中「不可用」项必须移除或改用替代写法。
|
|
5
|
+
|
|
6
|
+
## 目录
|
|
7
|
+
|
|
8
|
+
- §1 可用能力
|
|
9
|
+
- §2 Native 能力(JSBridge)
|
|
10
|
+
- §3 不可用能力(Web API)
|
|
11
|
+
- §4 不可用行为
|
|
12
|
+
- §5 WebGL / 图形计算边界
|
|
13
|
+
- §6 常见交互怎么实现
|
|
14
|
+
- §7 能力扫描清单
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. 可用能力
|
|
19
|
+
|
|
20
|
+
### 页面与渲染
|
|
21
|
+
|
|
22
|
+
标准 HTML / CSS / JS 可用,但最终产物须满足目标内核基线:JS 见 [js-compatibility.md](./js-compatibility.md),CSS 见 [css-compatibility.md](./css-compatibility.md)。可使用基线内的 Flexbox / Grid / 动画 / 媒体查询、Canvas 2D(`getContext('2d')`)、WebGL(`getContext('webgl'/'webgl2')`,能力边界见 §5、性能与低端机降级见 [performance-budget.md](./performance-budget.md) §4–5),文本选择不限制。
|
|
23
|
+
|
|
24
|
+
### 媒体与文件
|
|
25
|
+
|
|
26
|
+
| 能力 | 用法 | 约束 |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| 摄像头 | `navigator.mediaDevices.getUserMedia({ video: true })` | 用户手势触发 + 系统弹窗授权 |
|
|
29
|
+
| 麦克风 | `navigator.mediaDevices.getUserMedia({ audio: true })` | 用户手势触发 + 系统弹窗授权 |
|
|
30
|
+
| 选择图片 / 拍照 | `<input type="file">` | 系统选择器接管,**仅能选图片和视频**(无论 `accept` 如何设置) |
|
|
31
|
+
| 音视频播放 | `<video>` / `<audio>` | 内联播放,媒体文件须打包在内 |
|
|
32
|
+
|
|
33
|
+
### 数据存储
|
|
34
|
+
|
|
35
|
+
`localStorage` / `sessionStorage` / `IndexedDB` / `Cookie` / `Cache API` 均可用,按小工具独立隔离,其他小工具与外部无法访问。
|
|
36
|
+
|
|
37
|
+
Cookie 仅作本地存储:可读写、按 origin 隔离,但因不联网**不会随请求发往服务端**,不能用于登录态 / 鉴权。需要本地存储优先用 `localStorage` / `IndexedDB`。数据不保证永久持久化。
|
|
38
|
+
|
|
39
|
+
### 交互
|
|
40
|
+
|
|
41
|
+
`alert()` / `confirm()` 可用,以原生 UI 展示。
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Native 能力(JSBridge)
|
|
46
|
+
|
|
47
|
+
容器会注入 **`window.xhs.miniTool.*`**,通过它调用 Native 能力(发笔记、存相册、跳 App 页等)。
|
|
48
|
+
|
|
49
|
+
| 规则 | 说明 |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| 唯一入口 | 只用 `window.xhs.miniTool.<apiName>(options)`,**禁止**自行 `postMessage` 到 bridge |
|
|
52
|
+
| 契约来源 | 以 [`jsbridge-api.md`](./jsbridge-api.md) 为准 |
|
|
53
|
+
| 参数校验 | 必填项、长度、数组上限等以 [`jsbridge-api.md`](./jsbridge-api.md) 为准 |
|
|
54
|
+
| 本地路径 | `saveImageToPhotosAlbum.filePath` 不支持网络 URL;base64 可先 `writeTempFile` 换 `filePath` |
|
|
55
|
+
| data:uri | `writeTempFile.data` 必须是完整 `data:<mime>;base64,...`(`canvas.toDataURL()` 原样传),裸 base64 会失败 |
|
|
56
|
+
| 发笔记 | `postNote.mediaInfo` 必填;图片走 `image_resources[].url`,视频走 `video_resources` |
|
|
57
|
+
| 跳原生页 | `openRedPage.type` 须命中 Native 规则表白名单,`params` 为语义参数 |
|
|
58
|
+
|
|
59
|
+
完整 API 列表、字段表与示例 → **[jsbridge-api.md](./jsbridge-api.md)**。
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. 不可用能力(Web API)
|
|
64
|
+
|
|
65
|
+
以下 API 已禁用,调用会抛异常、返回空值或被拦截,必须移除或改用替代写法。
|
|
66
|
+
|
|
67
|
+
| 分类 | 涉及 API | 替代方案 |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| 定位 | `navigator.geolocation.getCurrentPosition` / `watchPosition` | 移除 |
|
|
70
|
+
| 剪贴板 | `navigator.clipboard.readText` / `writeText`、`document.execCommand('copy'/'cut'/'paste')` | 展示可选中文本,引导用户长按 / 选中手动复制 |
|
|
71
|
+
| 硬件连接 | `navigator.bluetooth` / `navigator.usb` / `navigator.hid` / `navigator.serial` | 移除 |
|
|
72
|
+
| 传感器 | `new Accelerometer()` / `new Gyroscope()` / `new Magnetometer()`、环境光、`DeviceMotionEvent` / `DeviceOrientationEvent` | 改用触摸 / 指针手势(见 §5),摇一摇类移除 |
|
|
73
|
+
| 实时通信 | `new WebSocket()`、`new EventSource()`、`new RTCPeerConnection()` | 移除(不联网,无轮询替代) |
|
|
74
|
+
| 后台运行 | Web Worker、SharedWorker、Service Worker(`navigator.serviceWorker.register`) | 移除,逻辑放主线程 |
|
|
75
|
+
| 屏幕 | `getDisplayMedia`(屏幕共享)、`Element.requestFullscreen`(全屏由容器统一管理) | 全屏用 CSS 沉浸式布局实现视觉全屏 |
|
|
76
|
+
| 设备信息 | `navigator.getBattery`、`navigator.connection`、`navigator.mediaDevices.enumerateDevices` | 移除 |
|
|
77
|
+
| 存储进阶 | `navigator.storage.persist`(持久化)、跨域存储访问 | 移除,本地缓存用 `localStorage` / `IndexedDB` |
|
|
78
|
+
| 凭据 | `navigator.credentials.get` / `create`(WebAuthn)、`navigator.locks` | 移除 |
|
|
79
|
+
| 窗口 | `window.open`(弹新窗口)、`window.prompt` | 单页内 JS 切换视图 DOM;输入用页内 Modal |
|
|
80
|
+
|
|
81
|
+
移动端 WebView 本身也不支持:支付 `PaymentRequest`、系统通知 / 推送、NFC、MIDI、XR / AR / VR、后台同步 / 下载、PWA 安装、窗口管理、指针 / 键盘锁定。一律移除。
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 4. 不可用行为
|
|
86
|
+
|
|
87
|
+
| 行为 | 说明 | 替代方案 |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| 网络请求 | `fetch` / `XMLHttpRequest`、加载外部图片 / 字体 / 媒体等一切联网请求 | 所有资源打包在内,改本地相对引用;仅小型配置 / 数据可随包提供,大型只读数据集不适合小工具,见 [performance-budget.md](./performance-budget.md) §2 |
|
|
90
|
+
| 动态执行代码 | `eval()`、`new Function()` | 改写为静态逻辑 |
|
|
91
|
+
| WebAssembly | WASM 编译执行(依赖 WASM 的库无法运行) | 移除或改用纯 JS 实现 |
|
|
92
|
+
| iframe / object | 内嵌 iframe / object,或被外部页面嵌入 | 内容直接写进页面 |
|
|
93
|
+
| 表单跳转提交 | `<form>` 提交跳转 | `addEventListener('submit', e => e.preventDefault())` 后用 JS 处理 |
|
|
94
|
+
| 文件下载 | `a[download]`、blob 下载 | 移除 |
|
|
95
|
+
| 打开外链 / 新窗口 | `target="_blank"`、`window.open`、跳转站外 URL | 单页内 JS 切换视图 DOM |
|
|
96
|
+
| 跳转其他小工具 | 小工具间互相跳转 | 移除 |
|
|
97
|
+
| 长按菜单 | 系统长按菜单已禁用 | 用自定义交互替代 |
|
|
98
|
+
| 插件 | Flash 等浏览器插件 | 移除 |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 5. WebGL / 图形计算边界
|
|
103
|
+
|
|
104
|
+
纯 WebGL 渲染可用,组合能力受限:
|
|
105
|
+
|
|
106
|
+
| 场景 | 是否可用 |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| 包内资源 / Canvas / 内存对象作为纹理 | ✅ |
|
|
109
|
+
| 外部域名图片作为纹理 | 🔴 不联网,纹理须打包在内 |
|
|
110
|
+
| 依赖 WASM 的加速库(Draco / Basis / ONNX / 抠图算法等) | 🔴 |
|
|
111
|
+
| 依赖 Worker 的离屏渲染(OffscreenCanvas + Worker) | 🔴 |
|
|
112
|
+
| SharedArrayBuffer 多线程 | 🔴 |
|
|
113
|
+
|
|
114
|
+
WebGL 适合用包内资源做本地渲染;AI 图像处理等重计算(需联网或 WASM 模型)无法支持。WebGL 可用不等于低端真机性能足够:DPR、像素、纹理、draw call、几何预算、动态降档与兜底必须遵守 [performance-budget.md](./performance-budget.md) §4–5。
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 6. 常见交互怎么实现
|
|
119
|
+
|
|
120
|
+
| 需求 | 实现 |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| 手势 / 拖拽 / 滑动 | `addEventListener('touchstart'/'touchmove'/'touchend')` 或 Pointer Events(`pointerdown`/`move`/`up`) |
|
|
123
|
+
| 拍照 / 录音 | `getUserMedia(...)`,由按钮点击等用户手势触发 + 授权 |
|
|
124
|
+
| 选择图片 / 视频 | `<input type="file">` |
|
|
125
|
+
| 复制文本 | 展示可选中文本,引导用户长按 / 选中复制 |
|
|
126
|
+
| 视觉全屏 | CSS 布局(`100vh` / flex + 隐藏滚动) |
|
|
127
|
+
| 页面跳转 | 单页内用 JS 切换视图 DOM |
|
|
128
|
+
| 输入弹窗 | 页内 Modal 组件 |
|
|
129
|
+
| 保存图片到相册 | `writeTempFile({ data: canvas.toDataURL(...) })`(须完整 data:uri)→ `saveImageToPhotosAlbum({ filePath })`,见 [jsbridge-api.md](./jsbridge-api.md) |
|
|
130
|
+
| 发布笔记 | `postNote({ mediaInfo, title?, content? })` |
|
|
131
|
+
| 跳转 App 搜索 / 用户页等 | `openRedPage({ type, params? })`,`type` 须在白名单内 |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 7. 能力扫描清单
|
|
136
|
+
|
|
137
|
+
扫描代码,命中下列模式则**必须删除或改用替代写法**:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
fetch( / XMLHttpRequest / new WebSocket( / new EventSource( / new RTCPeerConnection(
|
|
141
|
+
navigator.geolocation.getCurrentPosition / watchPosition
|
|
142
|
+
navigator.clipboard.readText / writeText · document.execCommand('copy')
|
|
143
|
+
navigator.bluetooth / navigator.usb / navigator.hid / navigator.serial
|
|
144
|
+
navigator.getBattery / navigator.connection / navigator.credentials / navigator.locks
|
|
145
|
+
navigator.mediaDevices.enumerateDevices / navigator.mediaDevices.getDisplayMedia
|
|
146
|
+
navigator.storage.persist / navigator.serviceWorker.register
|
|
147
|
+
new Worker( / new SharedWorker(
|
|
148
|
+
new Accelerometer() / new Gyroscope() / new Magnetometer()
|
|
149
|
+
DeviceMotionEvent / DeviceOrientationEvent / addEventListener('devicemotion' / 'deviceorientation')
|
|
150
|
+
Element.requestFullscreen / webkitRequestFullscreen
|
|
151
|
+
eval( / new Function( / WebAssembly.
|
|
152
|
+
window.open( / window.prompt(
|
|
153
|
+
location.href = / location.assign( (跳转站外 URL)
|
|
154
|
+
<form ...> 提交跳转 · <a ... download> · target="_blank"
|
|
155
|
+
<iframe> / <object>
|
|
156
|
+
<script src="https://..."> / <link href="https://..."> / <img src="https://..."> / CSS url(https://...)
|
|
157
|
+
—— 外部资源一律加载不到,须下载后打进 zip 改本地相对引用
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**允许保留**:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
navigator.mediaDevices.getUserMedia({ video / audio }) // 用户手势 + 授权
|
|
164
|
+
<input type="file"> // 选图片 / 视频
|
|
165
|
+
localStorage / sessionStorage / IndexedDB / Cookie / Cache API // 独立隔离
|
|
166
|
+
alert() / confirm()
|
|
167
|
+
touch / pointer events // 手势交互
|
|
168
|
+
标准 DOM / CSS / Canvas 2D / WebGL 渲染
|
|
169
|
+
```
|