首先,咱们建立一个名为koa2的工程目录,而后使用VS Code打开。而后,咱们建立app.js,输入如下代码:javascript
// 导入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将调用咱们传入的异步函数进行处理。例如:css
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版本号。例如:html
{ "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安装项目所需依赖包。安装完成后,项目的目录结构以下:java
hello-koa/ | +- .vscode/ | | | +- launch.json //VSCode 配置文件 | +- app.js //使用koa的js | +- package.json //项目配置文件 | +- node_modules/ //npm安装的全部依赖包
而后,使用npm start启动项目,便可看到效果。
固然,还能够直接用命令node app.js在命令行启动程序,该命名最终执行的是package.json文件中的start对应命令:node
"scripts": { "start": "node app.js" }
接下来,让咱们再仔细看看koa的执行逻辑,核心代码以下:jquery
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函数称为中间件。git
例如,能够用如下3个middleware组成处理链,依次打印日志,记录处理时间,输出HTML。github
// 导入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...');
在上面的例子中,咱们处理http请求一概返回相同的HTML,这样显得并非很友好,正常的状况是,咱们应该对不一样的URL调用不一样的处理函数,这样才能返回不一样的结果。web
为了处理URL跳转的问题,咱们须要引入koa-router中间件,让它负责处理URL映射。首先在package.json中添加koa-router依赖:ajax
"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
用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...');
所谓同源策略,即浏览器的一个安全功能,不一样源的客户端脚本在没有明确受权的状况下,不能读写对方资源,同源政策由 Netscape 公司引入浏览器。目前,全部浏览器都实行这个政策。最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。。所谓"同源"指的是"三个相同",即协议相同、域名相同和端口相同。
例如,有下面一个网址:http://www.netease.com/a.html, 协议是http://,域名是 www.netease.com,端口是80(默认端口能够省略),那么同源状况以下:
受浏览器同源策略的影响,不是同源的脚本不能操做其余源下面的对象,想要解决同源策略的就须要进行跨域操做。针对浏览器的Ajax请求跨域的主要有两种解决方案JSONP和CORS。
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机制,后面会具体解释。
虽然浏览器同源策略限制了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) 是一种机制,它使用额外的 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:该字段可选,用来指定本次预检请求的有效期,单位为秒。