乐趣区

Koa2开发入门

Koa2 入门
创建 Koa2
首先,我们创建一个名为 koa2 的工程目录,然后使用 VS Code 打开。然后,我们创建 app.js,输入以下代码:
// 导入 koa,和 koa 1.x 不同,在 koa2 中,我们导入的是一个 class,因此用大写的 Koa 表示:
const Koa = require(‘koa’);

// 创建一个 Koa 对象表示 web app 本身:
const app = new Koa();

// 对于任何请求,app 将调用该异步函数处理请求:
app.use(async (ctx, next) => {
await next();
ctx.response.type = ‘text/html’;
ctx.response.body = ‘<h1>Hello, koa2!</h1>’;
});

// 在端口 3000 监听:
app.listen(3000);
console.log(‘app started at port 3000…’);
对于每一个 http 请求,koa 将调用我们传入的异步函数进行处理。例如:
async (ctx, next) => {
await next();
// 设置 response 的 Content-Type:
ctx.response.type = ‘text/html’;
// 设置 response 的内容:
ctx.response.body = ‘<h1>Hello, koa2!</h1>’;
}
其中,参数 ctx 是由 koa 传入的封装了 request 和 response 的变量,我们可以通过它访问 request 和 response,next 是 koa 传入的将要处理的下一个异步函数。那么,怎么启动 koa 呢?首先,你需要安装 koa,可以直接使用 npm 进行安装,可以参考 Koa 官网资料。然后在刚才的 koa 的项目目录中新建一个 package.json,这个文件用于管理 koa 项目运行需要的依赖包,依赖时注意 koa 版本号。例如:
{
“name”: “hello-koa2”,
“version”: “1.0.0”,
“description”: “Hello Koa 2 example with async”,
“main”: “app.js”,
“scripts”: {
“start”: “node app.js”
},
“keywords”: [
“koa”,
“async”
],
“author”: “xzh”,
“license”: “Apache-2.0”,
“repository”: {
“type”: “git”,
“url”: “https://github.com/michaelliao/learn-javascript.git”
},
“dependencies”: {
“koa”: “2.7.0”
}
}
其中,dependencies 是我们的工程依赖的包以及版本号,需要注意版本号的对应。其他字段均用来描述项目信息,可任意填写。然后,在 koa 目录下执行 npm install 安装项目所需依赖包。安装完成后,项目的目录结构如下:
hello-koa/
|
+- .vscode/
| |
| +- launch.json //VSCode 配置文件
|
+- app.js // 使用 koa 的 js
|
+- package.json // 项目配置文件
|
+- node_modules/ //npm 安装的所有依赖包
然后,使用 npm start 启动项目,即可看到效果。当然,还可以直接用命令 node app.js 在命令行启动程序,该命名最终执行的是 package.json 文件中的 start 对应命令:
“scripts”: {
“start”: “node app.js”
}
接下来,让我们再仔细看看 koa 的执行逻辑,核心代码如下:
app.use(async (ctx, next) => {
await next();
ctx.response.type = ‘text/html’;
ctx.response.body = ‘<h1>Hello, koa2!</h1>’;
});
每收到一个 http 请求,koa 就会调用通过 app.use() 注册的 async 函数,并传入 ctx 和 next 参数。那为什么需要调用 await next() 呢?原因是 koa 把很多 async 函数组成一个处理链,每个 async 函数都可以做一些自己的事情,然后用 await next() 来调用下一个 async 函数,此处我们把每个 async 函数称为中间件。
例如,可以用以下 3 个 middleware 组成处理链,依次打印日志,记录处理时间,输出 HTML。
// 导入 koa,和 koa 1.x 不同,在 koa2 中,我们导入的是一个 class,因此用大写的 Koa 表示:
const Koa = require(‘koa’);

// 创建一个 Koa 对象表示 web app 本身:
const app = new Koa();

