Koa 學(xué)習(xí)總結(jié)

前言

Koa是基于Node.js的下一代web框架,由Express團(tuán)隊(duì)打造,特點(diǎn):優(yōu)雅、簡潔、靈活、體積小。幾乎所有功能都需要通過中間件實(shí)現(xiàn)。

準(zhǔn)備

  1. 檢查Node版本,至少在7.6.0以上,因?yàn)镵oa采用很多Es7的語法,比如async/await,
  2. 創(chuàng)建項(xiàng)目、安裝依賴
mkdir study_koa

cd study_koa

npm init -y

npm install koa

一、基本用法

  1. 創(chuàng)建一個(gè)應(yīng)用程序 新建app.js
const Koa = require('koa');

const app = new Koa();

app.use(async ctx => {
    // ctx.body 即服務(wù)端響應(yīng)的數(shù)據(jù)
    ctx.body = 'Hello Koa';
})

// 監(jiān)聽端口、啟動(dòng)程序
app.listen(3000, err => {
    if (err) throw err;
    console.log('runing...');
})
  1. 啟動(dòng)
node app.js
  1. 訪問 127.0.0.1:3000 會(huì)看到頁面顯示Hello Koa

即便沒有給ctx.body 設(shè)置響應(yīng)數(shù)據(jù),或訪問不存在的路由,頁面也會(huì)顯示Not Found,這是koa底層做了處理,不像原生Node或Express一樣頁面會(huì)一直處于響應(yīng)狀態(tài)。

二、Context

Koa將Node的request 和 response對象都封裝到了context中,每次請求都會(huì)創(chuàng)建一個(gè)ctx,并且在中間件中作為接收器使用。

以下是剛才訪問時(shí)的ctx對象

let ctx = {
    // 請求
    request: {
        method: 'GET',
        url: '/',
        // request header
        header: {
            host: '127.0.0.1:3030',
            connection: 'keep-alive',
            'cache-control': 'max-age=0',
            'upgrade-insecure-requests': '1',
            'user-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36',
            accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
            'accept-encoding': 'gzip, deflate, br',
            'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
            cookie: 'connect.sid=s%3AnQtQApNcQ55RmpjnkmQvWNTrdjYhZnlh.1FQUbVqpwpdRj8N6wjv8nOarf8hyzIpcxXN2LPYXGy0'
        }
    },
    // 響應(yīng)
    response: {
         status: 200, 
         message: 'ok', 
         header: { 
            'content-type': 'text/plain; charset=utf-8',
            'content-length': '9'
        }
    },
    app: { 
        subdomainOffset: 2, 
        proxy: false, 
        env: 'development' 
    },
    originalUrl: '/',
    // 原生Node的request對象
    req: '<original node req>',
    // 原生Node的reponse對象
    res: '<original node res>',
    socket: '<original node socket>'
}
剖析ctx
  1. 區(qū)分request、response、req、res
  • requset    ctx的請求對象
  • response    ctx的響應(yīng)對象
  • req      Node的請求對象
  • res      Node的響應(yīng)對象
注意:繞過Koa的response是不被處理的,避免使用Node的屬性和方法,比如:
res.statusCode
res.writeHead()
res.write()
res.end()

即便使用ctx.res.write()也不會(huì)得到預(yù)期結(jié)果,比如:ctx.res.write('hello'),結(jié)果是hellook,會(huì)把message的值拼接上。

  1. ctx.state
    推薦的命名空間,用于通過中間件傳遞信息到你的前端視圖。比如每個(gè)頁面都要用到用戶信息,那就可以掛載在ctx.state。類似于添加全局屬性。
ctx.state.userInfo = {
    name: 'Jack',
    age: 18
}
  1. ctx.app
    應(yīng)用程序?qū)嵗?/li>

有關(guān)cookie和session單獨(dú)介紹用法。

三、路由

Koa中的路由和Express不同,Express是把路由集成在Express中,Koa則需要通過kao-router模塊使用。

  1. 安裝:
npm i koa-router
  1. 使用
