Phân tích Module Ajax trong Thư viện Zepto

Module Ajax là một thành phần cốt lõi trong Zepto, cung cấp khả năng gửi yêu cầu không đồng bộ đến máy chủ thông qua XMLHttpRequest, đồng thời hỗ trợ cơ chế JSONP để xử lý các tình huống truy cập chéo miền (cross-origin). Bài viết này dựa trên mã nguồn Zepto phiên bản 1.2.0.

Thứ tự kích hoạt sự kiện trong quá trình Ajax

Zepto định nghĩa một chuỗi sự kiện toàn cục nhằm theo dõi vòng đời của mỗi yêu cầu Ajax. Thứ tự thực thi chuẩn như sau:
  1. ajaxStart: Kích hoạt ngay trước khi tạo thể hiện XMLHttpRequest.
  2. ajaxBeforeSend: Gọi ngay trước khi gửi yêu cầu — có thể hủy bỏ yêu cầu nếu trả về false.
  3. ajaxSend: Kích hoạt ngay sau khi yêu cầu được gửi đi.
  4. ajaxSuccess / ajaxError: Tương ứng với kết quả thành công hoặc thất bại.
  5. ajaxComplete: Luôn được gọi khi yêu cầu kết thúc — bất kể trạng thái.
  6. ajaxStop: Kích hoạt khi không còn yêu cầu Ajax nào đang chạy.

Các tùy chọn cấu hình chính

Dưới đây là giải thích ngắn gọn các tham số thường dùng trong hàm $.ajax():
  • type: Phương thức HTTP (GET, POST, v.v.).
  • url: Địa chỉ đích của yêu cầu.
  • data: Dữ liệu gửi kèm (có thể là đối tượng, chuỗi hoặc FormData).
  • processData: Nếu true, tự động chuyển dữ liệu sang dạng query string.
  • contentType: Giá trị tiêu đề Content-Type (mặc định: application/x-www-form-urlencoded).
  • dataType: Loại dữ liệu mong đợi từ máy chủ (json, xml, script, html, text, jsonp).
  • jsonp: Tên tham số chứa tên callback trong yêu cầu JSONP (mặc định: callback).
  • jsonpCallback: Tên hàm callback phía client (nếu không truyền, Zepto sẽ sinh tên tự động).
  • timeout: Thời gian chờ tối đa (đơn vị: mili-giây; 0 = vô hạn).
  • headers: Đối tượng chứa các tiêu đề HTTP tùy chỉnh.
  • async: Xác định yêu cầu có phải bất đồng bộ hay không (true mặc định).
  • global: Cho phép/ẩn các sự kiện toàn cục (true mặc định).
  • context: Đối tượng this được sử dụng khi gọi các callback (window mặc định).
  • traditional: Kiểm soát cách tuần tự hóa dữ liệu lồng sâu (falsep[nested]=v; truep=object).
  • xhrFields: Các thuộc tính cần gán trực tiếp vào thể hiện XMLHttpRequest.
  • cache: Kiểm soát cache cho yêu cầu GET (false thêm timestamp để bypass cache).
  • username/password: Thông tin xác thực HTTP cơ bản.
  • dataFilter: Hàm xử lý dữ liệu phản hồi trước khi phân tích.
  • xhr: Hàm tạo thể hiện XMLHttpRequest tùy chỉnh.
  • accepts: Danh sách MIME types ưu tiên gửi tới máy chủ.
  • beforeSend: Hàm được gọi trước khi gửi yêu cầu — trả về false để hủy.
  • success, error, complete: Các callback tương ứng với trạng thái yêu cầu.

Các hàm tiện ích nội bộ

triggerAndReturn(context, eventName, data)

Kích hoạt sự kiện và trả về false nếu hành vi mặc định bị ngăn chặn:

function triggerAndReturn(ctx, name, payload) {
  const evt = $.Event(name);
  $(ctx).trigger(evt, payload);
  return !evt.isDefaultPrevented();
}

triggerGlobal(settings, context, name, data)

Gọi triggerAndReturn trên document hoặc ngữ cảnh đã chỉ định nếu settings.global === true:

function triggerGlobal(opts, ctx, name, payload) {
  if (opts.global) {
    return triggerAndReturn(ctx || document, name, payload);
  }
}

ajaxStart(settings)

Tăng bộ đếm yêu cầu đang hoạt động và phát sự kiện ajaxStart nếu đây là yêu cầu đầu tiên:

