# URL拼接最佳实践

## 【背景意义】

在API开发中，URL拼接是常见操作，但不当的拼接方式会导致重复斜杠、路径错误等问题。科学的URL拼接能提高代码可靠性和可维护性。

## 【概念定义】

### URL拼接问题对比表

| 问题类型 | 错误示例 | 正确示例 | 影响 |
|---------|---------|---------|------|
| 重复斜杠 | `http://api.com//path` | `http://api.com/path` | 可能导致404错误 |
| 缺少斜杠 | `http://api.compath` | `http://api.com/path` | 路径解析错误 |
| 路径混乱 | `http://api.com/path//sub` | `http://api.com/path/sub` | 服务器解析异常 |
| 协议破坏 | `http:/api.com/path` | `http://api.com/path` | 无法访问 |

### 常见拼接方式对比表

| 拼接方式 | 优点 | 缺点 | 适用场景 |
|---------|------|------|---------|
| 字符串模板 | 简单直观 | 容易出错，难维护 | 简单固定路径 |
| 手动处理 | 灵活控制 | 代码冗余，易出错 | 特殊需求 |
| 工具函数 | 安全可靠，可复用 | 需要额外实现 | 生产环境推荐 |
| 第三方库 | 功能完善 | 增加依赖 | 复杂项目 |

## 【使用流程】

### 步骤1:创建URL工具类

```typescript
// src/utils/url.util.ts
export class UrlUtil {
  /**
   * 安全地拼接URL路径
   * @param baseUrl 基础URL - 可以带或不带尾部斜杠
   * @param paths 路径片段数组 - 可以带或不带前后斜杠
   * @returns 拼接后的完整URL
   */
  static joinUrl(baseUrl: string, ...paths: string[]): string {
    // 移除baseUrl的尾部斜杠
    let result = baseUrl.replace(/\/+$/, '');
    
    // 处理每个路径片段
    for (const path of paths) {
      if (path) {
        // 移除路径的首尾斜杠，然后添加单个斜杠前缀
        const cleanPath = path.replace(/^\/+|\/+$/g, '');
        if (cleanPath) {
          result += '/' + cleanPath;
        }
      }
    }
    
    return result;
  }

  /**
   * 标准化URL路径(移除重复斜杠）
   * @param url 原始URL
   * @returns 标准化后的URL
   */
  static normalizeUrl(url: string): string {
    // 保护协议部分的双斜杠(如 http://）
    const protocolMatch = url.match(/^([a-zA-Z][a-zA-Z\d+\-.]*:\/\/)/);
    const protocol = protocolMatch ? protocolMatch[1] : '';
    const restUrl = protocolMatch ? url.slice(protocol.length) : url;
    
    // 移除重复斜杠，但保留协议部分
    return protocol + restUrl.replace(/\/+/g, '/');
  }
}
```

### 步骤2:创建专用API URL构建器

```typescript
// 扩展UrlUtil类，添加WordPress专用方法
export class UrlUtil {
  // ... 基础方法 ...

  /**
   * 构建WordPress API URL
   * @param wpApiUrl WordPress站点URL
   * @param endpoint API端点路径
   * @returns 完整的API URL
   */
  static buildWpApiUrl(wpApiUrl: string, endpoint: string): string {
    return this.joinUrl(wpApiUrl, 'wp-json', endpoint);
  }

  /**
   * 构建WooCommerce API URL
   * @param wpApiUrl WordPress站点URL
   * @param endpoint WooCommerce API端点
   * @param version API版本，默认为v3
   * @returns 完整的WooCommerce API URL
   */
  static buildWcApiUrl(wpApiUrl: string, endpoint: string, version: string = 'v3'): string {
    return this.joinUrl(wpApiUrl, 'wp-json', 'wc', version, endpoint);
  }
}
```

### 步骤3:在服务中应用

```typescript
// src/service/wp.service.ts
import { UrlUtil } from '../utils/url.util';

@Provide()
export class WPService {
  async fetchPagedData<T>(
    endpoint: string,
    site: Site,
    page: number = 1,
    perPage: number = 100
  ): Promise<T[]> {
    const { wpApiUrl, consumerKey, consumerSecret } = site;
    
    // 原始方式(容易出错）
    // const url = `${wpApiUrl}/wp-json${endpoint}`;
    
    // 推荐方式(安全可靠）
    const url = UrlUtil.buildWpApiUrl(wpApiUrl, endpoint);
    
    // ... 其他代码
  }

  async createShipment(site: Site, orderId: string, data: Record<string, any>) {
    const { wpApiUrl, consumerKey, consumerSecret } = site;
    
    // 复杂路径拼接示例
    const url = UrlUtil.joinUrl(
      wpApiUrl, 
      'wp-json', 
      'wc-ast', 
      'v3', 
      'orders', 
      orderId, 
      'shipment-trackings'
    );
    
    // ... 其他代码
  }
}
```

