axios 네트워크 요청 프레임 소스 코드 분석

90329 단어 소스 코드 분석
초기 axios 0.1.0 버 전 은 IE 브 라 우 저 와 XmlHttpRequest 를 포함 하 는 브 라 우 저 를 지원 합 니 다.또한 요청 매개 변수 연결, JSon 대상 직렬 화 등 기본 기능 을 했다.
0.19.0 버 전이 되 었 을 때 내부 요청 은 Node 환경 에서 주류 브 라 우 저 와 의 지원 으로 바 뀌 었 습 니 다. 그 중에서 Node 환경 에서 http 요청 과 https 요청 을 지원 합 니 다.취소, 차단 도 지원 합 니 다.
Axios 실행 시작 초기 에 먼저 createInstance 방법 을 실행 하고 createInstance 방법 으로 새로운 Axios 인 스 턴 스 를 만 듭 니 다.그러나 여기 서 이상 하 게 도 돌아 오 는 예 는 진짜 new 에서 나 온 인 스 턴 스 가 아니 라 환영 이다.실제 실행 할 때 내부 코드 가 가리 키 는 방향 은 내부 Axios 대상 입 니 다.Axios 내부 에서 wrap 을 사용 하여 각 방법 을 표시 합 니 다. 실제 Axios 인 스 턴 스 를 숨 기기 위해 서 일 수도 있 습 니 다.이렇게 하 는 역할 은 외부 에서 내 부 를 수정 하 는 방법 을 방지 하고 포장 과 방 호 를 잘 하 는 데 있다.
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

그러나 외부 코드 는 내부 의 Axios 인 스 턴 스 에 접근 할 수 있 습 니 다. 환영 을 만 든 후에 다음 코드 를 계속 실행 합 니 다.
// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};

axios.spread = require('./helpers/spread');

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;

자, 여 기 는 외부의 구조 입 니 다.사실 우 리 는 axios 대상 을 받 았 을 때 요청 할 수 있 습 니 다.다음은 요청 과정 을 예시 로 설명 하 겠 습 니 다.다음은 우리 의 request 요청 층 예제 입 니 다.
//     ,     request.js
const service = axios.create({
    baseURL: BASE_API, 
    timeout: TIMEOUT 
});

service.interceptors.request.use(
    config => {
        return config;
    },
    error => {
        Promise.reject(error);
    }
);

service.interceptors.response.use(
    response => {
        return Promise.reject(response);
    },
    error => {
        return Promise.reject(error);
    }
);

export default service;

우리 의 업무 네트워크 층 이 위의 용법 이 라 고 가정 한 다음 에 구체 적 인 업무 코드 에서 get 방법 을 통 해 업무 요 구 를 했다.
axios.get('/get/server').then(function (response) {

}).catch(function (err) {

});

업무 요청 코드 가 시 작 될 때 구체 적 으로 실 행 된 것 은 lib / core / Axios. js 의 request 방법 입 니 다.
// lib/core/Axios.js
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);
  config.method = config.method ? config.method.toLowerCase() : 'get';

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    //        
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    //        
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

request 방법의 매개 변 수 는 config 대상 이 고 이 대상 은 다음 과 같은 정보 로 구성 되 어 있 습 니 다.
{
    method: 'get',
    url: '/get/server'
}

이 어 mergeConfig 방법 을 통 해 사용자 정의 config 대상 을 기본 config 대상 과 통합 합 니 다.
// lib/core/Axios.js
  config = mergeConfig(this.defaults, config);

여기 this. defaults 의 실제 내 부 는 다음 과 같 습 니 다.
// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),

  //        
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  //        ,       ,Json  
  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

아무튼 이따가 사용 할 기본 설정 입 니 다.통합 체 제 는 사용자 정의 설정 을 우선 취하 고 기본 설정 을 취하 여 새로운 통합 대상 을 구성 하 는 것 입 니 다.
request 의 방법 은 계속 아래로 내 려 가 관건 적 인 곳 에 도착 합 니 다.
// lib/core/Axios.js
  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    //        
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    //        
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

우리 의 업무 네트워크 층 이 interceptors.request.use, interceptors.response.use 라 는 글 자 를 사 용 했 기 때문에 우 리 는 대응 하 는 use 방법 이 무엇 을 했 는 지 봐 야 한다.
// lib/core/InterceptorManager.js
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

그리고 InterceptorManager 의 구조 방법 은 다음 과 같다.
// lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager 는 구조 초기 내부 에 하나의 배열 속성 handlers 만 있 었 기 때문에 업무 네트워크 층 이 use 방법 을 사용 할 때 해당 하 는 fulfilled, rejected 를 이 배열 에 새로운 대상 Push 로 구성 합 니 다.그래서 다시 foreach 로 돌아 오 세 요:
// lib/core/InterceptorManager.js
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