function ajaxStart(opts) {
  if (opts.global && $.active++ === 0) {
    triggerGlobal(opts, null, 'ajaxStart');
  }
}

ajaxStop(settings)

Giảm bộ đếm và phát ajaxStop khi không còn yêu cầu nào đang chạy:

function ajaxStop(opts) {
  if (opts.global && !(--$.active)) {
    triggerGlobal(opts, null, 'ajaxStop');
  }
}

ajaxBeforeSend(xhr, settings)

Xử lý logic trước khi gửi yêu cầu — gọi beforeSend và sự kiện ajaxBeforeSend. Nếu bất kỳ bước nào trả về false, yêu cầu sẽ bị hủy:

function ajaxBeforeSend(xhrInst, opts) {
  const ctx = opts.context;
  const prevent = opts.beforeSend.call(ctx, xhrInst, opts) === false ||
                  triggerGlobal(opts, ctx, 'ajaxBeforeSend', [xhrInst, opts]) === false;
  if (prevent) return false;
  triggerGlobal(opts, ctx, 'ajaxSend', [xhrInst, opts]);
}

ajaxComplete(status, xhr, settings)

Gọi callback complete, sau đó phát sự kiện ajaxComplete và kiểm tra dừng toàn cục:

function ajaxComplete(status, xhrInst, opts) {
  const ctx = opts.context;
  opts.complete.call(ctx, xhrInst, status);
  triggerGlobal(opts, ctx, 'ajaxComplete', [xhrInst, opts]);
  ajaxStop(opts);
}

ajaxSuccess(data, xhr, settings, deferred)

Xử lý phản hồi thành công: gọi success, resolve deferred, phát sự kiện ajaxSuccess, rồi gọi ajaxComplete:

function ajaxSuccess(resp, xhrInst, opts, def) {
  const ctx = opts.context;
  opts.success.call(ctx, resp, 'success', xhrInst);
  if (def) def.resolveWith(ctx, [resp, 'success', xhrInst]);
  triggerGlobal(opts, ctx, 'ajaxSuccess', [xhrInst, opts, resp]);
  ajaxComplete('success', xhrInst, opts);
}

ajaxError(error, type, xhr, settings, deferred)

Xử lý lỗi: gọi error, reject deferred, phát ajaxError, rồi gọi ajaxComplete:

function ajaxError(err, errType, xhrInst, opts, def) {
  const ctx = opts.context;
  opts.error.call(ctx, xhrInst, errType, err);
  if (def) def.rejectWith(ctx, [xhrInst, errType, err]);
  triggerGlobal(opts, ctx, 'ajaxError', [xhrInst, opts, err || errType]);
  ajaxComplete(errType, xhrInst, opts);
}

mimeToDataType(mime)

Chuyển đổi giá trị Content-Type từ header thành kiểu dữ liệu ngắn gọn (json, xml, script, html, text):

const htmlType = 'text/html';
const jsonType = 'application/json';
const scriptRE = /^(?:text|application)\/javascript/i;
const xmlRE = /^(?:text|application)\/xml/i;

function mimeToDataType(mimeStr) {
  if (!mimeStr) return 'text';
  const base = mimeStr.split(';', 2)[0];
  return base === htmlType ? 'html' :
         base === jsonType ? 'json' :
         scriptRE.test(base) ? 'script' :
         xmlRE.test(base) ? 'xml' : 'text';
}

appendQuery(url, query)

Nối chuỗi truy vấn vào URL, đảm bảo ký tự phân tách đúng (? thay vì && hay ?&):

function appendQuery(urlStr, queryStr) {
  if (!queryStr) return urlStr;
  return (urlStr + '&' + queryStr).replace(/[&?]{1,2}/, '?');
}

serialize(params, obj, traditional, keyPrefix)

Hàm đệ quy tuần tự hóa dữ liệu lồng sâu — hỗ trợ cả định dạng truyền thống và chuẩn mới:

function serialize(params, obj, isTraditional, prefix) {
  const isArray = $.isArray(obj);
  const isPlainObj = $.isPlainObject(obj);

  $.each(obj, (k, v) => {
    let key = k;
    const type = $.type(v);

    if (prefix) {
      key = isTraditional ? prefix : `${prefix}[${isPlainObj || type === 'object' || type === 'array' ? k : ''}]`;
    }

    if (!prefix && isArray) {
      params.add(v.name, v.value);
    } else if (type === 'array' || (!isTraditional && type === 'object')) {
      serialize(params, v, isTraditional, key);
    } else {
      params.add(key, v);
    }
  });
}

