亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 開發 > JS > 正文

如何從頭實現一個node.js的koa框架

2024-05-06 16:52:17
字體:
來源:轉載
供稿:網友

前言

koa.js是最流行的node.js后端框架之一,有很多網站都使用koa進行開發,同時社區也涌現出了一大批基于koa封裝的企業級框架。然而,在這些亮眼的成績背后,作為核心引擎的koa代碼庫本身,卻非常的精簡,不得不讓人驚嘆于其巧妙的設計。

在平時的工作開發中,筆者是koa的重度用戶,因此對其背后的原理自然也是非常感興趣,因此在閑暇之余進行了研究。不過本篇文章,并不是源碼分析,而是從相反的角度,向大家展示如何從頭開發實現一個koa框架,在這個過程中,koa中最重要的幾個概念和原理都會得到展現。相信大家在看完本文之后,會對koa有一個更深入的理解,同時在閱讀本文之后再去閱讀koa源碼,思路也將非常的順暢。

首先放出筆者實現的這個koa框架代碼庫地址:simpleKoa

需要說明的是,本文實現的koa是koa 2版本,也就是基于async/await的,因此需要node版本在7.6以上。如果讀者的node版本較低,建議升級,或者安裝babel-cli,利用其中的babel-node來運行例子。

四條主線

筆者認為,理解koa,主要需要搞懂四條主線,其實也是實現koa的四個步驟,分別是

  • 封裝node http Server
  • 構造resquest, response, context對象
  • 中間件機制
  • 錯誤處理

下面就一一進行分析。

主線一:封裝node http Server: 從hello world說起

首先,不考慮框架,如果使用原生http模塊來實現一個返回hello world的后端app,代碼如下:

let http = require('http');let server = http.createServer((req, res) => {res.writeHead(200);res.end('hello world');});server.listen(3000, () => {console.log('listenning on 3000');});

實現koa的第一步,就是對這個原生的過程進行封裝,為此,我們首先創建application.js實現一個Application對象:

// application.jslet http = require('http');class Application {/*** 構造函數*/constructor() {this.callbackFunc;}/*** 開啟http server并傳入callback*/listen(...args) {let server = http.createServer(this.callback());server.listen(...args);}/*** 掛載回調函數* @param {Function} fn 回調處理函數*/use(fn) {this.callbackFunc = fn;}/*** 獲取http server所需的callback函數* @return {Function} fn*/callback() {return (req, res) => {this.callbackFunc(req, res);};}}module.exports = Application;

然后創建example.js:

let simpleKoa = require('./application');let app = new simpleKoa();app.use((req, res) => {res.writeHead(200);res.end('hello world');});app.listen(3000, () => {console.log('listening on 3000');});

可以看到,我們已經初步完成了對于http server的封裝,主要實現了app.use注冊回調函數,app.listen語法糖開啟server并傳入回調函數了,典型的koa風格。

但是美中不足的是,我們傳入的回調函數,參數依然使用的是req和res,也就是node原生的request和response對象,這些原生對象和api提供的方法不夠便捷,不符合一個框架需要提供的易用性。因此,我們需要進入第二條主線了。

主線二:構造request, response, context對象

如果閱讀koa文檔,會發現koa有三個重要的對象,分別是request, response, context。其中request是對node原生的request的封裝,response是對node原生response對象的封裝,context對象則是回調函數上下文對象,掛載了koa request和response對象。下面我們一一來說明。

首先要明確的是,對于koa的request和response對象,只是提供了對node原生request和response對象的一些方法的封裝,明確了這一點,我們的思路是,使用js的getter和setter屬性,基于node的對象req/res對象封裝koa的request/response對象。

規劃一下我們要封裝哪些易用的方法。這里在文章中為了易懂,姑且只實現以下方法:

對于simpleKoa request對象,實現query讀取方法,能夠讀取到url中的參數,返回一個對象。

對于simpleKoa response對象,實現status讀寫方法,分別是讀取和設置http response的狀態碼,以及body方法,用于構造返回信息。

而simpleKoa context對象,則掛載了request和response對象,并對一些常用方法進行了代理。

首先創建request.js:

// request.jslet url = require('url');module.exports = {get query() {return url.parse(this.req.url, true).query;}};

很簡單,就是導出了一個對象,其中包含了一個query的讀取方法,通過url.parse方法解析url中的參數,并以對象的形式返回。需要注意的是,代碼中的this.req代表的是node的原生request對象,this.req.url就是node原生request中獲取url的方法。稍后我們修改application.js的時候,會為koa的request對象掛載這個req。

然后創建response.js:

// response.jsmodule.exports = {get body() {return this._body;},/*** 設置返回給客戶端的body內容** @param {mixed} data body內容*/set body(data) {this._body = data;},get status() {return this.res.statusCode;},/*** 設置返回給客戶端的stausCode** @param {number} statusCode 狀態碼*/set status(statusCode) {if (typeof statusCode !== 'number') {throw new Error('statusCode must be a number!');}this.res.statusCode = statusCode;}};

也很簡單。status讀寫方法分別設置或讀取this.res.statusCode。同樣的,這個this.res是掛載的node原生response對象。而body讀寫方法分別設置、讀取一個名為this._body的屬性。這里設置body的時候并沒有直接調用this.res.end來返回信息,這是考慮到koa當中我們可能會多次調用response的body方法覆蓋性設置數據。真正的返回消息操作會在application.js中存在。

然后我們創建context.js文件,構造context對象的原型:

// context.jsmodule.exports = {get query() {return this.request.query;},get body() {return this.response.body;},set body(data) {this.response.body = data;},get status() {return this.response.status;},set status(statusCode) {this.response.status = statusCode;}};

可以看到主要是做一些常用方法的代理,通過context.query直接代理了context.request.query,context.body和context.status代理了context.response.body與context.response.status。而context.request,context.response則會在application.js中掛載。

由于context對象定義比較簡單并且規范,當實現更多代理方法時候,這樣一個一個通過聲明的方式顯然有點笨,js中,設置setter/getter,可以通過對象的__defineSetter__和__defineSetter__來實現。為此,我們精簡了上面的context.js實現方法,精簡版本如下:

let proto = {};// 為proto名為property的屬性設置setterfunction delegateSet(property, name) {proto.__defineSetter__(name, function (val) {this[property][name] = val;});}// 為proto名為property的屬性設置getterfunction delegateGet(property, name) {proto.__defineGetter__(name, function () {return this[property][name];});}// 定義request中要代理的setter和getterlet requestSet = [];let requestGet = ['query'];// 定義response中要代理的setter和getterlet responseSet = ['body', 'status'];let responseGet = responseSet;requestSet.forEach(ele => {delegateSet('request', ele);});requestGet.forEach(ele => {delegateGet('request', ele);});responseSet.forEach(ele => {delegateSet('response', ele);});responseGet.forEach(ele => {delegateGet('response', ele);});module.exports = proto;

這樣,當我們希望代理更多request和response方法的時候,可以直接向requestGet/requestSet/responseGet/responseSet數組中添加method的名稱即可(前提是在request和response中實現了)。

最后讓我們來修改application.js,基于剛才的3個對象原型來創建request, response, context對象:

// application.jslet http = require('http');let context = require('./context');let request = require('./request');let response = require('./response');class Application {/*** 構造函數*/constructor() {this.callbackFunc;this.context = context;this.request = request;this.response = response;}/*** 開啟http server并傳入callback*/listen(...args) {let server = http.createServer(this.callback());server.listen(...args);}/*** 掛載回調函數* @param {Function} fn 回調處理函數*/use(fn) {this.callbackFunc = fn;}/*** 獲取http server所需的callback函數* @return {Function} fn*/callback() {return (req, res) => {let ctx = this.createContext(req, res);let respond = () => this.responseBody(ctx);this.callbackFunc(ctx).then(respond);};}/*** 構造ctx* @param {Object} req node req實例* @param {Object} res node res實例* @return {Object} ctx實例*/createContext(req, res) {// 針對每個請求,都要創建ctx對象let ctx = Object.create(this.context);ctx.request = Object.create(this.request);ctx.response = Object.create(this.response);ctx.req = ctx.request.req = req;ctx.res = ctx.response.res = res;return ctx;}/*** 對客戶端消息進行回復* @param {Object} ctx ctx實例*/responseBody(ctx) {let content = ctx.body;if (typeof content === 'string') {ctx.res.end(content);}else if (typeof content === 'object') {ctx.res.end(JSON.stringify(content));}}}

