JavaScript Cookie 解析器:从字符串到对象

156 阅读6分钟

在 Web 开发中,我们经常需要处理浏览器 cookies。本文将详细介绍如何使用 JavaScript 编写一个高效的 cookie 解析函数,将 cookie 字符串转换为方便使用的 JavaScript 对象。

理解 Cookie 字符串格式

在实现解析器之前,我们需要了解 cookie 字符串的标准格式:

"name=value; expires=Thu, 18 Dec 2023 12:00:00 UTC; path=/; domain=example.com; secure; HttpOnly"
  • 每个 cookie 键值对用分号分隔
  • 第一个键值对通常是主数据(如 name=value
  • 后面的属性是可选参数(如 expires, path, domain 等)
  • 键值对等号左右可能包含空格

最终实现代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>JavaScript Cookie 解析器</title>
  <style>
    :root {
      --primary: #4361ee;
      --secondary: #3f37c9;
      --accent: #4895ef;
      --light: #f8f9fa;
      --dark: #212529;
    }
    
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      line-height: 1.6;
      background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
      color: var(--dark);
      min-height: 100vh;
      padding: 20px;
    }
    
    .container {
      max-width: 1000px;
      margin: 0 auto;
      padding: 20px;
    }
    
    header {
      text-align: center;
      margin-bottom: 40px;
      padding: 20px;
      background: white;
      border-radius: 15px;
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
    }
    
    h1 {
      color: var(--secondary);
      margin-bottom: 10px;
      font-size: 2.5rem;
    }
    
    .subtitle {
      color: #6c757d;
      font-size: 1.2rem;
    }
    
    .content {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 30px;
      margin-bottom: 40px;
    }
    
    @media (max-width: 768px) {
      .content {
        grid-template-columns: 1fr;
      }
    }
    
    .card {
      background: white;
      border-radius: 15px;
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.07);
      overflow: hidden;
      transition: transform 0.3s ease;
    }
    
    .card:hover {
      transform: translateY(-5px);
    }
    
    .card-header {
      background: var(--primary);
      color: white;
      padding: 15px 20px;
    }
    
    .card-body {
      padding: 20px;
    }
    
    .input-container {
      margin-bottom: 20px;
    }
    
    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 500;
      color: var(--secondary);
    }
    
    textarea {
      width: 100%;
      min-height: 150px;
      padding: 12px;
      border: 1px solid #ddd;
      border-radius: 8px;
      font-family: monospace;
      font-size: 1rem;
      resize: vertical;
    }
    
    button {
      background: var(--accent);
      color: white;
      border: none;
      padding: 12px 20px;
      border-radius: 8px;
      cursor: pointer;
      font-size: 1rem;
      font-weight: 500;
      transition: background 0.3s ease;
      width: 100%;
    }
    
    button:hover {
      background: var(--secondary);
    }
    
    .result-container {
      border: 1px solid #e9ecef;
      border-radius: 8px;
      padding: 20px;
      background: var(--light);
    }
    
    pre {
      background: #2b2b2b;
      color: #f8f8f2;
      padding: 15px;
      border-radius: 8px;
      overflow-x: auto;
      max-height: 400px;
    }
    
    .explanation {
      margin-top: 40px;
      background: white;
      border-radius: 15px;
      padding: 25px;
      box-shadow: 0 5px 15px rgba(0, 0, 0, 0.07);
    }
    
    .explanation h2 {
      color: var(--secondary);
      margin-bottom: 20px;
      border-bottom: 2px solid var(--accent);
      padding-bottom: 10px;
    }
    
    .explanation-content {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 20px;
    }
    
    @media (max-width: 768px) {
      .explanation-content {
        grid-template-columns: 1fr;
      }
    }
    
    .step {
      background: var(--light);
      padding: 20px;
      border-radius: 10px;
    }
    
    .step h3 {
      color: var(--primary);
      margin-bottom: 10px;
    }
    
    .notes {
      background: #fff8e1;
      padding: 20px;
      border-radius: 10px;
      margin-top: 20px;
    }
    
    .notes h3 {
      color: #e65100;
      margin-bottom: 10px;
    }
    
    .examples {
      margin-top: 20px;
    }
    
    .example-btn {
      display: inline-block;
      margin-right: 10px;
      margin-bottom: 10px;
      background: #e9ecef;
      color: var(--dark);
      padding: 8px 15px;
      border-radius: 5px;
      cursor: pointer;
      font-size: 0.9rem;
    }
    
    .example-btn:hover {
      background: #dee2e6;
    }
  </style>
