Koa2开辟详解(自官网)

Koa

次世代nodejs 的 web框架

简介

koa是由Express幕后团队打造的,目标是更小,更快,更稳固的web运用和apis。经由过程杠杆生成器(leveraging generators)Koa可以让你指导(ditch)回调函数,极大的提拔毛病处置惩罚。Koa中心不集成任何的中间件,其自身供应的文雅的功用套件就可以写出既快又nice的效劳器。

装置

Koa须要node7.6.0或更高的版本,由于须要async function支撑。
你可以运用本身的版本管理器很快的装置一个支撑的版本。

nvm install 7
npm i koa
node my-koa-app.js

Async Function 连系 Babel

想要在较低版本的node中运用async函数,我们发起运用babel。

require('babel-core/register')
//然后在加载运用的主代码,这个必需在babel背面
const app = require('./app')

为了编译和转化async functions你须要在末了的紧缩版本中运用’transform-async-to-generator’或许transform-async-to-module-method插件。比方,在你的.babelrc文件中,举行以下设置。

{
    "plugins":["transform-async-to-generator"]
}

你也可以运用stage-3 persent来替换。

运用 Application

一个Koa运用是一个对象,其包含一个数组,数组有很多函数构成的中间件,这些函数鸠合起来守候请求,而且实行时是根据类栈的体式格局。koa和很多其他中间件体系相似,你或许是用过RubyRack,Connect等。然则一个设想的决议行要素是供应高品级”sugar”,与此同时低品级中间件层。因而提拔了交互性,鲁棒性(软件设想术语,即稳固性)而且使得编写中间件越发的带劲!

这包含一些经常使用使命的要领——比方链接谐和,缓存,代办支撑,别用等。只管供应了大批的有效的要领,然则koa依然坚持了一个较小的体积,由于没有绑定中间件。

怎么能偶少得了一个hello world运用。

const Koa = require('koa');
const app = new Koa();

app.use(ctx => {
    ctx.body = 'hello world';
})

app.listen(3000)

串连 Cascading

koa 的串连中间件运用了一个比较传统的体式格局,跟你通经常使用的东西很像。这是由于原本很难让用户有好的运用node的回调函数。然则运用异步函数我们能偶“真正得”是有中间件。相较于衔接的完成,这个更轻易供应一些列函数/功用来掌握,在末了返回便可。koa挪用”downstream”,掌握流返回”upstream”.

下面的例子返回“hello world”,然则最最先请求先经由过程x-response-timelogging中间件来纪录请求的最先。然后经由过程返回的中间件产出掌握。当一个中间件实行next()函数,来耽误和通报掌握给下一个声明的中间件。然后直到没有中间件须要实行downstream了,栈将会松开然后每一个中间件回复去展示本身的“upstream”行动。

设置 Settings

运用设置即在实例app上的属性。当前支撑以下:

  • app.env 默许是NODE_ENV或许“development”。

  • app.proxy 当设置为true时,porxy头部将被信托。

  • app.subdomainOffset 设置.subdomains的偏移量。替换[2]。

  • app.listen(…)
    一个Koa运用不是一对一的显现一个htpp效劳器。一个或很多个运用或许被增加到一块形成大的运用对应一个http效劳器。

建立返回一个http效劳器,通报给定的参数到Server#listen()。这些参数在nodejs.org都有申明。下面是一个无意义的Koa运用,绑定了端口3000

app.listen(…)要领是以下的一个语法糖。

const http = require('http')
const Koa = require('koa')
const app = new Koa()
http.createServer(app.callback()).listen(3000)

这申明你可以定义同一个运用为https和http或很多个地点。

const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
http.createServer(app.callback()).listen(3001);
  • app.callback()
    返回一个回调函数,相当于http.createServer()要领,来出了请求。

你也可以运用这个要领在你的Connect/Express运用中增加koa运用。

  • app.use(function)

增加一个给定的中间件要领来完成它的功用。检察Middleware相识更多。

  • app.keys=
    设置cookie的键。

这些键被通报到KeyGrip,或许你想运用本身的KeyGrip,可以以下做。

app.keys = ['im a newer secret', 'i like turtle'];
app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256');

这些键或许是轮回的,而且可以设置{signed:true}来运用。

