资讯专栏INFORMATION COLUMN

Koa2开发详解(自官网)

ZHAO_ / 1609人阅读

摘要:通过杠杆生成器可以让你引导回调函数,极大的提升错误处理。这是因为原先很难让用户有好的使用的回调函数。返回一个回调函数,相当于方法,来出了请求。使的有效期到期。检查请求缓存是否刷新,或者内容是否发生改变。

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", "");

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。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82821.html

相关文章

  • 2017-07-17 前端日报

    摘要:前端日报精选听说你没来总结个人使用过的移动端布局方法新特性简介用写组件坦然面对应对前端疲劳中文深入理解笔记函数前端架构经验分享系列教程之创建页面元素龙云全栈系列教程之定位页面元素龙云全栈第期与表单验证技术周刊期知乎 2017-07-17 前端日报 精选 听说你没来 JSConf 2017?总结个人使用过的移动端布局方法 - Rni-L - SegmentFaultNode.js v8....

    caiyongji 评论0 收藏0
  • 2017年1月前端月报

    摘要:平日学习接触过的网站积累,以每月的形式发布。年以前看这个网址概况在线地址前端开发群月报提交原则技术文章新的为主。 平日学习接触过的网站积累,以每月的形式发布。2017年以前看这个网址:http://www.kancloud.cn/jsfron... 概况 在线地址:http://www.kancloud.cn/jsfront/month/82796 JS前端开发群月报 提交原则: 技...

    FuisonDesign 评论0 收藏0
  • 2017年1月前端月报

    摘要:平日学习接触过的网站积累,以每月的形式发布。年以前看这个网址概况在线地址前端开发群月报提交原则技术文章新的为主。 平日学习接触过的网站积累,以每月的形式发布。2017年以前看这个网址:http://www.kancloud.cn/jsfron... 概况 在线地址:http://www.kancloud.cn/jsfront/month/82796 JS前端开发群月报 提交原则: 技...

    ivyzhang 评论0 收藏0
  • 2017年1月前端月报

    摘要:平日学习接触过的网站积累,以每月的形式发布。年以前看这个网址概况在线地址前端开发群月报提交原则技术文章新的为主。 平日学习接触过的网站积累,以每月的形式发布。2017年以前看这个网址:http://www.kancloud.cn/jsfron... 概况 在线地址:http://www.kancloud.cn/jsfront/month/82796 JS前端开发群月报 提交原则: 技...

    CloudwiseAPM 评论0 收藏0

发表评论

0条评论

ZHAO_

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<