const Koa = require('koa');
// 直接調(diào)用的方式
const router = require('koa-router')();
// 或 單獨(dú)創(chuàng)建router的實(shí)例
const Router = require('koa-router');
const router = new Router();

router.get('/', async ctx => {
    ctx.body = 'Hello Router';
})

// 啟動(dòng)路由
app.use(router.routes()).use(router.allowedMethods())
// 以上為官方推薦方式,allowedMethods用在routes之后,作用是根據(jù)ctx.status設(shè)置response header.

app.listen(3000, err => {
    if (err) throw err;
    console.log('runing...');
});

四、中間件

Koa最大的特色和最優(yōu)的設(shè)計(jì)就是中間件,就是在匹配路由之前和匹配路由之后執(zhí)行函數(shù)。
使用app.use()加載中間件。每個(gè)中間件接收兩個(gè)參數(shù),ctx對象和next函數(shù),通過調(diào)用next將執(zhí)行權(quán)交給下一個(gè)中間件。

中間件分為:

  • 應(yīng)用級(jí)中間件
  • 路由級(jí)中間件
  • 錯(cuò)誤處理中間件
  • 第三方中間件
  1. 應(yīng)用級(jí)中間件
const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

// 應(yīng)用級(jí)中間件
app.use(async (ctx, next) => {
    await next();
})

router.get('/', async ctx => {
    ctx.body = 'hello koa';
})

// 啟動(dòng)路由
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, err => {
    if (err) throw err;
    console.log('runing...')
})

任何路由都會(huì)先經(jīng)過應(yīng)用級(jí)中間件,當(dāng)執(zhí)行完成next后再去匹配相應(yīng)的路由。

  1. 路由中間件
router.get('/user', async (ctx, next) => {
    console.log(111)
    await next();
})

router.get('/user', async (ctx, next) => {
    console.log(222)
    await next();
})

router.get('/user', async ctx => {
    console.log(333)
    ctx.body = 'Hello'
})  

// 依次打印
111
222
333

路由匹配過程中,對于相同路由會(huì)從上往下依次執(zhí)行中間件,直到最后一個(gè)沒有next參數(shù)的中間件為止。

  1. 錯(cuò)誤處理中間件
app.use(async (ctx, next)=> {
    await next();
    if(ctx.status === 404){
        ctx.body="404頁面"
    }
});

路由在匹配成功并執(zhí)行完相應(yīng)的操作后還會(huì)再次進(jìn)入應(yīng)用級(jí)中間件執(zhí)行 next 之后的邏輯。所以對于404、500等錯(cuò)誤可以在最外層的(第一個(gè))應(yīng)用級(jí)中間件的next之后做相應(yīng)的處理。
如果只有一個(gè)應(yīng)用級(jí)中間件的話,順序就無所謂所有路由中間件之前和之后了。

  1. 第三方中間件
    類似于koa-router、koa-bodyparser等就是第三方中間件。

  2. 中間件的合成
    koa-compose 模塊可以將多個(gè)中間件合成為一個(gè)。

const compose = require('koa-compose')
const first = asycn (ctx, next) => {
    await next();
}
const second = async ctx => {
    ctx.body = 'Hello';
}

const middle = compose([first, second]);
app.use(middle);
  1. 中間件的執(zhí)行順序
    多個(gè)中間件會(huì)形成堆棧結(jié)構(gòu),按先進(jìn)后出順序執(zhí)行
app.use(async (ctx, next) => {
    console.log('1中間件第1次執(zhí)行')
    await next();
    console.log('7中間件第7次執(zhí)行')
})

app.use(async (ctx, next) => {
    console.log('2中間件第2次執(zhí)行');
    await next();
    console.log('6中間件第6次執(zhí)行')
})

router.get('/user', async (ctx, next) => {
    console.log('3中間件第3次執(zhí)行')
    await next()
    console.log('5中間件第5次執(zhí)行')
})

router.get('/user', (ctx, next) => {
    console.log('4中間件第4次執(zhí)行')
    ctx.body = 'Hello Koa';
})
// 1中間件第1次執(zhí)行
// 2中間件第2次執(zhí)行
// 3中間件第3次執(zhí)行
// 4中間件第4次執(zhí)行
// 5中間件第5次執(zhí)行
// 6中間件第6次執(zhí)行
// 7中間件第7次執(zhí)行

由此可以看出中間件的執(zhí)行順序是先進(jìn)后出的方式。類似于洋蔥圖。

洋蔥圖

五、獲取請求數(shù)據(jù)

  1. GET 傳值
  • Koa 中 GET傳值通過request接收,有兩種方式: query 和 querystring

    query:返回的是參數(shù)對象。 {name: 'jack', age: 12}
    querystring:返回的是請求字符串。 name=jack&age=12

  • query和querystring可以從request中獲取,也可以直接從ctx中獲取。

let request = ctx.request;
let query = request.query;
let querystring = request.querystring;

// 直接ctx獲取
ctx.query
ctx.querystring
  1. POST 傳值
    通過post傳遞的值我們可以通過原生Node封裝,也可以通過第三方模塊接收。
  • 自定義封裝
const querystring = require('querystring');

module.exports = ctx => {
    return new Promise((resolve, reject) => {
        try {
            let data = '';

            // ctx.req實(shí)際上就是原生node中的req
            ctx.req.on('data', (chunk) => {
                data += chunk;
            })

            ctx.req.on('end', () => {
                data = querystring.parse(data);
                resolve(data);
            })
        } 
        catch(err) {
            reject(err);
        }
    })
}
  • 使用koa-bodyparser模塊
const bodyParser = require('koa-bodyparser');
app.use(bodyParser());

// 獲取
ctx.request.body

六、處理靜態(tài)資源

對于諸如js、css、img等靜態(tài)資源采用koa-static中間件處理。

npm i koa-static
配置:

比如靜態(tài)目錄為static:

// 靜態(tài)資源配置
// app.use(require('koa-static')('static'))

// or
// app.use(require('koa-static')('./static'))

// or
// app.use(require('koa-static')(__dirname + '/static'))

// or 使用path.join() 的時(shí)候,static前面的/可加可不加,該方法會(huì)內(nèi)部會(huì)做處理
app.use(require('koa-static')(path.join(__dirname, 'static')));

在模板中即可訪問:

<link rel="stylesheet" href="/css/header.css">
<img src="/image/account.eb695dc.png"/>

七、模板引擎

koa生態(tài)的模板引擎挺多的,比如ejs、art-template等。

1. koa-ejs
npm i koa-ejs
const render = require('koa-ejs');
  • 配置
render(app, {
    // views 視圖根目錄
    root: path.join(__dirname, 'views'),
    layout: 'template',     // 公用文件 若要禁用,設(shè)置為false即可
    viewExt: 'html',        // 擴(kuò)展名
    cache: false,           // 緩存 default true
    debug: false            // 如果開啟debug模式,則會(huì)在終端實(shí)時(shí)打印信息 default false
});
  • 使用
/**
 * 參數(shù)1: 模板名
 * 參數(shù)2: 數(shù)據(jù)(可選)
 */
await ctx.render(templateName, data);
2. art-template
npm install --save art-template
npm install --save koa-art-template
  • 配置
const render = require('koa-art-template');

// 配置模板引擎
render(app, {
    root: path.join(__dirname, 'views'), // 視圖目錄
    extname: '.html',
    debug: process.env.NODE_ENV !== 'production'
});

使用方式和ejs一樣。

性能上相比,art-template比ejs快很多,開發(fā)中用的最多的還是art-template。

八、cookie 和 session

http是無狀態(tài)、無連接的。不會(huì)對之前發(fā)生過的請求和相應(yīng)狀態(tài)進(jìn)行管理。也就是說,無法根據(jù)之前的狀態(tài)進(jìn)行本次的請求處理。

比如訪問淘寶首頁并登錄賬號(hào)后,當(dāng)再打開淘寶其他頁面時(shí),因?yàn)槊恳淮蔚脑L問都是獨(dú)立的,服務(wù)器并不知道你已經(jīng)登錄,所以還是不能下單或者加購物車之類的操作。

cookie
  1. cookie簡單介紹

cookie是客戶端第一次訪問服務(wù)器的時(shí)候,服務(wù)器在下行HTTP報(bào)文時(shí)通過響應(yīng)頭的set-cookie字段給瀏覽器分配的一個(gè)具有特殊標(biāo)識(shí)的文本信息,此后當(dāng)客戶端再次訪問同一域名時(shí),便會(huì)將該字段通過請求頭攜帶到服務(wù)器。注意: 第一次訪問服務(wù)器是不可能攜帶cookie的。

缺陷: 1、cookie的數(shù)據(jù)存放在客戶端,不安全,容易被(CSRF)跨站請求偽造。攻擊者可以借助受害者的 Cookie 騙取服務(wù)器的信任,可以在受害者毫不知情的情況下以受害者名義偽造請求發(fā)送給受攻擊服務(wù)器,從而在并未授權(quán)的情況下執(zhí)行在權(quán)限保護(hù)之下的操作。2、單個(gè)cookie保存的數(shù)據(jù)不能超過4K,很多瀏覽器都限制一個(gè)站點(diǎn)最多保存20個(gè)cookie。

  1. Koa中使用cookie
  • 設(shè)置cookie
ctx.cookies.get(name, [options])

通過 options 獲取 cookie name:

signed 所請求的cookie應(yīng)該被簽名

  • 設(shè)置cookie
ctx.cookies.set(name, value, [options])

