@lytjs/common-query 6.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/README.md +93 -0
- package/dist/index.cjs +156 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +51 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.mjs +151 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +35 -0
- package/src/env.d.ts +4 -0
- package/src/index.ts +235 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @lytjs/common-query
|
|
2
|
+
|
|
3
|
+
URL 查询字符串解析与构建工具,提供轻量级的 URL 处理能力。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @lytjs/common-query
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### `parseQueryString(search: string): Record<string, string>`
|
|
14
|
+
|
|
15
|
+
解析 URL 查询字符串为对象。
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { parseQueryString } from '@lytjs/common-query';
|
|
19
|
+
|
|
20
|
+
parseQueryString('?key=value&key2=value2');
|
|
21
|
+
// { key: 'value', key2: 'value2' }
|
|
22
|
+
|
|
23
|
+
parseQueryString('key=value&key2=value2');
|
|
24
|
+
// { key: 'value', key2: 'value2' }
|
|
25
|
+
|
|
26
|
+
parseQueryString('?name=hello%20world');
|
|
27
|
+
// { name: 'hello world' }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `stringifyQueryString(params: Record<string, string | number | boolean>): string`
|
|
31
|
+
|
|
32
|
+
将对象序列化为查询字符串。
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { stringifyQueryString } from '@lytjs/common-query';
|
|
36
|
+
|
|
37
|
+
stringifyQueryString({ a: '1', b: '2' });
|
|
38
|
+
// 'a=1&b=2'
|
|
39
|
+
|
|
40
|
+
stringifyQueryString({ name: 'hello world' });
|
|
41
|
+
// 'name=hello%20world'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `parseURL(url: string): ParsedURL`
|
|
45
|
+
|
|
46
|
+
解析完整 URL,返回结构化信息。
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { parseURL } from '@lytjs/common-query';
|
|
50
|
+
|
|
51
|
+
parseURL('https://example.com:8080/path?name=test#section');
|
|
52
|
+
// {
|
|
53
|
+
// protocol: 'https://',
|
|
54
|
+
// host: 'example.com:8080',
|
|
55
|
+
// hostname: 'example.com',
|
|
56
|
+
// port: '8080',
|
|
57
|
+
// pathname: '/path',
|
|
58
|
+
// search: '?name=test',
|
|
59
|
+
// hash: '#section',
|
|
60
|
+
// searchParams: { name: 'test' },
|
|
61
|
+
// origin: 'https://example.com:8080',
|
|
62
|
+
// href: 'https://example.com:8080/path?name=test#section'
|
|
63
|
+
// }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `buildURL(base: string, params?, hash?): string`
|
|
67
|
+
|
|
68
|
+
构建完整 URL,支持合并已有查询参数。
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { buildURL } from '@lytjs/common-query';
|
|
72
|
+
|
|
73
|
+
buildURL('https://example.com/path', { a: '1', b: '2' });
|
|
74
|
+
// 'https://example.com/path?a=1&b=2'
|
|
75
|
+
|
|
76
|
+
buildURL('https://example.com/path?existing=1', { new: '2' });
|
|
77
|
+
// 'https://example.com/path?existing=1&new=2'
|
|
78
|
+
|
|
79
|
+
buildURL('/path', { page: 1 }, 'top');
|
|
80
|
+
// '/path?page=1#top'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 特性
|
|
84
|
+
|
|
85
|
+
- 零运行时依赖
|
|
86
|
+
- 体积 < 2KB(min+gzip)
|
|
87
|
+
- 支持绝对 URL 和相对 URL
|
|
88
|
+
- 自动编解码 URI 组件
|
|
89
|
+
- TypeScript 类型完整
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function parseQueryString(search) {
|
|
5
|
+
if (!search) return {};
|
|
6
|
+
const str = search.startsWith("?") ? search.slice(1) : search;
|
|
7
|
+
if (!str) return {};
|
|
8
|
+
const result = {};
|
|
9
|
+
const pairs = str.split("&");
|
|
10
|
+
for (const pair of pairs) {
|
|
11
|
+
if (!pair) continue;
|
|
12
|
+
const eqIndex = pair.indexOf("=");
|
|
13
|
+
let key;
|
|
14
|
+
let value;
|
|
15
|
+
if (eqIndex === -1) {
|
|
16
|
+
key = pair;
|
|
17
|
+
value = "";
|
|
18
|
+
} else {
|
|
19
|
+
key = pair.slice(0, eqIndex);
|
|
20
|
+
value = pair.slice(eqIndex + 1);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
key = decodeURIComponent(key);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
value = decodeURIComponent(value);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
result[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function stringifyQueryString(params) {
|
|
35
|
+
const keys = Object.keys(params);
|
|
36
|
+
if (keys.length === 0) return "";
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
const value = params[key];
|
|
40
|
+
parts.push(
|
|
41
|
+
encodeURIComponent(key) + "=" + encodeURIComponent(String(value))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return parts.join("&");
|
|
45
|
+
}
|
|
46
|
+
function parseURL(url) {
|
|
47
|
+
let rest = url;
|
|
48
|
+
let hash = "";
|
|
49
|
+
const hashIndex = rest.indexOf("#");
|
|
50
|
+
if (hashIndex !== -1) {
|
|
51
|
+
hash = rest.slice(hashIndex);
|
|
52
|
+
rest = rest.slice(0, hashIndex);
|
|
53
|
+
}
|
|
54
|
+
let search = "";
|
|
55
|
+
const searchIndex = rest.indexOf("?");
|
|
56
|
+
if (searchIndex !== -1) {
|
|
57
|
+
search = rest.slice(searchIndex);
|
|
58
|
+
rest = rest.slice(0, searchIndex);
|
|
59
|
+
}
|
|
60
|
+
let protocol = "";
|
|
61
|
+
const protocolIndex = rest.indexOf("://");
|
|
62
|
+
if (protocolIndex !== -1) {
|
|
63
|
+
protocol = rest.slice(0, protocolIndex + 3);
|
|
64
|
+
rest = rest.slice(protocolIndex + 3);
|
|
65
|
+
}
|
|
66
|
+
let pathname = "";
|
|
67
|
+
let host = "";
|
|
68
|
+
let hostname = "";
|
|
69
|
+
let port = "";
|
|
70
|
+
if (protocol) {
|
|
71
|
+
const slashIndex = rest.indexOf("/");
|
|
72
|
+
if (slashIndex !== -1) {
|
|
73
|
+
host = rest.slice(0, slashIndex);
|
|
74
|
+
pathname = rest.slice(slashIndex);
|
|
75
|
+
} else {
|
|
76
|
+
host = rest;
|
|
77
|
+
pathname = "";
|
|
78
|
+
}
|
|
79
|
+
if (host.startsWith("[")) {
|
|
80
|
+
const bracketEnd = host.indexOf("]");
|
|
81
|
+
if (bracketEnd !== -1) {
|
|
82
|
+
hostname = host.slice(0, bracketEnd + 1);
|
|
83
|
+
if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ":") {
|
|
84
|
+
port = host.slice(bracketEnd + 2);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
hostname = host;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const colonIndex = host.lastIndexOf(":");
|
|
91
|
+
if (colonIndex !== -1) {
|
|
92
|
+
hostname = host.slice(0, colonIndex);
|
|
93
|
+
port = host.slice(colonIndex + 1);
|
|
94
|
+
} else {
|
|
95
|
+
hostname = host;
|
|
96
|
+
port = "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
pathname = rest;
|
|
101
|
+
}
|
|
102
|
+
const searchParams = parseQueryString(search);
|
|
103
|
+
const origin = protocol ? protocol + host : "";
|
|
104
|
+
return {
|
|
105
|
+
protocol,
|
|
106
|
+
host,
|
|
107
|
+
hostname,
|
|
108
|
+
port,
|
|
109
|
+
pathname,
|
|
110
|
+
search,
|
|
111
|
+
hash,
|
|
112
|
+
searchParams,
|
|
113
|
+
origin,
|
|
114
|
+
href: url
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function buildURL(base, params, hash) {
|
|
118
|
+
if (!params && !hash) return base;
|
|
119
|
+
let baseWithoutHash = base;
|
|
120
|
+
let existingHash = "";
|
|
121
|
+
const hashIdx = base.indexOf("#");
|
|
122
|
+
if (hashIdx !== -1) {
|
|
123
|
+
existingHash = base.slice(hashIdx);
|
|
124
|
+
baseWithoutHash = base.slice(0, hashIdx);
|
|
125
|
+
}
|
|
126
|
+
let existingSearch = "";
|
|
127
|
+
let baseWithoutSearch = baseWithoutHash;
|
|
128
|
+
const searchIdx = baseWithoutHash.indexOf("?");
|
|
129
|
+
if (searchIdx !== -1) {
|
|
130
|
+
existingSearch = baseWithoutHash.slice(searchIdx + 1);
|
|
131
|
+
baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
|
|
132
|
+
}
|
|
133
|
+
const mergedParams = parseQueryString(existingSearch);
|
|
134
|
+
if (params) {
|
|
135
|
+
for (const key of Object.keys(params)) {
|
|
136
|
+
mergedParams[key] = String(params[key]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const queryString = stringifyQueryString(mergedParams);
|
|
140
|
+
let result = baseWithoutSearch;
|
|
141
|
+
if (queryString) {
|
|
142
|
+
result += "?" + queryString;
|
|
143
|
+
}
|
|
144
|
+
const finalHash = hash !== void 0 ? "#" + encodeURIComponent(hash) : existingHash;
|
|
145
|
+
if (finalHash) {
|
|
146
|
+
result += finalHash;
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
exports.buildURL = buildURL;
|
|
152
|
+
exports.parseQueryString = parseQueryString;
|
|
153
|
+
exports.parseURL = parseURL;
|
|
154
|
+
exports.stringifyQueryString = stringifyQueryString;
|
|
155
|
+
//# sourceMappingURL=index.cjs.map
|
|
156
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * @lytjs/common-query\r\n * URL 查询字符串解析与构建工具\r\n */\r\n\r\n/**\r\n * 解析后的 URL 结构\r\n */\r\nexport interface ParsedURL {\r\n protocol: string;\r\n host: string;\r\n hostname: string;\r\n port: string;\r\n pathname: string;\r\n search: string;\r\n hash: string;\r\n searchParams: Record<string, string>;\r\n origin: string;\r\n href: string;\r\n}\r\n\r\n/**\r\n * 解析 URL 查询字符串为对象\r\n *\r\n * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式\r\n * @returns 解析后的键值对对象\r\n */\r\nexport function parseQueryString(search: string): Record<string, string> {\r\n if (!search) return {};\r\n\r\n const str = search.startsWith('?') ? search.slice(1) : search;\r\n if (!str) return {};\r\n\r\n const result: Record<string, string> = {};\r\n\r\n const pairs = str.split('&');\r\n for (const pair of pairs) {\r\n if (!pair) continue;\r\n const eqIndex = pair.indexOf('=');\r\n let key: string;\r\n let value: string;\r\n if (eqIndex === -1) {\r\n key = pair;\r\n value = '';\r\n } else {\r\n key = pair.slice(0, eqIndex);\r\n value = pair.slice(eqIndex + 1);\r\n }\r\n try {\r\n key = decodeURIComponent(key);\r\n } catch {\r\n // keep key as-is if decode fails\r\n }\r\n try {\r\n value = decodeURIComponent(value);\r\n } catch {\r\n // keep value as-is if decode fails\r\n }\r\n result[key] = value;\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 将对象序列化为查询字符串\r\n *\r\n * @param params - 键值对对象\r\n * @returns 序列化后的查询字符串(不含前导 ?)\r\n */\r\nexport function stringifyQueryString(\r\n params: Record<string, string | number | boolean>,\r\n): string {\r\n const keys = Object.keys(params);\r\n if (keys.length === 0) return '';\r\n\r\n const parts: string[] = [];\r\n for (const key of keys) {\r\n const value = params[key];\r\n parts.push(\r\n encodeURIComponent(key) + '=' + encodeURIComponent(String(value)),\r\n );\r\n }\r\n\r\n return parts.join('&');\r\n}\r\n\r\n/**\r\n * 解析完整 URL\r\n *\r\n * @param url - URL 字符串,支持绝对 URL 和相对 URL\r\n * @returns 解析后的 ParsedURL 对象\r\n */\r\nexport function parseURL(url: string): ParsedURL {\r\n let rest = url;\r\n\r\n // Extract hash\r\n let hash = '';\r\n const hashIndex = rest.indexOf('#');\r\n if (hashIndex !== -1) {\r\n hash = rest.slice(hashIndex);\r\n rest = rest.slice(0, hashIndex);\r\n }\r\n\r\n // Extract search\r\n let search = '';\r\n const searchIndex = rest.indexOf('?');\r\n if (searchIndex !== -1) {\r\n search = rest.slice(searchIndex);\r\n rest = rest.slice(0, searchIndex);\r\n }\r\n\r\n // Extract protocol\r\n let protocol = '';\r\n const protocolIndex = rest.indexOf('://');\r\n if (protocolIndex !== -1) {\r\n protocol = rest.slice(0, protocolIndex + 3);\r\n rest = rest.slice(protocolIndex + 3);\r\n }\r\n\r\n // Extract pathname (everything after host)\r\n let pathname = '';\r\n let host = '';\r\n let hostname = '';\r\n let port = '';\r\n\r\n if (protocol) {\r\n // Absolute URL: extract host and pathname\r\n const slashIndex = rest.indexOf('/');\r\n if (slashIndex !== -1) {\r\n host = rest.slice(0, slashIndex);\r\n pathname = rest.slice(slashIndex);\r\n } else {\r\n host = rest;\r\n pathname = '';\r\n }\r\n\r\n // Parse host into hostname and port\r\n // Handle IPv6 addresses like [::1]:8080\r\n if (host.startsWith('[')) {\r\n const bracketEnd = host.indexOf(']');\r\n if (bracketEnd !== -1) {\r\n hostname = host.slice(0, bracketEnd + 1);\r\n if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {\r\n port = host.slice(bracketEnd + 2);\r\n }\r\n } else {\r\n hostname = host;\r\n }\r\n } else {\r\n const colonIndex = host.lastIndexOf(':');\r\n if (colonIndex !== -1) {\r\n hostname = host.slice(0, colonIndex);\r\n port = host.slice(colonIndex + 1);\r\n } else {\r\n hostname = host;\r\n port = '';\r\n }\r\n }\r\n } else {\r\n // Relative URL\r\n pathname = rest;\r\n }\r\n\r\n const searchParams = parseQueryString(search);\r\n const origin = protocol ? protocol + host : '';\r\n\r\n return {\r\n protocol,\r\n host,\r\n hostname,\r\n port,\r\n pathname,\r\n search,\r\n hash,\r\n searchParams,\r\n origin,\r\n href: url,\r\n };\r\n}\r\n\r\n/**\r\n * 构建完整 URL\r\n *\r\n * @param base - 基础 URL\r\n * @param params - 查询参数(可选)\r\n * @param hash - hash 片段(可选,不含 #)\r\n * @returns 构建后的完整 URL\r\n */\r\nexport function buildURL(\r\n base: string,\r\n params?: Record<string, string | number | boolean>,\r\n hash?: string,\r\n): string {\r\n if (!params && !hash) return base;\r\n\r\n // Extract and remove existing hash from base\r\n let baseWithoutHash = base;\r\n let existingHash = '';\r\n const hashIdx = base.indexOf('#');\r\n if (hashIdx !== -1) {\r\n existingHash = base.slice(hashIdx);\r\n baseWithoutHash = base.slice(0, hashIdx);\r\n }\r\n\r\n // Parse existing query params from base (without hash)\r\n let existingSearch = '';\r\n let baseWithoutSearch = baseWithoutHash;\r\n const searchIdx = baseWithoutHash.indexOf('?');\r\n if (searchIdx !== -1) {\r\n existingSearch = baseWithoutHash.slice(searchIdx + 1);\r\n baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);\r\n }\r\n\r\n // Merge existing and new params\r\n const mergedParams = parseQueryString(existingSearch);\r\n if (params) {\r\n for (const key of Object.keys(params)) {\r\n mergedParams[key] = String(params[key]);\r\n }\r\n }\r\n\r\n const queryString = stringifyQueryString(mergedParams);\r\n let result = baseWithoutSearch;\r\n if (queryString) {\r\n result += '?' + queryString;\r\n }\r\n // Use provided hash, or keep existing hash if no new hash is given\r\n const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;\r\n if (finalHash) {\r\n result += finalHash;\r\n }\r\n\r\n return result;\r\n}\r\n"]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lytjs/common-query
|
|
3
|
+
* URL 查询字符串解析与构建工具
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 解析后的 URL 结构
|
|
7
|
+
*/
|
|
8
|
+
interface ParsedURL {
|
|
9
|
+
protocol: string;
|
|
10
|
+
host: string;
|
|
11
|
+
hostname: string;
|
|
12
|
+
port: string;
|
|
13
|
+
pathname: string;
|
|
14
|
+
search: string;
|
|
15
|
+
hash: string;
|
|
16
|
+
searchParams: Record<string, string>;
|
|
17
|
+
origin: string;
|
|
18
|
+
href: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 解析 URL 查询字符串为对象
|
|
22
|
+
*
|
|
23
|
+
* @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
|
|
24
|
+
* @returns 解析后的键值对对象
|
|
25
|
+
*/
|
|
26
|
+
declare function parseQueryString(search: string): Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* 将对象序列化为查询字符串
|
|
29
|
+
*
|
|
30
|
+
* @param params - 键值对对象
|
|
31
|
+
* @returns 序列化后的查询字符串(不含前导 ?)
|
|
32
|
+
*/
|
|
33
|
+
declare function stringifyQueryString(params: Record<string, string | number | boolean>): string;
|
|
34
|
+
/**
|
|
35
|
+
* 解析完整 URL
|
|
36
|
+
*
|
|
37
|
+
* @param url - URL 字符串,支持绝对 URL 和相对 URL
|
|
38
|
+
* @returns 解析后的 ParsedURL 对象
|
|
39
|
+
*/
|
|
40
|
+
declare function parseURL(url: string): ParsedURL;
|
|
41
|
+
/**
|
|
42
|
+
* 构建完整 URL
|
|
43
|
+
*
|
|
44
|
+
* @param base - 基础 URL
|
|
45
|
+
* @param params - 查询参数(可选)
|
|
46
|
+
* @param hash - hash 片段(可选,不含 #)
|
|
47
|
+
* @returns 构建后的完整 URL
|
|
48
|
+
*/
|
|
49
|
+
declare function buildURL(base: string, params?: Record<string, string | number | boolean>, hash?: string): string;
|
|
50
|
+
|
|
51
|
+
export { type ParsedURL, buildURL, parseQueryString, parseURL, stringifyQueryString };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lytjs/common-query
|
|
3
|
+
* URL 查询字符串解析与构建工具
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 解析后的 URL 结构
|
|
7
|
+
*/
|
|
8
|
+
interface ParsedURL {
|
|
9
|
+
protocol: string;
|
|
10
|
+
host: string;
|
|
11
|
+
hostname: string;
|
|
12
|
+
port: string;
|
|
13
|
+
pathname: string;
|
|
14
|
+
search: string;
|
|
15
|
+
hash: string;
|
|
16
|
+
searchParams: Record<string, string>;
|
|
17
|
+
origin: string;
|
|
18
|
+
href: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 解析 URL 查询字符串为对象
|
|
22
|
+
*
|
|
23
|
+
* @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
|
|
24
|
+
* @returns 解析后的键值对对象
|
|
25
|
+
*/
|
|
26
|
+
declare function parseQueryString(search: string): Record<string, string>;
|
|
27
|
+
/**
|
|
28
|
+
* 将对象序列化为查询字符串
|
|
29
|
+
*
|
|
30
|
+
* @param params - 键值对对象
|
|
31
|
+
* @returns 序列化后的查询字符串(不含前导 ?)
|
|
32
|
+
*/
|
|
33
|
+
declare function stringifyQueryString(params: Record<string, string | number | boolean>): string;
|
|
34
|
+
/**
|
|
35
|
+
* 解析完整 URL
|
|
36
|
+
*
|
|
37
|
+
* @param url - URL 字符串,支持绝对 URL 和相对 URL
|
|
38
|
+
* @returns 解析后的 ParsedURL 对象
|
|
39
|
+
*/
|
|
40
|
+
declare function parseURL(url: string): ParsedURL;
|
|
41
|
+
/**
|
|
42
|
+
* 构建完整 URL
|
|
43
|
+
*
|
|
44
|
+
* @param base - 基础 URL
|
|
45
|
+
* @param params - 查询参数(可选)
|
|
46
|
+
* @param hash - hash 片段(可选,不含 #)
|
|
47
|
+
* @returns 构建后的完整 URL
|
|
48
|
+
*/
|
|
49
|
+
declare function buildURL(base: string, params?: Record<string, string | number | boolean>, hash?: string): string;
|
|
50
|
+
|
|
51
|
+
export { type ParsedURL, buildURL, parseQueryString, parseURL, stringifyQueryString };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function parseQueryString(search) {
|
|
3
|
+
if (!search) return {};
|
|
4
|
+
const str = search.startsWith("?") ? search.slice(1) : search;
|
|
5
|
+
if (!str) return {};
|
|
6
|
+
const result = {};
|
|
7
|
+
const pairs = str.split("&");
|
|
8
|
+
for (const pair of pairs) {
|
|
9
|
+
if (!pair) continue;
|
|
10
|
+
const eqIndex = pair.indexOf("=");
|
|
11
|
+
let key;
|
|
12
|
+
let value;
|
|
13
|
+
if (eqIndex === -1) {
|
|
14
|
+
key = pair;
|
|
15
|
+
value = "";
|
|
16
|
+
} else {
|
|
17
|
+
key = pair.slice(0, eqIndex);
|
|
18
|
+
value = pair.slice(eqIndex + 1);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
key = decodeURIComponent(key);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
value = decodeURIComponent(value);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
result[key] = value;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function stringifyQueryString(params) {
|
|
33
|
+
const keys = Object.keys(params);
|
|
34
|
+
if (keys.length === 0) return "";
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
const value = params[key];
|
|
38
|
+
parts.push(
|
|
39
|
+
encodeURIComponent(key) + "=" + encodeURIComponent(String(value))
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return parts.join("&");
|
|
43
|
+
}
|
|
44
|
+
function parseURL(url) {
|
|
45
|
+
let rest = url;
|
|
46
|
+
let hash = "";
|
|
47
|
+
const hashIndex = rest.indexOf("#");
|
|
48
|
+
if (hashIndex !== -1) {
|
|
49
|
+
hash = rest.slice(hashIndex);
|
|
50
|
+
rest = rest.slice(0, hashIndex);
|
|
51
|
+
}
|
|
52
|
+
let search = "";
|
|
53
|
+
const searchIndex = rest.indexOf("?");
|
|
54
|
+
if (searchIndex !== -1) {
|
|
55
|
+
search = rest.slice(searchIndex);
|
|
56
|
+
rest = rest.slice(0, searchIndex);
|
|
57
|
+
}
|
|
58
|
+
let protocol = "";
|
|
59
|
+
const protocolIndex = rest.indexOf("://");
|
|
60
|
+
if (protocolIndex !== -1) {
|
|
61
|
+
protocol = rest.slice(0, protocolIndex + 3);
|
|
62
|
+
rest = rest.slice(protocolIndex + 3);
|
|
63
|
+
}
|
|
64
|
+
let pathname = "";
|
|
65
|
+
let host = "";
|
|
66
|
+
let hostname = "";
|
|
67
|
+
let port = "";
|
|
68
|
+
if (protocol) {
|
|
69
|
+
const slashIndex = rest.indexOf("/");
|
|
70
|
+
if (slashIndex !== -1) {
|
|
71
|
+
host = rest.slice(0, slashIndex);
|
|
72
|
+
pathname = rest.slice(slashIndex);
|
|
73
|
+
} else {
|
|
74
|
+
host = rest;
|
|
75
|
+
pathname = "";
|
|
76
|
+
}
|
|
77
|
+
if (host.startsWith("[")) {
|
|
78
|
+
const bracketEnd = host.indexOf("]");
|
|
79
|
+
if (bracketEnd !== -1) {
|
|
80
|
+
hostname = host.slice(0, bracketEnd + 1);
|
|
81
|
+
if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ":") {
|
|
82
|
+
port = host.slice(bracketEnd + 2);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
hostname = host;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const colonIndex = host.lastIndexOf(":");
|
|
89
|
+
if (colonIndex !== -1) {
|
|
90
|
+
hostname = host.slice(0, colonIndex);
|
|
91
|
+
port = host.slice(colonIndex + 1);
|
|
92
|
+
} else {
|
|
93
|
+
hostname = host;
|
|
94
|
+
port = "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
pathname = rest;
|
|
99
|
+
}
|
|
100
|
+
const searchParams = parseQueryString(search);
|
|
101
|
+
const origin = protocol ? protocol + host : "";
|
|
102
|
+
return {
|
|
103
|
+
protocol,
|
|
104
|
+
host,
|
|
105
|
+
hostname,
|
|
106
|
+
port,
|
|
107
|
+
pathname,
|
|
108
|
+
search,
|
|
109
|
+
hash,
|
|
110
|
+
searchParams,
|
|
111
|
+
origin,
|
|
112
|
+
href: url
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function buildURL(base, params, hash) {
|
|
116
|
+
if (!params && !hash) return base;
|
|
117
|
+
let baseWithoutHash = base;
|
|
118
|
+
let existingHash = "";
|
|
119
|
+
const hashIdx = base.indexOf("#");
|
|
120
|
+
if (hashIdx !== -1) {
|
|
121
|
+
existingHash = base.slice(hashIdx);
|
|
122
|
+
baseWithoutHash = base.slice(0, hashIdx);
|
|
123
|
+
}
|
|
124
|
+
let existingSearch = "";
|
|
125
|
+
let baseWithoutSearch = baseWithoutHash;
|
|
126
|
+
const searchIdx = baseWithoutHash.indexOf("?");
|
|
127
|
+
if (searchIdx !== -1) {
|
|
128
|
+
existingSearch = baseWithoutHash.slice(searchIdx + 1);
|
|
129
|
+
baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
|
|
130
|
+
}
|
|
131
|
+
const mergedParams = parseQueryString(existingSearch);
|
|
132
|
+
if (params) {
|
|
133
|
+
for (const key of Object.keys(params)) {
|
|
134
|
+
mergedParams[key] = String(params[key]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const queryString = stringifyQueryString(mergedParams);
|
|
138
|
+
let result = baseWithoutSearch;
|
|
139
|
+
if (queryString) {
|
|
140
|
+
result += "?" + queryString;
|
|
141
|
+
}
|
|
142
|
+
const finalHash = hash !== void 0 ? "#" + encodeURIComponent(hash) : existingHash;
|
|
143
|
+
if (finalHash) {
|
|
144
|
+
result += finalHash;
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { buildURL, parseQueryString, parseURL, stringifyQueryString };
|
|
150
|
+
//# sourceMappingURL=index.mjs.map
|
|
151
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\r\n * @lytjs/common-query\r\n * URL 查询字符串解析与构建工具\r\n */\r\n\r\n/**\r\n * 解析后的 URL 结构\r\n */\r\nexport interface ParsedURL {\r\n protocol: string;\r\n host: string;\r\n hostname: string;\r\n port: string;\r\n pathname: string;\r\n search: string;\r\n hash: string;\r\n searchParams: Record<string, string>;\r\n origin: string;\r\n href: string;\r\n}\r\n\r\n/**\r\n * 解析 URL 查询字符串为对象\r\n *\r\n * @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式\r\n * @returns 解析后的键值对对象\r\n */\r\nexport function parseQueryString(search: string): Record<string, string> {\r\n if (!search) return {};\r\n\r\n const str = search.startsWith('?') ? search.slice(1) : search;\r\n if (!str) return {};\r\n\r\n const result: Record<string, string> = {};\r\n\r\n const pairs = str.split('&');\r\n for (const pair of pairs) {\r\n if (!pair) continue;\r\n const eqIndex = pair.indexOf('=');\r\n let key: string;\r\n let value: string;\r\n if (eqIndex === -1) {\r\n key = pair;\r\n value = '';\r\n } else {\r\n key = pair.slice(0, eqIndex);\r\n value = pair.slice(eqIndex + 1);\r\n }\r\n try {\r\n key = decodeURIComponent(key);\r\n } catch {\r\n // keep key as-is if decode fails\r\n }\r\n try {\r\n value = decodeURIComponent(value);\r\n } catch {\r\n // keep value as-is if decode fails\r\n }\r\n result[key] = value;\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 将对象序列化为查询字符串\r\n *\r\n * @param params - 键值对对象\r\n * @returns 序列化后的查询字符串(不含前导 ?)\r\n */\r\nexport function stringifyQueryString(\r\n params: Record<string, string | number | boolean>,\r\n): string {\r\n const keys = Object.keys(params);\r\n if (keys.length === 0) return '';\r\n\r\n const parts: string[] = [];\r\n for (const key of keys) {\r\n const value = params[key];\r\n parts.push(\r\n encodeURIComponent(key) + '=' + encodeURIComponent(String(value)),\r\n );\r\n }\r\n\r\n return parts.join('&');\r\n}\r\n\r\n/**\r\n * 解析完整 URL\r\n *\r\n * @param url - URL 字符串,支持绝对 URL 和相对 URL\r\n * @returns 解析后的 ParsedURL 对象\r\n */\r\nexport function parseURL(url: string): ParsedURL {\r\n let rest = url;\r\n\r\n // Extract hash\r\n let hash = '';\r\n const hashIndex = rest.indexOf('#');\r\n if (hashIndex !== -1) {\r\n hash = rest.slice(hashIndex);\r\n rest = rest.slice(0, hashIndex);\r\n }\r\n\r\n // Extract search\r\n let search = '';\r\n const searchIndex = rest.indexOf('?');\r\n if (searchIndex !== -1) {\r\n search = rest.slice(searchIndex);\r\n rest = rest.slice(0, searchIndex);\r\n }\r\n\r\n // Extract protocol\r\n let protocol = '';\r\n const protocolIndex = rest.indexOf('://');\r\n if (protocolIndex !== -1) {\r\n protocol = rest.slice(0, protocolIndex + 3);\r\n rest = rest.slice(protocolIndex + 3);\r\n }\r\n\r\n // Extract pathname (everything after host)\r\n let pathname = '';\r\n let host = '';\r\n let hostname = '';\r\n let port = '';\r\n\r\n if (protocol) {\r\n // Absolute URL: extract host and pathname\r\n const slashIndex = rest.indexOf('/');\r\n if (slashIndex !== -1) {\r\n host = rest.slice(0, slashIndex);\r\n pathname = rest.slice(slashIndex);\r\n } else {\r\n host = rest;\r\n pathname = '';\r\n }\r\n\r\n // Parse host into hostname and port\r\n // Handle IPv6 addresses like [::1]:8080\r\n if (host.startsWith('[')) {\r\n const bracketEnd = host.indexOf(']');\r\n if (bracketEnd !== -1) {\r\n hostname = host.slice(0, bracketEnd + 1);\r\n if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {\r\n port = host.slice(bracketEnd + 2);\r\n }\r\n } else {\r\n hostname = host;\r\n }\r\n } else {\r\n const colonIndex = host.lastIndexOf(':');\r\n if (colonIndex !== -1) {\r\n hostname = host.slice(0, colonIndex);\r\n port = host.slice(colonIndex + 1);\r\n } else {\r\n hostname = host;\r\n port = '';\r\n }\r\n }\r\n } else {\r\n // Relative URL\r\n pathname = rest;\r\n }\r\n\r\n const searchParams = parseQueryString(search);\r\n const origin = protocol ? protocol + host : '';\r\n\r\n return {\r\n protocol,\r\n host,\r\n hostname,\r\n port,\r\n pathname,\r\n search,\r\n hash,\r\n searchParams,\r\n origin,\r\n href: url,\r\n };\r\n}\r\n\r\n/**\r\n * 构建完整 URL\r\n *\r\n * @param base - 基础 URL\r\n * @param params - 查询参数(可选)\r\n * @param hash - hash 片段(可选,不含 #)\r\n * @returns 构建后的完整 URL\r\n */\r\nexport function buildURL(\r\n base: string,\r\n params?: Record<string, string | number | boolean>,\r\n hash?: string,\r\n): string {\r\n if (!params && !hash) return base;\r\n\r\n // Extract and remove existing hash from base\r\n let baseWithoutHash = base;\r\n let existingHash = '';\r\n const hashIdx = base.indexOf('#');\r\n if (hashIdx !== -1) {\r\n existingHash = base.slice(hashIdx);\r\n baseWithoutHash = base.slice(0, hashIdx);\r\n }\r\n\r\n // Parse existing query params from base (without hash)\r\n let existingSearch = '';\r\n let baseWithoutSearch = baseWithoutHash;\r\n const searchIdx = baseWithoutHash.indexOf('?');\r\n if (searchIdx !== -1) {\r\n existingSearch = baseWithoutHash.slice(searchIdx + 1);\r\n baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);\r\n }\r\n\r\n // Merge existing and new params\r\n const mergedParams = parseQueryString(existingSearch);\r\n if (params) {\r\n for (const key of Object.keys(params)) {\r\n mergedParams[key] = String(params[key]);\r\n }\r\n }\r\n\r\n const queryString = stringifyQueryString(mergedParams);\r\n let result = baseWithoutSearch;\r\n if (queryString) {\r\n result += '?' + queryString;\r\n }\r\n // Use provided hash, or keep existing hash if no new hash is given\r\n const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;\r\n if (finalHash) {\r\n result += finalHash;\r\n }\r\n\r\n return result;\r\n}\r\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lytjs/common-query",
|
|
3
|
+
"version": "6.0.0",
|
|
4
|
+
"description": "URL query string parsing and building utilities for LytJS",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"test:coverage": "vitest run --coverage",
|
|
23
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"type-check": "tsc --noEmit",
|
|
26
|
+
"clean": "rm -rf dist"
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"tsup": "^8.4.0",
|
|
32
|
+
"typescript": "^5.8.2",
|
|
33
|
+
"vitest": "^3.0.7"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/env.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lytjs/common-query
|
|
3
|
+
* URL 查询字符串解析与构建工具
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 解析后的 URL 结构
|
|
8
|
+
*/
|
|
9
|
+
export interface ParsedURL {
|
|
10
|
+
protocol: string;
|
|
11
|
+
host: string;
|
|
12
|
+
hostname: string;
|
|
13
|
+
port: string;
|
|
14
|
+
pathname: string;
|
|
15
|
+
search: string;
|
|
16
|
+
hash: string;
|
|
17
|
+
searchParams: Record<string, string>;
|
|
18
|
+
origin: string;
|
|
19
|
+
href: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 解析 URL 查询字符串为对象
|
|
24
|
+
*
|
|
25
|
+
* @param search - 查询字符串,支持 `?key=value` 和 `key=value` 格式
|
|
26
|
+
* @returns 解析后的键值对对象
|
|
27
|
+
*/
|
|
28
|
+
export function parseQueryString(search: string): Record<string, string> {
|
|
29
|
+
if (!search) return {};
|
|
30
|
+
|
|
31
|
+
const str = search.startsWith('?') ? search.slice(1) : search;
|
|
32
|
+
if (!str) return {};
|
|
33
|
+
|
|
34
|
+
const result: Record<string, string> = {};
|
|
35
|
+
|
|
36
|
+
const pairs = str.split('&');
|
|
37
|
+
for (const pair of pairs) {
|
|
38
|
+
if (!pair) continue;
|
|
39
|
+
const eqIndex = pair.indexOf('=');
|
|
40
|
+
let key: string;
|
|
41
|
+
let value: string;
|
|
42
|
+
if (eqIndex === -1) {
|
|
43
|
+
key = pair;
|
|
44
|
+
value = '';
|
|
45
|
+
} else {
|
|
46
|
+
key = pair.slice(0, eqIndex);
|
|
47
|
+
value = pair.slice(eqIndex + 1);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
key = decodeURIComponent(key);
|
|
51
|
+
} catch {
|
|
52
|
+
// keep key as-is if decode fails
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
value = decodeURIComponent(value);
|
|
56
|
+
} catch {
|
|
57
|
+
// keep value as-is if decode fails
|
|
58
|
+
}
|
|
59
|
+
result[key] = value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 将对象序列化为查询字符串
|
|
67
|
+
*
|
|
68
|
+
* @param params - 键值对对象
|
|
69
|
+
* @returns 序列化后的查询字符串(不含前导 ?)
|
|
70
|
+
*/
|
|
71
|
+
export function stringifyQueryString(
|
|
72
|
+
params: Record<string, string | number | boolean>,
|
|
73
|
+
): string {
|
|
74
|
+
const keys = Object.keys(params);
|
|
75
|
+
if (keys.length === 0) return '';
|
|
76
|
+
|
|
77
|
+
const parts: string[] = [];
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
const value = params[key];
|
|
80
|
+
parts.push(
|
|
81
|
+
encodeURIComponent(key) + '=' + encodeURIComponent(String(value)),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parts.join('&');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 解析完整 URL
|
|
90
|
+
*
|
|
91
|
+
* @param url - URL 字符串,支持绝对 URL 和相对 URL
|
|
92
|
+
* @returns 解析后的 ParsedURL 对象
|
|
93
|
+
*/
|
|
94
|
+
export function parseURL(url: string): ParsedURL {
|
|
95
|
+
let rest = url;
|
|
96
|
+
|
|
97
|
+
// Extract hash
|
|
98
|
+
let hash = '';
|
|
99
|
+
const hashIndex = rest.indexOf('#');
|
|
100
|
+
if (hashIndex !== -1) {
|
|
101
|
+
hash = rest.slice(hashIndex);
|
|
102
|
+
rest = rest.slice(0, hashIndex);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract search
|
|
106
|
+
let search = '';
|
|
107
|
+
const searchIndex = rest.indexOf('?');
|
|
108
|
+
if (searchIndex !== -1) {
|
|
109
|
+
search = rest.slice(searchIndex);
|
|
110
|
+
rest = rest.slice(0, searchIndex);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Extract protocol
|
|
114
|
+
let protocol = '';
|
|
115
|
+
const protocolIndex = rest.indexOf('://');
|
|
116
|
+
if (protocolIndex !== -1) {
|
|
117
|
+
protocol = rest.slice(0, protocolIndex + 3);
|
|
118
|
+
rest = rest.slice(protocolIndex + 3);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract pathname (everything after host)
|
|
122
|
+
let pathname = '';
|
|
123
|
+
let host = '';
|
|
124
|
+
let hostname = '';
|
|
125
|
+
let port = '';
|
|
126
|
+
|
|
127
|
+
if (protocol) {
|
|
128
|
+
// Absolute URL: extract host and pathname
|
|
129
|
+
const slashIndex = rest.indexOf('/');
|
|
130
|
+
if (slashIndex !== -1) {
|
|
131
|
+
host = rest.slice(0, slashIndex);
|
|
132
|
+
pathname = rest.slice(slashIndex);
|
|
133
|
+
} else {
|
|
134
|
+
host = rest;
|
|
135
|
+
pathname = '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Parse host into hostname and port
|
|
139
|
+
// Handle IPv6 addresses like [::1]:8080
|
|
140
|
+
if (host.startsWith('[')) {
|
|
141
|
+
const bracketEnd = host.indexOf(']');
|
|
142
|
+
if (bracketEnd !== -1) {
|
|
143
|
+
hostname = host.slice(0, bracketEnd + 1);
|
|
144
|
+
if (host.length > bracketEnd + 1 && host[bracketEnd + 1] === ':') {
|
|
145
|
+
port = host.slice(bracketEnd + 2);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
hostname = host;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
const colonIndex = host.lastIndexOf(':');
|
|
152
|
+
if (colonIndex !== -1) {
|
|
153
|
+
hostname = host.slice(0, colonIndex);
|
|
154
|
+
port = host.slice(colonIndex + 1);
|
|
155
|
+
} else {
|
|
156
|
+
hostname = host;
|
|
157
|
+
port = '';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// Relative URL
|
|
162
|
+
pathname = rest;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const searchParams = parseQueryString(search);
|
|
166
|
+
const origin = protocol ? protocol + host : '';
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
protocol,
|
|
170
|
+
host,
|
|
171
|
+
hostname,
|
|
172
|
+
port,
|
|
173
|
+
pathname,
|
|
174
|
+
search,
|
|
175
|
+
hash,
|
|
176
|
+
searchParams,
|
|
177
|
+
origin,
|
|
178
|
+
href: url,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 构建完整 URL
|
|
184
|
+
*
|
|
185
|
+
* @param base - 基础 URL
|
|
186
|
+
* @param params - 查询参数(可选)
|
|
187
|
+
* @param hash - hash 片段(可选,不含 #)
|
|
188
|
+
* @returns 构建后的完整 URL
|
|
189
|
+
*/
|
|
190
|
+
export function buildURL(
|
|
191
|
+
base: string,
|
|
192
|
+
params?: Record<string, string | number | boolean>,
|
|
193
|
+
hash?: string,
|
|
194
|
+
): string {
|
|
195
|
+
if (!params && !hash) return base;
|
|
196
|
+
|
|
197
|
+
// Extract and remove existing hash from base
|
|
198
|
+
let baseWithoutHash = base;
|
|
199
|
+
let existingHash = '';
|
|
200
|
+
const hashIdx = base.indexOf('#');
|
|
201
|
+
if (hashIdx !== -1) {
|
|
202
|
+
existingHash = base.slice(hashIdx);
|
|
203
|
+
baseWithoutHash = base.slice(0, hashIdx);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Parse existing query params from base (without hash)
|
|
207
|
+
let existingSearch = '';
|
|
208
|
+
let baseWithoutSearch = baseWithoutHash;
|
|
209
|
+
const searchIdx = baseWithoutHash.indexOf('?');
|
|
210
|
+
if (searchIdx !== -1) {
|
|
211
|
+
existingSearch = baseWithoutHash.slice(searchIdx + 1);
|
|
212
|
+
baseWithoutSearch = baseWithoutHash.slice(0, searchIdx);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Merge existing and new params
|
|
216
|
+
const mergedParams = parseQueryString(existingSearch);
|
|
217
|
+
if (params) {
|
|
218
|
+
for (const key of Object.keys(params)) {
|
|
219
|
+
mergedParams[key] = String(params[key]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const queryString = stringifyQueryString(mergedParams);
|
|
224
|
+
let result = baseWithoutSearch;
|
|
225
|
+
if (queryString) {
|
|
226
|
+
result += '?' + queryString;
|
|
227
|
+
}
|
|
228
|
+
// Use provided hash, or keep existing hash if no new hash is given
|
|
229
|
+
const finalHash = hash !== undefined ? '#' + encodeURIComponent(hash) : existingHash;
|
|
230
|
+
if (finalHash) {
|
|
231
|
+
result += finalHash;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
}
|