精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Axios Node 端請求是如何實現的?

開發 前端
本文主要帶大家學習了 axios 的 Node 端實現。相比較于瀏覽器端要稍微復雜一些,不僅是因為我們要考慮請求可能的最大跳轉(maxRedirects),還要同時監聽請求實例以及響應流數據上的事件,確保整個請求過程被完整監聽。
本文我們將討論 axios 的 Node 環境實現。我們都知道使用 axios 可以讓我們在瀏覽器和 Node 端獲得一致的使用體驗。這部分是通過適配器模式來實現的。

axios 內置了 2 個適配器(截止到 v1.6.8 版本)[8]:xhr.js 和 http.js。

圖片圖片

顧名思義,xhr.js 是針對瀏覽器環境提供的 XMLHttpRequest 封裝的;http.js 則是針對 Node 端的 http/https 模塊進行封裝的。

不久前,我們詳細講解了瀏覽器端的實現,本文就來看看 Node 環境又是如何實現的。

Node 端請求案例

老規矩,在介紹實現之前,先看看 axios 在瀏覽器器環境的使用。

首先創建項目,安裝 axios 依賴:

mdir axios-demos
cd axios-demos
npm init
npm install axios
# 使用 VS Code 打開當前目錄
code .

寫一個測試文件 index.js:

// index.js
const axios = require('axios')

axios.get('https://httpstat.us/200')
  .then(res => {
    console.log('res >>>>', res)
  })

執行文件:

node --watch index.js

注意:--watch[9] 是 Node.js 在 v16.19.0 版本引入的實驗特性,在 v22.0.0 已轉為正式特性。

打印出來結果類似:

Restarting 'index.js'
res >>>> {
  status: 200,
  statusText: 'OK'
  headers: Object [AxiosHeaders] {}
  config: {}
  request: <ref *1> ClientRequest {}
  data: { code: 200, description: 'OK' }
}
Completed running 'index.js'

修改 Index.js 文件內容保存:

const axios = require('axios')

axios.get('https://httpstat.us/404')
  .catch(err => {
    console.log('err >>>>', err)
  })

打印結果類似:

Restarting 'index.js'
err >>>> AxiosError: Request failed with status code 404 {
  code: 'ERR_BAD_REQUEST',
  config: {}
  request: <ref *1> ClientRequest {}
  response: {
    status: 404,
    statusText: 'Not Found',
    data: { code: 404, description: 'Not Found' }
  }
}

以上我們就算講完了 axios 在 Node 端的簡單使用,這就是 axios 好處所在,統一的使用體驗,免去了我們在跨平臺的學習成本,提升了開發體驗。

源碼分析

接下來就來看看 axios 的 Node 端實現。源代碼位于 lib/adapters/http.js[10] 下。

// /v1.6.8/lib/adapters/http.js#L160
export default isHttpAdapterSupported && function httpAdapter(config) {/* ... */}

Node 端發出的請求最終都是交由 httpAdapter(config) 函數處理的,其核心實現如下:

import http from 'http';
import https from 'https';