ctx.cookies.set('name','tobi',{signed:true})
  • app.context
    app.context是ctx的泉源。你可以运用app.context增加分外的属性到ctx。这关于建立逾越悉数app运用的属性或许要领来讲是有效的,而且机能更好,在依靠上也跟简朴,可以斟酌做一个anti-pattern

例子,从ctx增加一个数据库援用。

add.context.db = db()

app.use(async (ctx)=>{
    console.log(ctx.db)
})

注重:

  • 经由过程getter和setter以及Object.difineProperty()设置的属性,你只能在app.context运用Object.defineProperty()来编辑他们。(不引荐)

  • 运用父级的ctx和设置来增加当前的运用。如许增加的app就可以运用到那些中间件。

毛病处置惩罚 Error Handling

除非设置app.silent是true,不然一切的出无输出都是规范输出。默许的毛病输出不会处置惩罚像是err.sttus是404或许err.expose是true。为了自定义毛病输出比方日记,你可以增加毛病事宜监听。

app.on('error', err =>
  log.error('server error', err)
);

当 req/res 周期中涌现任何毛病且没法响应客户端时,Koa 会把 Context(上下文) 实例作为第二个参数通报给 error 事宜:

app.on('error', (err, ctx) =>
  log.error('server error', err, ctx)
);

假如有毛病发作, 而且还能响应客户端(即没有数据被写入到 socket), Koa 会返回 500 “Internal Server Error”. 这两种状况都邑触发 app-level 的 error 事宜, 用于 logging.

环境(Context)

一个Koa环境(实例)封装了node原生的要乞降返回对象到一个零丁的对象中,这个零丁的对象供应了很多运用的要领,可以编写web运用和API。这些HTTP效劳器开辟中经经常使用到的操纵被增加到当前品级,而不是高品级。他将强迫中间件从新完成这些经常使用的功用。

一个环境Context在每次请求时被建立,而且被援用至中间件作为接收器,或许定义成this。以下所示。

app.use(function *(){
    this;//这里是koa环境context
    this.request;//是一个koa请求
    this.response;//是一个koa返回
})

很多context环境接见器和要领只是ctx.requestkoa请求或许ctx.responsekoa返回的代办,主要是为了轻易。比方ctx.typectx.length代表response返回对象,ctx.pahtctx.methos代表请求。

API 接口。
环境(Context)定义的要领和接见器。

  • ctx.req Node的request对象。

  • ctx.res Node的response对象。
    绕开运用koa的response处置惩罚是不支撑的。防止运用下面的node属性。

    • res.statusCode

    • res.writeHead()

    • res.write()

    • res.end()

  • ctx.request 一个Koa的request对象。

  • ctx.response 一个Koa的response对象。

  • ctx.state
    引荐的经由过程中间件通报信息给前端view(显现)的定名空间。

  • ctx.app 运用实例的援用。

  • ctx.cookies.get(name,[options])
    经由过程options取得cookie名字。

    • signed 请求cookie已署名。
      koa运用cookie模块,这里只是传入选项即可。

  • ctx.coolies.set(name,value,[options])
    运用options设置name的值value

    • signed 署名cookie的值。

    • expires 使cookie的有效期到期。

    • path cookie途径,默许/

    • domain cookie域

    • secure 庇护coolie

    • httpOnly 效劳器端cookie,默许值true
      经由过程设置options来运用cookie模块。

  • ctx.throw([msg],[status],[properties])
    处置惩罚抛出毛病的辅佐要领,默许.status的值为500时抛出,koa在返回的信息中恰当处置惩罚。限免的组合也是可疑的。

    • this.throw(403);

    • this.throw(‘name require’, 400);

    • this.throw(400,’name require’);

    • this.throw(‘something exploded’);

比方:this.throw('name require', 400)即是

var err = new Error('name require');
err.status = 400;
throw err;

注重这些是用户自定义的毛病,运用err.expose发出。因而只适宜某些客户端的反应。这些毛病不同于内置的毛病信息提示,由于毛病的详细信息不会泄漏。

你或许通报一个properties选项对象,他和原本的毛病信息举行了整合,关于人性化体验很有协助,它报告个给请求者一个回溯流(upsteam)。

this.throw(401,'access_denied',{user:user});
this.throw('access_denied',{user:user});

