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/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 = `<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網頁不能開啟,除非這兩個網頁"同源"。。所謂"同源"指的是"三個相同",即協議相同、域名相同和埠相同。
例如,有下面一個網址: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:該欄位可選,用來指定本次預檢請求的有效期,單位為秒。
參考: