---
title: 开发文档
order: 3
category: pro
---

﻿# PisellUrl 组件开发文档

## 1 组件信息

### 1.1 基本信息

- **组件名称**：PisellUrl
- **组件版本**：V1.0.0
- **组件分类**：表单输入 / 文本展示
- **开发状态**：开发中
- **最后更新**：2026-01-25
- **设计稿链接
  **：https://www.figma.com/design/pM8Ho6d7kCMv9vIBvFHUlj/Pisell-2.0-Component-Library?node-id=3373-5841&m=dev

### 1.2 组件描述

PisellUrl 是一个专注于 URL 链接处理的输入/展示组件，支持只读、编辑、禁用三种状态模式。组件基于 Ant Design Input 组件开发，提供
URL 格式校验、协议自动补全、链接跳转和协议/路径显示控制功能，并复用 PisellSingleLineText 的公共逻辑。

## 2 需求点罗列

### 2.1 功能需求

#### 核心功能

- ✅ 支持三种状态：只读态、编辑态、禁用态
- ✅ 支持受控和非受控两种使用模式
- ✅ 基于 Ant Design Input 组件封装
- ✅ 完整的校验框架（复用 @pisell/utils）
- ✅ 复用公共逻辑（@pisell/utils）

#### URL 特有功能

- ✅ URL 格式校验（使用原生 URL 构造函数）
- ✅ 协议自动补全（http/https）
- ✅ 协议白名单限制
- ✅ 链接跳转功能（新窗口打开，安全性保障）
- ✅ 协议/路径显示控制（隐藏协议、www、路径）

#### 展示功能（只读态）

- ✅ 显示 URL 地址
- ✅ 点击新窗口打开链接
- ✅ 协议/路径隐藏
- ✅ 链接图标（可选）
- ✅ 超链接样式

#### 编辑功能（编辑态）

- ✅ 失焦时自动补全协议
- ✅ 实时格式校验
- ✅ 错误提示展示
- ✅ 支持链接图标前缀

#### 校验功能

- ✅ 必填校验
- ✅ 格式校验（URL 构造函数）
- ✅ 协议白名单校验
- ✅ 协议必需校验
- ✅ 自定义校验
- ✅ 错误提示展示

### 2.2 非功能需求

- ✅ TypeScript 类型定义完整
- ✅ 代码注释使用 JSDoc 规范
- ✅ 性能优化（React.memo、useCallback、useMemo）
- ✅ 代码复用（hooks 和 utils 来自 @pisell/utils）
- ✅ 安全性保障（noopener noreferrer）

## 3 组件 API

### 3.1 Props 定义

#### 核心属性（继承自 BaseTextInputProps）

| 属性名          | 类型               | 默认值    | 必填 | 说明       |
|--------------|------------------|--------|----|----------|
| value        | string           | -      | 否  | URL 地址   |
| defaultValue | string           | -      | 否  | 默认值（非受控） |
| mode         | 'read' \| 'edit' | 'edit' | 否  | 状态模式     |
| disabled     | boolean          | false  | 否  | 是否禁用     |

#### URL 特有配置

| 属性名                  | 类型                             | 默认值               | 必填 | 说明            |
|----------------------|--------------------------------|-------------------|----|---------------|
| strictValidation     | boolean                        | true              | 否  | 是否使用严格校验      |
| allowedProtocols     | string[]                       | ['http', 'https'] | 否  | 允许的协议列表       |
| requireProtocol      | boolean                        | false             | 否  | 是否要求必须有协议     |
| autoCompleteProtocol | boolean                        | true              | 否  | 是否自动补全协议      |
| defaultProtocol      | 'http://' \| 'https://'        | 'https://'        | 否  | 默认协议          |
| onProtocolComplete   | (url: string) => void          | -                 | 否  | 协议补全回调        |
| enableLinkClick      | boolean                        | true              | 否  | 是否启用链接跳转（只读态） |
| openInNewWindow      | boolean                        | true              | 否  | 是否新窗口打开       |
| showLinkIcon         | boolean                        | true              | 否  | 是否显示链接图标      |
| onLinkClick          | (url: string) => void          | -                 | 否  | 链接点击回调        |
| showProtocol         | boolean                        | false             | 否  | 是否显示协议（只读态）   |
| showWww              | boolean                        | false             | 否  | 是否显示 www（只读态） |
| showPathname         | boolean                        | true              | 否  | 是否显示路径（只读态）   |
| displayFormat        | 'full' \| 'domain' \| 'custom' | 'full'            | 否  | 显示格式（只读态）     |
| customFormat         | (url: string) => string        | -                 | 否  | 自定义格式化函数      |