koa运用http-errors来建立毛病。

  • ctx.assert(value,[msg],[status],[properties])

    跑出毛病辅佐要领,相似`.throw()`,当`!value`是相似node的`assert()`要领。
this.assert(this.sate.user,401,'User not found, Please login!');

koa运用http-assert完成断言(assertions)

  • ctx.response
    经由过程绕开koa内置的返回处置惩罚(response handling),你可以明白的设置this.response = false;假如你想运用原生的res对象处置惩罚而不是koa的response处置惩罚,那末就运用它。

注重那种用法koa不支撑。这或许会打断koa中间件原本的功用,或许koa也被打断。运用这个属性最好斟酌一下hack,这是运用传统的fn(req,res)要领和koa中间件的唯一轻易的要领。

请求别号Request aliases
下面的接见起和Request别号相称。

  • ctx.header

  • ctx.headers

  • ctx.method

  • ctx.method=

  • ctx.url

  • ctx.url=

  • ctx.originalUrl

  • ctx.origin

  • ctx.href

  • ctx.path

  • ctx.query

  • ctx.query=

  • ctx.querystring

  • ctx.querystring=

  • ctx.host

  • ctx.hostname

  • ctx.fresh

  • ctx.stale

  • ctx.socket

  • ctx.protocol

  • ctx.secure

  • ctx.ip

  • ctx.ips

  • ctx.subdomains

  • ctx.is()

  • ctx.accepts()

  • ctx.acceptsEncodings()

  • ctx.acceptsCharsets()

  • ctx.acceptsLanguages()

  • ctx.get()

返回别号Response aliases
下面的接见起和返回别号相称

  • ctx.body

  • ctx.body=

  • ctx.status

  • ctx.status=

  • ctx.message

  • ctx.message=

  • ctx.length=

  • ctx.length

  • ctx.type

  • ctx.type=

  • ctx.handerSent

  • ctx.redirect()

  • ctx.attachment()

  • ctx.set()

  • ctx.append()

  • ctx.remove()

  • ctx.lastModified=

  • ctx.etag=

请求 Request

一个koa请求Request对象是个建立在node请求request之上的笼统。供应了一些分外的功用,这对每一个http效劳器开辟者来讲异常有效。

API

  • request.header

    Request header 对象
  • request.headers

    Requests header 对象,别号`request.header`。
  • request.method

    request.method
  • request.method=

    设置request method,完成中间件很有效,比方`methodoverride()`。
  • request.length

    返回request Content-lenght值是数字或许undefined。
  • request.url

    返回rquest URL
  • request.url=

    设置rquest URL,重写url时有效。
  • request.originalUrl

    返回request 原始 URL
  • request.orgin

    获得URL的域,包含协媾和host(主机号)。
this.request.origin
//=>http://example.com
  • request.href

    返回悉数request URL,包含协定,主机号,和url。
this.request.href
//=>http://example.com/foo/bar?q=1
  • request.path

    返回途径名(pathname)。
  • request.path=

    设置请求途径名字,保留查询参数
  • rquest.querystring

    获得原始的查询参数,不包含`?`。
  • request.querystring=

    设置原始的查询参数。
  • request.search

    获得原始的查询字符,带`?`。
  • request.search=

    设置原始的查询字符。
  • rquest.host

    获得主机号(hostname:port)当显现时。支撑`X-Forwarded-Host`当`app.proxy`是true,不然是经常使用的`host`。
  • request.hostname

    当偶然返回hostname,支撑`X-Frowarded-Host`当`app.proxy`是true,不然是经常使用的。
  • request.type

    返回request的`Content-type`,无效的一些参数,如`charset`。
var ct = this.request.type.
//=>'image/png'
  • request.charset

    当偶然返回request的charset,或许`undefined`。
this.request.charset
//=>'utf-8'
  • request.query

    返回剖析过的查询字符query-string,假如没有则返回一个空对象。注重,这个getter不支撑嵌套的剖析nested parsing。比方:`color=blue&size=small`。
    
{
    color:'blue',
    size:'small'
}
  • request.query=
    设置查询字符query-string到给定的对象。注重给设置setter不支撑嵌套对象。

this.query = {next:'/login'};
  • request.fresh

    搜检请求缓存是不是“革新fresh”,或许内容是不是发作转变。这个要领是为了`if-None-Match`和`if-Modified-Since`以及`last-modified`之间的缓存沟通。他必需可以援用到变动以后的返转头部response headers