export default isHttpAdapterSupported && function httpAdapter(config) {
  // 1)
  return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) {
    
    // 2)
    let {data, lookup, family} = config;
    const {responseType, responseEncoding} = config;
    const method = config.method.toUpperCase();
    
    // Parse url
    const fullPath = buildFullPath(config.baseURL, config.url);
    const parsed = new URL(fullPath, 'http://localhost');
    
    const headers = AxiosHeaders.from(config.headers).normalize();
    
    if (data && !utils.isStream(data)) {
      if (Buffer.isBuffer(data)) {
        // Nothing to do...
      } else if (utils.isArrayBuffer(data)) {
        data = Buffer.from(new Uint8Array(data));
      } else if (utils.isString(data)) {
        data = Buffer.from(data, 'utf-8');
      } else {
        return reject(new AxiosError(
          'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream',
          AxiosError.ERR_BAD_REQUEST,
          config
        ));
      }
    }
    
    const options = {
      path,
      method: method,
      headers: headers.toJSON(),
      agents: { http: config.httpAgent, https: config.httpsAgent },
      auth,
      protocol,
      family,
      beforeRedirect: dispatchBeforeRedirect,
      beforeRedirects: {}
    };
    
    // 3)  
    let transport;
    const isHttpsRequest = /https:?/.test(options.protocol);
    
    if (config.maxRedirects === 0) {
      transport = isHttpsRequest ? https : http;
    }
    
    // Create the request
    req = transport.request(options, function handleResponse(res) {
      // ...
    }
    
    // 4)
    // Handle errors
    req.on('error', function handleRequestError(err) {
      // @todo remove
      // if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return;
      reject(AxiosError.from(err, null, config, req));
    });
    
    // 5)
    // Handle request timeout
    if (config.timeout) {
      req.setTimeout(timeout, function handleRequestTimeout() {
        if (isDone) return;
        let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
        const transitional = config.transitional || transitionalDefaults;
        if (config.timeoutErrorMessage) {
          timeoutErrorMessage = config.timeoutErrorMessage;
        }
        reject(new AxiosError(
          timeoutErrorMessage,
          transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
          config,
          req
        ));
        abort();
      });
    }
    
    // 6)
    // Send the request
    if (utils.isStream(data)) {
      let ended = false;
      let errored = false;

      data.on('end', () => {
        ended = true;
      });

      data.once('error', err => {
        errored = true;
        req.destroy(err);
      });

      data.on('close', () => {
        if (!ended && !errored) {
          abort(new CanceledError('Request stream has been aborted', config, req));
        }
      });

      data.pipe(req);
    } else {
      req.end(data);
    }
  }

是有點長,但大概瀏覽一遍就行,后面會詳細講。實現主要有 6 部分:

  1. 這里的 wrapAsync 是對 return new Promise((resolve, resolve) => {}) 的包裝,暴露出 resolve、reject 供 dispatchHttpRequest 函數內部調用使用,代表請求成功或失敗
  2. 接下里,就是根據傳入的 config 信息組裝請求參數 options 了
  3. axios 會根據傳入的 url 的協議,決定是采用 http 還是 https 模塊創建請求
  4. 監聽請求 req 上的異常(error)事件
  5. 跟 4) 一樣,不過監聽的是請求 req 上的超時事件。而其他諸如取消請求、完成請求等其他兼容事件則是在 2) 創建請求的回調函數 handleResponse(res) 中處理的
  6. 最后,調用 req.end(data) 發送請求即可。當然,這里會針對 data 是 Stream 類型的情況特別處理一下

大概介紹了之后,我們再深入每一步具體學習一下。

包裝函數 wrapAsync

首先,httpAdapter(config) 內部的實現是經過 wrapAsync 包裝函數返回的。

// /v1.6.8/lib/adapters/http.js#L122-L145
const wrapAsync = (asyncExecutor) => {
  return new Promise((resolve, reject) => {
    let onDone;
    let isDone;

    const done = (value, isRejected) => {
      if (isDone) return;
      isDone = true;
      onDone && onDone(value, isRejected);
    }

    const _resolve = (value) => {
      done(value);
      resolve(value);
    };

    const _reject = (reason) => {
      done(reason, true);
      reject(reason);
    }

    asyncExecutor(_resolve, _reject, (onDoneHandler) => (onDone = onDoneHandler)).catch(_reject);
  })
};

調用 wrapAsync 函數會返回一個 Promise 對象,除了跟原生 Promise 構造函數一樣會返回 resolve、reject 之外,還額外拓展了一個 onDone 參數,確保 Promise 狀態改變后,總是會調用 onDone。

組裝請求參數

在處理好返回值后,接下來要做的就是組裝請求參數了,請求參數最終會交由 http.request(options)[11]/https.request(options)[12] 處理,因此需要符合其類型定義。

http 模塊的請求案例

在理解 options 參數之前,先了解一下 http 模塊的請求案例。

const http = require('node:http');

const options = {
  hostname: 'www.google.com',
  port: 80,
  path: '/upload',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(postData),
  },
};

const req = http.request(options, (res) => {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.setEncoding('utf8');
  res.on('data', (chunk) => {
    console.log(`BODY: ${chunk}`);
  });
  res.on('end', () => {
    console.log('No more data in response.');
  });
});

req.on('error', (e) => {
  console.error(`problem with request: ${e.message}`);
});

req.end(JSON.stringify({
  'msg': 'Hello World!',
}));

以上,我們向 http://www.google.com/upload 發起了一個 POST 請求(https 請求與此類次)。

值得注意的是,請求參數 options 中并不包含請求體數據,請求體數據最終是以 req.end(data) 發動出去的,這一點跟 XMLHttpRequest 實例的做法類似。

組裝請求參數

再來看看 axios 中關于這塊請求參數的組裝邏輯。

首先,使用 .baseURL 和 .url 參數解析出跟 URL 相關數據。

/v1.6.8/lib/adapters/http.js#L221
// Parse url
const fullPath = buildFullPath(config.baseURL, config.url);
const parsed = new URL(fullPath, 'http://localhost');
const protocol = parsed.protocol || supportedProtocols[0];