</head>
<body>
  <div class="container">
    <header>
      <h1>JavaScript Cookie 解析器</h1>
      <p class="subtitle">一个强大的工具,用于将cookie字符串转换为JavaScript对象</p>
    </header>
    
    <div class="content">
      <div class="card">
        <div class="card-header">
          <h2>输入 Cookie 字符串</h2>
        </div>
        <div class="card-body">
          <div class="input-container">
            <label for="cookieInput">输入您的 cookie 字符串:</label>
            <textarea id="cookieInput" placeholder="例如: name=value; expires=Fri, 31 Dec 2023 23:59:59 GMT; path=/; secure">user=Jane%20Doe; session=abcd1234; theme=dark; language=en-US; visited=true</textarea>
          </div>
          
          <div class="examples">
            <strong>示例:</strong><br>
            <span class="example-btn" data-example="user=John%20Doe; lang=en; loggedIn=true">简单 cookies</span>
            <span class="example-btn" data-example="token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9; expires=Mon, 01 Jan 2024 00:00:00 GMT; secure; HttpOnly">带安全属性</span>
            <span class="example-btn" data-example="preferences=theme:dark|layout:compact|notifications:off; sessionId=abcd1234efgh5678">复杂值</span>
          </div>
          
          <button id="parseButton">解析 Cookies</button>
        </div>
      </div>
      
      <div class="card">
        <div class="card-header">
          <h2>解析结果 (JavaScript 对象)</h2>
        </div>
        <div class="card-body">
          <div class="result-container">
            <pre id="result"></pre>
          </div>
        </div>
      </div>
    </div>
    
    <div class="explanation">
      <h2>实现解析器的详细步骤</h2>
      
      <div class="explanation-content">
        <div class="step">
          <h3>1. 准备解析函数</h3>
          <p>创建一个函数,接受 cookie 字符串作为输入,返回解析后的对象:</p>
          <pre>function parseCookies(cookieString) {
  // 解析逻辑将放在这里
}</pre>
        </div>
        
        <div class="step">
          <h3>2. 初始化结果对象</h3>
          <p>创建一个空对象用于存储解析结果:</p>
          <pre>const cookies = {};</pre>
        </div>
        
        <div class="step">
          <h3>3. 处理空字符串</h3>
          <p>检查输入字符串是否为空:</p>
          <pre>if (!cookieString || cookieString.trim() === '') {
  return cookies;
}</pre>
        </div>
        
        <div class="step">
          <h3>4. 分割 cookie 条目</h3>
          <p>使用分号分隔符分割 cookie 字符串:</p>
          <pre>const items = cookieString.split(';');</pre>
        </div>
        
        <div class="step">
          <h3>5. 处理每个 cookie</h3>
          <p>遍历所有 cookie 条目:</p>
          <pre>items.forEach(item => {
  // 处理每个条目
});</pre>
        </div>
        
        <div class="step">
          <h3>6. 分割键值对</h3>
          <p>将每个条目按第一个等号分割为键和值:</p>
          <pre>const [key, ...values] = item.split('=');
const value = values.join('=');</pre>
        </div>
        
        <div class="step">
          <h3>7. 解码和清理</h3>
          <p>去除键值两端的空格,并解码 URL 编码的值:</p>
          <pre>const cleanedKey = key.trim();