## 实际应用示例

### 问题场景演示

```typescript
// 问题场景:配置文件中的URL格式不统一
const configs = [
  { wpApiUrl: 'http://wp-test.local/' },    // 带尾部斜杠
  { wpApiUrl: 'http://wp-test.local' },     // 不带尾部斜杠
  { wpApiUrl: 'https://wp-prod.com/' },     // HTTPS带斜杠
];

const endpoints = [
  '/wc/v3/orders',     // 带前缀斜杠
  'wc/v3/products',    // 不带前缀斜杠
  '//wc/v3/customers', // 多重斜杠
];

// 原始拼接方式的问题
configs.forEach(config => {
  endpoints.forEach(endpoint => {
    const badUrl = `${config.wpApiUrl}/wp-json${endpoint}`;
    console.log('问题URL:', badUrl);
    // 输出可能包含:
    // http://wp-test.local//wp-json/wc/v3/orders
    // http://wp-test.localwp-json/wc/v3/products
  });
});
```

### 解决方案演示

```typescript
// 使用UrlUtil的安全拼接
configs.forEach(config => {
  endpoints.forEach(endpoint => {
    const safeUrl = UrlUtil.buildWpApiUrl(config.wpApiUrl, endpoint);
    console.log('安全URL:', safeUrl);
    // 输出始终正确:
    // http://wp-test.local/wp-json/wc/v3/orders
    // http://wp-test.local/wp-json/wc/v3/products
  });
});
```

## 测试用例

### 单元测试示例

```typescript
// tests/utils/url.util.test.ts
describe('UrlUtil', () => {
  describe('joinUrl', () => {
    it('应该正确处理带尾部斜杠的baseUrl', () => {
      const result = UrlUtil.joinUrl('http://api.com/', 'path', 'sub');
      expect(result).toBe('http://api.com/path/sub');
    });

    it('应该正确处理不带尾部斜杠的baseUrl', () => {
      const result = UrlUtil.joinUrl('http://api.com', '/path/', '/sub/');
      expect(result).toBe('http://api.com/path/sub');
    });

    it('应该处理空路径片段', () => {
      const result = UrlUtil.joinUrl('http://api.com', '', 'path', null, 'sub');
      expect(result).toBe('http://api.com/path/sub');
    });
  });

  describe('buildWpApiUrl', () => {
    it('应该构建正确的WordPress API URL', () => {
      const result = UrlUtil.buildWpApiUrl('http://wp.local/', '/wc/v3/orders');
      expect(result).toBe('http://wp.local/wp-json/wc/v3/orders');
    });
  });
});
```

## 性能优化建议

### 性能对比表

| 方案 | 执行时间 | 内存占用 | 代码复杂度 | 推荐指数 |
|------|---------|---------|-----------|---------|
| 字符串模板 | 最快 | 最低 | 低 | ⭐⭐ |
| 正则处理 | 中等 | 中等 | 中 | ⭐⭐⭐⭐ |
| 工具函数 | 稍慢 | 稍高 | 低 | ⭐⭐⭐⭐⭐ |
| 第三方库 | 最慢 | 最高 | 最低 | ⭐⭐⭐ |

### 缓存优化

```typescript
// 为频繁调用的URL拼接添加缓存
export class UrlUtil {
  private static urlCache = new Map<string, string>();

  static joinUrlCached(baseUrl: string, ...paths: string[]): string {
    const cacheKey = `${baseUrl}|${paths.join('|')}`;
    
    if (this.urlCache.has(cacheKey)) {
      return this.urlCache.get(cacheKey)!;
    }
    
    const result = this.joinUrl(baseUrl, ...paths);
    this.urlCache.set(cacheKey, result);
    
    return result;
  }
}
```

## 最佳实践总结

### 开发规范表

| 规范项 | 要求 | 示例 | 备注 |
|--------|------|------|------|
| 基础URL | 统一格式，可带可不带尾部斜杠 | `http://api.com` | 工具会自动处理 |
| 路径片段 | 使用数组传递，避免手动拼接 | `['wp-json', 'wc', 'v3']` | 提高可读性 |
| 变量路径 | 作为独立参数传递 | `orderId`, `productId` | 便于参数验证 |
| 错误处理 | 验证URL格式和参数有效性 | 检查空值、特殊字符 | 提高健壮性 |

### 代码审查清单

- [ ] 是否使用了UrlUtil工具类
- [ ] 是否避免了字符串模板拼接URL
- [ ] 是否处理了空值和特殊字符
- [ ] 是否添加了适当的注释
- [ ] 是否编写了单元测试

## 参考文档

- [MDN URL API文档](https://developer.mozilla.org/en-US/docs/Web/API/URL)
- [Node.js URL模块](https://nodejs.org/api/url.html)
- [RFC 3986 URI规范](https://tools.ietf.org/html/rfc3986)