長文,Koa2 和 Express 的使用對比
theme: nico
本文正在參加「金石計劃 . 瓜分6萬現金大獎」
介紹
伺服器端 Web 框架用來:從資料庫中獲取資料然後顯示到一個頁面中、確認使用者輸入的資料以及儲存到一個數據庫中、檢查使用者的許可權、登陸使用者、路由跳轉等。
Express
Express 是目前最流行的 NodeJS Web 框架(基於使用 Express 的 知名企業 的數量、維護程式碼庫的人數),也是許多其它流行 NodeJS 框架的底層庫。它提供了以下機制: - 為不同 URL 路徑中使用不同 HTTP 動詞的請求(路由)編寫處理程式。 - 集成了“檢視”渲染引擎,以便通過將資料插入模板來生成響應。 - 設定常見 web 應用設定,比如用於連線的埠,以及渲染響應模板的位置。 - 在請求處理管道的任何位置新增額外的請求處理“中介軟體”。
Koa
雖然 Express 的 API 很簡單,但是它是基於 ES5 的語法,要實現非同步,需要回調。如果非同步巢狀層次過多,程式碼會變得很臃腫。NodeJs 開始支援 ES6 後,Express 的原班團隊又基於 ES6 的 Generator 語法重新編寫了 Koa 1.0。使用 generator 語法實現非同步,類似於下面: ```js var koa = require('koa'); var app = koa();
app.use('/test', function *() { yield doReadFile1(); var data = yield doReadFile2(); this.body = data; });
app.listen(3000); ``` NodeJs 開始支援 async 和 await 語法後,Koa 團隊又基於 Promise 和 async 和 await 語法改寫為 Koa2,所以需要 node v7.6.0 以上版本支援。Koa 因為沒有捆綁中介軟體,所以保持了一個很小的體積。
使用對比
- 傳送資訊。與 Express 函式建立不同,Koa 需要通過 new 來建立。
- Koa 提供了一個 Context 物件,表示一次對話的上下文,它將 node 的 request 和 response 物件封裝到單個物件中,通過它來操作 HTTP 請求和響應。
- ctx.app 為應用程式例項引用。
- ctx.req 為 Node 的 request 物件。
- ctx.res 為 Node 的 response 物件。
- ctx.request 為 koa 的 Request 物件。
- ctx.response 為 koa 的 Response 物件。
- 繞過 Koa 的 response 處理是不被支援的,例如:
- res.statusCode
- res.writeHead()
- res.write()
- res.end()
- 其中,ctx.type 和 ctx.body 分別是 ctx.response.type 和 ctx.response.body 的別名。 ```js const express = require('express'); const app = express(); app.all('*', (req, res) => { res.type('xml'); res.send('Hello World'); }).listen(8080, 'localhost');
const Koa = require('koa'); const app2 = new Koa(); app2.use((ctx) => { ctx.type = 'xml'; ctx.body = 'Hello World'; }).listen(3000, 'localhost'); ```
- 地址(簡單路由)。網站一般都有多個頁面,所以常常需要地址的切換。其中,ctx.path 是 ctx.request.path 的別名。 ```js const app = require('express')(); app.get('/', (req, res) => { res.send('Hello World'); }).get('/*', (req, res) => { res.send('Index Page'); }).listen(8080, 'localhost');
const app2 = new(require('koa')); app2.use(ctx => { ctx.body = ctx.path === '/' ? 'Hello World' : 'Index Page'; }).listen(3000, 'localhost'); ```
- 路由。對於複雜的 HTTP 請求,簡單的地址切換就無法勝任了,Express 自帶了路由方法和 express.Router() 完整的路由中介軟體系統。Koa 需要使用 koa-route 或 koa-router(功能更豐富)路由模組。
``js const express = require('express'); const app = express(); app.route('/').all((req, res) => { res.send('Hello World'); }); app.route('/user/:name').get((req, res) => { res.send(
Hello ${req.params.name.fontcolor('magenta')}!); }).delete((req, res) => { res.send(
Delete ${req.params.name}!`); }); app.listen(8080, 'localhost');
const app2 = new(require('koa'));
const route = require('koa-route');
const main = route.all('/', ctx => {
ctx.body = 'Hello World';
});
const getUser = route.get('/user/:name', function(ctx, name) {
ctx.type = 'html';
ctx.body = Hello ${name.fontcolor('magenta')}!
;
})
const deleteUser = route.delete('/user/:name', function(ctx, name) {
ctx.type = 'html';
ctx.body = Delete ${name}!
;
});
const all = route.get('/*', ctx => {
ctx.body = 'Index Page';
});
app2.use(main).use(getUser).use(deleteUser).use(all);
app2.listen(3000, 'localhost');
```
- 靜態資源。如果網站提供靜態資源(圖片、字型、樣式表、指令碼等),為它們一個個寫路由就很麻煩,也沒必要。Express 自帶了 static 方法,而 Koa 需要使用 koa-static 模組。 ```js const express = require('express'); const app = express(); app.use(express.static('.')).listen(8080, 'localhost', function() { console.log(this.address(), app.get('env')); });
const app2 = new(require('koa')); app2.use(require('koa-static')('.')).listen(3000, 'localhost', function() { console.log(this.address(), app2.env); }); ```
- 重定向。有些場合,伺服器需要重定向(redirect)訪問請求。比如,使用者登陸以後,將他重定向到登陸前的頁面。Express 通過 res.redirect(),而 Koa 通過 ctx.response.redirect() 方法,可以發出一個 302 跳轉,重定向到另一個路由。
```js
const app = require('express')();
app.get('/', (req, res) => {
res.send('
Hello
'); }).get('/redirect', (req, res) => { res.redirect('/'); }).listen(8080, 'localhost');
const app2 = new(require('koa')); const route = require('koa-route'); app2.use(route.get('/', ctx => { ctx.body = '
Hello
'; })).use(route.get('/redirect', ctx => { ctx.redirect('/'); })).listen(3000, 'localhost'); ```- 中介軟體的合成。Express 通過呼叫 app.use() 可以傳入中介軟體的陣列,而 Koa 通過 koa-compose 模組可以將多箇中間件合成為一個。
``js const app = require('express')(); const logger = (req, res, next) => { console.log(
${Date.now()} ${req.method} ${req.url}`); next(); } const main = (req, res) => { res.send('Hello World'); }; app.use([logger, main]).listen(8080, 'localhost');
const app2 = new(require('koa'));
const logger2 = (ctx, next) => {
console.log(${Date.now()} ${ctx.method} ${ctx.url}
);
next();
}
const main2 = ctx => {
ctx.body = 'Hello World';
};
const middlewares = require('koa-compose')([logger2, main2]);
app2.use(middlewares).listen(3000, 'localhost');
```
- 表單處理。Web 應用離不開處理表單。本質上,表單就是 POST 方法傳送到伺服器的鍵值對。Express 自帶了表單解析功能,而 Koa 通過 koa-body 模組可以從 POST 請求的資料體裡面提取鍵值對。 ```js const express = require('express'); const app = express(); app.use(express.json()); // for parsing application/json app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded app.all('*', function(req, res) { const { query, body } = req; res.send({ query, body }); }).listen(8080, 'localhost');
const app2 = new(require('koa')); const main = function(ctx) { const query = ctx.request.query; // get const body = ctx.request.body; // post if (!query.name && !body.name) ctx.throw(400, 'name required'); ctx.body = { query, body }; }; app2.use(require('koa-body')()); app2.use(main).listen(3000, 'localhost'); ```
-
檔案上傳。Express 可以通過 multer 中介軟體,來處理 enctype="multipart/form-data" 的表單資料,而 Koa 通過 koa-body 模組還可以用來處理檔案上傳。
js const express = require('express'); const app = express(); const multer = require('multer'); const fs = require('fs'); app.use(express.static('./')); app.use(express.json()); // for parsing application/json app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded app.use(multer({ dest: 'upload/' }).array('image')); app.get('/process_get', function(req, res) { res.json(req.query); }).post('/process_post', function(req, res) { res.json(req.body); }).post('/file_upload', function(req, res) { req.files.forEach(file => { const des_file = "upload/" + file.originalname; console.log(des_file); fs.readFile(file.path, function(err, data) { if (err) console.log(err); else fs.writeFile(des_file, data, function(err) { if (err) console.log(err); }); }); }); res.json(req.files); }); app.listen(8080, 'localhost');
js const fs = require('fs'); const app2 = new(require('koa')); const main = async function(ctx) { const tmpdir = require('os').tmpdir(); const filePaths = []; console.log(ctx.request.files, ctx.request.body.files); const files = ctx.request.files || {}; for (let key in files) { const file = files[key]; if (!fs.existsSync('upload')) fs.mkdirSync('upload'); console.log(file.originalFilename); const filePath = require('path').join('upload', file.originalFilename); const reader = fs.createReadStream(file.filepath); const writer = fs.createWriteStream(filePath); reader.pipe(writer); filePaths.push(filePath); } ctx.body = filePaths; }; app2.use(require('koa-body')({ multipart: true, encoding: 'utf-8' })); app2.use(require('koa-static')('.')); app2.use(main).listen(3000, 'localhost');
-
錯誤處理。如果程式碼執行過程中發生錯誤,我們需要把錯誤資訊返回給使用者。HTTP 協定約定這時要返回 500 狀態碼。
-
Express 隨附一個內建的錯誤處理程式,負責處理應用程式中可能遇到的任何錯誤。這個預設的錯誤處理中介軟體函式需要新增在中介軟體函式集的末尾才能捕獲錯誤。錯誤處理中介軟體的定義方式與其他中介軟體函式基本相同,差別在於錯誤處理函式有四個自變數而不是三個:(err, req, res, next)。
js app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send(err.stack); });
-
Koa 提供了 ctx.throw() 方法,用來丟擲錯誤,ctx.throw(500) 就是丟擲 500 錯誤。如果將 ctx.response.status 設定成 404,就相當於 ctx.throw(404),返回 404 錯誤。
js const main = ctx => { ctx.response.status = 404; ctx.response.body = 'Page Not Found'; };
-
為每個中介軟體都寫 try...catch 太麻煩,我們可以讓最外層的中介軟體,負責所有中介軟體的錯誤處理。 ```js const handler = async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.statusCode || err.status || 500; ctx.body = { message: err.message }; } };
const main = ctx => { ctx.throw(500); };
app.use(handler); app.use(main); ```
-
執行過程中一旦出錯,Koa 會觸發一個 error 事件。監聽這個事件,也可以處理錯誤。
js app.on('error', (err, ctx) => console.error('server error', err); );
-
需要注意的是,如果錯誤被 try...catch 捕獲,就不會觸發 error 事件。這時,必須呼叫 ctx.app.emit(),手動釋放 error 事件,才能讓監聽函式生效。
js const handler = async (ctx, next) => { try { await next(); } catch (err) { ctx.status = err.statusCode || err.status || 500; ctx.type = 'html'; ctx.body = '<p>Something wrong, please contact administrator.</p>'; ctx.app.emit('error', err, ctx); } };