Koa2开发入门

栏目: Node.js · 发布时间: 6年前

内容简介:首先,我们创建一个名为koa2的工程目录,然后使用VS Code打开。然后,我们创建app.js,输入以下代码:对于每一个http请求,koa将调用我们传入的异步函数进行处理。例如:其中,参数ctx是由koa传入的封装了request和response的变量,我们可以通过它访问request和response,next是koa传入的将要处理的下一个异步函数。

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官网资料

Koa2开发入门

然后在刚才的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启动项目,即可看到效果。

Koa2开发入门

当然,还可以直接用命令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/

Koa2开发入门

当在浏览器中输入: http://localhost :3000/hello/koa

Koa2开发入门

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(默认端口可以省略),那么同源情况如下:

Koa2开发入门

跨域

受浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象,想要解决同源策略的就需要进行跨域操作。针对浏览器的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机制,后面会具体解释。

Koa2开发入门

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请求。可以看到浏览器发送了两次请求。

Koa2开发入门

OPTIONS的响应头表示服务端设置了Access-Control-Allow-Origin:*,于是发送POST请求,得到服务器返回值。

Koa2开发入门

除此之外,在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:该字段可选,用来指定本次预检请求的有效期,单位为秒。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

XML Hacks

XML Hacks

Michael Fitzgerald / O'Reilly Media, Inc. / 2004-07-27 / USD 24.95

Developers and system administrators alike are uncovering the true power of XML, the Extensible Markup Language that enables data to be sent over the Internet from one computer platform to another or ......一起来看看 《XML Hacks》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试