《前端之路》--- 重溫 Koa2


一、簡單介紹

1.1、快速開始 (這里省略了安裝的過程)

const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
  ctx.body = 'hello koa2'
})

app.listen(3000)

1.2、源碼簡單解析

源碼文件主要包含了 application.js 、context.js 、request.js 、response.js

  • application.js 是 Koa 的入口文件封裝了 ctx、request、response, 以及核心的中間件處理流程
  • context.js 處理應用上下文,里面直接封裝部分request.js和response.js的方法
  • request.js 處理http請求
  • response.js 處理http響應

1.3、中間件的簡單開發

這里主要介紹如何使用 async/await 在 koa2 中進行中間件的開發

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 中間件

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 的 router

經過思考🤔, 實現路由的基本原理: 通過請求進來的 url 匹配到對應的頁面文件,然后通過 fs 讀取對應文件的內容,並返回給 ctx.body, 那下面我們就按照這個思路來實現一下路由。


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 中間件 

三、請求數據

3.1、 GET 請求數據獲取

GET 請求數據獲取的方法有2中,如下

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
    };
});

返回結果

url: "/index?page=1"
req_query: {page: "1"}
req_queryString: "page=1"
ctx_query: {page: "1"}
ctx_queryString: "page=1"

疑惑🤔的 點: 從上線文中獲取的request對象和直接通過上線文獲取的參數 有什么區別? 為什么要這么設計?

  • 從 Koa2 的框架設計層面 app.js 中封裝了 ctx、request、response
  • 從 Koa2 的框架設計層面 ctx.js 中封裝了 request、response 方法
  • 從上下文中獲取和從 ctx.request 獲取的參數是一樣的,因為底層方法是一致的
  • 直接從上下文中獲取的方式簡單、快捷
  • 從上下文中的 request 對象中獲取的話,會更加的明確該屬性來源,不容易混淆。

注意:ctx.request是context經過封裝的請求對象,ctx.req是context提供的node.js原生HTTP請求對象, 和這里的 ctx.query 和 ctx.request.query 是沒有關系的。

3.2、 POST 請求數據獲取

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>';
    }

3.3、 koa-bodyparser中間件

實際上是封裝了一層 post 的數據處理方法,然后將其賦值給了 ctx.request 的 body 屬性

const bodyParser = require('koa-bodyparser')

// 使用ctx.body解析中間件
app.use(bodyParser())

// 處理 method 為 POST 的方法
let postData = ctx.request.body
ctx.body = postData

四、 靜態資源加載

4.1、靜態資源加載源碼解析

	// 核心代碼
│   ├── content.js # 讀取請求內容
│   ├── dir.js # 讀取目錄內容
│   ├── file.js # 讀取文件內容
│   ├── mimes.js # 文件類型列表
│   └── walk.js # 遍歷目錄內容
└── index.js # 啟動入口文件

4.1.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.2、content.js 為讀取當前請求內容 (判斷當前文件請求路徑是是否存在且判斷是 文件夾還是文件, 如果是文件夾則讀取文件內容)

// 核心代碼
    //判斷訪問地址是文件夾還是文件
    let stat = fs.statSync( reqPath )

    if( stat.isDirectory() ) {
      //如果為目錄,則渲讀取目錄內容
      content = dir( ctx.url, reqPath )

    } else {
      // 如果請求為文件,則讀取文件內容
      content = await file( reqPath )
    }

4.1.3、dir.js 為讀取目錄內容

	// 核心部分代碼
	// 遍歷讀取當前目錄下的文件、子目錄
	let contentList = walk( reqPath )

4.1.4、 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.5、 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.6、 walk.js 文件類型列表

// 核心代碼 通過遍歷,得到當前文件夾內的文件夾名稱、和最后的文件名稱
let result = dirList.concat( fileList );
// 疑惑的點: 為什么需要把文件名稱也加上呢? 大家也可以作為一個思考

五、 Koa2 使用 cookie/session

簡單粗暴的直接上代碼吧, 里面有一些需要注意的問題點,都在注釋點中了。關鍵點就在與 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';
    }
});

5.2、koa2 使用 session

這里需要注意下,koa 本身沒有提供 session 的方法,這里的例子是通過中間件來實現一些你需要的能力。這里的兩種實現 session 能力的方案。這兩個方案的區別就在於 存儲信息的大小。

5.2.1、 通過 koa-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 來判斷當前用戶是否登陸成功、是否在有效期內等等
5.2.2、 通過 koa-mysql-session 和 koa-session-minimal 將信息存儲在 mysql 中
// 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: ''
};

六、 koa2加載模板引擎

6.1、 koa2 加載模板引擎 (ejs)

這里直接展示使用的 demo

app.use(
    views(path.join(__dirname, './ejs'), {
        extension: 'ejs'
    })
);

app.use(async ctx => {
    let title = 'hello 404';
    await ctx.render('404', {
        title
    });
});

另外,我們附上 ejs 官方文檔

七、 koa2 中簡單使用 mysql 數據庫

這里寫了一個簡單的 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 最新使用文檔

八、 koa2 中使用單元檢測

這里的單元測試主要是正對 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();
            });
    });
});

// 這里發現,我們在測試我們的借口返回數據的類型、數值、錯誤碼等類型的時候會有非常大的幫助的。以后如果需要用起來的話,推薦使用之。

九、 node 服務端開發過程中的 開發 debug 方式

9.1、vscode 進行debug

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 的文件為執行的入口文件即可。(這里推薦使用)

9.2、chrome 瀏覽器進行 debug

通過 node --inspect index.js 啟動服務,則可以在 chrome 瀏覽器控制台看到對應的 node 的小圖標,點擊然后就有一個對應的小彈框進行 debug 啦。試了下,也推薦吧,哈哈,看個人喜好了。

十、總結

看完整個koa2 的api,以及使用了一些特性之后,我們不難發現,koa2 相對於 express 真的要簡潔很多,其核心也在於洋蔥圖和中間件的機制,那么能夠編寫中間件和從茫茫大海中找到高可用的中間件非常重要,這二點是大家未來需要注意的地方。過完年了,自己身為湖北人,因為這次肺炎沒能回到老家過年,那就讓自己多學習一些知識吧~ 同時也希望這次的疫情可以快速的被消滅掉~奧利給!!!

GitHub 地址:(歡迎 star 、歡迎推薦 : )
《前端之路》 - 重溫Koa2


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM