首先,咱們建立一個名爲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:該字段可選,用來指定本次預檢請求的有效期,單位爲秒。