可以看到,最主要的是增加了createContext方法,基于我們之前創建的context 為原型,使用Object.create(this.context)方法創建了ctx,并同樣通過Object.create(this.request)和Object.create(this.response)創建了request/response對象并掛在到了ctx對象上面。此外,還將原生node的req/res對象掛載到了ctx.request.req/ctx.req和ctx.response.res/ctx.res對象上。

回過頭去看我們之前的context/request/response.js文件,就能知道當時使用的this.res或者this.response之類的是從哪里來的了,原來是在這個createContext方法中掛載到了對應的實例上。一張圖來說明其中的關系:

構建了運行時上下文ctx之后,我們的app.use回調函數參數就都基于ctx了。

下面一張圖描述了ctx對象的結構和繼承關系:

node.js,koa框架

最后回憶我們的ctx.body方法,并沒有直接返回消息體,而是將消息存儲在了一個變量屬性中。為了每次回調函數處理結束之后返回消息,我們創建了responseBody方法,主要作用就是通過ctx.body讀取存儲的消息,然后調用ctx.res.end返回消息并關閉連接。從方法中知道,我們的body消息體可以是字符串,也可以是對象(會序列化為字符串返回)。注意這個方法的調用是在回調函數結束之后調用的,而我們的回調函數是一個async函數,其執行結束后會返回一個Promise對象,因此我們只需要在其后通過.then方法調用我們的responseBody即可,這就是this.callbackFunc(ctx).then(respond)的意義。

然后我們來測試一下目前為止的框架。修改example.js如下:

let simpleKoa = require('./application');let app = new simpleKoa();app.use(async ctx => {ctx.body = 'hello ' + ctx.query.name;});app.listen(3000, () => {console.log('listening on 3000');});

可以看到這個時候我們通過app.use傳入的已經不再是原生的function (req, res)回調函數,而是koa2中的async函數,接收ctx作為參數。為了測試,在瀏覽器訪問localhost:3000?name=tom,可以看到返回了'hello tom',符合預期。

這里再插入分析一個知識概念。從剛才的實現中,我們知道了this.context是我們的中間件中上下文ctx對象的原型。因此在實際開發中,我們可以將一些常用的方法掛載到this.context上面,這樣,在中間件ctx中,我們也可以方便的使用這些方法了,這個概念就叫做ctx的擴展,一個例子是阿里的egg.js框架已經把這個擴展機制作為一部分,融入到了框架開發中。

下面就展示一個例子,我們寫一個echoData的方法作為擴展,傳入errno, data, errmsg,能夠給客戶端返回結構化的消息結果:

let SimpleKoa = require('./application');let app = new SimpleKoa();// 對ctx進行擴展app.context.echoData = function (errno = 0, data = null, errmsg = '') {this.res.setHeader('Content-Type', 'application/json;charset=utf-8');this.body = {errno: errno,data: data,errmsg: errmsg};};app.use(async ctx => {let data = {name: 'tom',age: 16,sex: 'male'}// 這里使用擴展,方便的返回utf-8格式編碼,帶有errno和errmsg的消息體ctx.echoData(0, data, 'success');});app.listen(3000, () => {console.log('listenning on 3000');});

主線三:中間件機制

到目前為止,我們成功封裝了http server,并構造了context, request, response對象。但最重要的一條主線卻還沒有實現,那就是koa的中間件機制。

關于koa的中間件洋蔥執行模型,koa 1中使用的是generator + co.js執行的方式,koa 2中則使用了async/await。關于koa 1中的中間件原理,我曾寫過一篇文章進行解釋,請移步:深入探析koa之中間件流程控制篇

這里我們實現的是基于koa 2的,因此再描述一下原理。為了便于理解,假設我們有3個async函數:

async function m1(next) {console.log('m1');await next();}async function m2(next) {console.log('m2');await next();}async function m3() {console.log('m3');}

我們希望能夠構造出一個函數,實現的效果是讓三個函數依次執行。首先考慮想讓m2執行完畢后,await next()去執行m3函數,那么顯然,需要構造一個next函數,作用是調用m3,然后作為參數傳給m2

let next1 = async function () {await m3();}m2(next1);// 輸出:m2,m3

進一步,考慮從m1開始執行,那么,m1的next參數需要是一個執行m2的函數,并且給m2傳入的參數是m3,下面來模擬:

let next1 = async function () {await m3();}let next2 = async function () {await m2(next1);}m1(next2);// 輸出:m1,m2,m3