serializeData(options)

Chuẩn bị dữ liệu gửi đi: tuần tự hóa data nếu cần, và nối vào URL nếu là yêu cầu GET hoặc jsonp:

function serializeData(opts) {
  if (opts.processData && opts.data && $.type(opts.data) !== 'string') {
    opts.data = $.param(opts.data, opts.traditional);
  }
  if (opts.data && (!opts.type || /get|jsonp/i.test(opts.type))) {
    opts.url = appendQuery(opts.url, opts.data);
    opts.data = undefined;
  }
}

Giao diện công khai

$.active

Số lượng yêu cầu Ajax hiện đang chạy — khởi tạo bằng 0.

$.ajaxSettings

Cấu hình mặc định toàn cục cho mọi yêu cầu Ajax:

$.ajaxSettings = {
  type: 'GET',
  beforeSend: $.noop,
  success: $.noop,
  error: $.noop,
  complete: $.noop,
  context: null,
  global: true,
  xhr: () => new XMLHttpRequest(),
  accepts: {
    script: 'text/javascript, application/javascript',
    json: 'application/json',
    xml: 'application/xml, text/xml',
    html: 'text/html',
    text: 'text/plain'
  },
  crossDomain: false,
  timeout: 0,
  processData: true,
  cache: true,
  dataFilter: $.noop
};

$.param(obj, traditional)

Biến đổi đối tượng thành chuỗi truy vấn — sử dụng serialize và mã hóa URI:

$.param = function(obj, isTraditional) {
  const parts = [];
  parts.add = function(k, v) {
    if ($.isFunction(v)) v = v();
    if (v == null) v = '';
    this.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
  };
  serialize(parts, obj, isTraditional);
  return parts.join('&').replace(/%20/g, '+');
};

$.ajaxJSONP(options, deferred)

Cơ chế JSONP: chèn thẻ <script>, thiết lập callback toàn cục, xử lý timeout và lỗi:

let jsonpCounter = Date.now();

$.ajaxJSONP = function(opts, def) {
  if (!('type' in opts)) return $.ajax(opts);

  const cbNameRaw = opts.jsonpCallback;
  const cbName = typeof cbNameRaw === 'function' 
    ? cbNameRaw() 
    : cbNameRaw || `Zepto${jsonpCounter++}`;
  
  const script = document.createElement('script');
  const originalCb = window[cbName];
  let response;
  let abortTimer;

  const xhr = { abort: () => {
      clearTimeout(abortTimer);
      $(script).remove();
      window[cbName] = originalCb;
    }
  };

  if (def) def.promise(xhr);

  $(script).on('load error', function(e, errType) {
    clearTimeout(abortTimer);
    $(script).off().remove();

    if (e.type === 'error' || !response) {
      ajaxError(null, errType || 'error', xhr, opts, def);
    } else {
      ajaxSuccess(response[0], xhr, opts, def);
    }

    window[cbName] = originalCb;
    if (response && $.isFunction(originalCb)) {
      originalCb(response[0]);
    }
  });

  if (ajaxBeforeSend(xhr, opts) === false) {
    xhr.abort();
    return xhr;
  }

  window[cbName] = function() {
    response = arguments;
  };

  script.src = opts.url.replace(/\?(.+)=\?/, `?$1=${cbName}`);
  document.head.appendChild(script);

  if (opts.timeout > 0) {
    abortTimer = setTimeout(() => xhr.abort(), opts.timeout);
  }

  return xhr;
};

$.ajax(options)

Hàm trung tâm xử lý tất cả yêu cầu Ajax — bao gồm: hợp nhất cấu hình, kiểm tra miền, tuần tự hóa dữ liệu, thiết lập header, mở kết nối và xử lý phản hồi:

$.ajax = function(opts) {
  const settings = $.extend({}, $.ajaxSettings, opts);
  const def = $.Deferred && $.Deferred();
  const xhr = settings.xhr();

  ajaxStart(settings);

  // Phát hiện cross-domain
  const origin = document.createElement('a');
  origin.href = location.href;
  if (!settings.crossDomain) {
    const target = document.createElement('a');
    target.href = settings.url;
    settings.crossDomain = `${origin.protocol}//${origin.host}` !== `${target.protocol}//${target.host}`;
  }

  // Xử lý URL và dữ liệu
  if (!settings.url) settings.url = location.href;
  if (settings.url.includes('#')) settings.url = settings.url.split('#')[0];
  serializeData(settings);

  // Bypass cache cho script/jsonp
  const isJsonp = /(?:\?.+=\?)/.test(settings.url) || settings.dataType === 'jsonp';
  if (isJsonp) settings.dataType = 'jsonp';
  if (settings.cache === false || (isJsonp && settings.cache !== true)) {
    settings.url = appendQuery(settings.url, '_=' + Date.now());
  }

  // Chuyển hướng sang JSONP nếu cần
  if (settings.dataType === 'jsonp') {
    if (!isJsonp) {
      const param = settings.jsonp === false ? '' : (settings.jsonp || 'callback') + '=?';
      settings.url = appendQuery(settings.url, param);
    }
    return $.ajaxJSONP(settings, def);
  }

  // Thiết lập header
  const headers = {};
  const setHeader = (name, val) => headers[name.toLowerCase()] = [name, val];
  setHeader('Accept', settings.accepts[settings.dataType] || '*/*');

  if (!settings.crossDomain) {
    setHeader('X-Requested-With', 'XMLHttpRequest');
  }

  if (settings.contentType || (settings.data && /post|put|delete/i.test(settings.type))) {
    setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded');
  }

  if (settings.headers) {
    $.each(settings.headers, (k, v) => setHeader(k, v));
  }

  xhr.setRequestHeader = setHeader;

  // Gọi beforeSend
  if (ajaxBeforeSend(xhr, settings) === false) {
    xhr.abort();
    ajaxError(null, 'abort', xhr, settings, def);
    return xhr;
  }

  // Mở kết nối
  const async = settings.async !== false;
  xhr.open(settings.type, settings.url, async, settings.username, settings.password);

  // Áp dụng xhrFields và headers
  if (settings.xhrFields) {
    $.each(settings.xhrFields, (k, v) => xhr[k] = v);
  }
  $.each(headers, (k, v) => xhr.setRequestHeader.apply(xhr, v));

  // Xử lý phản hồi
  xhr.onreadystatechange = function() {
    if (xhr.readyState !== 4) return;
    xhr.onreadystatechange = $.noop;
    clearTimeout(abortTimer);

    let result;
    const isSuccess = (xhr.status >= 200 && xhr.status < 300) ||
                       xhr.status === 304 ||
                       (xhr.status === 0 && location.protocol === 'file:');

    if (isSuccess) {
      const mimeType = settings.mimeType || xhr.getResponseHeader('content-type');
      const dataType = settings.dataType || mimeToDataType(mimeType);

      if (xhr.responseType === 'arraybuffer' || xhr.responseType === 'blob') {
        result = xhr.response;
      } else {
        result = xhr.responseText;
        try {
          result = ajaxDataFilter(result, dataType, settings);
          if (dataType === 'script') (1, eval)(result);
          else if (dataType === 'xml') result = xhr.responseXML;
          else if (dataType === 'json') result = /^\s*$/.test(result) ? null : $.parseJSON(result);
        } catch (e) {
          return ajaxError(e, 'parsererror', xhr, settings, def);
        }
      }
      ajaxSuccess(result, xhr, settings, def);
    } else {
      ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, def);
    }
  };

  // Thiết lập timeout
  if (settings.timeout > 0) {
    abortTimer = setTimeout(() => {
      xhr.onreadystatechange = $.noop;
      xhr.abort();
      ajaxError(null, 'timeout', xhr, settings, def);
    }, settings.timeout);
  }

  // Gửi yêu cầu
  xhr.send(settings.data || null);

  return xhr;
};

Các phương thức tiện ích

  • $.get(url, data, success, dataType) — gọi $.ajax với type: 'GET'.
  • $.post(url, data, success, dataType) — gọi $.ajax với type: 'POST'.
  • $.getJSON(url, data, success) — gọi $.ajax với dataType: 'json'.
  • $.fn.load(url, data, success) — tải nội dung HTML và chèn vào phần tử hiện tại, hỗ trợ selector con.

Thẻ: zepto ajax jsonp xhr JavaScript

Đăng vào ngày 17 tháng 05 lúc 23:18