Source: querystring.js

/**
 * 本模块提供 URL 查询字符串的操作方法。
 * @module querystring
 */

import { hasOwnProp } from './lang';

/**
 * 把查询字符串反序列化为键值对集合。
 * @author luoliquan
 * @param {string} str 查询字符串。
 * @return {Object} 键值对集合。
 * @example
 * parse('a=1&%E9%94%AE=%E5%80%BC'); // { a: 1, '键': '值' }
 * parse('a=1&a=2&b=3'); // { a: [1, 2], b: 3 }
 */
export function parse(str) {
  if (typeof str !== 'string') {
    throw new Error('The str argument must be a string type');
  }

  const result = {};

  str.split('&').forEach(function(pair) {
    if (!pair) { return; }
    pair = pair.split('=');
    const key = decodeURIComponent(pair[0]);
    const value = decodeURIComponent(pair[1] || '');

    if (hasOwnProp(result, key)) {
      // 出现重复 key 值时解析为数组
      if (!Array.isArray(result[key])) {
        result[key] = [result[key]];
      }
      result[key].push(value);
    } else {
      result[key] = value;
    }
  });

  return result;
}

/**
 * 把键值对集合序列化为查询字符串。
 * @author luoliquan
 * @param {Object} data 键值对集合。
 * @param {Object} [options] 参数。
 *   @param {Boolean} [options.ignoreEmpty=false] 是否忽略空值(包括null、undefined、空字符串)。
 * @return {string} 序列化结果。
 * @example
 * stringify({ a: 1, '键': '值' }); // 'a=1&%E9%94%AE=%E5%80%BC'
 * stringify({ a: [1, 2], b: 3 }); // 'a=1&a=2&b=3'
 */
export function stringify(data, options) {
  options = options || {};

  const result = [];
  function addToResult(key, value) {
    if (value == null) { value = ''; }
    // 忽略空值的情况
    if (value === '' && options.ignoreEmpty) { return; }

    result.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
  }

  let key, value;

  // 避免在循环中生成匿名函数,提到循环外
  function loopItem(item) { addToResult(key, item); }

  for (key in data) {
    if (hasOwnProp(data, key)) {
      value = data[key];
      if (Array.isArray(value)) {
        value.forEach(loopItem);
      } else {
        addToResult(key, value);
      }
    }
  }

  return result.join('&');
}

/**
 * 把键值对集合序列化为查询字符串后拼接到指定URL。
 * @author luoliquan
 * @param {string} url 指定URL。
 * @param {Object|string} data 键值对集合。
 * @param {Object} [options] 参数。
 *   @param {Boolean} [options.ignoreEmpty] 序列化时是否忽略空值(包括null、undefined、空字符串)。
 * @return {String} 处理后的URL。
 * @example
 * append('http://abc.com?a=1', { b: 2, c: 3 }); // 'http://abc.com?a=1&b=2&c=3'
 * append('http://abc.com', { a: 1, b: 2 }); // 'http://abc.com?a=1&b=2'
 */
export function append(url, data, options) {
  if (url == null) { return url; }
  url = String(url);

  // 如果url中包含hash,要先剪出来
  const temp = url.indexOf('#');
  let hash = '';
  if (temp !== -1) {
    hash = url.substring(temp, url.length);
    url = url.substring(0, temp);
  }

  // 移除位于末尾的?和&,方便拼接
  url = url.replace(/[?&]$/, '');

  if (typeof data !== 'string') {
    data = stringify(data, options);
  } else {
    // 移除位于开头的?和&,方便拼接
    data = data.replace(/^[?&]/, '');
  }

  return url + (
    data ? ((url.indexOf('?') !== -1 ? '&' : '?') + data) : ''
  ) + hash;
}