通過 options 設(shè)置 cookie name 的 value:

  • maxAge: 有效期(一個(gè)數(shù)字表示從 Date.now() 得到的毫秒數(shù))
  • signed: cookie 簽名值
  • expires: cookie 過期的 Date
  • path: cookie作用域(指定了主機(jī)下的哪些路徑可以接受Cookie), 默認(rèn)是 /
  • domain: cookie作用域 (指定了哪些主機(jī)可以接受Cookie。如果不指定,默認(rèn)為當(dāng)前文檔的主機(jī)<不包含子域名>;如果指定了Domain,則一般包含子域名。
  • secure: 安全 cookie。即標(biāo)記為 Secure 的Cookie只可以通過被HTTPS協(xié)議加密過的請求發(fā)送給服務(wù)端。
  • httpOnly: 只可讓服務(wù)器通過http請求訪問cookie,不可通過js的document.cookieAPI訪問,可以避免XSS攻擊。 默認(rèn)是 true。
  • SameSite: 允許服務(wù)器要求某個(gè)cookie在跨站請求時(shí)不會(huì)被發(fā)送,從而可以阻止跨站請求偽造攻擊(CSRF)??蛇x值:
    1. None:瀏覽器會(huì)在同站請求、跨站請求下繼續(xù)發(fā)送cookies,不區(qū)分大小寫。
    2. Strict: 瀏覽器將只在訪問相同站點(diǎn)時(shí)發(fā)送cookie。
    3. Lax: 在新版本瀏覽器中,為默認(rèn)選項(xiàng)。大多數(shù)情況不發(fā)送第三方 Cookie,只有當(dāng)用戶從外部站點(diǎn)導(dǎo)航到URL時(shí)才會(huì)發(fā)送,包括鏈接(a標(biāo)簽)、預(yù)加載(link)、get表單。
  • overwrite: 一個(gè)布爾值,表示是否覆蓋以前設(shè)置的同名的 cookie (默認(rèn)是 false). 如果是 true, 在同一個(gè)請求中設(shè)置相同名稱的所有 Cookie(不管路徑或域)是否在設(shè)置此Cookie 時(shí)從 Set-Cookie 標(biāo)頭中過濾掉。
設(shè)置中文cookie

通過buffer轉(zhuǎn)成base64存進(jìn)去,取出來是再轉(zhuǎn)回中文。

// 轉(zhuǎn)base64
Buffer.from('張三').toString('base64');

// 轉(zhuǎn)回中文
Buffer.from(buf, 'base64').toString();
session
  1. seeion簡單介紹

session是另一種記錄客戶狀態(tài)的機(jī)制,不同的是cookie保存在客戶端瀏覽器中,而session保存在服務(wù)器上。

前面說過,cookie 是存放在客戶端,不是很安全,用戶可以自己手動(dòng)把cookie種在客戶端以欺騙服務(wù)器。而session是存儲(chǔ)在服務(wù)端的,所以對于較重要的數(shù)據(jù)存儲(chǔ)在session。

缺點(diǎn): session會(huì)在一定時(shí)間內(nèi)保存在服務(wù)器上。當(dāng)訪問增多,會(huì)比較占用你服務(wù)器的性能。

  1. session 的工作機(jī)制

當(dāng)瀏覽器第一次請求服務(wù)器時(shí),服務(wù)器端會(huì)創(chuàng)建一個(gè)session對象,生成一個(gè)類似于key-value的鍵值對, 然后將key(cookie)下發(fā)到客戶端,當(dāng)客戶端再訪問時(shí),攜帶key(cookie),找到對應(yīng)的session(value)。 生產(chǎn)中用戶的信息都保存在session中。

  1. koa-session 的使用
npm install koa-session
const Koa = require('koa');
const session = require('koa-session');

const app = new Koa();

app.keys = ['some secret hurr'];
 
const CONFIG = {
  key: 'koa:sess',      // 返給瀏覽器 cookie 的key 默認(rèn)是 'kao:sess'
  maxAge: 86400000,     // cookie的過期時(shí)間 maxAge in ms (default is 1 days)
  autoCommit: true,     // (boolean) 自動(dòng)給客戶端下發(fā)cookie 并設(shè)置session
  overwrite: true,      // 是否可以覆蓋之前同名的cookie    (默認(rèn)default true)
  httpOnly: true,       // cookie是否只有服務(wù)器端可以訪問 httpOnly or not (default true)
  signed: true,         // 簽名默認(rèn)true
  rolling: false,       // 在每次響應(yīng)時(shí)強(qiáng)制設(shè)置session標(biāo)識(shí)符cookie,到期時(shí)被重置設(shè)置過期倒計(jì)時(shí)。(默認(rèn)為false)
  renew: false,         // 當(dāng)session快過期時(shí)更新session,這樣就可以始終保持用戶登錄 默認(rèn)是false
};

以上配置選項(xiàng)常用的就是key、maxAge、httpOver。

renew應(yīng)用:比如我們登錄賬號(hào)寫一篇博客,寫了一半cookie過期了,當(dāng)我們提交的時(shí)候就會(huì)退出登錄,體驗(yàn)很不好,而且寫好的博客丟失。

九、重定向

301和302重定向狀態(tài)碼區(qū)別。302為臨時(shí)重定向,301永久重定向。Koa中默認(rèn)為302。詳細(xì)信息查看這篇博客 301和302重定向介紹

字符串 “back” 是特別提供Referrer支持的,當(dāng)Referrer不存在時(shí),使用 alt 或“/”。

ctx.redirect('back');
ctx.redirect('back', '/login');
ctx.redirect('/login');

要更改 “302” 的默認(rèn)狀態(tài),只需在該調(diào)用之前或之后分配狀態(tài)。要變更主體請?jiān)诖苏{(diào)用之后:

ctx.status = 301;
ctx.redirect('/cart');
ctx.body = 'Redirecting to shopping cart';

十、跨域請求

解決跨域的方式有很多種,個(gè)人認(rèn)為最好的方案是在服務(wù)器端設(shè)置支持跨域。

原生設(shè)置方式:

下面詳細(xì)說明在koa2中設(shè)置具體的請求頭信息:

app.use(async (ctx, next) => {
    // 允許所有域名請求
    ctx.set("Access-Control-Allow-Origin", "*")
    //  只允許 http://localhost:8080 域名的請求
    // ctx.set("Access-Control-Allow-Origin", "http://localhost:8080");

    // 設(shè)置允許的跨域請求方式
    ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE")

    // 字段是必需的。值一個(gè)逗號(hào)分隔的字符串,表示服務(wù)器所支持的所有頭信息字段.
    ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type")

    // 服務(wù)器收到請求以后,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,確認(rèn)允許跨源請求,即可做出響應(yīng)。

    // Content-Type表示具體請求中的媒體類型信息
    ctx.set("Content-Type", "application/json;charset=utf-8")

    // 該字段可選。它的值是一個(gè)布爾值,表示是否允許發(fā)送Cookie。默認(rèn)情況下,Cookie不包括在CORS請求之中。 當(dāng)設(shè)置成允許請求攜帶憑證cookie時(shí),需要保證"Access-Control-Allow-Origin"是服務(wù)器有的域名,而不能是"*";
    ctx.set("Access-Control-Allow-Credentials", true);

    // 該字段可選,用來指定本次預(yù)檢請求的有效期,單位為秒。
    // 當(dāng)請求方法是PUT或DELETE等特殊方法或者Content-Type字段的類型是application/json時(shí),服務(wù)器會(huì)提前發(fā)送一次請求進(jìn)行驗(yàn)證
    // 下面的的設(shè)置只本次驗(yàn)證的有效時(shí)間,即在該時(shí)間段內(nèi)服務(wù)端可以不用進(jìn)行驗(yàn)證
    ctx.set("Access-Control-Max-Age", 300);

    /*
    CORS請求時(shí),XMLHttpRequest對象的getResponseHeader()方法只能拿到6個(gè)基本字段:
        Cache-Control、
        Content-Language、
        Content-Type、
        Expires、
        Last-Modified、
        Pragma。
    */
    // 需要獲取其他字段時(shí),使用Access-Control-Expose-Headers,
    // getResponseHeader('myData')可以返回我們所需的值
    ctx.set("Access-Control-Expose-Headers", "myData")

    await next()
})
使用中間件方式

在koa2中,解決跨域請求還可使用中間件koa2-cors

  1. 安裝:
npm i koa2-cors -S
  1. 注冊中間件
const cors = require('koa2-cors')
app.use(cors())

十一、node發(fā)送郵件

node 發(fā)送郵件可以使用nodemailer三方模塊。

安裝:

npm i nodemailer

const nodemailer = require("nodemailer")

async function main() {
  let transporter = nodemailer.createTransport({
    host: "smtp.qq.com",
    port: 465,
    secure: true, // true for 465, false for other ports
    auth: {
      user: 'test@qq.com', 
      pass: 'akphfubplzqdbdfh'
    }
  })

  let info = await transporter.sendMail({
    from: 'test@qq.com', // sender address
    to: "bar@qq.com", // 接收地址 多個(gè)郵箱使用 ','分割
    subject: "Hello 老胖", // 郵件主題
    text: "胖子蹲啊胖子蹲胖子蹲完瘦子蹲", // plain text body
    // html: "<b>嗯好</b>" // html body
  })

  console.log("Message sent: %s", info.messageId)
  console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info))
}

main().catch(console.error)

使用說明:

  1. 創(chuàng)建transporter配置信息
    node_modules>nodemailer>lib>well_known>services.json中查看對應(yīng)的配置信息。配置發(fā)件郵箱相應(yīng)的host、port及secure。

  2. 獲取發(fā)件箱密碼(類似于授權(quán),獲取key),以qq郵箱為例
    qq郵箱>設(shè)置>賬戶>POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服務(wù),點(diǎn)擊開啟獲取pass

    授權(quán).png
  3. 配置user和pass

    1. user即發(fā)件地址
    2. pass就是剛才授權(quán)獲取的密碼
  1. sendMail配置信息

    1. from 發(fā)件地址
    2. to 收件地址 如果多個(gè)收件地址,用 ,分割即可
    3. subject 郵件主題
    4. text 文件內(nèi)容
    5. html html內(nèi)容

    注意:text和html只能配置一個(gè)

更多詳細(xì)配置信息及功能參照官網(wǎng)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容