//freshness check requeire stats 20x or 304
this.status = 200;
this.set('ETag','123');

//cache is ok
if(this.fresh) {
    this.status = 304;
    return;
}

//cache is stale
//fetch new data
shis.body = yield db.find('something');
  • request.stale

    与`request.fresh`相反
  • request.protocol

    返回请求协定,`https`或许`http`。支撑`X-Forwarded-Proto`当`app.proxy`是true。
  • request.secure

    `this.protocol == "https"`的速记,用以搜检一个讨情可否经由过程平安传输层。
  • request.ip

    请求的长途地点。支撑`X-Forwarded-For`当`app.proxy`为true。
  • request.ips

    当有`X-Forwarded-For`而且`app.proxy`可用,那末返回这些的ip的一个数组。
    从回溯upstream——>downstream预定,当上述不可用则返回一个空数组。
  • request.subdomains

    返回子域数组。
    子域是在主域之前的部份,由点`.`离开。默许状况下,运用的主域都被假设成倒数的那两个。可以经由过程`app.subdomainOffset`来转变。
    比方,假如域是`tobi.ferrest.example.com`,而且`app.subdomainOffset`没有设置,那末这个子域是['ferrets','tobi']。假如设置`app.subdomainOffset`为3,那末子域是['tobi']。
  • request.is(type…)

    搜检接下来的请求是不是包含`Content-Type`头部内容,它包含任何的mime范例。假如这里没有请求体,返回undefined。假如没有内容范例,或许婚配失利,返回false。其他的直接返回内容范例(mime)。
    
//Contetn-type:text/html;charset=utf-8
this.is('html');//=>'html'
this.is('text/html');//=>'text/html'
this.is('text/*', 'test/html');//=>'test/html'

//when Content-type is application/json
this.is('json','urlencoded');//=>'json'
this.is('application/json',);//=>'application/json'
this.is('html','application/*',);//=>'application/json'

this.is('html');//=>false

例子:你只想只要图片可以发送到路由

if(this.is('image/*')) {
    //process
}else{
    this.throw(415,'image only!');
}
  • 内容协商 Content Negotiation
    koa请求request包含有效的内容写上东西,由acceptsnegotaitor支撑完成,这些东西是:

    • request accepts(types)

    • rquest acceptsEncoding(types)

    • rquest acceptsCharsets(charsets)

    • rquest acceptsLanguages(langs)
      假如没有供应范例,那末一切可接收的范例将被返回。

假如供应了多个范例,最优婚配奖杯返回。假如没有婚配到,返回false,而且你应当发送406 "Not Acceptable"返回response给客户端。

在可以接收任何范例的处所丧失了accept头部。第一个婚配到的将被返回。因而供应科技收的范例是很主要的。

  • request.accepts(types)

    搜检给定的范例是不是是可接收的。当为true则返回最好婚配,不然false。范例`type`的值或许是一个或很多个mime范例字符,比方'application/json',扩大名是'josn',或许一个数组`['josn','html','text/plain']`。
//Accept:text/html
this.accepts('html')
//=>'html'

//Accept:text/*, application/json
this.accepts('html')
//=>'html'
this.accepts('json', 'text')
//=>'json'
this.accepts('application/json')
//=>'application/json'

//Accept.text/*, application/json
this.accepts('image/png')
this.accepts('png')
//=>false

//Accept:text/*,q=.5, application/json
this.accepts(['html', 'json'])
this.accepts('html', 'json')
//=>json

//No Accepts header
this.accpts('html', 'json')
//=>html
this.accepts('json','html')
//=> json

你或许挪用this.accepts()很屡次,或许运用switch语句。

switch(this.accepts('json', 'html', 'text')) {
    case 'json': bareak;
    case 'html': bareak;
    case 'text': bareak;
    default: this.throw(406, 'json , html or text only');
}
  • request.acceptsEncodings(encodings)

    搜检编码`encodings`是不是可接收,true时返回最优婚配,不然返回false。
    注重,你应当包含一个`indentity`作为编码`encodings`之一。
//Accept-Encoding:gzip
this.acceptsEncodings('gzip', 'deflate', 'identify');
//=>gzip