foreach 내 부 는 handlers 만 옮 겨 다 니 며 배열 의 대상 을 foreach 의 리 셋 방법 으로 전달 하기 때문에 Axios 의 request 방법 에 서 는 비 즈 니스 네트워크 층 이 정의 하 는 request interceptors 와 response interceptors 를 배열 chain 에 누 르 는 것 입 니 다.그래서 두 개의 foreach 가 실 행 된 후에 배열 chain 의 실행 은 다음 과 같다.
chain = [request.interceptors.success, request.interceptors.fail, dispatchRequest, undefined, response.interceptors.success, response.interceptors.fail]

그리고 근본 적 인 일환 이다.
// lib/core/Axios.js
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

while 방법 이 실 행 될 때 chain 에 들 어 가 는 여러 방법 을 촉발 합 니 다.여기 서 먼저 실 행 된 것 은 request. interceptors. success, 즉 업무 네트워크 층 이 정의 하 는 요청 차단 층 입 니 다.우리 의 예제 에 서 는 아무것도 하지 않 았 기 때문에 dispatch Request 를 다시 실행 합 니 다.이 dispatch Request 는 요청 의 핵심 입 니 다.
// lib/core/dispatchRequest.js
function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Support baseURL config
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  // Ensure headers exist
  config.headers = config.headers || {};

  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  //       Header      ?
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data

    //          
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};