#### 展示配置（只读态）

| 属性名        | 类型               | 默认值     | 必填 | 说明        |
|------------|------------------|---------|----|-----------|
| fontSize   | string \| number | 16      | 否  | 字号        |
| fontWeight | string \| number | 500     | 否  | 字重        |
| color      | string           | #101828 | 否  | 文本颜色      |
| linkColor  | string           | #7F56DA | 否  | 链接颜色（只读态） |

#### 编辑配置（编辑态）

| 属性名         | 类型      | 默认值        | 必填 | 说明     |
|-------------|---------|------------|----|--------|
| placeholder | string  | 请输入 URL 地址 | 否  | 占位文本   |
| maxLength   | number  | 2048       | 否  | 最大长度   |
| allowClear  | boolean | false      | 否  | 显示清空按钮 |
| autoFocus   | boolean | false      | 否  | 自动聚焦   |

#### 校验配置

| 属性名             | 类型                                 | 默认值      | 必填 | 说明      |
|-----------------|------------------------------------|----------|----|---------|
| required        | boolean                            | false    | 否  | 是否必填    |
| customValidator | (url: string) => boolean \| string | -        | 否  | 自定义校验函数 |
| validateTrigger | 'onChange' \| 'onBlur' \| 'both'   | 'onBlur' | 否  | 校验触发时机  |
| errorMessage    | string                             | -        | 否  | 自定义错误提示 |

#### 事件回调

| 属性名                | 类型                                           | 说明     |
|--------------------|----------------------------------------------|--------|
| onChange           | (value: string) => void                      | 值变化回调  |
| onBlur             | (value: string) => void                      | 失焦回调   |
| onFocus            | () => void                                   | 聚焦回调   |
| onLinkClick        | (url: string) => void                        | 链接点击回调 |
| onProtocolComplete | (url: string) => void                        | 协议补全回调 |
| onValidate         | (isValid: boolean, errors: string[]) => void | 校验回调   |

## 4 组件模块拆解

### 4.1 目录结构

pisellUrl/
├── index.tsx # 导出文件
├── PisellUrl.tsx # 组件主文件
├── PisellUrl.less # 样式文件
├── PisellUrl.test.tsx # 单元测试
├── PisellUrl.stories.tsx # Storybook 故事
├── types.ts # 类型定义
├── utils/ # 工具函数（URL 特有）
│ ├── index.ts
│ ├── urlValidator.ts # URL 校验工具
│ ├── urlHelper.ts # URL 辅助函数
│ └── urlFormatter.ts # URL 格式化工具
├── components/ # 子组件
│ ├── ReadOnlyView.tsx # 只读态视图（链接跳转）
│ ├── EditView.tsx # 编辑态视图（协议补全）
│ └── DisabledView.tsx # 禁用态视图
└── docs/ # 文档
├── pisellUrl.md # 使用文档
├── pisellUrl.$tab-design.md # 设计文档
├── pisellUrl.$tab-dev.md # 开发文档（本文档）
└── pisellUrl.$tab-test.md # 测试文档

### 4.2 模块职责

#### 主组件（PisellUrl.tsx）

- 统筹组件渲染逻辑
- 根据 mode 和 disabled 决定渲染哪个子组件
- 管理组件整体状态

#### 子组件

**EditView.tsx**（核心差异）

- 职责：渲染编辑态输入
- 功能：
  - 使用 Ant Design Input 组件
  - 失焦时自动补全协议（如果未输入）
  - 实时 URL 格式校验
  - 校验和错误提示
- 依赖：Ant Design Input、LinkOutlined

**ReadOnlyView.tsx**

- 职责：渲染只读态展示和链接跳转
- 功能：
  - 显示 URL 地址（可选隐藏协议/路径）
  - 点击新窗口打开链接（安全性保障）
  - 可选链接图标
  - 超链接样式
- 依赖：Ant Design Typography.Link/Text、LinkOutlined

**DisabledView.tsx**

- 职责：渲染禁用态视图
- 功能：基于原始 mode 渲染，透明度 40%
- 依赖：ReadOnlyView 或 EditView

#### 工具函数（utils/）

**urlValidator.ts**

- validateUrl - URL 格式校验（使用 URL 构造函数）
- validateUrlByRegex - 正则校验（备选）
- parseUrl - 解析 URL 各部分
- validateUrlProtocol - 协议校验（白名单）

