urlify-utils
v1.0.1
Published
A comprehensive, native-like, zero-learning-cost URL toolkit for frontend development
Maintainers
Readme
URLify 设计文档
背景与目标
前端开发中,URL 操作是一个高频且繁琐的场景。原生 URL API 虽然功能完备,但在实际使用中存在以下痛点:
- 提取困难:获取 query 的某个字段需要写
url.searchParams.get('key'),无法直接拿到对象形式。 - 拼接繁琐:构建一个带参数的 URL 需要手动拼接字符串,容易出错且代码冗长。
- 缺乏工具方法:如获取相对路径、路径分段、文件扩展名等常用功能需自行实现。
- 类型安全:原生 API 缺乏 TypeScript 类型支持,开发体验不佳。
目标:打造一个大而全、类原生、零学习成本的 URL 瑞士军刀工具库(Browser + Node.js)。
架构设计
核心思路
- 核心类
URLify:继承并增强原生URL,100% 兼容原生属性与行为。 - 不可变更新:所有修改操作(如
withQuery,withPath)均返回新实例,避免副作用。 - 工具函数剥离:静态方法提供纯函数式工具(如
URLify.join,URLify.isValid),便于 tree-shaking。
核心 API 设计
1. 实例属性(兼容原生 + 增强)
原生属性(继承自 URL)
⚠️ 注意:为了支持不可变更新,URLify 的所有属性均为 readonly。如果使用者需要修改 URL,请使用 with... 方法返回新实例。这是唯一与原生 URL 不兼容的地方。
| 属性名 | 类型 | 示例 |
|--------|------|------|
| href | readonly string | 'https://user:[email protected]:8080/path?query=string#hash' |
| protocol | string | 'https:' |
| host | string | 'example.com:8080' |
| hostname | string | 'example.com' |
| port | string | '8080' |
| pathname | string | '/path' |
| search | string | '?query=string' |
| hash | string | '#hash' |
| username | string | 'user' |
| password | string | 'pass' |
| origin | string | 'https://example.com:8080' |
增强属性
| 属性名 | 类型 | 说明 | 示例 |
|--------|------|------|------|
| query | Record<string, string \| string[]> | 解析后的 query 对象 | { query: 'string' } |
| queryKeys | string[] | query 中的所有键名 | ['query'] |
| pathSegments | string[] | 路径分段数组 | ['path'] |
| isAbsolute | boolean | 是否为绝对 URL | true |
| hasTrailingSlash | boolean | 路径是否以 / 结尾 | false |
| fileExtension | string \| undefined | 文件扩展名 | undefined |
2. 实例方法
| 方法名 | 参数 | 返回 | 说明 |
|--------|------|------|------|
| toString() | - | string | 返回完整 URL 字符串 |
| toJSON() | - | object | 返回包含所有属性的纯对象 |
| toRelative() | - | string | 返回相对路径 '/path?query=string#hash' |
| clone() | - | URLify | 深拷贝实例 |
| getQuery(key) | string | string \| string[] \| undefined | 获取指定 query 参数 |
| withQuery(query) | object | URLify | 不可变 合并/覆盖 query,返回新实例 |
| withoutQuery(key) | string \| string[] | URLify | 不可变 删除指定 query(支持批量删除),返回新实例 |
| setQuery(query) | object | URLify | 不可变 完全替换所有 query 参数,返回新实例 |
| clearQuery() | - | URLify | 不可变 清空所有 query 参数,返回新实例 |
| withPath(path) | string | URLify | 不可变 替换路径,返回新实例 |
| withHash(hash) | string | URLify | 不可变 替换 hash,返回新实例 |
3. 静态工具方法
| 方法名 | 参数 | 返回 | 说明 |
|--------|------|------|------|
| URLify.parse(url) | string | URLify | 等同 new URLify(url) |
| URLify.isValid(url) | string | boolean | 校验 URL 格式是否合法 |
| URLify.join(...parts) | ...string[] | string | 安全拼接 URL 路径(自动去重斜杠) |
| URLify.fromObject(obj) | object | URLify | 从对象 { protocol, host, pathname, ... } 构建 URL |
TypeScript 类型定义
declare class URLify {
// 原生属性
readonly href: string;
readonly protocol: string;
readonly host: string;
readonly hostname: string;
readonly port: string;
readonly pathname: string;
readonly search: string;
readonly hash: string;
readonly username: string;
readonly password: string;
readonly origin: string;
// 增强属性
readonly query: Record<string, string | string[]>;
readonly queryKeys: string[];
readonly pathSegments: string[];
readonly isAbsolute: boolean;
readonly hasTrailingSlash: boolean;
readonly fileExtension?: string;
constructor(url: string | URL, base?: string | URL);
// 实例方法
toString(): string;
toJSON(): URLifyJSON;
toRelative(): string;
clone(): URLify;
getQuery(key: string): string | string[] | undefined;
withQuery(query: Record<string, string | number | boolean | undefined>): URLify;
withoutQuery(key: string | string[]): URLify;
withPath(path: string): URLify;
withHash(hash: string): URLify;
// 静态方法
static parse(url: string): URLify;
static isValid(url: string): boolean;
static join(...parts: string[]): string;
static fromObject(obj: {
protocol?: string;
username?: string;
password?: string;
hostname?: string;
port?: string | number;
pathname?: string;
search?: string;
hash?: string;
query?: Record<string, string | string[]>;
}): URLify;
}
// toJSON 返回的完整结构
interface URLifyJSON {
href: string;
protocol: string;
host: string;
hostname: string;
port: string;
pathname: string;
search: string;
hash: string;
username: string;
password: string;
origin: string;
query: Record<string, string | string[]>;
queryKeys: string[];
pathSegments: string[];
isAbsolute: boolean;
hasTrailingSlash: boolean;
fileExtension?: string;
}使用示例
import { URLify } from 'urlify';
// 基础解析
const url = new URLify('https://example.com/api/v1/users?page=1&sort=desc&sort=asc');
url.query; // { page: '1', sort: ['desc', 'asc'] }
url.pathSegments; // ['api', 'v1', 'users']
url.getQuery('sort'); // ['desc', 'asc']
// 不可变更新
const newUrl = url.withQuery({ page: 2 }).withoutQuery('sort');
newUrl.toString(); // 'https://example.com/api/v1/users?page=2'
// 静态工具
URLify.isValid('not-a-url'); // false
URLify.join('https://example.com/', '/api/', '/users'); // 'https://example.com/api/users'边界情况与错误处理
- 非法 URL 输入:
const url = new URLify('bad-url')→ 抛出TypeError: Invalid URL(与原生URL保持一致)。 - 无协议 URL:需要传入
base参数,如new URLify('/path', 'https://example.com')。不支持无 base 的相对 URL,会抛出TypeError(与原生URL保持一致)。 isAbsolute判断:基于传入的url字符串是否以协议开头(如https://)来判断。如果是通过base参数构建的 URL,isAbsolute为true。- 特殊字符编码:内部使用原生
URLSearchParams处理,确保与浏览器行为一致。 - 数组参数:
query属性中,同名参数自动聚合为数组。- 单个参数:
?foo=1→{ foo: '1' } - 多个同名参数:
?foo=1&foo=2→{ foo: ['1', '2'] }
- 单个参数:
构建配置
- 打包工具:Rollup(输出 ESM + CJS + UMD + .d.ts)
- 语言:TypeScript
- 测试:Vitest(覆盖率 > 90%)
- 代码规范:ESLint + Prettier
- 包大小:目标 < 5KB (gzip)
- 导出策略:同时提供 named exports(如
import { join, isValid } from 'urlify')和 class 的 static 方法,确保真正的 Tree-shaking 能力。
依赖
- 零运行时依赖
- 开发依赖:TypeScript、Rollup、Vitest、ESLint、Prettier