那么對于n個async函數,希望他們按順序依次執行呢?可以看到,產生nextn的過程能夠抽象為一個函數:

function createNext(middleware, oldNext) {return async function () {await middleware(oldNext);}}let next1 = createNext(m3, null);let next2 = createNext(m2, next1);let next3 = createNext(m1, next2);next3();// 輸出m1, m2, m3

進一步精簡:

let middlewares = [m1, m2, m3];let len = middlewares.length;// 最后一個中間件的next設置為一個立即resolve的promise函數let next = async function () {return Promise.resolve();}for (let i = len - 1; i >= 0; i--) {next = createNext(middlewares[i], next);}next();// 輸出m1, m2, m3

至此,我們也有了koa中間件機制實現的思路,新的application.js如下:

/*** @file simpleKoa application對象*/let http = require('http');let context = require('./context');let request = require('./request');let response = require('.//response');class Application {/*** 構造函數*/constructor() {this.middlewares = [];this.context = context;this.request = request;this.response = response;}// ...省略中間/*** 中間件掛載* @param {Function} middleware 中間件函數*/use(middleware) {this.middlewares.push(middleware);}/*** 中間件合并方法,將中間件數組合并為一個中間件* @return {Function}*/compose() {// 將middlewares合并為一個函數,該函數接收一個ctx對象return async ctx => {function createNext(middleware, oldNext) {return async () => {await middleware(ctx, oldNext);}}let len = this.middlewares.length;let next = async () => {return Promise.resolve();};for (let i = len - 1; i >= 0; i--) {let currentMiddleware = this.middlewares[i];next = createNext(currentMiddleware, next);}await next();};}/*** 獲取http server所需的callback函數* @return {Function} fn*/callback() {return (req, res) => {let ctx = this.createContext(req, res);let respond = () => this.responseBody(ctx);let fn = this.compose();return fn(ctx).then(respond);};}// ...省略后面}module.exports = Application;

可以看到,首先對app.use進行改造了,每次調用app.use,就向this.middlewares中push一個回調函數。然后增加了一個compose()方法,利用我們前文分析的原理,對middlewares數組中的函數進行組裝,返回一個最終的函數。最后,在callback()方法中,調用compose()得到最終回調函數,并執行。

改寫example.js驗證一下中間件機制:

let simpleKoa = require('./application');let app = new simpleKoa();let responseData = {};app.use(async (ctx, next) => {responseData.name = 'tom';await next();ctx.body = responseData;});app.use(async (ctx, next) => {responseData.age = 16;await next();});app.use(async ctx => {responseData.sex = 'male';});app.listen(3000, () => {console.log('listening on 3000');});// 返回{ name: "tom", age: 16, sex: "male"}

例子中一共三個中間件,分別對responseData增加了name, age, sex屬性,最后返回該數據。

至此,一個koa框架基本已經浮出水面了,不過我們還需要進行最后一個主線的分析:錯誤處理。

主線四:錯誤處理

一個健壯的框架,必須保證在發生錯誤的時候,能夠捕獲錯誤并有降級方案返回給客戶端。但顯然現在我們的框架還做不到這一點,假設我們修改一下例子,我們的中間件中,有一個發生錯誤拋出了異常:

let simpleKoa = require('./application');let app = new simpleKoa();let responseData = {};app.use(async (ctx, next) => {responseData.name = 'tom';await next();ctx.body = responseData;});app.use(async (ctx, next) => {responseData.age = 16;await next();});app.use(async ctx => {responseData.sex = 'male';// 這里發生了錯誤,拋出了異常throw new Error('oooops');});app.listen(3000, () => {console.log('listening on 3000');});

這個時候訪問瀏覽器,是得不到任何響應的,這是因為異常并沒有被我們的框架捕獲并進行降級處理?;仡櫸覀僡pplication.js中的中間件執行代碼:

// application.js// ...callback() {return (req, res) => {let ctx = this.createContext(req, res);let respond = () => this.responseBody(ctx);let fn = this.compose();return fn(ctx).then(respond);};}// ...

其中我們知道,fn是一個async函數,執行后返回一個promise,回想promise的錯誤處理是怎樣的?沒錯,我們只需要定義一個onerror函數,里面進行錯誤發生時候的降級處理,然后在promise的catch方法中引用這個函數即可。

于此同時,回顧koa框架,我們知道在錯誤發生的時候,app對象可以通過app.on('error', callback)訂閱錯誤事件,這有助于我們幾種處理錯誤,比如打印日志之類的操作。為此,我們也要對Application對象進行改造,讓其繼承nodejs中的events對象,然后在onerror方法中emit錯誤事件。改造后的application.js如下:

/*** @file simpleKoa application對象*/let EventEmitter = require('events');let http = require('http');let context = require('./context');let request = require('./request');let response = require('./response');class Application extends EventEmitter {/*** 構造函數*/constructor() {super();this.middlewares = [];this.context = context;this.request = request;this.response = response;}// .../*** 獲取http server所需的callback函數* @return {Function} fn*/callback() {return (req, res) => {let ctx = this.createContext(req, res);let respond = () => this.responseBody(ctx);let onerror = (err) => this.onerror(err, ctx);let fn = this.compose();// 在這里catch異常,調用onerror方法處理異常return fn(ctx).then(respond).catch(onerror);};}// .../*** 錯誤處理* @param {Object} err Error對象* @param {Object} ctx ctx實例*/onerror(err, ctx) {if (err.code === 'ENOENT') {ctx.status = 404;}else {ctx.status = 500;}let msg = err.message || 'Internal error';ctx.res.end(msg);// 觸發error事件this.emit('error', err);}}module.exports = Application;

可以看到,onerror方法的對異常的處理主要是獲取異常狀態碼,當err.code為'ENOENT'的時候,返回的消息頭設置為404,否則默認設置為500,然后消息體設置為err.message,如果異常中message屬性為空,則默認消息體設置為'Internal error'。此后調用ctx.res.end返回消息,這樣就能保證即使異常情況下,客戶端也能收到返回值。最后通過this.emit出發error事件。

然后我們寫一個example來驗證錯誤處理:

let simpleKoa = require('./application');let app = new simpleKoa();app.use(async ctx => {throw new Error('ooops');});app.on('error', (err) => {console.log(err.stack);});app.listen(3000, () => {console.log('listening on 3000');});

瀏覽器訪問'localhost:3000'的時候,得到返回'ooops',同時http狀態碼為500 。同時app.on('error')訂閱到了異常事件,在回調函數中打印出了錯誤棧信息。

關于錯誤處理,這里多說一點。雖然koa中內置了錯誤處理機制,但是實際業務開發中,我們往往希望能夠自定義錯誤處理方式,這個時候,比較好的辦法是在最開頭增加一個錯誤捕獲中間件,然后根據錯誤進行定制化的處理,比如:

// 錯誤處理中間件app.use(async (ctx, next) => {try {await next();}catch (err) {// 在這里進行定制化的錯誤處理}});// ...其他中間件

至此,我們就完整實現了一個輕量版的koa框架。

結語

完整的simpleKoa代碼庫地址為:simpleKoa,里面還附帶了一些example。

理解了這個輕量版koa的實現原理,讀者還可以去看看koa的源碼,會發現機制和我們實現的框架是非常類似的,無非是多了一些細節,比如說,完整koa的context/request/response方法上面掛載了更多好用的method,或者很多方法中容錯處理更好等等。具體在本文中就不展開講了,留給感興趣的讀者去探索吧~。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。


注:相關教程知識閱讀請移步到JavaScript/Ajax教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
国产一区二区欧美日韩| 91av视频导航| 伊人久久久久久久久久久久久| 全球成人中文在线| 黑丝美女久久久| 欧美电影电视剧在线观看| 国内精品久久久久久| 亚洲精品www久久久久久广东| 日韩视频第一页| 精品国产一区二区三区久久久狼| 色婷婷成人综合| 国产精品福利在线| 欧美日韩一区二区免费在线观看| 亚洲国产精品久久久| 成人免费淫片视频软件| 国产一区二区三区在线免费观看| 亚洲一区二区免费在线| 国产视频综合在线| 国产精品国产亚洲伊人久久| 欧美激情女人20p| 欧美极品在线视频| 久久久之久亚州精品露出| 国产精品福利久久久| 日本亚洲欧美成人| 日本免费久久高清视频| 色狠狠av一区二区三区香蕉蜜桃| 亚洲男人的天堂在线| 日韩电影免费在线观看中文字幕| 久久精品亚洲精品| 亚洲色图五月天| 69精品小视频| 国产97在线亚洲| 成人免费直播live| 国产综合色香蕉精品| 韩国国内大量揄拍精品视频| 日产日韩在线亚洲欧美| 97国产精品久久| 午夜精品福利在线观看| 韩剧1988在线观看免费完整版| 国产精品美女免费看| 欧美精品免费在线观看| 欧美在线视频免费| 成人高清视频观看www| 亚洲电影免费在线观看| 亚洲一区二区在线| 5278欧美一区二区三区| 欧美极品xxxx| 国模吧一区二区| 国产精品美乳一区二区免费| 揄拍成人国产精品视频| 久久久国产成人精品| 欧美高清理论片| 日韩av手机在线观看| 97香蕉超级碰碰久久免费软件| 最近免费中文字幕视频2019| 欧美成人午夜影院| 欧美日韩在线一区| 2019亚洲男人天堂| 中文字幕欧美精品日韩中文字幕| 中文字幕日韩视频| 最好看的2019的中文字幕视频| 97在线免费观看视频| 国产午夜精品久久久| 久久成人18免费网站| 国产精品午夜国产小视频| 欧美华人在线视频| 中文字幕亚洲欧美日韩高清| 欧美亚洲国产日韩2020| 欧美精品中文字幕一区| 正在播放国产一区| 国产精品久久久久久久久男| 九九九久久久久久| 欧美一级电影免费在线观看| 国产精品久久久久久久电影| 精品爽片免费看久久| 欧美成人精品在线观看| 日韩中文字幕免费| 久久久成人的性感天堂| 亚洲一区二区三区在线免费观看| 欧美电影在线观看完整版| 国产精品一区二区久久精品| 日韩成人小视频| 美日韩丰满少妇在线观看| 综合国产在线观看| 欧美丰满片xxx777| 久久躁日日躁aaaaxxxx| 国产精品大片wwwwww| 久久久久久噜噜噜久久久精品| 亚洲人成伊人成综合网久久久| 亚洲日韩中文字幕| 欧美疯狂性受xxxxx另类| 成人免费网站在线看| 久久精品国产亚洲精品2020| 日韩av成人在线观看| 成人黄色短视频在线观看| 国产精品流白浆视频| 国产精品视频自在线| 社区色欧美激情 | 国产专区精品视频| 亚洲欧洲一区二区三区在线观看| 97视频在线观看亚洲| 亚洲精品美女网站| 国产精品免费在线免费| 欧美日韩亚洲一区二区三区| 亚洲老头老太hd| 亚洲精品wwwww| 日韩在线视频免费观看高清中文| 亚州成人av在线| 亚洲一区二区三区成人在线视频精品| 大量国产精品视频| 亚洲欧洲视频在线| 国产精品海角社区在线观看| 欧美在线视频播放| 色噜噜狠狠狠综合曰曰曰88av| 人九九综合九九宗合| 国产精品精品视频一区二区三区| 一区二区欧美久久| 日韩国产高清视频在线| 国产z一区二区三区| 国产一区二区三区网站| 国产玖玖精品视频| 国产91精品久久久久久| 日韩美女毛茸茸| 国产成人一区二| 中国日韩欧美久久久久久久久| 日韩国产欧美精品在线| 丝袜美腿精品国产二区| 国产ts一区二区| 亚洲一区二区三区xxx视频| 精品视频久久久久久| 91性高湖久久久久久久久_久久99| 久久久视频在线| 日韩美女视频免费看| 欧美日韩国产黄| 九九热视频这里只有精品| 91在线视频精品| 国产精品网站视频| 国产精品99导航| 欧美自拍大量在线观看| 国产精品国产亚洲伊人久久| 国产精品高清在线观看| 久久久国产精品一区| 揄拍成人国产精品视频| 日韩高清免费观看| 日韩电影大片中文字幕| 国产91精品黑色丝袜高跟鞋| 日本韩国在线不卡| 国产国语刺激对白av不卡| 久久久久久香蕉网| 日本高清不卡的在线| 欧美另类第一页| 久久777国产线看观看精品| 91av视频在线播放| 一级做a爰片久久毛片美女图片| 国产成人精品免费久久久久| 欧美日韩中国免费专区在线看| 亚洲精品一区久久久久久| 欧美日韩一区二区在线播放| 另类视频在线观看| 国产精品久久久久国产a级| 69久久夜色精品国产69乱青草| 97人洗澡人人免费公开视频碰碰碰| 最新国产精品拍自在线播放| 亚洲一区免费网站|