**urlHelper.ts**

- normalizeUrl - 规范化 URL（补全协议）
- smartNormalizeUrl - 智能补全（localhost 用 http）
- handleUrlClick - 处理链接跳转（安全性）
- extractDomain - 提取域名
- isSecureUrl - 检测是否为 HTTPS

**urlFormatter.ts**

- formatUrlDisplay - 格式化 URL 显示
- hideUrlProtocol - 隐藏协议
- hideWwwPrefix - 隐藏 www
- extractUrlParams - 提取 URL 参数

## 5 组件实现方案

### 5.1 技术栈

- React 18+
- TypeScript
- Ant Design 5.x（Input、Typography、Space）
- Less（样式）
- 公共逻辑来自 @pisell/utils

### 5.2 核心实现

#### 5.2.1 URL 格式校验

```
// utils/urlValidator.ts

/**
 * URL 格式校验（推荐方法）
 * 使用原生 URL 构造函数
 * @param url URL 地址
 * @returns 是否通过校验
 */
export function validateUrl(url: string): boolean {
  if (!url) return true;
  
  try {
    new URL(url);
    return true;
  } catch (error) {
    return false;
  }
}

/**
 * URL 正则校验（备选方法）
 * @param url URL 地址
 * @returns 是否通过校验
 */
export function validateUrlByRegex(url: string): boolean {
  if (!url) return true;
  
  const URL_REGEX = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
  return URL_REGEX.test(url);
}

/**
 * 解析 URL 各部分
 * @param url URL 地址
 * @returns URL 各部分或 null
 */
export function parseUrl(url: string): {
  protocol: string;
  host: string;
  port: string;
  pathname: string;
  search: string;
  hash: string;
} | null {
  try {
    const urlObj = new URL(url);
    return {
      protocol: urlObj.protocol.replace(':', ''),
      host: urlObj.hostname,
      port: urlObj.port,
      pathname: urlObj.pathname,
      search: urlObj.search,
      hash: urlObj.hash,
    };
  } catch {
    return null;
  }
}

/**
 * 协议白名单校验
 * @param url URL 地址
 * @param allowedProtocols 允许的协议列表
 * @returns 校验结果
 */
export function validateUrlProtocol(
  url: string,
  allowedProtocols: string[]
): { isValid: boolean; errors: string[] } {
  const errors: string[] = [];
  const parts = parseUrl(url);
  
  if (!parts) {
    return { isValid: true, errors };
  }
  
  if (parts.protocol && !allowedProtocols.includes(parts.protocol)) {
    errors.push(`只允许以下协议：${allowedProtocols.join(', ')}`);
  }
  
  return {
    isValid: errors.length === 0,
    errors,
  };
}
```

#### 5.2.2 协议自动补全

```
// utils/urlHelper.ts

/**
 * 规范化 URL（补全协议）
 * @param url URL 地址
 * @param defaultProtocol 默认协议
 * @returns 规范化后的 URL
 * 
 * @example
 * normalizeUrl('example.com', 'https://') // 'https://example.com'
 * normalizeUrl('http://example.com', 'https://') // 'http://example.com'
 */
export function normalizeUrl(
  url: string,
  defaultProtocol: string = 'https://'
): string {
  if (!url) return '';
  
  const trimmedUrl = url.trim();
  
  // 已有协议，直接返回
  if (/^[a-z]+:\/\//i.test(trimmedUrl)) {
    return trimmedUrl;
  }
  
  // 无协议，添加默认协议
  return defaultProtocol + trimmedUrl;
}

/**
 * 智能补全协议（识别 localhost）
 * @param url URL 地址
 * @returns 规范化后的 URL
 * 
 * @example
 * smartNormalizeUrl('localhost:3000') // 'http://localhost:3000'
 * smartNormalizeUrl('example.com') // 'https://example.com'
 */
export function smartNormalizeUrl(url: string): string {
  if (!url) return '';
  
  const trimmedUrl = url.trim();
  
  // 已有协议
  if (/^[a-z]+:\/\//i.test(trimmedUrl)) {
    return trimmedUrl;
  }
  
  // localhost 或 127.0.0.1 使用 http
  if (
    trimmedUrl.startsWith('localhost') ||
    trimmedUrl.startsWith('127.0.0.1') ||
    trimmedUrl.startsWith('192.168.')
  ) {
    return 'http://' + trimmedUrl;
  }
  
  // 其他使用 https
  return 'https://' + trimmedUrl;
}

/**
 * 提取域名
 * @param url URL 地址
 * @returns 域名
 * 
 * @example
 * extractDomain('https://www.example.com/path') // 'www.example.com'
 */
export function extractDomain(url: string): string {
  try {
    const normalizedUrl = url.startsWith('http') ? url : 'https://' + url;
    const urlObj = new URL(normalizedUrl);
    return urlObj.hostname;
  } catch {
    return url;
  }
}

/**
 * 检测是否为安全链接（HTTPS）
 * @param url URL 地址
 * @returns 是否为 HTTPS
 */
export function isSecureUrl(url: string): boolean {
  return url.startsWith('https://');
}
```