dispatchRequest 방법 은 다음 과 같은 일 을 실 행 했 습 니 다.
  • 1. 이번 요청 이 취소 되 었 는 지 확인 합 니 다.
  • 2. 완전한 구조 요청 Url.
  • 3. header 의 존 재 를 확인 합 니 다.
  • 4. 요청 데이터 와 header 를 변환 합 니 다.변환 방법 은 여기 서도 config 를 통 해 사용자 정의 할 수 있 습 니 다.기본 값 을 JSon 문자열 로 변환 합 니 다.
  • 5. config. headers 를 합병 합 니 다.
  • 6. config. headers 에서 요청 한 방법 에 대응 하 는 값 을 삭제 합 니 다.
  • 7. 해당 하 는 네트워크 요청 어댑터 를 가 져 와 요청 합 니 다.기본 adapter 는 주로 두 가지 가 있 습 니 다. 1. 브 라 우 저 환경 에서 XML HttpRequest. 2. Node 환경 에서 https 라 이브 러 리 나 http 라 이브 러 리 입 니 다. 사용자 정의 네트워크 요청 프레임 워 크 도 지원 합 니 다.

  • 이 코드 에서 실행 환경 을 브 라 우 저 로 가정 하면 브 라 우 저 환경 에 들 어 가 는 dispatch Request 방법:
    // lib/adapters/xhr.js
    function xhrAdapter(config) {
      return new Promise(function dispatchXhrRequest(resolve, reject) {
        var requestData = config.data;
        var requestHeaders = config.headers;
    
        if (utils.isFormData(requestData)) {
          delete requestHeaders['Content-Type']; // Let the browser set it
        }
    
        var request = new XMLHttpRequest();
    
        // HTTP basic authentication
        if (config.auth) {
          var username = config.auth.username || '';
          var password = config.auth.password || '';
          requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
        }
    
        //    method    ?
        request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);
    
        // Set the request timeout in MS
        request.timeout = config.timeout;
    
        // Listen for ready state
        request.onreadystatechange = function handleLoad() {
          if (!request || request.readyState !== 4) {
            return;
          }
    
          // The request errored out and we didn't get a response, this will be
          // handled by onerror instead
          // With one exception: request that using file: protocol, most browsers
          // will return status as 0 even though it's a successful request
          if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
            return;
          }
    
          // Prepare the response
          var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
          var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
          var response = {
            data: responseData,
            status: request.status,
            statusText: request.statusText,
            headers: responseHeaders,
            config: config,
            request: request
          };
    
          settle(resolve, reject, response);
    
          // Clean up request
          request = null;
        };
    
        // Handle browser request cancellation (as opposed to a manual cancellation)
        request.onabort = function handleAbort() {
          if (!request) {
            return;
          }
    
          reject(createError('Request aborted', config, 'ECONNABORTED', request));
    
          // Clean up request
          request = null;
        };
    
        // Handle low level network errors
        request.onerror = function handleError() {
          // Real errors are hidden from us by the browser
          // onerror should only fire if it's a network error
          reject(createError('Network Error', config, null, request));
    
          // Clean up request
          request = null;
        };
    
        // Handle timeout
        request.ontimeout = function handleTimeout() {
          reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
            request));
    
          // Clean up request
          request = null;
        };
    
        // Add xsrf header
        // This is only done if running in a standard browser environment.
        // Specifically not if we're in a web worker, or react-native.
        if (utils.isStandardBrowserEnv()) {
          var cookies = require('./../helpers/cookies');
    
          // Add xsrf header
          var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
            cookies.read(config.xsrfCookieName) :
            undefined;
    
          if (xsrfValue) {
            requestHeaders[config.xsrfHeaderName] = xsrfValue;
          }
        }
    
        // Add headers to the request
        if ('setRequestHeader' in request) {
          utils.forEach(requestHeaders, function setRequestHeader(val, key) {
            if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
              // Remove Content-Type if data is undefined
              delete requestHeaders[key];
            } else {
              // Otherwise add header to the request
              request.setRequestHeader(key, val);
            }
          });
        }
    
        // Add withCredentials to request if needed
        if (config.withCredentials) {
          request.withCredentials = true;
        }
    
        // Add responseType to request if needed
        if (config.responseType) {
          try {
            request.responseType = config.responseType;
          } catch (e) {
            // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2.
            // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function.
            if (config.responseType !== 'json') {
              throw e;
            }
          }
        }
    
        // Handle progress if needed
        if (typeof config.onDownloadProgress === 'function') {
          request.addEventListener('progress', config.onDownloadProgress);
        }
    
        // Not all browsers support upload events
        if (typeof config.onUploadProgress === 'function' && request.upload) {
          request.upload.addEventListener('progress', config.onUploadProgress);
        }
    
        if (config.cancelToken) {
          // Handle cancellation
          config.cancelToken.promise.then(function onCanceled(cancel) {
            if (!request) {
              return;
            }
    
            request.abort();
            reject(cancel);
            // Clean up request
            request = null;
          });
        }
    
        if (requestData === undefined) {
          requestData = null;
        }
    
        // Send the request
        request.send(requestData);
      });
    };
    

    xhrAdapter 방법 은 브 라 우 저 환경 에서 의 핵심 으로 다음 과 같은 일 을 했 습 니 다.
  • 1. 구조 XML HttpRequest 대상.
  • 2. 사용자 이름 비밀 번 호 를 설정 할 지 여 부 를 판단 하고 있 으 면 Header 에 추가 합 니 다.
  • 3. 오픈 방법 으로 요청 을 엽 니 다.
  • 4. 시간 초과 설정.
  • 5. onreadystatechange 리 셋 방법 을 설정 합 니 다.
  • 6. onabort 리 셋 방법 을 설정 합 니 다.
  • 7. onerror 리 셋 방법 을 설정 합 니 다.
  • 8. ontimeout 리 셋 방법 을 설정 합 니 다.
  • 9. 표준 브 라 우 저 환경 이 라면 xsrf 값 을 추가 합 니 다.
  • 10. withCredentials 를 요구 하면 withCredentials 를 true 로 설정 합 니 다.
  • 11. response Type 을 설정 하면 response Type 을 설정 합 니 다.
  • 12. 다운로드 리 셋 방법 이 설정 되 어 있 으 면 다운로드 리 셋 방법 을 추가 합 니 다.
  • 13. 업로드 리 셋 방법 이 설정 되 어 있 으 면 업로드 리 셋 방법 을 추가 합 니 다.
  • 14. 이때 청 구 를 취 소 했 는 지 판단 하고, 취소 하면 청 구 를 취소한다.
  • 15. 요청 합 니 다.

  • xhrAdapter 내부 에서 우 리 를 위해 다양한 적합 과 검 사 를 해 준 것 을 볼 수 있 습 니 다.요청 을 한 후 네트워크 가 정상적으로 실행 되면 우리 의 관심 사 는 onready statechange 리 셋 방법 에 있어 야 합 니 다.
    // lib/adapters/xhr.js
    request.onreadystatechange = function handleLoad() {
          if (!request || request.readyState !== 4) {
            return;
          }
    
          // The request errored out and we didn't get a response, this will be
          // handled by onerror instead
          // With one exception: request that using file: protocol, most browsers
          // will return status as 0 even though it's a successful request
          if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
            return;
          }
    
          // Prepare the response
          var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
          var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
          var response = {
            data: responseData,
            status: request.status,
            statusText: request.statusText,
            headers: responseHeaders,
            config: config,
            request: request
          };
    
          settle(resolve, reject, response);
    
          // Clean up request
          request = null;
        };
    

    onready statechange 내부 에서 다음 과 같은 일 을 했 습 니 다.
  • 1. file: 프로 토 콜 에 대해 특수 처 리 를 요청 합 니 다.
  • 2. 구조 응답 대상.
  • 3. 정착 방법 에 들어간다.
  • // lib/core/settle.js
    module.exports = function settle(resolve, reject, response) {
      var validateStatus = response.config.validateStatus;
      if (!validateStatus || validateStatus(response.status)) {
        resolve(response);
      } else {
        reject(createError(
          'Request failed with status code ' + response.status,
          response.config,
          null,
          response.request,
          response
        ));
      }
    };
    

    sett 내 부 는 수신 가능 한 상 태 를 스스로 판단 하고 vaidate Status 방법 으로 검증 할 수 있 습 니 다.이 책 을 다 집행 한 후에 집행 이 완 료 된 셈 이다.
    지금까지 xhrAdapter 에서 돌아 온 Promise 를 실행 해 야 합 니 다. 이 Promise 에 대응 하 는 then 방법 으로 돌아 가 야 합 니 다.
    // lib/core/dispatchRequest.js
      return adapter(config).then(function onAdapterResolution(response) {
        throwIfCancellationRequested(config);
    
        // Transform response data
    
        //          
        response.data = transformData(
          response.data,
          response.headers,
          config.transformResponse
        );
    
        return response;
      }, function onAdapterRejection(reason) {
        if (!isCancel(reason)) {
          throwIfCancellationRequested(config);
    
          // Transform response data
          if (reason && reason.response) {
            reason.response.data = transformData(
              reason.response.data,
              reason.response.headers,
              config.transformResponse
            );
          }
        }
    
        return Promise.reject(reason);
      });
    

    모든 것 이 순 조 롭 습 니 다. 여 기 는 onAdapter Resolution 방법 을 실행 해 야 합 니 다.onAdapter Resolution 방법 에서 다음 과 같은 일 을 실 행 했 습 니 다.
  • 1. 요청 을 취소 하 는 지 확인 합 니 다.
  • 2. 돌아 오 는 대상 에 대해 반 직렬 화 처 리 를 하고 사용자 정의 반 직렬 화 방법 도 지원 합 니 다.
  • 3. 반 직렬 화 된 대상 으로 돌아간다.

  • 여기까지, 우 리 는 또 어디로 가서 대응 하 는 then 방법 을 찾 아야 합 니까?우 리 는 층 층 이 돌아 가 야 한다.방금 dispatch Request 방법 은 lib / core / Axios. js 의 request 에서 호출 되 었 기 때문에 여기 then 방법 은 여기에 있 을 것 입 니 다.전에 request 에서 실 행 했 던 이 코드 기억 나 세 요?
      while (chain.length) {
        promise = promise.then(chain.shift(), chain.shift());
      }
    

    맞습니다. 이전 chain 에 서 는 비 즈 니스 네트워크 계층 의 사용자 정의 요청 차단기 와 응답 차단기 가 저장 되 어 있 었 기 때문에 dispatch Request 방법 은 Promise 에 의 해 비 즈 니스 네트워크 계층 의 응답 차단기 에 실 행 됩 니 다. 비 즈 니스 네트워크 계층 의 응답 차단 기 를 다시 붙 입 니 다.
    service.interceptors.response.use(
        response => {
            return Promise.reject(response);
        },
        error => {
            return Promise.reject(error);
        }
    );
    

    응, 우 리 는 아직 아무것도 처리 하지 않 았 어. 여기 서 실 행 된 후에 우리 의 업무 요청 처 의 then 방법 을 촉발 하기 시 작 했 어.
    axios.get('/get/server').then(function (response) {
    
    }).catch(function (err) {
    
    });
    

    당시 우리 의 업무 코드 에는 아무것도 쓰 여 있 지 않 았 는데, 콘 솔 을 통 해 출력 하면 최종 반환 결 과 를 볼 수 있 었 다.
    여기까지 가 Axios 의 전체 요청 과정 이 었 습 니 다. 잘 아 시 겠 습 니까?
    Axios 소스 코드 를 읽 고 다음 과 같은 요약 이 있 습 니 다.
  • Promise 는 그 중의 접착제 이 고 합작 약속 을 했다.
  • 대상 을 위 한 숙련 된 사용.
  • 좋 은 코드 규범 은 참여 번호, 칸 막 이, 주석 을 참조 합 니 다.
  • 좋 은 설명 문서:https://www.npmjs.com/package/axios 。
  • 데이터 변환 방식 과 같은 내부 의 더 많은 세부 사항 을 제어 할 수 있 습 니 다.

  • 단점:
  • 높 은 병발 에 대해 처리 하지 않 았 다.

  • 전단 의 다른 소스 코드 분석 글: Promise 소스 코드 분석https://sahadev.blog.csdn.net/article/details/90722543 Node. js setTimeout 방법의 실행 과정 을 깊이 분석 합 니 다.https://sahadev.blog.csdn.net/article/details/90703250 Vue 소스 코드 탐구 노트https://sahadev.blog.csdn.net/article/details/87943168

    좋은 웹페이지 즐겨찾기