不支持的請求協議會報錯。

// /v1.6.8/lib/platform/node/index.js#L11
protocols: [ 'http', 'https', 'file', 'data' ]
// /v1.6.8/lib/adapters/http.js#L44
const supportedProtocols = platform.protocols.map(protocol => {
  return protocol + ':';
});

// /v1.6.8/lib/adapters/http.js#L265-L271
if (supportedProtocols.indexOf(protocol) === -1) {
  return reject(new AxiosError(
    'Unsupported protocol ' + protocol,
    AxiosError.ERR_BAD_REQUEST,
    config
  ));
}

錯誤 CODE 是 ERR_BAD_REQUEST,類似 4xx 錯誤。

接下來,將 headers 參數轉成 AxiosHeaders 實例。

// /v1.6.8/lib/adapters/http.js#L273
const headers = AxiosHeaders.from(config.headers).normalize();

最后,處理下請求體數據 config.data。

// /v1.6.8/lib/adapters/http.js#L287-L326
// support for spec compliant FormData objects
if (utils.isSpecCompliantForm(data)) {
  const userBoundary = headers.getContentType(/boundary=([-_\w\d]{10,70})/i);

  data = formDataToStream(data, (formHeaders) => {
    headers.set(formHeaders);
  }, {
    tag: `axios-${VERSION}-boundary`,
    boundary: userBoundary && userBoundary[1] || undefined
  });
  // support for https://www.npmjs.com/package/form-data api
} else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) {
  headers.set(data.getHeaders());

  if (!headers.hasContentLength()) {
    try {
      const knownLength = await util.promisify(data.getLength).call(data);
      Number.isFinite(knownLength) && knownLength >= 0 && headers.setContentLength(knownLength);
      /*eslint no-empty:0*/
    } catch (e) {
    }
  }
} else if (utils.isBlob(data)) {
  data.size && headers.setContentType(data.type || 'application/octet-stream');
  headers.setContentLength(data.size || 0);
  data = stream.Readable.from(readBlob(data));
} else if (data && !utils.isStream(data)) {
  if (Buffer.isBuffer(data)) {
    // Nothing to do...
  } else if (utils.isArrayBuffer(data)) {
    data = Buffer.from(new Uint8Array(data));
  } else if (utils.isString(data)) {
    data = Buffer.from(data, 'utf-8');
  } else {
    return reject(new AxiosError(
      'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream',
      AxiosError.ERR_BAD_REQUEST,
      config
    ));
  }

axios 會針對傳入的不同類型的 config.data 做統一處理,最終不是處理成 Stream 就是處理成 Buffer。

不過,當傳入的 data 是對象時,在調用 httpAdapter(config) 之前,會先經過 transformRequest() 函數處理成字符串。

// /v1.6.8/lib/defaults/index.js#L91-L94
if (isObjectPayload || hasJSONContentType ) {
  headers.setContentType('application/json', false);
  return stringifySafely(data);
}

針對這個場景,data 會進入到下面的處理邏輯,將字符串處理成 Buffer。

// /v1.6.8/lib/adapters/http.js#L287-L326
if (utils.isString(data)) {
  data = Buffer.from(data, 'utf-8');
}

然后,獲得請求路徑 path。

// /v1.6.8/lib/adapters/http.js#L384C4-L397C1
try {
  path = buildURL(
    parsed.pathname + parsed.search,
    config.params,
    config.paramsSerializer
  ).replace(/^\?/, '');
} catch (err) {
   // ...
}

最后,組裝 options 參數。

// /v1.6.8/lib/adapters/http.js#L403C1-L413C7
const options = {
  path,
  method: method,
  headers: headers.toJSON(),
  agents: { http: config.httpAgent, https: config.httpsAgent },
  auth,
  protocol,
  family,
  beforeRedirect: dispatchBeforeRedirect,
  beforeRedirects: {}
};

創建請求

再看創建請求環節。

獲得請求實例

首先,是獲得請求實例。

import followRedirects from 'follow-redirects';
const {http: httpFollow, https: httpsFollow} = followRedirects;

// /v1.6.8/lib/adapters/http.js#L426-L441
let transport;
const isHttpsRequest = isHttps.test(options.protocol);
options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;
if (config.transport) {
  transport = config.transport;
} else if (config.maxRedirects === 0) {
  transport = isHttpsRequest ? https : http;
} else {
  if (config.maxRedirects) {
    options.maxRedirects = config.maxRedirects;
  }
  if (config.beforeRedirect) {
    options.beforeRedirects.config = config.beforeRedirect;
  }
  transport = isHttpsRequest ? httpsFollow : httpFollow;
}

如上所示,你可以通過 config.transport 傳入,但通常不會這么做。否則,axios 內部會根據你是否傳入 config.maxRedirects(默認 undefined) 決定使用原生 http/https 模塊還是 follow-redirects 包里提供的 http/https 方法。

如果沒有傳入 config.maxRedirects,axios 默認會使用 follow-redirects 包里提供的 http/https 方法發起請求,它的用法跟原生 http/https 模塊一樣,這里甚至可以只使用 follow-redirects 就夠了。

創建請求

下面就是創建請求了。

// Create the request
req = transport.request(options, function handleResponse(res) {}

我們在 handleResponse 回調函數里處理返回數據 res。

function request(options: RequestOptions | string | URL, callback?: (res: IncomingMessage) => void): ClientRequest;
function request(
    url: string | URL,
    options: RequestOptions,
    callback?: (res: IncomingMessage) => void,
): ClientRequest;

根據定義,我們知道 res 是 IncomingMessage 類型,繼承自 stream.Readable[13],是一種可讀的 Stream。

const readable = getReadableStreamSomehow();
readable.on('data', (chunk) => {
  console.log(`Received ${chunk.length} bytes of data.`);
});

res 的處理我們會放到處理請求一節講述,下面就是發出請求了。

發出請求

這部分代碼比較簡單,而數據體也是在這里傳入的。

// /v1.6.8/lib/adapters/http.js#L658C5-L681C6
// Send the request
if (utils.isStream(data)) {
  let ended = false;
  let errored = false;

  data.on('end', () => {
    ended = true;
  });

  data.once('error', err => {
    errored = true;
    req.destroy(err);
  });

  data.on('close', () => {
    if (!ended && !errored) {
      abort(new CanceledError('Request stream has been aborted', config, req));
    }
  });

  data.pipe(req);
} else {
  req.end(data);
}

如果你的請求體是 Buffer 類型的,那么直接傳入 req.end(data) 即可,否則(Stream 類型)則需要以管道形式傳遞給 req。

處理請求

接著創建請求一節,下面開始分析請求的處理。

Node.js 部分的請求處理,比處理 XMLHttpRequest 稍微復雜一些。你要在 2 個地方做監聽處理。

  1. transport.request 返回的 req 實例
  2. 另一個,則是 transport.request 回調函數 handleResponse 返回的 res(也就是 responseStream)

監聽 responseStream

首先,用 res/responseStream 上已有的信息組裝響應數據 response。

// /v1.6.8/lib/adapters/http.js#L478
// decompress the response body transparently if required
let responseStream = res;

// return the last request in case of redirects
const lastRequest = res.req || req;

const response = {
  status: res.statusCode,
  statusText: res.statusMessage,
  headers: new AxiosHeaders(res.headers),
  config,
  request: lastRequest
};

這是不完整的,因為我們還沒有設置 response.data。

// /v1.6.8/lib/adapters/http.js#L535C7-L538C15
if (responseType === 'stream') {
  response.data = responseStream;
  settle(resolve, reject, response);
} else {
  // ...
}

如果用戶需要的是響應類型是 stream,那么一切就變得簡單了,直接將數據都給 settle 函數即可。

// /v1.6.8/lib/core/settle.js
export default function settle(resolve, reject, response) {
  const validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(new AxiosError(
      'Request failed with status code ' + response.status,
      [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
      response.config,
      response.request,
      response
    ));
  }
}

settle 函數會根據傳入的 response.status 和 config.validateStatus() 決定請求是成功(resolve)還是失敗(reject)。

當然,如果需要的響應類型不是 stream,就監聽 responseStream 對象上的事件,處理請求結果。

// /v1.6.8/lib/adapters/http.js#L538C1-L591C8
} else {
  const responseBuffer = [];
  let totalResponseBytes = 0;

  // 1)
  responseStream.on('data', function handleStreamData(chunk) {
    responseBuffer.push(chunk);
    totalResponseBytes += chunk.length;

    // make sure the content length is not over the maxContentLength if specified
    if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) {
      // stream.destroy() emit aborted event before calling reject() on Node.js v16
      rejected = true;
      responseStream.destroy();
      reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded',
        AxiosError.ERR_BAD_RESPONSE, config, lastRequest));
    }
  });
  
  // 2)
  responseStream.on('aborted', function handlerStreamAborted() {
    if (rejected) {
      return;
    }

    const err = new AxiosError(
      'maxContentLength size of ' + config.maxContentLength + ' exceeded',
      AxiosError.ERR_BAD_RESPONSE,
      config,
      lastRequest
    );
    responseStream.destroy(err);
    reject(err);
  });

  // 3)
  responseStream.on('error', function handleStreamError(err) {
    if (req.destroyed) return;
    reject(AxiosError.from(err, null, config, lastRequest));
  });
  
  // 4)
  responseStream.on('end', function handleStreamEnd() {
    try {
      let responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer);
      if (responseType !== 'arraybuffer') {
        responseData = responseData.toString(responseEncoding);
        if (!responseEncoding || responseEncoding === 'utf8') {
          responseData = utils.stripBOM(responseData);
        }
      }
      response.data = responseData;
    } catch (err) {
      return reject(AxiosError.from(err, null, config, response.request, response));
    }
    settle(resolve, reject, response);
  });
}