#### 5.2.3 链接跳转（安全实现）

```
// utils/urlHelper.ts

/**
 * 处理链接跳转（安全性保障）
 * @param url URL 地址
 * @param openInNewWindow 是否新窗口打开
 * @param defaultProtocol 默认协议
 */
export function handleUrlClick(
  url: string,
  openInNewWindow: boolean = true,
  defaultProtocol: string = 'https://'
): void {
  if (!url) return;
  
  // 规范化 URL（补全协议）
  const normalizedUrl = normalizeUrl(url, defaultProtocol);
  
  // 验证 URL
  if (!validateUrl(normalizedUrl)) {
    console.warn('Invalid URL:', normalizedUrl);
    return;
  }
  
  if (openInNewWindow) {
    // 新窗口打开（添加安全性）
    window.open(normalizedUrl, '_blank', 'noopener,noreferrer');
  } else {
    // 当前窗口打开
    window.location.href = normalizedUrl;
  }
}
```

#### 5.2.4 URL 格式化显示

```
// utils/urlFormatter.ts

/**
 * 格式化 URL 显示
 * @param url URL 地址
 * @param options 格式化选项
 * @returns 格式化后的 URL
 */
export function formatUrlDisplay(
  url: string,
  options: {
    showProtocol?: boolean;
    showWww?: boolean;
    showPathname?: boolean;
    displayFormat?: 'full' | 'domain' | 'custom';
    customFormat?: (url: string) => string;
  }
): string {
  if (!url) return '';
  
  // 自定义格式
  if (options.displayFormat === 'custom' && options.customFormat) {
    return options.customFormat(url);
  }
  
  // 仅域名
  if (options.displayFormat === 'domain') {
    return extractDomain(url);
  }
  
  // 完整格式（根据配置调整）
  let display = url;
  
  // 隐藏协议
  if (options.showProtocol === false) {
    display = display.replace(/^https?:\/\//, '');
  }
  
  // 隐藏 www
  if (options.showWww === false) {
    display = display.replace(/^(https?:\/\/)?(www\.)/, '$1');
  }
  
  // 隐藏路径
  if (options.showPathname === false) {
    try {
      const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
      display = urlObj.hostname;
      if (options.showProtocol !== false && urlObj.protocol) {
        display = urlObj.protocol + '//' + display;
      }
    } catch {
      // 解析失败，返回原值
    }
  }
  
  return display;
}

/**
 * 隐藏 URL 协议
 * @param url URL 地址
 * @returns 隐藏协议后的 URL
 */
export function hideUrlProtocol(url: string): string {
  return url.replace(/^https?:\/\//, '');
}

/**
 * 隐藏 www 前缀
 * @param url URL 地址
 * @returns 隐藏 www 后的 URL
 */
export function hideWwwPrefix(url: string): string {
  return url.replace(/^(https?:\/\/)?(www\.)/, '$1');
}
```

#### 5.2.5 EditView 实现（协议补全）

