在Koajs中实现文件上传的接口

文件上传是一个基本的功能,每个系统几乎都会有,比如上传图片、上传Excel等。那么在Node Koa应用中如何实现一个支持文件上传的接口呢?本文从环境准备开始、最后分别用 Postman 和一个HTML页面来测试。 环境准备首先当然是要初始化一个Koa项目了,安装 Koa、koa-router 即可。 npm install koa koa-router设置图片上传目录,把图片上传到指定的目录中,在 app 路径下新建 public 文件夹,目录结构如下: koa-upload/--app----public------uploads----index.js--package.json编写 index.js const koa = require('koa')const app = new koa()router.post('/upload', ctx => { ctx.body = 'koa upload demo'})app.use(router.routes());app.listen(3000, () => { console.log('启动成功') console.log('http://localhost:3000')});然后启动,确保这一步没有问题。 使用 koa-body 中间件获取上传的文件koa-body 支持文件、json、form格式的请求体,安装 koa-body npm install koa-body设置 koaBody 配置参数,index.js const koa = require('koa')const koaBody = require('koa-body')const path = require('path')const app = new koa()app.use(koaBody({ // 支持文件格式 multipart: true, formidable: { // 上传目录 uploadDir: path.join(__dirname, 'public/uploads'), // 保留文件扩展名 keepExtensions: true, }}));... ...接下来完善 /upload 路由,获取文件,然后直接返回文件路径 ...

October 7, 2019 · 1 min · jiezi

koa-router-多文件引入

背景koa-router路由越来越多,api下的router都要使用下面的方式引入,怎么才能方便快捷的将api下的所有文件都引入呢这次记录的就是如果将koa-router 一次性循环引入 const book = require('./app/api/v1/book')const classic = require('./app/api/v1/classic')// ...app.use(book.routes(), book.allowedMethods())app.use(classic.routes(), classic.allowedMethods())//...文件目录koa-demo/ |-api/ |-books.js |-classic.js |-users.js |-articles.js |-package.json |-app.js传统方式引入router app.js const Koa = require('koa')const app = new Koa()const book = require('./app/api/v1/book')const classic = require('./app/api/v1/classic')app.use(book.routes(), book.allowedMethods())app.use(classic.routes(), classic.allowedMethods())app.listen(3333)require-directory引入require-directory用来递归地迭代指定的目录,并返回这些模块。github随着文件增加,如何高效的开发就是我们要追求的事情了 首先 npm install require-directoryapp.js const Koa = require('koa')const app = new Koa()const Router = require('koa-router')// 使用require-directory加载路由文件夹下的所有routerconst requireDirectory = require('require-directory')// 将所有的路由加载上,自动加载代码const modules = requireDirectory(module, './api', { visit: whenLoadModule })function whenLoadModule(obj) { if (obj instanceof Router) { app.use(obj.routes(), obj.allowedMethods()) }}app.listen(3333)路由文件就按照传统的方式写就行books.js ...

May 22, 2019 · 1 min · jiezi

让我们来重新设计一下 koa-router

前言koa-router 是目前用的比较多的 Koa 的路由中间件之一,前段时间由于作者没有精力继续维护而将其公开售卖。我们有些项目也用到了这个库,但是目前很多我们想要的特性都没有,比如生成接口文档。本身这个库代码实现还比较简单,因此综合考虑打算重写一个。项目地址:https://github.com/d-band/koa…特性:支持几乎所有的 koa-router 特性支持 params 校验params 支持从 path, header, query, cookie 中获取支持 body parser支持 request body 校验支持参数类型自动转换支持自动生成 OpenAPI简单例子:index.jsimport Koa from ‘koa’;import Mapper from ‘koa-mapper’;import * as service from ‘./service’;const Mapper = new Mapper();mapper.get(’/users/:id/projects’, { params: { id: { type: ’number’ }, status: { type: ‘array<string>’, in: ‘query’ }, token: { type: ‘string’, in: ‘header’ } }}, service.getProjects);mapper.post(’/users/:id/projects’, { params: { id: { type: ’number’ } }, body: ‘Project’}, service.addProject);mapper.schema(‘Project’, { id: { type: ’number’, required: true }, name: { type: ‘string’, required: true }, status: { type: ‘array<Status>’, required: true }});mapper.schema(‘Status’, { id: { type: ‘integer’ }, name: { type: ‘string’ }}, { required: [‘id’, ’name’]});app.use(mapper.routes());app.use(mapper.allowedMethods());app.listen(3000);// open http://localhost:3000/openapi.jsonservice.jsexport async function getProjects(ctx) { const { id, status, token } = ctx.params; await checkToken(id, token); ctx.body = await Project.findAll({ where: { userId: id, status: { $in: status } } });}export async function addProject(ctx) { const { body } = ctx.request; ctx.body = await Project.create({ …body, userId: id });}路由定义:mapper.get(path, [options], …middlewares);mapper.post(path, [options], …middlewares);mapper.put(path, [options], …middlewares);mapper.del(path, [options], …middlewares);…options 为可选参数,包含:name: 路由名称params: 请求参数定义body: 请求 Body 定义其他 OpenAPI 中 Operation Object 的参数options.params 为请求参数定义,如:params = { id: { type: ’number’ }, name: { type: ‘string’, in: ‘query’ }, user: { type: ‘User’, in: ‘query’ }}type: 参数类型,包含基本类型(number、string、integer、date、time、datetime),数组类型(array<string>),自定义类型(如 User),自定义数组类型(array<User>),多个类型(number|string)in: 参数来源,包含 path,header,query,cookie其他 OpenAPI 中 Parameter Object 的参数自定义类型mapper.define(schemaName, properties, options);// ormapper.schema(schemaName, properties, options);支持类型组合,如:mapper.schema(‘Status’, { id: { type: ‘integer’ }, name: { type: ‘string’ }}, { required: [‘id’]});mapper.schema(‘Project’, { id: { type: ’number’, required: true }, name: { type: ‘string’, required: true }, status: { type: ‘array<Status>’, required: true }});支持继承,如:mapper.schema(‘Model’, { id: { type: ’number’ }, createdAt: { type: ‘datetime’ }, updatedAt: { type: ‘datetime’ }});mapper.schema(‘User: Model’, { name: { type: ‘string’ }});Body Parsermapper.post(’/users’, { body: ‘User’}, (ctx) => { const { id, name } = ctx.request.body;});支持文件上传,如:mapper.post(’/uploadImage’, { bodyparser: { multipart: true }, body: { user: { type: ’number’ }, image: { type: ‘file’ } }}, (ctx) => { const { user, image } = ctx.request.body;});结尾目前 koa-mapper 刚发布,测试覆盖率达到 100%,有哪些有兴趣的小伙伴欢迎一起维护。 ...

March 8, 2019 · 2 min · jiezi

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 = &lt;h1&gt;Hello, ${name}!&lt;/h1&gt;;});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/koapost请求用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 = &lt;h1&gt;Hello, ${name}!&lt;/h1&gt;;});router.get(’/’, async (ctx, next) => { ctx.response.body = &lt;h1&gt;Index&lt;/h1&gt; &lt;form action="/signin" method="post"&gt; &lt;p&gt;Name: &lt;input name="name" value="koa"&gt;&lt;/p&gt; &lt;p&gt;Password: &lt;input name="password" type="password"&gt;&lt;/p&gt; &lt;p&gt;&lt;input type="submit" value="Submit"&gt;&lt;/p&gt; &lt;/form&gt;;});//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 = &lt;h1&gt;Welcome, ${name}!&lt;/h1&gt;; } else { ctx.response.body = &lt;h1&gt;Login failed!&lt;/h1&gt; &lt;p&gt;&lt;a href="/"&gt;Try again&lt;/a&gt;&lt;/p&gt;; }});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 = &lt;h1&gt;Index&lt;/h1&gt; &lt;form action="/signin" method="post"&gt; &lt;p&gt;Name: &lt;input name="name" value="koa"&gt;&lt;/p&gt; &lt;p&gt;Password: &lt;input name="password" type="password"&gt;&lt;/p&gt; &lt;p&gt;&lt;input type="submit" value="Submit"&gt;&lt;/p&gt; &lt;/form&gt;;};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 = &lt;h1&gt;Welcome, ${name}!&lt;/h1&gt;; } else { ctx.response.body = &lt;h1&gt;Login failed!&lt;/h1&gt; &lt;p&gt;&lt;a href="/"&gt;Try again&lt;/a&gt;&lt;/p&gt;; }};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。AjaxAjax 是一种用于创建快速动态网页的技术,无需重新加载整个网页的情况下即将实现网页的局部更新。下面通过Ajax进行跨域请求的情景,首先通过koa启了两个本地服务:一个port为3200,一个为3201。app1.jsonst 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.jsconst 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: trueAccess-Control-Allow-Headers: Content-Type,Authorization,AcceptAccess-Control-Allow-Methods: GET,POST,DELETEAccess-Control-Max-Age: 5Access-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开发框架 ...

February 21, 2019 · 6 min · jiezi