this.acceptsEncodings(['gzip', 'deflate', 'identify'])
//=>gzip

当没有参数时,一切可接收的编码作为数组元素返回

//Accept-Encoding:gzip, deflate
this.acceptsEncodings();
//=>['gzip','deflate','identify']

注重假如用户明白发送identify为identify,q=0。虽然这是个特别例子,你依然须要处置惩罚这个状况,当要领返回false时。

  • request.acceptsCharsets(charsets)

    搜检charset是不是可接收,为true时返回最优婚配,不然返回false。

    //Accept-Charset:utf-8, iso-8859-1;q=0.2,utf-7;q=0.5

this.acceptsCharsets(‘utf-8′,’utf-7’)
//=>utf-8

this.acceptsCharsets([‘utf-7′,’utf-8’]);
//=>utf-8

假如没有参数是则返回一切可接收的编码到一个数组。

//Accept-Charset:utf-8,iso-8859-1;q=0.2,utf-7;q=0.5
this.acceptsCharsets();
//=>['utf-8','utf-7','iso-8859-7']
  • request.acceptLanguages(langs)

    搜检langs是不是可接收,假如为true则返回最有婚配,不然返回false。
//Accept-Language:en;q=0.8,es,pt
this.acceptsLanguages('es','en');
//=>'es'

this.acceptsLanguages(['en','es']);
//=>'es'

当没有传入参数则返回一切的言语。

//Accept-Language:en;q=0.8, es,pt
this.acceptsLanguages();
//=>['es', 'pt', 'en']
  • request.idempotent

    价差请求是不是idempotent(幂等)
  • request.socket

    返回请求的socket
  • request.get(field)

    返回请求头header

返回 Response

一个koa返回Response对象是个建立在node请求request之上的笼统。供应了一些分外的功用,这对每一个http效劳器开辟者来讲异常有效。

API

  • response.header
    返回header对象

  • response。headers
    返回header对象。response.header的别号

  • response.status
    返回response的状况,默许状况下response.status没有默许值,而res.statusCode的默许值是200。

  • response.status =
    经由过程数字设置状况值

    • 100 ‘continue’继承

    • 101 ‘switch protocols’换协定

    • 102 ‘processing’处置惩罚中

    • 200 ‘ok’ ok

    • 201 ‘created’已建立

    • 202 ‘accepted’ 已接收

    • 203 ‘non-authoritative information’无作者信息

    • 204 ‘no content’ 无内容

    • 205 ‘reset content’ 重置内容

    • 206 “partial content” 部份内容

    • 207 “multi-status” 多状况

    • 300 “multiple choices” 多挑选

    • 301 “moved permanently” 移动到永远

    • 302 “moved temporarily” 移动到临时

    • 303 “see other” 看其他

    • 304 “not modified” 没有修正

    • 305 “use proxy” 运用代办

    • 307 “temporary redirect” 临时改向

    • 400 “bad request” 坏请求

    • 401 “unauthorized” 未经受权

    • 402 “payment required” 请求付款

    • 403 “forbidden” 制止

    • 404 “not found” 没有发明

    • 405 “method not allowed” 要领不允许

    • 406 “not acceptable” 不接收

    • 407 “proxy authentication required” 请求代办受权

    • 408 “request time-out” 请求超时

    • 409 “conflict” 争执

    • 410 “gone” 消逝

    • 411 “length required” 请求长度

    • 412 “precondition failed” 预处置惩罚失利

    • 413 “request entity too large” 请求量太大

    • 414 “request-uri too large” 请求赞同资本太大

    • 415 “unsupported media type” 不支撑的媒体范例

    • 416 “requested range not satisfiable” 不满足请求局限

    • 417 “expectation failed” 不是期望值

    • 418 “i’m a teapot” 我是个茶壶???

    • 422 “unprocessable entity” 毛病实体

    • 423 “locked” 已锁定

    • 424 “failed dependency” 依靠毛病

    • 425 “unordered collection” 未预定鸠合

    • 426 “upgrade required” 请求更新

    • 428 “precondition required” 请求条件

    • 429 “too many requests” 过量请求

    • 431 “request header fields too large” 请求头的域太大

    • 500 “internal server error” 效劳器内部毛病

    • 501 “not implemented” 没有完成

    • 502 “bad gateway” 网关毛病

    • 503 “service unavailable” 不可效劳

    • 504 “gateway time-out” 网关超时

    • 505 “http version not supported” http版本不支撑

    • 506 “variant also negotiates” 多样协商

    • 507 “insufficient storage” 存储不足

    • 509 “bandwidth limit exceeded” 凌驾带宽

    • 510 “not extended” 扩大毛病

    • 511 “network authentication required” 请求网路受权证实
      注重:不要担心要记太多东西,你可以随时检察。

  • response.message
    获得返回状况的信息。默许状况下,response.message是和response.status婚配的。

  • response.message=
    设置返回状况信息。

  • response.length=
    设置内容的长度

  • response.length
    返回内容的长度,或许计算出的this.body的大小。值为数字。或许undifined

  • response.body
    获得response的body。

  • response.body=
    设置返回体(response.body)为以下之一:

    • String written

    • Buffer written

    • Stream piped

    • Object json-stringified

    • null no content response

    String
    Content-type是text/html或许text/plain,charset是utf-8.Content-length也须要设置。
    Buffer
    Content-type是application/octet-stream,Content-length也要设置。
    Stream
    Content-type是application/octet-stream.
    Object
    Content-type是application/json.

  • response.get(field)
    获得response头部的field的值,不辨别大小写。

