refactor(util): 统一 URL 参数序列化与解析逻辑

1. 新增_toParamValueString处理复杂参数序列化
2. 新增parsePageParams作为page_params统一解析工具
3. 重构多处参数处理逻辑,避免重复代码和对象转字符串异常
4. 统一处理对象/数组参数的JSON序列化,避免出现[object Object]
This commit is contained in:
2026-07-10 13:56:37 +08:00
parent a90245b201
commit 679c53c501

View File

@@ -35,6 +35,57 @@ export function decodeEntity(str, amp) {
}
/**
* 把任意值序列化成可作为 URL 参数的字符串。
* - null / undefined → '' (由调用方负责过滤空值)
* - 对象 / 数组 → JSON.stringify (避免 String({}) 退化成 "[object Object]")
* - 其它类型(number / boolean / string)→ String(v),保持原行为
*
* 与消费端约定:page_params 一律以 JSON 字符串形式落 URL,
* 消费侧用 parsePageParams 还原为对象。
*
* @example
* _toParamValueString({a:1}) // '{"a":1}'
* _toParamValueString('hello') // 'hello'
* _toParamValueString(null) // ''
* _toParamValueString([1, 2]) // '[1,2]'
*
* @param {*} v
* @returns {string}
*/
function _toParamValueString(v) {
if (v === null || v === undefined) return ''
if (typeof v === 'object') return JSON.stringify(v)
return String(v)
}
/**
* 把 URL 上的 page_params 还原成对象。是 _toParamValueString 的逆操作。
* 容错策略:任何解析失败都安全降级到 defaultValue,不抛异常。
*
* @example
* parsePageParams('{"a":1}') // { a: 1 }
* parsePageParams({a: 1}) // { a: 1 } (已是对象直接返回)
* parsePageParams('[object Object]') // {} (老 bug 残留安全降级)
* parsePageParams('') // {}
* parsePageParams(null) // {}
* parsePageParams(undefined) // {}
* parsePageParams('not json', {x:1}) // { x: 1 } (解析失败走默认值)
*
* @param {*} raw page_params 原始值(可能是 object / string / null / undefined)
* @param {Object} [defaultValue={}] 解析失败时的兜底值
* @returns {Object}
*/
export function parsePageParams(raw, defaultValue = {}) {
if (raw == null || raw === '') return defaultValue
if (typeof raw === 'object') return raw
if (typeof raw === 'string') {
try { return JSON.parse(raw) } catch (e) { return defaultValue }
}
return defaultValue
}
// 关于页面路径,使用常量导出
export const INDEX_PAGE_URL = '/pages/index/index';
export const CATEGORY_PAGE_URL = '/pages_goods/category';
@@ -282,13 +333,16 @@ export const resolvePageUrl = (page_key, params = {}) => {
Object.keys(params).forEach(function (key) {
const value = params[key];
if (value === null || value === undefined || value === '') return;
// 对象/数组走 JSON.stringify,避免 String({}) 退化成 "[object Object]"
// 消费端 resolveLinkUrl 已用 JSON.parse 还原
const encodedValue = _toParamValueString(value);
// H5:浏览器不自动编码,需要手动 encodeURIComponent
// 小程序/App:uni.navigateTo 会自动编码,这里不能再 encode,否则双重编码
// #ifdef H5
parts.push(key + '=' + encodeURIComponent(value));
parts.push(key + '=' + encodeURIComponent(encodedValue));
// #endif
// #ifndef H5
parts.push(key + '=' + value);
parts.push(key + '=' + encodedValue);
// #endif
});
if (parts.length > 0) {
@@ -386,12 +440,8 @@ export const resolveLinkUrl = (link) => {
const v = eqIdx === -1 ? '' : kv.substring(eqIdx + 1);
if (k) urlParams[k] = v;
});
// 解析 page_params (可能是字符串或对象)
let pageParams = link.page_params;
if (typeof pageParams === 'string' && pageParams) {
try { pageParams = JSON.parse(pageParams); } catch (e) { pageParams = {}; }
}
pageParams = pageParams || {};
// 解析 page_params (可能是字符串或对象) — 走统一 helper,避免分散 try/catch
const pageParams = parsePageParams(link.page_params);
// 合并: page_params 覆盖 urlParams (新机制优先)
link.page_params = Object.assign({}, urlParams, pageParams);
// 把 link.wap_url 改为不含 query 的 path
@@ -400,11 +450,8 @@ export const resolveLinkUrl = (link) => {
}
// 1. 优先使用 page_key(新机制)
if (link.page_key) {
let params = link.page_params;
if (typeof params === 'string' && params) {
try { params = JSON.parse(params); } catch (e) { params = {}; }
}
const url = resolvePageUrl(link.page_key, params || {});
const params = parsePageParams(link.page_params);
const url = resolvePageUrl(link.page_key, params);
if (url) return url;
}
// 2. 回退:旧的 wap_url(已被处理为不含 query 的 path)
@@ -618,6 +665,9 @@ export default {
/** 解析 link 对象得到最终可跳转的 URL(优先级: page_key → wap_url) */
resolveLinkUrl,
/** 把 page_params 还原成对象(JSON.parse 的安全封装) */
parsePageParams,
/** 判断给定路径是否是 TabBar 根页面(用于动态控制返回按钮显示) */
isTabBarRootPage,
@@ -658,7 +708,8 @@ export default {
// - H5: 浏览器不会自动编码 url,若参数含 & ? = 等特殊字符会被 queryString 错位解析
// => 这里仍需手动 encodeURIComponent,decode->encode roundtrip 兼容调用方已编码的入参
const safeEncode = (v) => {
const s = v == null ? '' : String(v);
// 对象/数组走 JSON.stringify,避免 String({}) 退化成 "[object Object]"
const s = _toParamValueString(v);
// #ifdef H5
try {
return encodeURIComponent(decodeURIComponent(s));
@@ -667,6 +718,18 @@ export default {
}
// #endif
// #ifndef H5
// 非 H5:uni.navigateTo / router.pushUrl 内部会再对 url 整体编码一次。
// 如果调用方已经手动 encode 过 page_params (或上游已经 encode 过的 url 字符串),
// 这里再走一遍就会出现双重编码(形如 %25xx)。
// 兜底:仅当输入里包含 %xx 这种"看起来是已编码"的模式时,先 decode 一次,让 uni 再 encode 时得到单层编码。
// 原始字符串(没有 %xx)不受影响,避免误把含 % 的原文吃掉。
if (/%[0-9a-fA-F]{2}/.test(s)) {
try {
return decodeURIComponent(s);
} catch (e) {
return s;
}
}
return s;
// #endif
};
@@ -2009,7 +2072,8 @@ export default {
const urlObj = new URL(targetUrl);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
urlObj.searchParams.append(key, String(value));
// 对象/数组走 JSON.stringify,避免 String({}) 退化成 "[object Object]"
urlObj.searchParams.append(key, _toParamValueString(value));
}
}
return urlObj.toString();
@@ -2039,7 +2103,8 @@ export default {
// 这里不能再 encodeURIComponent,否则会被平台再编码一层导致双重编码
const queryString = Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${key}=${value}`)
// 对象/数组走 JSON.stringify,避免 String({}) 退化成 "[object Object]"
.map(([key, value]) => `${key}=${_toParamValueString(value)}`)
.join('&');
// 重新拼接成完整的 URL