```
import React from 'react';
import { Input } from 'antd';
import { LinkOutlined } from '@ant-design/icons';
import { useTextInputState } from '@pisell/utils';
import { validateUrl, normalizeUrl } from '../utils';
import type { PisellUrlProps } from '../types';

const EditView: React.FC<PisellUrlProps> = (props) => {
  const {
    placeholder = '请输入 URL 地址',
    maxLength = 2048,
    allowClear = false,
    autoFocus = false,
    disabled = false,
    className,
    style,
    autoCompleteProtocol = true,
    defaultProtocol = 'https://',
    strictValidation = true,
    allowedProtocols,
    requireProtocol = false,
    onProtocolComplete,
  } = props;

  // 状态管理（复用公共 Hook）
  const { value, isValid, errors, handleChange, handleBlur, handleFocus } = useTextInputState({
    ...props,
    validator: (val) => {
      if (!val) return true;

      // 规范化 URL（用于校验）
      const normalizedUrl = autoCompleteProtocol
        ? normalizeUrl(val, defaultProtocol)
        : val;

      // 1. 格式校验
      if (strictValidation && !validateUrl(normalizedUrl)) {
        return props.errorMessage || 'URL 地址格式不正确';
      }

      // 2. 协议必需校验
      if (requireProtocol && !val.match(/^[a-z]+:\/\//i)) {
        return 'URL 必须包含协议（如 http:// 或 https://）';
      }

      // 3. 协议白名单校验
      if (allowedProtocols && allowedProtocols.length > 0) {
        const parts = parseUrl(normalizedUrl);
        if (parts && parts.protocol && !allowedProtocols.includes(parts.protocol)) {
          return `只允许以下协议：${allowedProtocols.join(', ')}`;
        }
      }

      return true;
    },
  });

  // 失焦时自动补全协议
  const handleBlurWithProtocol = () => {
    if (autoCompleteProtocol && value && !value.match(/^[a-z]+:\/\//i)) {
      const normalized = normalizeUrl(value, defaultProtocol);
      if (normalized !== value) {
        handleChange(normalized);
        onProtocolComplete?.(normalized);
      }
    }
    handleBlur();
  };

  return (
    <div className="pisell-url-edit">
      <Input
        type="url"
        value={value}
        onChange={(e) => handleChange(e.target.value)}
        onBlur={handleBlurWithProtocol}
        onFocus={handleFocus}
        prefix={<LinkOutlined />}
        placeholder={placeholder}
        maxLength={maxLength}
        allowClear={allowClear}
        autoFocus={autoFocus}
        disabled={disabled}
        status={isValid ? undefined : 'error'}
        className={className}
        style={style}
        aria-label={props['aria-label']}
        aria-describedby={props['aria-describedby']}
      />

      {/* 错误提示 */}
      {!isValid && errors.length > 0 && (
        <div className="pisell-url-edit-error">{errors[0]}</div>
      )}
    </div>
  );
};

export default EditView;
```

#### 5.2.6 ReadOnlyView 实现（链接跳转）

```
import React from 'react';
import { Typography, Space } from 'antd';
import { LinkOutlined } from '@ant-design/icons';
import { handleUrlClick, formatUrlDisplay } from '../utils';
import type { PisellUrlProps } from '../types';

const ReadOnlyView: React.FC<PisellUrlProps> = (props) => {
  const {
    value = '',
    enableLinkClick = true,
    openInNewWindow = true,
    showLinkIcon = true,
    showProtocol = false,
    showWww = false,
    showPathname = true,
    displayFormat = 'full',
    customFormat,
    defaultProtocol = 'https://',
    fontSize = 16,
    fontWeight = 500,
    color = '#101828',
    linkColor = '#7F56DA',
    onLinkClick,
  } = props;

  // 格式化显示
  const displayUrl = formatUrlDisplay(value, {
    showProtocol,
    showWww,
    showPathname,
    displayFormat,
    customFormat,
  });

  // 链接跳转处理
  const handleClick = () => {
    if (enableLinkClick && value) {
      onLinkClick?.(value);
      handleUrlClick(value, openInNewWindow, defaultProtocol);
    }
  };

  const canClick = enableLinkClick && !!value;

  return (
    <div className="pisell-url-read">
      <Space size="small">
        {/* 链接图标 */}
        {showLinkIcon && (
          <LinkOutlined style={{ color: linkColor }} />
        )}

        {/* 链接文本 */}
        {canClick ? (
          <Typography.Link
            onClick={handleClick}
            style={{
              fontSize,
              fontWeight,
              color: linkColor,
            }}
            aria-label={props['aria-label']}
            aria-describedby={props['aria-describedby']}
          >
            {displayUrl}
          </Typography.Link>
        ) : (
          <Typography.Text
            style={{
              fontSize,
              fontWeight,
              color,
            }}
            aria-label={props['aria-label']}
            aria-describedby={props['aria-describedby']}
          >
            {displayUrl}
          </Typography.Text>
        )}
      </Space>
    </div>
  );
};

export default ReadOnlyView;
```

## 6 组件组成与引用

### 6.1 依赖的外部组件和工具