// 尝试解码值,失败时使用原值
let cleanedValue;
try {
  cleanedValue = decodeURIComponent(value.trim());
} catch (e) {
  cleanedValue = value.trim();
}</pre>
        </div>
        
        <div class="step">
          <h3>8. 添加到结果对象</h3>
          <p>将解析后的键值对添加到结果对象:</p>
          <pre>if (cleanedKey && cleanedValue !== undefined) {
  cookies[cleanedKey] = cleanedValue;
}</pre>
        </div>
      </div>
      
      <div class="notes">
        <h3>重要注意事项</h3>
        <ul>
          <li><strong>URL 解码</strong>: 对值使用 <code>decodeURIComponent()</code> 方法</li>
          <li><strong>错误处理</strong>: 当解码无效编码值时回退到原始值</li>
          <li><strong>空格处理</strong>: 正确处理键值前后的空格</li>
          <li><strong>多个等号</strong>: 值中可能包含等号,因此只按第一个等号分割</li>
          <li><strong>Cookie 属性</strong>: 只保留主键值对,忽略如 expires、path 等属性</li>
        </ul>
      </div>
    </div>
  </div>
  
  <script>
    // 功能完整的 cookie 解析函数
    function parseCookies(cookieString) {
      // 如果没有输入或为空字符串,返回空对象
      if (!cookieString || cookieString.trim() === '') {
        return {};
      }
      
      // 创建结果对象
      const cookies = {};
      
      // 按分号分割字符串
      const items = cookieString.split(';');
      
      // 处理每个 cookie 条目
      items.forEach(item => {
        // 按第一个等号分割键值对
        const [key, ...values] = item.split('=');
        
        // 如果没有值,则跳过
        if (values.length === 0) return;
        
        // 合并值部分(可能包含等号)
        const value = values.join('=');
        
        // 清理键和值中的空格
        const cleanedKey = key.trim();
        const trimmedValue = value.trim();
        
        // 尝试解码 URL 编码的值
        let cleanedValue;
        try {
          cleanedValue = decodeURIComponent(trimmedValue);
        } catch (e) {
          // 如果解码失败,使用原始值
          cleanedValue = trimmedValue;
        }
        
        // 将有效的键值对添加到结果对象
        if (cleanedKey && cleanedValue !== undefined) {
          cookies[cleanedKey] = cleanedValue;
        }
      });
      
      return cookies;
    }

    // DOM 操作和事件绑定
    document.addEventListener('DOMContentLoaded', () => {
      const cookieInput = document.getElementById('cookieInput');
      const parseButton = document.getElementById('parseButton');
      const resultElement = document.getElementById('result');
      const exampleButtons = document.querySelectorAll('.example-btn');
      
      // 初始解析
      updateResult();
      
      // 解析按钮点击事件
      parseButton.addEventListener('click', updateResult);
      
      // 回车键触发解析
      cookieInput.addEventListener('keydown', e => {
        if (e.key === 'Enter' && !e.shiftKey) {
          e.preventDefault();
          updateResult();
        }
      });
      
      // 示例按钮事件
      exampleButtons.forEach(button => {
        button.addEventListener('click', () => {
          cookieInput.value = button.dataset.example;
          updateResult();
        });
      });
      
      // 更新结果显示
      function updateResult() {
        const cookieStr = cookieInput.value;
        const result = parseCookies(cookieStr);
        
        // 美化输出格式
        resultElement.innerHTML = JSON.stringify(result, null, 2)
          .replace(/ /g, '&nbsp;')
          .replace(/\n/g, '<br>');
      }
    });
  </script>
</body>
</html>

解析器的核心实现

以上页面展示了一个完整的 cookie 解析器实现。以下是核心代码:

function parseCookies(cookieString) {
  // 如果输入为空,返回空对象
  if (!cookieString || cookieString.trim() === '') {
    return {};
  }
  
  const cookies = {};
  
  // 用分号分割字符串
  const items = cookieString.split(';');
  
  items.forEach(item => {
    // 按第一个等号分割键值对
    const [key, ...values] = item.split('=');
    
    // 如果没有值,跳过此项
    if (values.length === 0) return;
    
    // 合并值部分(可能包含等号)
    const value = values.join('=');
    
    // 清理字符串两端空格
    const cleanedKey = key.trim();
    const trimmedValue = value.trim();
    
    // 尝试解码URL编码的值
    let cleanedValue;
    try {
      cleanedValue = decodeURIComponent(trimmedValue);
    } catch (e) {
      // 解码失败,使用原始值
      cleanedValue = trimmedValue;
    }
    
    // 将有效的键值对添加到结果对象
    if (cleanedKey && cleanedValue !== undefined) {
      cookies[cleanedKey] = cleanedValue;
    }
  });
  
  return cookies;
}

关键设计考虑

  1. URL 解码:使用 decodeURIComponent() 处理 URL 编码的值
  2. 错误处理:在解码失败时回退到原始值
  3. 空格处理:删除键值前后的空白字符
  4. 等号支持:正确处理值中的等号字符
  5. 空值处理:跳过无效数据条目
  6. 性能优化:简洁高效的遍历算法

应用场景

这个解析器适用于:

  • 浏览器端处理document.cookie
  • 服务器端处理HTTP请求中的Cookie头
  • 调试和分析应用中的Cookie数据
  • 在无法使用document.cookie API时手动处理Cookies