app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`); // 打印 URL
await next(); // 调用下一个 middleware
});

app.use(async (ctx, next) => {
const start = new Date().getTime(); // 当前时间
await next(); // 调用下一个 middleware
const ms = new Date().getTime() – start; // 耗费时间
console.log(`Time: ${ms}ms`); // 打印耗费时间
});

app.use(async (ctx, next) => {
await next();
ctx.response.type = ‘text/html’;
ctx.response.body = ‘<h1>Hello, koa2!</h1>’;
});

// 在端口 3000 监听:
app.listen(3000);
console.log(‘app started at port 3000…’);
koa-router
在上面的例子中,我们处理 http 请求一律返回相同的 HTML,这样显得并不是很友好,正常的情况是,我们应该对不同的 URL 调用不同的处理函数,这样才能返回不同的结果。
为了处理 URL 跳转的问题,我们需要引入 koa-router 中间件,让它负责处理 URL 映射。首先在 package.json 中添加 koa-router 依赖:
“koa-router”: “7.4.0”
然后用 npm install 安装依赖。接下来,我们修改 app.js,使用 koa-router 来处理 URL 映射。
const Koa = require(‘koa’);

// 注意 require(‘koa-router’) 返回的是函数:
const router = require(‘koa-router’)();

const app = new Koa();

app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}…`);
await next();
});

router.get(‘/hello/:name’, async (ctx, next) => {
var name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});

router.get(‘/’, async (ctx, next) => {
ctx.response.body = ‘<h1>Index</h1>’;
});

app.use(router.routes());
app.listen(3000);
console.log(‘app started at port 3000…’);
需要说明的是,require(‘koa-router’) 返回的是函数,其作用类似于:
const fn_router = require(‘koa-router’);
const router = fn_router();
然后,我们使用 router.get(‘/path’, async fn) 来注册一个 GET 请求。可以在请求路径中使用带变量的 /hello/:name,变量可以通过 ctx.params.name 来完成访问。当我们在输入首页:http://localhost:3000/ 当在浏览器中输入:http://localhost:3000/hello/koa
post 请求
用 router.get(‘/path’, async fn) 处理的是 get 请求。如果要处理 post 请求,可以用 router.post(‘/path’, async fn)。
用 post 请求处理 URL 时,我们会遇到一个问题:post 请求通常会发送一个表单、JSON 作为 request 的 body 发送,但无论是 Node.js 提供的原始 request 对象,还是 koa 提供的 request 对象,都不提供解析 request 的 body 的功能!此时需要借助 koa-bodyparser 插件。
所以,使用 koa-router 进行 post 请求时,需要在 package.json 中添加 koa-bodyparser 依赖:
“koa-bodyparser”: “4.2.1”
现在,我们就可以使用 koa-bodyparser 进行 post 请求了,例如:
const Koa = require(‘koa’);

// 注意 require(‘koa-router’) 返回的是函数:
const router = require(‘koa-router’)();
const bodyParser = require(‘koa-bodyparser’);