responseStream 上會監聽 4 個事件。

  1. data:Node 請求的響應默認都是以流數據形式接收的,而 data 就是在接收過程中會不斷觸發的事件。我們在這里將接收到的數據存儲在 responseBuffer 中,以便后續使用
  2. aborted:會在接收響應數據超過時,或是調用 .destory() 時觸發
  3. err:在流數據接收錯誤時調用
  4. end:數據結束接收,將收集到的 responseBuffer 先轉換成 Buffer 類型,再轉換成字符串,最終賦值給 response.data

監聽 req

以上,我們完成了對響應數據的監聽。我們再來看看,對請求實例 req 的監聽。

// /v1.6.8/lib/adapters/http.js#L606
// Handle errors
req.on('error', function handleRequestError(err) {
  // @todo remove
  // if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return;
  reject(AxiosError.from(err, null, config, req));
});

// /v1.6.8/lib/adapters/http.js#L619
// Handle request timeout
if (config.timeout) {
  req.setTimeout(timeout, function handleRequestTimeout() {
    if (isDone) return;
    let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
    const transitional = config.transitional || transitionalDefaults;
    if (config.timeoutErrorMessage) {
      timeoutErrorMessage = config.timeoutErrorMessage;
    }
    reject(new AxiosError(
      timeoutErrorMessage,
      transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
      config,
      req
    ));
    abort();
  });
}