var etag = this.get('ETag');
  • response.set(field, value)

    设置response头部field的值。
this.set('Cache-control', 'no-cache');
  • response.append(field, value)

    给头部增加额为的域和值。
this.append('Link', '<http://127.0.0.1/>');
  • response.set(fields)

    运用对象设置头部的fields
this.set({
    'Etag':'1234',
    'Last-modified':date
});
  • response.remove(field)

    移除头部的某个域。
  • resposne.type

    返回Content-type的范例,没有其他参数——如‘charset’。
var ct = this.type;
//=>image/png
  • response.type

    经由过程名字或许扩大名设置Content-type
    
this.type = 'text/plain;charset=utf-8';
this.type = 'image/png';
this.type='.png';
this.type='png';

注重,每一个字符编码charset都是为你选的最适宜的,比方response.type='html',那末默许的字符编码是utf-8,然则明白定义一个完全的范例,如response.type='text/html',将不会有指定的字符编码。

  • response.is(type…)

    很相似于`this.request.is()`。搜检response的范例是不是是被支撑的范例。这在建立那些对返回举行操纵的中间件是异常有效。
    
    示例:这是一个紧缩html返回response的中间件,除了stream不被紧缩。
var minify = require('html-minifier');

app.use(function *minifyHtml(next){
    yield next;

    if(!this.response.is('html')) return;

    var body = this.body;
    if(!body||body.pipe) return;

    if(Buffer.isBuffer(body)) body = body.toString();
    this.body = minify(body);
})
  • response.redirect(url, [alt])

    把[302]状况码重导向至`url`。
    字符串`back`是一个特别的例子,供应了援用这支撑,当援用者不存在或许`/`没有运用。
    
this.redirect('back');
this.redirect('back','/index.html');
this.redirect('login');
this.redirect('http://google.com');

为了转变默许的状况302,只需在这个状况吗涌现之前或许涌现以后举行重导向即可。为了转变body,在其挪用以后举行重定向。

this.status = 301;
this.redirect('/cart');
this.body = 'Redirecting to shopping cart';
  • response.attachment([filename])

    设置`Content-disposition`为"attachment"为客户端发出下载的信号。
    文件的名字是可以指定的。
  • response.headerSent

    搜检返转头response header是不是早已发送。检察客户端是不是关照毛病信号异常有效。
  • response.lastModified

    返回`Last-Modified`末了修正头部的数据(假如存在)。
  • response.LastModified=

    设置`Last-Modified`头部为一个适宜的UTC(国际规范时间)字符串。你也可以设置其为一个日期或许日期字符串。
this.response.lastModified = new Date();
  • response.etag=

    设置ETag到一个返回中,包含表面的双引号。注重,这里没有响应的response.etag的getter。
this.response.etag = crypto.createHash('md5'),update(this.body).digest('hex');
  • response.vary(field)
    激活field。

    原文作者:farmerz
    原文地址: https://segmentfault.com/a/1190000009283162
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