fix(util): 修复内部页面绝对URL被误判为外链的问题

新增extractInternalAppPath和resolveNavMode工具函数,在diyRedirectTo中增加内部页面绝对URL还原逻辑:
1. 支持通过open_mode配置三种导航模式:auto/online/internal
2. 严格校验域名避免误伤外部站点,支持强制内部跳转场景
3. 修复H5整页刷新丢失状态、MP被塞进web-view的问题
This commit is contained in:
2026-07-16 16:32:10 +08:00
parent 61e7a0d859
commit cf6d54e2bf
3 changed files with 307 additions and 18 deletions

View File

@@ -37,7 +37,7 @@ function loadNavInSandbox() {
.replace(/^export\s+let\s+/gm, 'let ')
.replace(/^export\s+var\s+/gm, 'var ')
+ '\nmodule.exports = { _toParamValueString, parsePageParams, resolveLinkUrl, '
+ 'cleanParamUrls, normalizeUrl, appendQueryParams, getPlatform, shouldEncodeUrl, '
+ 'cleanParamUrls, normalizeUrl, appendQueryParams, extractInternalAppPath, resolveNavMode, getPlatform, shouldEncodeUrl, '
+ '_safeEncodeForPlatform, _navigateTo, createNavigateApi };\n';
const sandbox = { console, globalThis: {}, module: moduleObj, exports: moduleObj.exports };
sandbox.globalThis = sandbox;
@@ -54,6 +54,7 @@ test('util-navigate.js: 能被 vm 加载,所有 export 都存在', () => {
const expected = [
'_toParamValueString', 'parsePageParams', 'resolveLinkUrl',
'cleanParamUrls', 'normalizeUrl', 'appendQueryParams',
'extractInternalAppPath', 'resolveNavMode',
'getPlatform', 'shouldEncodeUrl',
'_safeEncodeForPlatform', '_navigateTo', 'createNavigateApi',
];
@@ -341,17 +342,19 @@ test('util.js: 通过 import 引用 util-navigate 的方法,挂载到 default ex
// 关键 import
assert.ok(utilSrc.includes("from './util-navigate.js'"),
'util.js 应 import util-navigate.js');
assert.ok(utilSrc.includes('_cleanParamUrls'),
'util.js 应 import _cleanParamUrls 别名');
assert.ok(utilSrc.includes('_normalizeUrl'),
'util.js 应 import _normalizeUrl 别名');
assert.ok(utilSrc.includes('_navigateTo'),
'util.js 应 import _navigateTo 别名');
// default export 里的挂载
assert.ok(utilSrc.includes('cleanParamUrls: (v) => _cleanParamUrls(v)'),
assert.ok(utilSrc.includes('cleanParamUrls'),
'util.js 应 import cleanParamUrls');
assert.ok(utilSrc.includes('normalizeUrl as _normalizeUrlBase'),
'util.js 应 import normalizeUrl 别名 _normalizeUrlBase');
assert.ok(utilSrc.includes('extractInternalAppPath as _extractInternalAppPath'),
'util.js 应 import extractInternalAppPath 别名');
assert.ok(utilSrc.includes('resolveNavMode as _resolveNavMode'),
'util.js 应 import resolveNavMode 别名');
// default export 里的挂载(cleanParamUrls 直接挂, normalizeUrl 用包装函数委托)
assert.ok(/cleanParamUrls,/.test(utilSrc),
'util.js 应把 cleanParamUrls 挂载到 default export');
assert.ok(utilSrc.includes('_navigateTo: (args) => _navigateTo(args)'),
'util.js 应把 _navigateTo 挂载到 default export');
assert.ok(utilSrc.includes('return _normalizeUrlBase(urlString)'),
'util.js 的 normalizeUrl 应委托给 _normalizeUrlBase');
});
// =============================================================
@@ -368,13 +371,152 @@ test('util.js: redirectTo / diyRedirectTo 仍在 util.js(没被搬走)', () => {
// diyRedirectTo 函数定义
assert.ok(/^\s*diyRedirectTo\s*\(\s*link\s*\)\s*\{/m.test(utilSrc),
'diyRedirectTo 应仍在 util.js');
// redirectTo 内部调 this._safeEncodeForPlatform
assert.ok(utilSrc.includes('this._safeEncodeForPlatform'),
'redirectTo 应调 this._safeEncodeForPlatform');
// redirectTo 内部调 this._navigateTo
assert.ok(utilSrc.includes('this._navigateTo('),
'redirectTo 应调 this._navigateTo');
// diyRedirectTo 内部调 this.cleanParamUrls
// redirectTo 内部调 _safeEncodeForPlatform(模块函数直调)
assert.ok(utilSrc.includes('_safeEncodeForPlatform('),
'redirectTo 应调 _safeEncodeForPlatform');
// diyRedirectTo 内部调 this.cleanParamUrls 清理脏 link
assert.ok(utilSrc.includes('this.cleanParamUrls('),
'diyRedirectTo 应调 this.cleanParamUrls');
// diyRedirectTo 内部调 _resolveNavMode + _extractInternalAppPath(内部页绝对 URL 还原)
assert.ok(utilSrc.includes('_resolveNavMode('),
'diyRedirectTo 应调 _resolveNavMode');
assert.ok(utilSrc.includes('_extractInternalAppPath('),
'diyRedirectTo 应调 _extractInternalAppPath');
});
// =============================================================
// extractInternalAppPath — 内部页面绝对 URL 还原(双闸门)
// 修复: 内部页被存成绝对 URL 时被误判外链 → 整页刷新 / web-view
// =============================================================
test('extractInternalAppPath: 相对内部路径直接放行', () => {
const nav = loadNavInSandbox();
assert.equal(nav.extractInternalAppPath('/pages_goods/detail?id=9'), '/pages_goods/detail?id=9');
assert.equal(nav.extractInternalAppPath('/pages/index/index'), '/pages/index/index');
});
test('extractInternalAppPath: 同域绝对 URL → 还原为相对路径', () => {
const nav = loadNavInSandbox();
const H = ['xcx6.aigc-quickapp.com'];
assert.equal(
nav.extractInternalAppPath('https://xcx6.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata?id=1', H),
'/pages_tool/form/formdata?id=1',
);
});
test('extractInternalAppPath: 同平台不同子域(域名不完全一致)→ null(严格模式)', () => {
const nav = loadNavInSandbox();
const H = ['xcx6.aigc-quickapp.com']; // App 跑在 xcx6, 数据链接是 xcx3 → 视为外链
assert.equal(
nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata', H),
null,
);
});
test('extractInternalAppPath: 传入完整 URL 作为白名单也能提取 host(完全一致才还原)', () => {
const nav = loadNavInSandbox();
const H = ['https://xcx6.aigc-quickapp.com/']; // Config.h5Domain 形式
assert.equal(
nav.extractInternalAppPath('https://xcx6.aigc-quickapp.com/hwappx/common/pages/index/index', H),
'/pages/index/index',
);
// host 不一致 → null
assert.equal(
nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages/index/index', H),
null,
);
});
test('extractInternalAppPath: 外链域名不同(Shopify / Facebook)→ 不误伤, 返回 null', () => {
const nav = loadNavInSandbox();
const H = ['xcx6.aigc-quickapp.com'];
assert.equal(nav.extractInternalAppPath('https://xxx.myshopify.com/pages/about-us', H), null);
assert.equal(nav.extractInternalAppPath('https://www.facebook.com/pages/Name/123', H), null);
assert.equal(nav.extractInternalAppPath('https://example.com/pages_tool/x', H), null);
});
test('extractInternalAppPath: 绝对 URL 但路径无 /pages 段 → null(真外链)', () => {
const nav = loadNavInSandbox();
const H = ['xcx6.aigc-quickapp.com'];
assert.equal(nav.extractInternalAppPath('https://xcx6.aigc-quickapp.com/activity/2026', H), null);
});
test('extractInternalAppPath: 无白名单时绝对 URL 一律 null(不猜测)', () => {
const nav = loadNavInSandbox();
assert.equal(nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages/index/index'), null);
assert.equal(nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages/index/index', []), null);
});
test('extractInternalAppPath: 空/非法输入安全返回 null', () => {
const nav = loadNavInSandbox();
assert.equal(nav.extractInternalAppPath('', ['a.com']), null);
assert.equal(nav.extractInternalAppPath(null, ['a.com']), null);
assert.equal(nav.extractInternalAppPath(undefined), null);
});
test('extractInternalAppPath: host 大小写不敏感 + 忽略端口', () => {
const nav = loadNavInSandbox();
const H = ['xcx6.aigc-quickapp.com'];
// host 大写
assert.equal(
nav.extractInternalAppPath('https://XCX6.AIGC-QUICKAPP.COM/hwappx/common/pages/index/index', H),
'/pages/index/index',
);
// 带端口(白名单不带端口, 也应命中)
assert.equal(
nav.extractInternalAppPath('https://xcx6.aigc-quickapp.com:8443/hwappx/common/pages_goods/detail', H),
'/pages_goods/detail',
);
});
test('extractInternalAppPath: 各分包页面根都能识别 + 保留 hash', () => {
const nav = loadNavInSandbox();
const H = ['h.com'];
assert.equal(nav.extractInternalAppPath('https://h.com/base/pages_promotion/point/list', H), '/pages_promotion/point/list');
assert.equal(nav.extractInternalAppPath('https://h.com/base/pages_order/detail?id=2', H), '/pages_order/detail?id=2');
assert.equal(nav.extractInternalAppPath('/pages/index/index#top'), '/pages/index/index#top');
});
// =============================================================
// extractInternalAppPath: 强制内部(allowedHosts === true 跳过 host 校验)
// =============================================================
test('extractInternalAppPath: allowedHosts=true → 跳过 host 校验(强制内部)', () => {
const nav = loadNavInSandbox();
// 不同子域, 正常严格模式下会 null; 传 true 强制还原
assert.equal(
nav.extractInternalAppPath('https://xcx3.aigc-quickapp.com/hwappx/common/pages_tool/form/formdata?id=1', true),
'/pages_tool/form/formdata?id=1',
);
// 完全陌生域名也能被强制还原(调用方已明确表示是内部页)
assert.equal(
nav.extractInternalAppPath('https://any-host.example.net/x/pages/index/index', true),
'/pages/index/index',
);
// 但仍要求路径带 /pages 段, 没有则 null
assert.equal(nav.extractInternalAppPath('https://any-host.example.net/activity/2026', true), null);
});
// =============================================================
// resolveNavMode — 统一开关 open_mode(internal/online/auto)
// =============================================================
test('resolveNavMode: open_mode=internal/spa → internal', () => {
const { resolveNavMode } = loadNavInSandbox();
assert.equal(resolveNavMode({ open_mode: 'internal' }), 'internal');
assert.equal(resolveNavMode({ open_mode: 'SPA' }), 'internal');
assert.equal(resolveNavMode({ openMode: 'internal' }), 'internal');
assert.equal(resolveNavMode({ page_params: { open_mode: 'internal' } }), 'internal');
});
test('resolveNavMode: open_mode=online/web → online', () => {
const { resolveNavMode } = loadNavInSandbox();
assert.equal(resolveNavMode({ open_mode: 'online' }), 'online');
assert.equal(resolveNavMode({ open_mode: 'WEB' }), 'online');
assert.equal(resolveNavMode({ page_params: { open_mode: 'web' } }), 'online');
});
test('resolveNavMode: 未设置 / 未知值 → auto', () => {
const { resolveNavMode } = loadNavInSandbox();
assert.equal(resolveNavMode({}), 'auto');
assert.equal(resolveNavMode({ open_mode: 'xxx' }), 'auto');
assert.equal(resolveNavMode(null), 'auto');
assert.equal(resolveNavMode(undefined), 'auto');
});