一共監聽了 2 個事件:

  1. error:請求出錯
  2. req.setTimeout():請求超時

以上,我們就完成了請求處理的所有內容。可以發現,Node 端處理請求的邏輯會比瀏覽器端稍微復雜一些:你需要同時監聽請求實例以及響應流數據上的事件,確保整個請求過程被完整監聽。

總結

本文主要帶大家學習了 axios 的 Node 端實現。

相比較于瀏覽器端要稍微復雜一些,不僅是因為我們要考慮請求可能的最大跳轉(maxRedirects),還要同時監聽請求實例以及響應流數據上的事件,確保整個請求過程被完整監聽。

參考資料

[1]axios 是如何實現取消請求的?: https://juejin.cn/post/7359444013894811689

[2]你知道嗎?axios 請求是 JSON 響應優先的: https://juejin.cn/post/7359580605320036415

[3]axios 跨端架構是如何實現的?: https://juejin.cn/post/7362119848660451391

[4]axios 攔截器機制是如何實現的?: https://juejin.cn/post/7363545737874161703

[5]axios 瀏覽器端請求是如何實現的?: https://juejin.cn/post/7363928569028821029

[6]axios 對外出口API是如何設計的?: https://juejin.cn/post/7364614337371308071

[7]axios 中是如何處理異常的?: https://juejin.cn/post/7369951085194739775

[8]axios 內置了 2 個適配器(截止到 v1.6.8 版本): https://github.com/axios/axios/tree/v1.6.8/lib/adapters

[9]--watch: https://nodejs.org/api/cli.html#--watch

[10]lib/adapters/http.js: https://github.com/axios/axios/blob/v1.6.8/lib/adapters/http.js

[11]http.request(options): https://nodejs.org/docs/latest/api/http.html#httprequestoptions-callback

[12]https.request(options): https://nodejs.org/docs/latest/api/https.html#httpsrequestoptions-callback

[13]stream.Readable: https://nodejs.org/docs/latest/api/stream.html#class-streamreadable

責任編輯:武曉燕 來源: 寫代碼的寶哥
相關推薦

2021-04-22 05:37:14

Axios 開源項目HTTP 攔截器

2024-04-30 09:53:12

axios架構適配器

2019-09-20 09:12:03

服務器互聯網TCP

2019-08-20 08:56:18

Linux設計數據庫

