[TOC]javascript
const Koa = require('koa')
const app = new Koa()
app.use( async ( ctx ) => {
ctx.body = 'hello koa2'
})
app.listen(3000)
複製代碼
源碼文件主要包含了 application.js 、context.js 、request.js 、response.jscss
這裏主要介紹如何使用 async/await 在 koa2 中進行中間件的開發html
middleware 在 koa2 中如何使用前端
const Koa = require('koa')
const logger = require('./middleware/logger-async')
const app = new Koa()
app.use(logger())
app.use(ctx => {
ctx.body = 'hello middleware'
})
app.listen(3000)
複製代碼
如何編寫一個簡單的 middleware 中間件java
function log(ctx) {
console.log( ctx.method, ctx.header.host + ctx.url )
}
module.exports = function() {
return async function(ctx, next) {
log(ctx)
await next()
}
}
// 對,就是這樣,so easy
複製代碼
原生 JS 實現 koa 的 routernode
通過思考🤔, 實現路由的基本原理: 經過請求進來的 url 匹配到對應的頁面文件,而後經過 fs 讀取對應文件的內容,並返回給 ctx.body, 那下面咱們就按照這個思路來實現一下路由。mysql
function render(page) {
return new Promise((resolve, reject) => {
let viewUrl = `./view/${page}`;
fs.readFile(viewUrl, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
async function route(url) {
let view = '404.html';
switch (url) {
case '/':
view = 'index.html';
break;
case '/index':
view = 'index.html';
break;
case '/login':
view = 'login.html';
break;
case '/404':
view = '404.html';
break;
default:
break;
}
let html = render(view);
return html;
}
app.use(async ctx => {
let url = ctx.request.url;
let html = await route(url);
ctx.body = html;
});
// 固然還有 koa-router 中間件
複製代碼
GET 請求數據獲取的方法有2中,以下git
app.use(async ctx => {
let url = ctx.request.url;
let html = await route(url);
// 從上下文對 request 對象中獲取
let request = ctx.request;
let req_query = request.query;
let req_queryString = request.querystring;
// 從 上下文中直接獲取
let ctx_query = ctx.query;
let ctx_queryString = ctx.querystring;
ctx.body = {
ctx,
request,
url,
req_query,
req_queryString,
ctx_query,
ctx_queryString,
html
};
});
複製代碼
返回結果github
url: "/index?page=1"
req_query: {page: "1"}
req_queryString: "page=1"
ctx_query: {page: "1"}
ctx_queryString: "page=1"
複製代碼
疑惑🤔的 點: 從上線文中獲取的request對象和直接經過上線文獲取的參數 有什麼區別? 爲何要這麼設計?sql
注意:ctx.request是context通過封裝的請求對象,ctx.req是context提供的node.js原生HTTP請求對象, 和這裏的 ctx.query 和 ctx.request.query 是沒有關係的。
POST 請求的話,須要咱們在頁面mock一個表單,這樣的話,能夠更好的查看咱們請求的數據。
<h1>koa2 request post demo</h1>
<form method="POST" action="/">
<p>userName</p>
<input name="userName" /><br />
<p>nickName</p>
<input name="nickName" /><br />
<p>email</p>
<input name="email" /><br />
<button type="submit">submit</button>
</form>
複製代碼
if (ctx.method === 'GET') {
ctx.body = html;
} else if (ctx.url === '/' && ctx.method === 'POST') {
ctx.body = html + `<script> alert('提交成功!') </script>`;
} else {
ctx.body = '<h1>404!!! o(╯□╰)o</h1>';
}
複製代碼
其實是封裝了一層 post 的數據處理方法,而後將其賦值給了 ctx.request 的 body 屬性
const bodyParser = require('koa-bodyparser')
// 使用ctx.body解析中間件
app.use(bodyParser())
// 處理 method 爲 POST 的方法
let postData = ctx.request.body
ctx.body = postData
複製代碼
// 核心代碼
│ ├── content.js # 讀取請求內容
│ ├── dir.js # 讀取目錄內容
│ ├── file.js # 讀取文件內容
│ ├── mimes.js # 文件類型列表
│ └── walk.js # 遍歷目錄內容
└── index.js # 啓動入口文件
複製代碼
4.1.一、index.js 入口文件(對於文本類型和圖片類型返回請求數據的方式是不同的)
// 核心部分代碼 - 非所有
// 輸出靜態資源內容
if ( _mime && _mime.indexOf('image/') >= 0 ) {
// 若是是圖片,則用node原生res,輸出二進制數據
ctx.res.writeHead(200)
ctx.res.write(_content, 'binary')
ctx.res.end()
} else {
// 其餘則輸出文本
ctx.body = _content
}
複製代碼
4.1.二、content.js 爲讀取當前請求內容 (判斷當前文件請求路徑是是否存在且判斷是 文件夾仍是文件, 若是是文件夾則讀取文件內容)
// 核心代碼
//判斷訪問地址是文件夾仍是文件
let stat = fs.statSync( reqPath )
if( stat.isDirectory() ) {
//若是爲目錄,則渲讀取目錄內容
content = dir( ctx.url, reqPath )
} else {
// 若是請求爲文件,則讀取文件內容
content = await file( reqPath )
}
複製代碼
4.1.三、dir.js 爲讀取目錄內容
// 核心部分代碼
// 遍歷讀取當前目錄下的文件、子目錄
let contentList = walk( reqPath )
複製代碼
4.1.四、 file.js 讀取文件內容
// 核心代碼,讀取對應文件的內容(此處讀取出來的文件內行)
function file ( filePath ) {
let content = fs.readFileSync(filePath[, options])
return content
}
// 這裏須要註釋一下 fs.readFileSync(filePath[, options]) 中的 options 分別有 encoding 和 flag 二種選項,其中若是指定了 encoding 選項,則此函數返回字符串,不然返回 buffer。 就是說默認爲 buffer
複製代碼
4.1.五、 mimes.js 文件類型列表
let mimes = {
'css': 'text/css',
'less': 'text/css',
'gif': 'image/gif',
'html': 'text/html',
'ico': 'image/x-icon',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'js': 'text/javascript',
'json': 'application/json',
'pdf': 'application/pdf',
'png': 'image/png',
'svg': 'image/svg+xml',
'swf': 'application/x-shockwave-flash',
'tiff': 'image/tiff',
'txt': 'text/plain',
'wav': 'audio/x-wav',
'wma': 'audio/x-ms-wma',
'wmv': 'video/x-ms-wmv',
'xml': 'text/xml'
}
// 其中除了咱們常見的 text/xxx 的文本類型、還有 image/xxx 圖片類型和等等等
複製代碼
4.1.六、 walk.js 文件類型列表
// 核心代碼 經過遍歷,獲得當前文件夾內的文件夾名稱、和最後的文件名稱
let result = dirList.concat( fileList );
// 疑惑的點: 爲何須要把文件名稱也加上呢? 你們也能夠做爲一個思考
複製代碼
簡單粗暴的直接上代碼吧, 裏面有一些須要注意的問題點,都在註釋點中了。關鍵點就在與 koa 自己提供了 cookie 的 set 和 get 方法,能夠很是簡單的獲取到對應想要的,可是裏面咱們常見的一些設置的參數,簡單看一眼,其實就很是不簡單了,maxAge、expires、httpOnly、overwrite 等等,這些都是咱們在使用 cookie 的時候須要注意的,安全問題,http 請求問題。每一點都值得仔細來說講。
app.use(async ctx => {
if (ctx.url === '/index') {
ctx.cookies.set('cid', 'hello world', {
domain: '127.0.0.1',
// 寫cookie所在的域名, 須要注意的是若是訪問的域名和這裏的 domain 不一致的化,是沒法成功寫入的
path: '/index', // 寫cookie所在的路徑
maxAge: 10 * 60 * 1000, // cookie有效時長
expires: new Date('2017-02-15'), // cookie失效時間
httpOnly: false, // 是否只用於http請求中獲取
overwrite: false // 是否容許重寫
});
ctx.body = 'cookies is ok';
} else {
ctx.body = 'hello koa2';
}
});
複製代碼
這裏須要注意下,koa 自己沒有提供 session 的方法,這裏的例子是經過中間件來實現一些你須要的能力。這裏的兩種實現 session 能力的方案。這兩個方案的區別就在於 存儲信息的大小。
使用 koa-session 中間件的核心在於須要對於給出的 對應 config 配置的理解。
const Koa = require('koa'); // 導入Koa
const Koa_Session = require('koa-session'); // 導入koa-session
// 配置
const session_signed_key = ["some secret hurr"]; // 這個是配合signed屬性的簽名key
const session_config = {
key: 'koa:sess', /** cookie的key。 (默認是 koa:sess) */
maxAge: 4000, /** session 過時時間,以毫秒ms爲單位計算 。*/
autoCommit: true, /** 自動提交到響應頭。(默認是 true) */
overwrite: true, /** 是否容許重寫 。(默認是 true) */
httpOnly: true, /** 是否設置HttpOnly,若是在Cookie中設置了"HttpOnly"屬性,那麼經過程序(JS腳本、Applet等)將沒法讀取到Cookie信息,這樣能有效的防止XSS攻擊。 (默認 true) */
signed: true, /** 是否簽名。(默認是 true) */
rolling: true, /** 是否每次響應時刷新Session的有效期。(默認是 false) */
renew: false, /** 是否在Session快過時時刷新Session的有效期。(默認是 false) */
};
// 而後經過 ctx.session.logged 來判斷當前用戶是否登錄成功、是否在有效期內等等
複製代碼
// session 中間件
app.use(
session({
key: 'SESSION_ID',
store: store,
cookie: cookie
})
);
// 數據庫配置
let store = new MysqlSession({
user: 'root',
password: '123456',
database: 'hellothinkjs',
host: '127.0.0.1'
});
// 存放sessionId的cookie配置
let cookie = {
maxAge: '', // cookie有效時長
expires: '', // cookie失效時間
path: '', // 寫cookie所在的路徑
domain: '', // 寫cookie所在的域名
httpOnly: true, // 是否只用於http請求中獲取
overwrite: '', // 是否容許重寫
secure: '',
sameSite: '',
signed: ''
};
複製代碼
這裏直接展現使用的 demo
app.use(
views(path.join(__dirname, './ejs'), {
extension: 'ejs'
})
);
app.use(async ctx => {
let title = 'hello 404';
await ctx.render('404', {
title
});
});
複製代碼
另外,咱們附上 ejs 官方文檔
這裏寫了一個簡單的 demo 大體的介紹了下,koa 中 mysql 的使用方式
// 鏈接數據庫
const connection = mysql.createConnection({
host: '127.0.0.1', // 數據庫地址
user: 'root', // 數據庫用戶
password: '123456', // 數據庫密碼
database: 'hellothinkjs' // 選中數據庫
});
let title = 'hello 404';
let users = [];
connection.connect();
connection.query('SELECT * FROM think_user', async (error, results, fields) => {
if (error) throw error;
// connected !
console.log(results);
users = results;
app.use(async ctx => {
await ctx.render('404', {
title,
users
});
});
});
connection.end();
複製代碼
這裏須要注意一點的是: 由於網上以前找的文檔中,不少關於 mysql modules 的使用方式比較古老了,不太適合新版本的 mysql 的連接和使用。 mysql 最新使用文檔
這裏的單元測試主要是正對 node 提供的API 服務來進行測試,測試框架選擇: mocha(測試框架)、chai(斷言庫,用來判斷是否知足預期結果)、supertest(用來模擬 API 請求)固然這三個庫,每個看上去都會有更多的特性,這裏只是簡單的介紹了一些基礎自動化測試的demo
// api.js api server
const server = async (ctx, next) => {
let result = {
success: true,
data: null
};
if (ctx.method === 'GET') {
if (ctx.url === '/getString.json') {
result.data = 'this is string data';
} else if (ctx.url === '/getNumber.json') {
result.data = 123456;
} else {
result.success = false;
}
ctx.body = result;
next && next();
} else if (ctx.method === 'POST') {
if (ctx.url === '/postData.json') {
result.data = 'ok';
} else {
result.success = false;
}
ctx.body = result;
next && next();
} else {
ctx.body = 'hello world';
next && next();
}
};
複製代碼
// index.test.js test server
describe('開始測試智商稅了', () => {
// 測試用例
it('測試你的智商是否是二百五', done => {
request
.post('/postData.json')
.expect(200)
.end((err, res) => {
// 斷言判斷結果是否爲object類型
expect(res.body).to.be.an('object');
expect(res.body.success).to.be.an('boolean');
expect(res.body.data).to.be.an('string');
done();
});
});
});
// 這裏發現,咱們在測試咱們的藉口返回數據的類型、數值、錯誤碼等類型的時候會有很是大的幫助的。之後若是須要用起來的話,推薦使用之。
複製代碼
vscode 自帶 debug 能力,這裏須要花費必定時間去理解的地方是 debug 啓動程序的時候,須要配置一個 launch.json 文件,這裏給一個對應的 demo。
{
// 使用 IntelliSense 瞭解相關屬性。
// 懸停以查看現有屬性的描述。
// 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "啓動程序",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/api.js"
}
]
}
複製代碼
修改對應的 program 的 value 的文件爲執行的入口文件便可。(這裏推薦使用)
經過 node --inspect index.js 啓動服務,則能夠在 chrome 瀏覽器控制檯看到對應的 node 的小圖標,點擊而後就有一個對應的小彈框進行 debug 啦。試了下,也推薦吧,哈哈,看我的喜愛了。
看完整個koa2 的api,以及使用了一些特性以後,咱們不難發現,koa2 相對於 express 真的要簡潔不少,其核心也在於洋蔥圖和中間件的機制,那麼可以編寫中間件和從茫茫大海中找到高可用的中間件很是重要,這二點是你們將來須要注意的地方。過完年了,本身身爲湖北人,由於此次肺炎沒能回到老家過年,那就讓本身多學習一些知識吧~ 同時也但願此次的疫情能夠快速的被消滅掉~奧利給!!!
GitHub 地址:(歡迎 star 、歡迎推薦 : ) 《前端之路》 - 重溫Koa2