const app = new Koa();
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}…`);
await next();
});

router.get(‘/hello/:name’, async (ctx, next) => {
var name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});

router.get(‘/’, async (ctx, next) => {
ctx.response.body = `<h1>Index</h1>
<form action=”/signin” method=”post”>
<p>Name: <input name=”name” value=”koa”></p>
<p>Password: <input name=”password” type=”password”></p>
<p><input type=”submit” value=”Submit”></p>
</form>`;
});

//POST 请求
router.post(‘/signin’, async (ctx, next) => {
var
name = ctx.request.body.name || ”,
password = ctx.request.body.password || ”;
console.log(`signin with name: ${name}, password: ${password}`);
if (name === ‘koa’ && password === ‘12345’) {
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.body = `<h1>Login failed!</h1>
<p><a href=”/”>Try again</a></p>`;
}
});

router.get(‘/’, async (ctx, next) => {
ctx.response.body = ‘<h1>Index</h1>’;
});

app.use(bodyParser());
app.use(router.routes());

app.listen(3000);
console.log(‘app started at port 3000…’);
然后,当我们使用 npm start 启动服务,输入 koa 和 12345 时,就能通过测试。
优化
现在,虽然我们可以根据输入处理不同的 URL,但是代码的可阅读和扩展性极差。正确的写法是页面和逻辑分离,于是我们把 url-koa 复制一份,重命名为 url2-koa,并重构项目。重构的项目目录结构如下:
url2-koa/
|
+- .vscode/
| |
| +- launch.json
|
+- controllers/
| |
| +- login.js // 处理 login 相关 URL
| |
| +- users.js // 处理用户管理相关 URL
|
+- app.js // 使用 koa 的 js
|
+- package.json
|
+- node_modules/ //npm 安装的所有依赖包
我们在 controllers 目录下添加一个 index.js 文件,并添加如下内容:
var fn_index = async (ctx, next) => {
ctx.response.body = `<h1>Index</h1>
<form action=”/signin” method=”post”>
<p>Name: <input name=”name” value=”koa”></p>
<p>Password: <input name=”password” type=”password”></p>
<p><input type=”submit” value=”Submit”></p>
</form>`;
};

var fn_signin = async (ctx, next) => {
var
name = ctx.request.body.name || ”,
password = ctx.request.body.password || ”;
console.log(`signin with name: ${name}, password: ${password}`);
if (name === ‘koa’ && password === ‘12345’) {
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.body = `<h1>Login failed!</h1>
<p><a href=”/”>Try again</a></p>`;
}
};

module.exports = {
‘GET /’: fn_index,
‘POST /signin’: fn_signin
};
上面示例中,index.js 通过 module.exports 把两个 URL 处理函数暴露出来。然后,我们修改 app.js,让它自动扫描 controllers 目录,找到所有的 js 文件并注册每个 URL。
var files = fs.readdirSync(__dirname + ‘/controllers’);

// 过滤出.js 文件:
var js_files = files.filter((f)=>{
return f.endsWith(‘.js’);
});

// 处理每个 js 文件:
for (var f of js_files) {
console.log(`process controller: ${f}…`);
// 导入 js 文件:
let mapping = require(__dirname + ‘/controllers/’ + f);
for (var url in mapping) {
if (url.startsWith(‘GET ‘)) {
// 如果 url 类似 ”GET xxx”:
var path = url.substring(4);
router.get(path, mapping[url]);
console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith(‘POST ‘)) {
// 如果 url 类似 ”POST xxx”:
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`register URL mapping: POST ${path}`);
} else {
// 无效的 URL:
console.log(`invalid URL: ${url}`);
}
}
}
如果上面的例子看起来有点费劲,可以对上面的功能进行拆分。
function addMapping(router, mapping) {
for (var url in mapping) {
if (url.startsWith(‘GET ‘)) {
var path = url.substring(4);
router.get(path, mapping[url]);
console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith(‘POST ‘)) {
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`register URL mapping: POST ${path}`);
} else {
console.log(`invalid URL: ${url}`);
}
}
}

function addControllers(router) {
var files = fs.readdirSync(__dirname + ‘/controllers’);
var js_files = files.filter((f) => {
return f.endsWith(‘.js’);
});

for (var f of js_files) {
console.log(`process controller: ${f}…`);
let mapping = require(__dirname + ‘/controllers/’ + f);
addMapping(router, mapping);
}
}

addControllers(router);
为了方便,我们把扫描 controllers 目录和创建 router 的代码从 app.js 中提取出来作为一个中间件,并将它命名为:controller.js。
const fs = require(‘fs’);

function addMapping(router, mapping) {
for (var url in mapping) {
if (url.startsWith(‘GET ‘)) {
var path = url.substring(4);
router.get(path, mapping[url]);
console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith(‘POST ‘)) {
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`register URL mapping: POST ${path}`);
} else {
console.log(`invalid URL: ${url}`);
}
}
}

function addControllers(router) {
var files = fs.readdirSync(__dirname + ‘/controllers’);
var js_files = files.filter((f) => {
return f.endsWith(‘.js’);
});

for (var f of js_files) {
console.log(`process controller: ${f}…`);
let mapping = require(__dirname + ‘/controllers/’ + f);
addMapping(router, mapping);
}
}

module.exports = function (dir) {
let
controllers_dir = dir || ‘controllers’, // 如果不传参数,扫描目录默认为 ’controllers’
router = require(‘koa-router’)();
addControllers(router, controllers_dir);
return router.routes();
};
然后,我们在 app.js 文件中可以直接使用 controller.js。例如:
const Koa = require(‘koa’);
const bodyParser = require(‘koa-bodyparser’);

const app = new Koa();

// 导入 controller 中间件
const controller = require(‘./controller’);

app.use(bodyParser());
app.use(controller());

app.listen(3000);
console.log(‘app started at port 3000…’);
Koa2 跨域
同源策略
所谓同源策略,即浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页 ” 同源 ”。。所谓 ” 同源 ” 指的是 ” 三个相同 ”,即协议相同、域名相同和端口相同。
例如,有下面一个网址:http://www.netease.com/a.html,协议是 http://,域名是 www.netease.com,端口是 80(默认端口可以省略),那么同源情况如下:
跨域
受浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象,想要解决同源策略的就需要进行跨域操作。针对浏览器的 Ajax 请求跨域的主要有两种解决方案 JSONP 和 CORS。
Ajax
Ajax 是一种用于创建快速动态网页的技术,无需重新加载整个网页的情况下即将实现网页的局部更新。下面通过 Ajax 进行跨域请求的情景,首先通过 koa 启了两个本地服务:一个 port 为 3200,一个为 3201。
app1.js
onst koa = require(‘koa’);
const app = new koa();

const Router = require(‘koa-router’);
const router = new Router();

const serve = require(‘koa-static’);

const path = require(‘path’);

const staticPath = path.resolve(__dirname, ‘static’);

// 设置静态服务
const staticServe = serve(staticPath, {
setHeaders: (res, path, stats) => {
if (path.indexOf(‘jpg’) > -1) {
res.setHeader(‘Cache-Control’, [‘private’, ‘max-age=60’]);
}
}
});
app.use(staticServe);

router.get(‘/ajax’, async (ctx, next) => {
console.log(‘get request’, ctx.request.header.referer);
ctx.body = ‘received’;
});

app.use(router.routes());

app.listen(3200);
console.log(‘koa server is listening port 3200’);
app2.js
const koa = require(‘koa’);
const app = new koa();
const Router = require(‘koa-router’);

const router = new Router();

router.get(‘/ajax’, async (ctx, next) => {
console.log(‘get request’, ctx.request.header.referer);
ctx.body = ‘received’;
});

app.use(router.routes());

app.listen(3200);
console.log(‘app2 server is listening port 3200’);
由于此示例需要使用 koa-static 插件,所以启动服务前需要安装 koa-static 插件。然后新增一个 origin.html 文件,添加如下代码:
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<meta http-equiv=”X-UA-Compatible” content=”ie=edge”>
<title>cross-origin test</title>
</head>
<body style=”width: 600px; margin: 200px auto; text-align: center”>
<button onclick=”getAjax()”>AJAX</button>
<button onclick=”getJsonP()”>JSONP</button>
</body>
<script type=”text/javascript”>

var baseUrl = ‘http://localhost:3201’;

function getAjax() {
var xhr = new XMLHttpRequest();
xhr.open(‘GET’, baseUrl + ‘/ajax’, true);
xhr.onreadystatechange = function() {
// readyState == 4 说明请求已完成
if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) {
// 从服务器获得数据
alert(xhr.responseText);
} else {
console.log(xhr.status);
}
};
xhr.send();
}
</script>
</html>
当 ajax 发送跨域请求时,控制台报错:
Failed to load http://localhost:3201/ajax: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:3200’ is therefore not allowed access.
虽然控制台有报错,但 AJAX 请求收到了 200,这是因为浏览器的 CORS 机制,后面会具体解释。
JSONP
虽然浏览器同源策略限制了 XMLHttpRequest 请求不同域数据的限制。但是,在页面上引入不同域的 js 脚本是可以的,而且 script 元素请求的脚本会被浏览器直接运行。
在 origin.html 的脚本文件中添加如下脚本:
function getJsonP() {
var script = document.createElement(‘script’);
script.src = baseUrl + ‘/jsonp?type=json&callback=onBack’;
document.head.appendChild(script);
}

function onBack(res) {
alert(‘JSONP CALLBACK: ‘, JSON.stringify(res));
}
当点击 JSONP 按钮时,getJsonP 方法会在当前页面添加一个 script,src 属性指向跨域的 GET 请求:http://localhost:3201/jsonp?type=json&callback=onBack,通过 query 格式带上请求的参数。callback 是关键,用于定义跨域请求回调的函数名称,这个值必须后台和脚本保持一致。
然后在 app2.js 中添加 jsonp 请求的路由代码:
router.get(‘/jsonp’, async (ctx, next) => {
const req = ctx.request.query;
console.log(req);
const data = {
data: req.type
}
ctx.body = req.callback + ‘(‘+ JSON.stringify(data) +’)’;
})

app.use(router.routes());
然后重新刷新即可看的效果。需要说明的是,jquery、zepto 这些 js 第三方库,其提供的 ajax 方法都有对 jsonp 请求进行封装,如 jquery 发 jsonp 的 ajax 请求如下:
function getJsonPByJquery() {
$.ajax({
url: baseUrl + ‘/jsonp’,
type: ‘get’,
dataType: ‘jsonp’,
jsonpCallback: “onBack”,
data: {
type: ‘json’
}
});
}
CORS
跨域资源共享 (CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
实现跨域 ajax 请求的方式有很多,其中一个是利用 CORS,而这个方法关键是在服务器端进行配置。
CORS 将请求分为简单请求和非简单请求。其中,简单请求就是没有加上额外请求头部的 get 和 post 请求,并且如果是 post 请求,请求格式不能是 application/json。而其余的,put、post 请求,Content-Type 为 application/json 的请求,以及带有自定义的请求头部的请求,就为非简单请求。
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 ” 预检 ” 请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
首先,在 origin.html 中添加一个 post 请求,并添加如下代码:
function corsWithJson() {
$.ajax({
url: baseUrl + ‘/cors’,
type: ‘post’,
contentType: ‘application/json’,
data: {
type: ‘json’,
},
success: function(data) {
console.log(data);
}
})
}

通过设置 Content-Type 为 appliaction/json 使其成为非简单请求,” 预检 ” 请求的方法为 OPTIONS,服务器判断 Origin 为跨域,所以返回 404。除了 Origin 字段,” 预检 ” 请求的头信息包括两个特殊字段:
Access-Control-Request-Method 该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 PUT。Access-Control-Request-Headers 该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,例如示例中的 content-type。
同时,CORS 允许服务端在响应头中添加一些头信息来响应跨域请求。然后在 app2.js 引入 koa2-cors,并添加如下代码:
app.use(cors({
origin: function (ctx) {
if (ctx.url === ‘/cors’) {
return “*”;
}
return ‘http://localhost:3201’;
},
exposeHeaders: [‘WWW-Authenticate’, ‘Server-Authorization’],
maxAge: 5,
credentials: true,
allowMethods: [‘GET’, ‘POST’, ‘DELETE’],
allowHeaders: [‘Content-Type’, ‘Authorization’, ‘Accept’],
}));
重启服务后,浏览器重新发送 POST 请求。可以看到浏览器发送了两次请求。OPTIONS 的响应头表示服务端设置了 Access-Control-Allow-Origin:*,于是发送 POST 请求,得到服务器返回值。除此之外,在 OPTIONS 的请求响应报文中,头信息里有一些 CORS 提供的其他字段:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type,Authorization,Accept
Access-Control-Allow-Methods: GET,POST,DELETE
Access-Control-Max-Age: 5
Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。Access-Control-Allow-Headers:如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在 ” 预检 ” 中请求的字段。Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。Access-Control-Max-Age:该字段可选,用来指定本次预检请求的有效期,单位为秒。
参考:Koa2 框架利用 CORS 完成跨域 ajax 请求 Koa- 基于 Node.js 的下一代 Web 开发框架

退出移动版