2021-07-27 14:50:15

axiosHTTP前端

2021-04-12 05:55:29

緩存數據Axios

2021-04-06 06:01:11

AxiosWeb 項目開發

2020-11-12 09:55:02

OAuth2

2023-10-04 07:35:03

2021-12-02 07:25:58

ASP.NET CorAjax請求

2024-08-12 12:32:53

Axios機制網絡

2018-07-30 16:31:00

javascriptaxioshttp

2024-08-27 08:55:32

Axios底層網絡

2022-10-19 09:27:39

2022-09-02 10:20:44

網絡切片網絡5G

2022-02-22 11:17:31

Kafka架構代碼

2018-04-22 00:01:43

JavaScript Node 語言

2022-01-28 14:20:53

前端代碼中斷

2021-08-20 09:50:41

Web指紋前端

2021-03-09 08:03:21

Node.js 線程JavaScript
點贊
收藏

51CTO技術棧公眾號

肥熟一91porny丨九色丨| 欧美日韩在线综合| 国产一区二区三区四区五区在线| 久久久久久久久艹| jizz国产精品| 亚洲成av人片在线| 国产经品一区二区| 日韩伦理在线视频| 国产99亚洲| 欧美日韩中字一区| 黄色污污在线观看| 色欲久久久天天天综合网| 午夜在线视频一区二区区别| 在线观看国产成人av片| 手机精品视频在线| 日本小视频在线免费观看| 不卡av在线网| 国产99久久精品一区二区| 99久久99久久精品免费| 日本精品在线观看| 欧美性猛交xxxx黑人| 亚洲午夜精品久久久中文影院av| 国产毛片一区二区三区va在线| 欧美激情麻豆| 亚洲高清久久久久久| av免费中文字幕| 五月香视频在线观看| 国产精品亚洲第一区在线暖暖韩国 | 在线看日本不卡| 中文字幕中文字幕在线中一区高清| 国产sm主人调教女m视频| 影音先锋亚洲一区| 最近2019好看的中文字幕免费| 美女被艹视频网站| 激情开心成人网| 一区二区三区在线看| 日本一区免费在线观看| www男人的天堂| 视频在线在亚洲| 日韩一区二区福利| 中文字幕久久久久久久| 88xx成人永久免费观看| 亚洲一区视频在线观看视频| 日本一区二区三区免费看| 亚洲黄色在线免费观看| 人禽交欧美网站| 97久久精品国产| 加勒比婷婷色综合久久| 精品高清久久| 精品sm捆绑视频| 国产无遮挡猛进猛出免费软件| 国产乱码午夜在线视频| 亚洲女与黑人做爰| 蜜桃精品久久久久久久免费影院| 99精品久久久久久中文字幕 | 黄www在线观看| 久草中文在线| 亚洲国产成人一区二区三区| 麻豆一区区三区四区产品精品蜜桃| 国产免费黄色网址| 日本怡春院一区二区| 4438全国成人免费| 国产波霸爆乳一区二区| 99热在线成人| 中国日韩欧美久久久久久久久| 中文字幕xxx| 激情亚洲另类图片区小说区| 欧美mv日韩mv亚洲| 国产乱女淫av麻豆国产| 久久久久久久性潮| 欧美色国产精品| 国产成人a亚洲精v品无码| av影院在线| 亚洲成av人片| 日韩在线视频在线观看| 深夜成人在线| 精品久久久久久久久久ntr影视| 欧美另类videosbestsex日本| 超碰最新在线| 亚洲精品乱码久久久久久久久| 欧美a级黄色大片| 成人av福利| 一区二区三区影院| av免费看网址| 在线能看的av网址| 色婷婷av一区| 婷婷激情四射五月天| 亚洲小少妇裸体bbw| 欧美午夜性色大片在线观看| 日韩a在线播放| 黄色精品视频| 717成人午夜免费福利电影| 亚洲一二区在线观看| 天堂av一区| 精品国产sm最大网站| 无码人妻精品一区二区三| 国产精品videossex| 亚洲精品在线不卡| 日韩中文字幕有码| 久久蜜桃av| 欧美国产日韩一区二区在线观看| 国产一级久久久| 天堂在线亚洲视频| 国产中文字幕91| av中文字幕播放| 99久久精品免费看国产| 欧美午夜免费| 国自产拍在线网站网址视频| 国产精品传媒入口麻豆| 妞干网这里只有精品| 青青青免费在线视频| 亚洲成人午夜电影| 老头吃奶性行交视频| 国产一区精品二区| 亚洲精品福利在线观看| 亚洲第一成人网站| 久久久9色精品国产一区二区三区| 欧美激情a∨在线视频播放| 欧美日韩乱国产| 老司机精品视频在线| 国产精品久久久久久久久久久久午夜片| 午夜成人鲁丝片午夜精品| 中文字幕一区二区三区在线观看| 免费视频爱爱太爽了| 日本精品另类| 精品毛片乱码1区2区3区| 成人午夜剧场视频网站| 欧美jizzhd精品欧美巨大免费| 欧洲中文字幕国产精品| 国产福利免费视频| 99久久精品国产精品久久| 亚洲最新免费视频| 正在播放日韩精品| 日韩欧美黄色影院| 嘿嘿视频在线观看| 国产欧美高清| 亚洲最大成人在线| 黄色电影免费在线看| 亚洲一区免费观看| 日本三级黄色网址| 亚欧洲精品视频在线观看| 欧美日韩国产成人在线| 亚洲综合久久网| 成人av综合在线| 懂色av一区二区三区四区五区| 手机看片久久| 亚洲精品久久久久久久久| 亚洲波多野结衣| 日本 国产 欧美色综合| 欧美欧美一区二区| 免费在线小视频| 日韩欧美国产三级电影视频| 亚洲综合第一区| 免费在线看一区| 欧美日韩在线高清| 樱花草涩涩www在线播放| 欧美大片在线观看一区二区| 99热6这里只有精品| 视频一区二区欧美| 欧美激情论坛| 欧美激情网站| 日韩精品在线私人| 日本少妇裸体做爰| www.欧美亚洲| 日本aa在线观看| 国产激情综合| 这里只有精品视频在线| 黄色av网站免费| 91麻豆国产福利精品| 成熟老妇女视频| 亚洲人成亚洲精品| 琪琪第一精品导航| 欧美日韩在线精品一区二区三区激情综| 亚洲国产中文字幕在线视频综合 | 中文成人无字幕乱码精品区| 狠狠噜噜久久| 国内一区二区在线视频观看| 91九色在线播放| 日韩精品高清在线| 在线观看黄网站| 91丨九色丨蝌蚪富婆spa| 精品欧美一区免费观看α√| 婷婷成人在线| 日本精品免费一区二区三区| 黄色av网站在线免费观看| 欧美日韩免费看| 亚洲av人人澡人人爽人人夜夜| 99精品欧美| 日韩区国产区| 榴莲视频成人app| 78m国产成人精品视频| 91精彩视频在线观看| 91精品国产综合久久婷婷香蕉| 国产在线一区视频| 国产无遮挡一区二区三区毛片日本| 超碰在线97免费| 欧美另类综合| 日本一区网站| 成人高潮视频| 国产精自产拍久久久久久蜜| 深夜国产在线播放| 亚洲天堂av图片| 国产成人麻豆精品午夜在线 | www国产精品视频| 亚洲精品字幕在线| 欧美色视频在线观看| 黄色小视频在线免费看| 欧美国产禁国产网站cc| 久久久久亚洲av成人网人人软件| 久久久久久久欧美精品| 麻豆传媒网站在线观看| 国产精品一区二区99| 91久久极品少妇xxxxⅹ软件| 三上悠亚激情av一区二区三区| 欧美成人激情视频免费观看| freemovies性欧美| 亚洲аv电影天堂网| 国产精品久久久久久久免费看| 精品久久久国产精品999| 成人免费黄色小视频| 久久九九全国免费| 国产日韩视频一区| 久久99精品久久久久久| 99精品免费在线观看| 欧美日韩国产综合网| 一区二区三区四区欧美日韩| 香蕉久久精品| 国产一区免费视频| 国产精品1区| 国产欧美亚洲精品| 蜜臀国产一区| **欧美日韩vr在线| 国产三线在线| 欧美日韩国产成人高清视频| 毛片在线看网站| 在线观看欧美视频| 国产中文字幕在线看| 亚洲国产日韩欧美在线99| 一本色道久久综合精品婷婷| 色综合久久88色综合天天6| 日韩精品成人一区| 亚洲成人动漫一区| 久久网一区二区| 亚洲精品伦理在线| 男女性高潮免费网站| 国产精品久久久久久久久图文区| 久久久久久久毛片| 久久精品男人天堂av| 男人天堂av电影| 久久久久久久久久美女| 搡老熟女老女人一区二区| av在线播放成人| 色婷婷精品久久二区二区密| 国产99精品在线观看| 大尺度在线观看| 成人性生交大片免费看中文网站| 亚洲天堂小视频| 国产成人免费视频网站高清观看视频| 中文字幕亚洲影院| 韩国三级电影一区二区| 中文字幕第六页| 国产成人综合自拍| 国产免费a级片| 91在线码无精品| 精品黑人一区二区三区观看时间| 91在线视频18| 夫妇交换中文字幕| 中文字幕在线视频一区| 亚洲一二三在线观看| 亚洲欧美电影院| 国产亚洲精品av| 亚洲成国产人片在线观看| 欧美一级特黄视频| 欧美性受xxxx| 国产女人高潮时对白| 欧美一级夜夜爽| 手机看片1024日韩| 亚洲欧美中文日韩在线v日本| fc2在线中文字幕| 久久影院资源网| 久久一卡二卡| 青青久久av北条麻妃海外网| 超碰这里只有精品| 91黄色精品| 亚洲欧洲美洲国产香蕉| 亚洲欧美日韩另类精品一区二区三区| 久久要要av| 被灌满精子的波多野结衣| 久久久久中文| 天天操夜夜操很很操| av在线一区二区| 日本美女bbw| 亚洲一级片在线观看| av手机天堂网| 日韩欧美国产1| 九色在线免费| 九色精品美女在线| 成人免费看视频网站| 91久久久久久国产精品| 四虎884aa成人精品最新| 亚洲欧美综合一区| 136国产福利精品导航网址| 在线观看的毛片| 国产成人精品一区二区三区网站观看| 中文字幕丰满乱子伦无码专区| 国产精品系列在线| 日本三级中文字幕| 欧美麻豆精品久久久久久| 欧美特黄一级视频| www.欧美三级电影.com| 中文字幕在线视频久| 91久久极品少妇xxxxⅹ软件| 精品一区二区三区中文字幕老牛| 精品人妻人人做人人爽| 美腿丝袜在线亚洲一区| 久久久久亚洲AV成人无码国产| 中文字幕一区二| 手机在线看片1024| 精品日韩成人av| 国产美女福利在线| 国产精品久久久久久久一区探花| 亚洲一区二区三区日本久久九| 亚洲mv在线看| 香蕉精品999视频一区二区| 911亚洲精选| 亚洲欧美激情视频在线观看一区二区三区| 三级视频在线观看| 精品国产第一区二区三区观看体验 | 国产精品白浆一区二小说| 欧美区视频在线观看| 国产大片在线免费观看| 2019中文字幕在线观看| 婷婷视频一区二区三区| 正义之心1992免费观看全集完整版| 老司机午夜精品视频| 欧美做受高潮6| 欧美日韩国产精品一区二区不卡中文| www.精品久久| 九九热这里只有精品6| 涩涩涩久久久成人精品| 亚洲视频sss| 免费av网站大全久久| 亚洲自拍偷拍图| 色先锋aa成人| 激情小视频在线观看| 青青精品视频播放| 婷婷综合电影| 麻豆av免费在线| 久久久久久综合| 999视频在线| 国产亚洲欧美另类中文| 蜜桃精品在线| 午夜欧美一区二区三区免费观看| 老司机免费视频久久| 国产又粗又黄又猛| 欧美曰成人黄网| 婷婷在线视频| 91免费人成网站在线观看18| 欧美永久精品| 无码人妻aⅴ一区二区三区玉蒲团| 一区二区高清在线| 免费观看黄色av| 国产69久久精品成人看| 久久99久久人婷婷精品综合| 国产裸体舞一区二区三区| 国产日产精品1区| 国产女主播喷水视频在线观看 | 成人日批视频| av一区二区三区在线观看| 国产精品va| 亚洲欧美色图视频| 欧美亚洲精品一区| 久做在线视频免费观看| 99se婷婷在线视频观看| 亚洲伦理一区| 一级片久久久久| 91麻豆精品久久久久蜜臀| 色呦呦在线资源| 久久成人资源| 麻豆精品一区二区av白丝在线| 男女性高潮免费网站| 亚洲黄色www| 国产欧美自拍| 精品少妇人欧美激情在线观看| 26uuu色噜噜精品一区| 中文字幕在线观看视频一区| 久久国产精品电影| 亚洲人成亚洲精品| 亚洲涩涩在线观看| 婷婷夜色潮精品综合在线| 97人人在线| 国产精品久久久久免费| 首页综合国产亚洲丝袜| 91嫩草|国产丨精品入口| 亚洲韩国欧洲国产日产av| 国产精品原创视频| 亚洲 自拍 另类小说综合图区| 欧美极品aⅴ影院| 欧美 日韩 人妻 高清 中文| 国产精品一区二区三区免费视频|