| 组件/工具             | 来源                | 用途       |
|-------------------|-------------------|----------|
| Input             | antd              | URL 输入框  |
| Typography.Link   | antd              | 只读态链接展示  |
| Typography.Text   | antd              | 只读态文本展示  |
| Space             | antd              | 布局       |
| LinkOutlined      | @ant-design/icons | 链接图标     |
| useTextInputState | @pisell/utils     | 状态管理（复用） |
| getDisplayState   | @pisell/utils     | 状态计算（复用） |

### 6.2 组件结构图

```
PisellUrl
├── EditView
│   ├── Input（前缀：链接图标）
│   └── 错误提示
├── ReadOnlyView
│   ├── LinkOutlined（链接图标）
│   └── Typography.Link（URL 地址，可点击跳转）
└── DisabledView
    ├── EditView（复用）
    └── ReadOnlyView（复用）
```

### 6.3 复用关系

```
PisellSingleLineText（基础组件）
    ↓ 公共逻辑（@pisell/utils）
PisellUrl（URL 链接组件）
    ↓ 复用
├── useTextInputState（状态管理）
├── validateValue（基础校验）
└── getDisplayState（状态计算）
    ↓ 扩展
├── validateUrl（URL 格式校验）
├── normalizeUrl（协议补全）
├── handleUrlClick（链接跳转）
└── formatUrlDisplay（URL 格式化）
```

## 7 扩展性设计

### 7.1 通用性

- 支持 http/https/ftp 等协议
- 支持协议白名单限制
- 不绑定业务逻辑
- 支持自定义校验和格式化

### 7.2 可扩展点

1. **自定义协议列表**：通过 allowedProtocols 配置
2. **自定义默认协议**：通过 defaultProtocol 配置
3. **自定义显示格式**：通过 displayFormat 和 customFormat 配置
4. **自定义校验**：通过 customValidator 函数
5. **安全性控制**：通过 openInNewWindow 配置

### 7.3 安全性设计

**防止安全风险**：

1. 新窗口打开时添加 `noopener,noreferrer`
2. 校验 URL 格式防止无效链接
3. 支持协议白名单限制

## 8 性能优化

### 8.1 优化策略

1. **React.memo**：避免不必要的重渲染
2. **useMemo**：缓存格式化后的 URL 显示
3. **useCallback**：缓存事件处理函数
4. **复用公共逻辑**：减少代码量

### 8.2 性能指标

- 首次渲染：< 20ms
- 重渲染：< 10ms
- 链接跳转响应：< 5ms

## 9 开发注意事项

### 9.1 代码规范

1. 使用 TypeScript 严格模式
2. 所有函数添加 JSDoc 注释
3. 遵循 ESLint 和 Prettier 规则
4. 复用 @pisell/utils 中的公共逻辑

### 9.2 关键实现点

1. **协议补全时机**：
  - 失焦时自动补全（如果未输入协议）
  - 使用 smartNormalizeUrl 智能识别

2. **URL 校验方法**：
  - 优先使用 URL 构造函数（更可靠）
  - 备选使用正则表达式

3. **链接跳转安全性**：
  - 新窗口打开时添加 noopener,noreferrer
  - 防止 window.opener 被利用

4. **格式化优先级**：
  - customFormat > displayFormat='domain' > 默认显示
  - 然后根据 showProtocol、showWww、showPathname 调整

5. **校验逻辑顺序**：
  - 格式校验 > 协议必需校验 > 协议白名单校验 > 自定义校验

### 9.3 常见问题

**Q1：为什么失焦才补全协议，而不是实时补全？**
A：避免干扰用户输入，用户输入 "example" 时不应该立即变成 "https://example"

**Q2：localhost 为什么用 http 而不是 https？**
A：本地开发环境通常不配置 SSL 证书，使用 http 更符合实际

**Q3：如何防止 XSS 攻击？**
A：URL 构造函数会自动处理，同时新窗口打开时使用 noopener,noreferrer

## 10 总结

PisellUrl 是一个功能丰富的 URL 链接输入组件，通过复用 @pisell/utils 的公共逻辑，大幅减少了开发工作量。组件提供了 URL
格式校验、协议自动补全、安全链接跳转和灵活的显示控制等特性。

**核心优势**：

- ✅ 使用原生 URL 构造函数校验（更可靠）
- ✅ 智能协议补全（localhost 用 http）
- ✅ 安全链接跳转（noopener noreferrer）
- ✅ 灵活的显示控制（协议/www/路径）
- ✅ 协议白名单限制
- ✅ 完整复用公共逻辑（70% 代码复用）
- ✅ 与 SingleLineText 一致的 API 风格
