koa2開發入門

一.koa2入門javascript

1.建立koa2工程

首先,咱們建立一個目錄hello-koa並做爲工程目錄用VS Code打開。而後,咱們建立app.js,輸入如下代碼:css

// 導入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將調用咱們傳入的異步函數來處理:html

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傳入的將要處理的下一個異步函數。java

上面的異步函數中,咱們首先用await next();處理下一個異步函數,而後,設置response的Content-Type和內容。node

async標記的函數稱爲異步函數,在異步函數中,能夠用await調用另外一個異步函數,這兩個關鍵字將在ES7中引入。git

如今咱們遇到第一個問題:koa這個包怎麼裝,app.js才能正常導入它?github

方法一:能夠用npm命令直接安裝koa。先打開命令提示符,務必把當前目錄切換到hello-koa這個目錄,而後執行命令:web

C:\...\hello-koa> npm install koa@2.0.0

npm會把koa2以及koa2依賴的全部包所有安裝到當前目錄的node_modules目錄下。數據庫

方法二:在hello-koa這個目錄下建立一個package.json,這個文件描述了咱們的hello-koa工程會用到哪些包。完整的文件內容以下:npm

{
    "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": "Michael Liao",
    "license": "Apache-2.0",
    "repository": {
        "type": "git",
        "url": "https://github.com/michaelliao/learn-javascript.git"
    },
    "dependencies": {
        "koa": "2.0.0"
    }
}

其中,dependencies描述了咱們的工程依賴的包以及版本號。其餘字段均用來描述項目信息,可任意填寫。

而後,咱們在hello-koa目錄下執行npm install就能夠把所需包以及依賴包一次性所有裝好:

C:\...\hello-koa> npm install

很顯然,第二個方法更靠譜,由於咱們只要在package.json正確設置了依賴,npm就會把全部用到的包都裝好。

注意,任什麼時候候均可以直接刪除整個node_modules目錄,由於用npm install命令能夠完整地從新下載全部依賴。而且,這個目錄不該該被放入版本控制中。

如今,咱們的工程結構以下:

hello-koa/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 項目描述文件
|
+- node_modules/ <-- npm安裝的全部依賴包

緊接着,咱們在package.json中添加依賴包:

"dependencies": {
    "koa": "2.0.0"
}

而後使用npm install命令安裝後,在VS Code中執行app.js,調試控制檯輸出以下:

node --debug-brk=40645 --nolazy app.js 
Debugger listening on port 40645
app started at port 3000...

咱們打開瀏覽器,輸入http://localhost:3000,便可看到效果:

koa-browser

還能夠直接用命令node app.js在命令行啓動程序,或者用npm start啓動。npm start命令會讓npm執行定義在package.json文件中的start對應命令:

"scripts": {
    "start": "node app.js"
}

2.koa middleware

讓咱們再仔細看看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函數,並傳入ctxnext參數。

咱們能夠對ctx操做,並設置返回內容。可是爲何要調用await next()

緣由是koa把不少async函數組成一個處理鏈,每一個async函數均可以作一些本身的事情,而後用await next()來調用下一個async函數。咱們把每一個async函數稱爲middleware,這些middleware能夠組合起來,完成不少有用的功能。

例如,能夠用如下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...');

middleware的順序很重要,也就是調用app.use()的順序決定了middleware的順序。

此外,若是一個middleware沒有調用await next(),會怎麼辦?答案是後續的middleware將再也不執行了。這種狀況也很常見,例如,一個檢測用戶權限的middleware能夠決定是否繼續處理請求,仍是直接返回403錯誤:

app.use(async (ctx, next) => {
    if (await checkUserPermission(ctx)) {
        await next();
    } else {
        ctx.response.status = 403;
    }
});

理解了middleware,咱們就已經會用koa了!

最後注意ctx對象有一些簡寫的方法,例如ctx.url至關於ctx.request.urlctx.type至關於ctx.response.type

  

二.處理URL

 在hello-koa工程中,咱們處理http請求一概返回相同的HTML,這樣雖然很是簡單,可是用瀏覽器一測,隨便輸入任何URL都會返回相同的網頁。

正常狀況下,咱們應該對不一樣的URL調用不一樣的處理函數,這樣才能返回不一樣的結果。例如像這樣寫:

app.use(async (ctx, next) => {
    if (ctx.request.path === '/') {
        ctx.response.body = 'index page';
    } else {
        await next();
    }
});

app.use(async (ctx, next) => {
    if (ctx.request.path === '/test') {
        ctx.response.body = 'TEST page';
    } else {
        await next();
    }
});

app.use(async (ctx, next) => {
    if (ctx.request.path === '/error') {
        ctx.response.body = 'ERROR page';
    } else {
        await next();
    }
});

這麼寫是能夠運行的,可是好像有點蠢。

應該有一個能集中處理URL的middleware,它根據不一樣的URL調用不一樣的處理函數,這樣,咱們才能專心爲每一個URL編寫處理函數。

1.koa-router

爲了處理URL,咱們須要引入koa-router這個middleware,讓它負責處理URL映射。

咱們把上一節的hello-koa工程複製一份,重命名爲url-koa

先在package.json中添加依賴項:

"koa-router": "7.0.0"

而後用npm install安裝。

接下來,咱們修改app.js,使用koa-router來處理URL:

const Koa = require('koa');

// 注意require('koa-router')返回的是函數:
const router = require('koa-router')();

const app = new Koa();

// log request URL:
app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    await next();
});

// add url-route:
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>';
});

// add router middleware:
app.use(router.routes());

app.listen(3000);
console.log('app started at port 3000...');

注意導入koa-router的語句最後的()是函數調用:

const router = require('koa-router')();

至關於:

const fn_router = require('koa-router');
const router = fn_router();

而後,咱們使用router.get('/path', async fn)來註冊一個GET請求。能夠在請求路徑中使用帶變量的/hello/:name,變量能夠經過ctx.params.name訪問。

再運行app.js,咱們就能夠測試不一樣的URL:

輸入首頁:http://localhost:3000/

url-index

輸入:http://localhost:3000/hello/koa

url-hello

2.處理post請求

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的功能!

因此,咱們又須要引入另外一個middleware來解析原始request請求,而後,把解析後的參數,綁定到ctx.request.body中。

koa-bodyparser就是用來幹這個活的。

咱們在package.json中添加依賴項:

"koa-bodyparser": "3.2.0"

而後使用npm install安裝。

下面,修改app.js,引入koa-bodyparser

const bodyParser = require('koa-bodyparser');

在合適的位置加上:

app.use(bodyParser());

因爲middleware的順序很重要,這個koa-bodyparser必須在router以前被註冊到app對象上。

如今咱們就能夠處理post請求了。寫一個簡單的登陸表單:

const Koa = require('koa');

// 注意require('koa-router')返回的是函數:
const router = require('koa-router')();
const bodyParser = require('koa-bodyparser');

const app = new Koa();

// log request URL:
app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    await next();
});

// add url-route:
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>`;
});

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

// add router middleware:
app.use(bodyParser());
app.use(router.routes());

app.listen(3000);
console.log('app started at port 3000...');

注意到咱們用var name = ctx.request.body.name || ''拿到表單的name字段,若是該字段不存在,默認值設置爲''

相似的,put、delete、head請求也能夠由router處理。

3.重構

如今,咱們已經能夠處理不一樣的URL了,可是看看app.js,總以爲仍是有點不對勁。

全部的URL處理函數都放到app.js裏顯得很亂,並且,每加一個URL,就須要修改app.js。隨着URL愈來愈多,app.js就會愈來愈長。

若是能把URL處理函數集中到某個js文件,或者某幾個js文件中就行了,而後讓app.js自動導入全部處理URL的函數。這樣,代碼一分離,邏輯就顯得清楚了。最好是這樣:

url2-koa/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- controllers/
|  |
|  +- login.js <-- 處理login相關URL
|  |
|  +- users.js <-- 處理用戶管理相關URL
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 項目描述文件
|
+- node_modules/ <-- npm安裝的全部依賴包

因而咱們把url-koa複製一份,重命名爲url2-koa,準備重構這個項目。

咱們先在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處理函數暴露出來。

相似的,hello.js把一個URL處理函數暴露出來:

var fn_hello = async (ctx, next) => {
    var name = ctx.params.name;
    ctx.response.body = `<h1>Hello, ${name}!</h1>`;
};

module.exports = {
    'GET /hello/:name': fn_hello
};

如今,咱們修改app.js,讓它自動掃描controllers目錄,找到全部js文件,導入,而後註冊每一個URL:

// 先導入fs模塊,而後用readdirSync列出文件
// 這裏能夠用sync是由於啓動時只運行一次,不存在性能問題:
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);

確保每一個函數功能很是簡單,一眼能看明白,是代碼可維護的關鍵。

4.Controller Middleware

最後,咱們把掃描controllers目錄和建立router的代碼從app.js中提取出來,做爲一個簡單的middleware使用,命名爲controller.js

const fs = require('fs');

function addMapping(router, mapping) {
    ...
}

function addControllers(router, dir) {
    ...
}

module.exports = function (dir) {
    let
        controllers_dir = dir || 'controllers', // 若是不傳參數,掃描目錄默認爲'controllers'
        router = require('koa-router')();
    addControllers(router, controllers_dir);
    return router.routes();
};

完整內容以下:

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 middleware:
const controller = require('./controller');

...

// 使用middleware:
app.use(controller());

...

完整內容以下所示:

// 導入koa,和koa 1.x不一樣,在koa2中,咱們導入的是一個class,所以用大寫的Koa表示:
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');

// 建立一個Koa對象表示web app自己:
const app = new Koa();

// 導入controller middleware:
const controller = require('./controller');

// 使用middleware:
app.use(bodyParser());
app.use(controller());

// 在端口3000監聽:
app.listen(3000);
console.log('app started at port 3000...');

通過從新整理後的工程url2-koa目前具有很是好的模塊化,全部處理URL的函數按功能組存放在controllers目錄,從此咱們也只須要不斷往這個目錄下加東西就能夠了,app.js保持不變。

 

三.使用Nunjucks

Nunjucks是什麼東東?其實它是一個模板引擎。

那什麼是模板引擎?

模板引擎就是基於模板配合數據構造出字符串輸出的一個組件。好比下面的函數就是一個模板引擎:

function examResult (data) {
    return `${data.name}同窗一年級期末考試語文${data.chinese}分,數學${data.math}分,位於年級第${data.ranking}名。`
}

若是咱們輸入數據以下:

examResult({
    name: '小明',
    chinese: 78,
    math: 87,
    ranking: 999
});

該模板引擎把模板字符串裏面對應的變量替換之後,就能夠獲得如下輸出:

小明同窗一年級期末考試語文78分,數學87分,位於年級第999名。

模板引擎最多見的輸出就是輸出網頁,也就是HTML文本。固然,也能夠輸出任意格式的文本,好比Text,XML,Markdown等等。

有同窗要問了:既然JavaScript的模板字符串能夠實現模板功能,那爲何咱們還須要另外的模板引擎?

由於JavaScript的模板字符串必須寫在JavaScript代碼中,要想寫出新浪首頁這樣複雜的頁面,是很是困難的。

輸出HTML有幾個特別重要的問題須要考慮:

a.轉義

對特殊字符要轉義,避免受到XSS攻擊。好比,若是變量name的值不是小明,而是小明<script>...</script>,模板引擎輸出的HTML到了瀏覽器,就會自動執行惡意JavaScript代碼。

b.格式化

對不一樣類型的變量要格式化,好比,貨幣須要變成12,345.00這樣的格式,日期須要變成2016-01-01這樣的格式。

c.簡單邏輯

模板還須要能執行一些簡單邏輯,好比,要按條件輸出內容,須要if實現以下輸出:

{{ name }}同窗,
{% if score >= 90 %}
    成績優秀,應該獎勵
{% elif score >=60 %}
    成績良好,繼續努力
{% else %}
    不及格,建議回家打屁股
{% endif %}

因此,咱們須要一個功能強大的模板引擎,來完成頁面輸出的功能。

1.Nunjucks

咱們選擇Nunjucks做爲模板引擎。Nunjucks是Mozilla開發的一個純JavaScript編寫的模板引擎,既能夠用在Node環境下,又能夠運行在瀏覽器端。可是,主要仍是運行在Node環境下,由於瀏覽器端有更好的模板解決方案,例如MVVM框架。

若是你使用過Python的模板引擎jinja2,那麼使用Nunjucks就很是簡單,二者的語法幾乎是如出一轍的,由於Nunjucks就是用JavaScript從新實現了jinjia2。

從上面的例子咱們能夠看到,雖然模板引擎內部可能很是複雜,可是使用一個模板引擎是很是簡單的,由於本質上咱們只須要構造這樣一個函數:

function render(view, model) {
    // TODO:...
}

其中,view是模板的名稱(又稱爲視圖),由於可能存在多個模板,須要選擇其中一個。model就是數據,在JavaScript中,它就是一個簡單的Object。render函數返回一個字符串,就是模板的輸出。

下面咱們來使用Nunjucks這個模板引擎來編寫幾個HTML模板,而且用實際數據來渲染模板並得到最終的HTML輸出。

咱們建立一個use-nunjucks的VS Code工程結構以下:

use-nunjucks/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- views/
|  |
|  +- hello.html <-- HTML模板文件
|
+- app.js <-- 入口js
|
+- package.json <-- 項目描述文件
|
+- node_modules/ <-- npm安裝的全部依賴包

其中,模板文件存放在views目錄中。

咱們先在package.json中添加nunjucks的依賴:

"nunjucks": "2.4.2"

注意,模板引擎是能夠獨立使用的,並不須要依賴koa。用npm install安裝全部依賴包。

緊接着,咱們要編寫使用Nunjucks的函數render。怎麼寫?方法是查看Nunjucks的官方文檔,仔細閱讀後,在app.js中編寫代碼以下:

const nunjucks = require('nunjucks');

function createEnv(path, opts) {
    var
        autoescape = opts.autoescape === undefined ? true : opts.autoescape,
        noCache = opts.noCache || false,
        watch = opts.watch || false,
        throwOnUndefined = opts.throwOnUndefined || false,
        env = new nunjucks.Environment(
            new nunjucks.FileSystemLoader('views', {
                noCache: noCache,
                watch: watch,
            }), {
                autoescape: autoescape,
                throwOnUndefined: throwOnUndefined
            });
    if (opts.filters) {
        for (var f in opts.filters) {
            env.addFilter(f, opts.filters[f]);
        }
    }
    return env;
}

var env = createEnv('views', {
    watch: true,
    filters: {
        hex: function (n) {
            return '0x' + n.toString(16);
        }
    }
});

var s = env.render('hello.html', { name: '小明' });
console.log(s);

var s2 = env.render('hello.html', { name: '<script>alert("小明")</script>' });
console.log(s2);

var s3 = env.render('list.html', { fruits: ['apple', 'orange', 'banana'] });
console.log(s3);

console.log(env.render('extend.html', {
    header: 'Hello',
    body: 'bla bla bla...'
}));

變量env就表示Nunjucks模板引擎對象,它有一個render(view, model)方法,正好傳入viewmodel兩個參數,並返回字符串。

建立env須要的參數能夠查看文檔獲知。咱們用autoescape = opts.autoescape && true這樣的代碼給每一個參數加上默認值,最後使用new nunjucks.FileSystemLoader('views')建立一個文件系統加載器,從views目錄讀取模板。

咱們編寫一個hello.html模板文件,放到views目錄下,內容以下:

<h1>Hello {{ name }}</h1>

而後,咱們就能夠用下面的代碼來渲染這個模板:

var s = env.render('hello.html', { name: '小明' });
console.log(s);

得到輸出以下:

<h1>Hello 小明</h1>

咋一看,這和使用JavaScript模板字符串沒啥區別嘛。不過,試試:

var s = env.render('hello.html', { name: '<script>alert("小明")</script>' });
console.log(s);

得到輸出以下:

<h1>Hello &lt;script&gt;alert("小明")&lt;/script&gt;</h1>

這樣就避免了輸出惡意腳本。

此外,可使用Nunjucks提供的功能強大的tag,編寫條件判斷、循環等功能,例如:

<!-- 循環輸出名字 -->
<body>
    <h3>Fruits List</h3>
    {% for f in fruits %}
    <p>{{ f }}</p>
    {% endfor %}
</body>

Nunjucks模板引擎最強大的功能在於模板的繼承。仔細觀察各類網站能夠發現,網站的結構其實是相似的,頭部、尾部都是固定格式,只有中間頁面部份內容不一樣。若是每一個模板都重複頭尾,一旦要修改頭部或尾部,那就須要改動全部模板。

更好的方式是使用繼承。先定義一個基本的網頁框架base.html

<html><body>
{% block header %} <h3>Unnamed</h3> {% endblock %}
{% block body %} <div>No body</div> {% endblock %}
{% block footer %} <div>copyright</div> {% endblock %}
</body>

base.html定義了三個可編輯的塊,分別命名爲headerbodyfooter。子模板能夠有選擇地對塊進行從新定義:

{% extends 'base.html' %}

{% block header %}<h1>{{ header }}</h1>{% endblock %}

{% block body %}<p>{{ body }}</p>{% endblock %}

而後,咱們對子模板進行渲染:

console.log(env.render('extend.html', {
    header: 'Hello',
    body: 'bla bla bla...'
}));

輸出HTML以下:

<html><body>
<h1>Hello</h1>
<p>bla bla bla...</p>
<div>copyright</div> <-- footer沒有重定義,因此仍使用父模板的內容
</body>

2.性能

最後咱們要考慮一下Nunjucks的性能。

對於模板渲染自己來講,速度是很是很是快的,由於就是拼字符串嘛,純CPU操做。

性能問題主要出如今從文件讀取模板內容這一步。這是一個IO操做,在Node.js環境中,咱們知道,單線程的JavaScript最不能忍受的就是同步IO,但Nunjucks默認就使用同步IO讀取模板文件。

好消息是Nunjucks會緩存已讀取的文件內容,也就是說,模板文件最多讀取一次,就會放在內存中,後面的請求是不會再次讀取文件的,只要咱們指定了noCache: false這個參數。

在開發環境下,能夠關閉cache,這樣每次從新加載模板,便於實時修改模板。在生產環境下,必定要打開cache,這樣就不會有性能問題。

Nunjucks也提供了異步讀取的方式,可是這樣寫起來很麻煩,有簡單的寫法咱們就不會考慮複雜的寫法。保持代碼簡單是可維護性的關鍵。

 

四.使用MVC

1.MVC

咱們已經能夠用koa處理不一樣的URL,還能夠用Nunjucks渲染模板。如今,是時候把這二者結合起來了!

當用戶經過瀏覽器請求一個URL時,koa將調用某個異步函數處理該URL。在這個異步函數內部,咱們用一行代碼:

ctx.render('home.html', { name: 'Michael' });

經過Nunjucks把數據用指定的模板渲染成HTML,而後輸出給瀏覽器,用戶就能夠看到渲染後的頁面了:

mvc

這就是傳說中的MVC:Model-View-Controller,中文名「模型-視圖-控制器」。

異步函數是C:Controller,Controller負責業務邏輯,好比檢查用戶名是否存在,取出用戶信息等等;

包含變量{{ name }}的模板就是V:View,View負責顯示邏輯,經過簡單地替換一些變量,View最終輸出的就是用戶看到的HTML。

MVC中的Model在哪?Model是用來傳給View的,這樣View在替換變量的時候,就能夠從Model中取出相應的數據。

上面的例子中,Model就是一個JavaScript對象:

{ name: 'Michael' }

下面,咱們根據原來的url2-koa建立工程view-koa,把koa二、Nunjucks整合起來,而後,把原來直接輸出字符串的方式,改成ctx.render(view, model)的方式。

工程view-koa結構以下:

view-koa/
|
+- .vscode/
|  |
|  +- launch.json <-- VSCode 配置文件
|
+- controllers/ <-- Controller
|
+- views/ <-- html模板文件
|
+- static/ <-- 靜態資源文件
|
+- controller.js <-- 掃描註冊Controller
|
+- app.js <-- 使用koa的js
|
+- package.json <-- 項目描述文件
|
+- node_modules/ <-- npm安裝的全部依賴包

package.json中,咱們將要用到的依賴包有:

"koa": "2.0.0",
"koa-bodyparser": "3.2.0",
"koa-router": "7.0.0",
"nunjucks": "2.4.2",
"mime": "1.3.4",
"mz": "2.4.0"

先用npm install安裝依賴包。

而後,咱們準備編寫如下兩個Controller:

2.處理首頁 GET /

咱們定義一個async函數處理首頁URL/

async (ctx, next) => {
    ctx.render('index.html', {
        title: 'Welcome'
    });
}

注意到koa並無在ctx對象上提供render方法,這裏咱們假設應該這麼使用,這樣,咱們在編寫Controller的時候,最後一步調用ctx.render(view, model)就完成了頁面輸出。

3.處理登陸請求 POST /signin

咱們再定義一個async函數處理登陸請求/signin

async (ctx, next) => {
    var
        email = ctx.request.body.email || '',
        password = ctx.request.body.password || '';
    if (email === 'admin@example.com' && password === '123456') {
        // 登陸成功:
        ctx.render('signin-ok.html', {
            title: 'Sign In OK',
            name: 'Mr Node'
        });
    } else {
        // 登陸失敗:
        ctx.render('signin-failed.html', {
            title: 'Sign In Failed'
        });
    }
}

因爲登陸請求是一個POST,咱們就用ctx.request.body.<name>拿到POST請求的數據,並給一個默認值。

登陸成功時咱們用signin-ok.html渲染,登陸失敗時咱們用signin-failed.html渲染,因此,咱們一共須要如下3個View:

  • index.html
  • signin-ok.html
  • signin-failed.html

4.編寫View

在編寫View的時候,咱們其實是在編寫HTML頁。爲了讓頁面看起來美觀大方,使用一個現成的CSS框架是很是有必要的。咱們用Bootstrap這個CSS框架。從首頁下載zip包後解壓,咱們把全部靜態資源文件放到/static目錄下:

view-koa/
|
+- static/
   |
   +- css/ <- 存放bootstrap.css等
   |
   +- fonts/ <- 存放字體文件
   |
   +- js/ <- 存放bootstrap.js等

這樣咱們在編寫HTML的時候,能夠直接用Bootstrap的CSS,像這樣:

<link rel="stylesheet" href="/static/css/bootstrap.css">

如今,在使用MVC以前,第一個問題來了,如何處理靜態文件?

咱們把全部靜態資源文件所有放入/static目錄,目的就是能統一處理靜態文件。在koa中,咱們須要編寫一個middleware,處理以/static/開頭的URL。

5.編寫middleware

咱們來編寫一個處理靜態文件的middleware。編寫middleware實際上一點也不復雜。咱們先建立一個static-files.js的文件,編寫一個能處理靜態文件的middleware:

const path = require('path');
const mime = require('mime');
const fs = require('mz/fs');

function staticFiles(url, dir) {
    return async (ctx, next) => {
        let rpath = ctx.request.path;
        console.log(rpath)
        console.log(url)
        console.log(dir)
        if (rpath.startsWith(url)) {
            let fp = path.join(dir, rpath.substring(url.length));
            console.log(fp)
            if (await fs.exists(fp)) {
                ctx.response.type = mime.lookup(rpath);
                ctx.response.body = await fs.readFile(fp);
            } else {
                ctx.response.status = 404;
            }
        } else {
            await next();
        }
    };
}

module.exports = staticFiles;

staticFiles是一個普通函數,它接收兩個參數:URL前綴和一個目錄,而後返回一個async函數。這個async函數會判斷當前的URL是否以指定前綴開頭,若是是,就把URL的路徑視爲文件,併發送文件內容。若是不是,這個async函數就不作任何事情,而是簡單地調用await next()讓下一個middleware去處理請求。

咱們使用了一個mz的包,並經過require('mz/fs');導入。mz提供的API和Node.js的fs模塊徹底相同,但fs模塊使用回調,而mz封裝了fs對應的函數,並改成Promise。這樣,咱們就能夠很是簡單的用await調用mz的函數,而不須要任何回調。

全部的第三方包均可以經過npm官網搜索並查看其文檔:

https://www.npmjs.com/

最後,這個middleware使用起來也很簡單,在app.js里加一行代碼:

let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));

注意:也能夠去npm搜索能用於koa2的處理靜態文件的包並直接使用。

6.集成Nunjucks

集成Nunjucks實際上也是編寫一個middleware,這個middleware的做用是給ctx對象綁定一個render(view, model)的方法,這樣,後面的Controller就能夠調用這個方法來渲染模板了。

咱們建立一個templating.js來實現這個middleware:

const nunjucks = require('nunjucks');

function createEnv(path, opts) {
    var
        autoescape = opts.autoescape === undefined ? true : opts.autoescape,
        noCache = opts.noCache || false,
        watch = opts.watch || false,
        throwOnUndefined = opts.throwOnUndefined || false,
        env = new nunjucks.Environment(
            new nunjucks.FileSystemLoader(path, {
                noCache: noCache,
                watch: watch,
            }), {
                autoescape: autoescape,
                throwOnUndefined: throwOnUndefined
            });
    if (opts.filters) {
        for (var f in opts.filters) {
            env.addFilter(f, opts.filters[f]);
        }
    }
    return env;
}

function templating(path, opts) {
    var env = createEnv(path, opts);
    return async (ctx, next) => {
        ctx.render = function (view, model) {
            ctx.response.body = env.render(view, Object.assign({}, ctx.state || {}, model || {}));
            ctx.response.type = 'text/html';
        };
        await next();
    };
}

module.exports = templating;

注意到createEnv()函數和前面使用Nunjucks時編寫的函數是如出一轍的。咱們主要關心tempating()函數,它會返回一個middleware,在這個middleware中,咱們只給ctx「安裝」了一個render()函數,其餘什麼事情也沒幹,就繼續調用下一個middleware。

使用的時候,咱們在app.js添加以下代碼:

const isProduction = process.env.NODE_ENV === 'production';

app.use(templating('views', {
    noCache: !isProduction,
    watch: !isProduction
}));

這裏咱們定義了一個常量isProduction,它判斷當前環境是不是production環境。若是是,就使用緩存,若是不是,就關閉緩存。在開發環境下,關閉緩存後,咱們修改View,能夠直接刷新瀏覽器看到效果,不然,每次修改都必須重啓Node程序,會極大地下降開發效率。

Node.js在全局變量process中定義了一個環境變量env.NODE_ENV,爲何要使用該環境變量?由於咱們在開發的時候,環境變量應該設置爲'development',而部署到服務器時,環境變量應該設置爲'production'。在編寫代碼的時候,要根據當前環境做不一樣的判斷。

注意:生產環境上必須配置環境變量NODE_ENV = 'production',而開發環境不須要配置,實際上NODE_ENV多是undefined,因此判斷的時候,不要用NODE_ENV === 'development'

相似的,咱們在使用上面編寫的處理靜態文件的middleware時,也能夠根據環境變量判斷:

if (! isProduction) {
    let staticFiles = require('./static-files');
    app.use(staticFiles('/static/', __dirname + '/static'));
}

這是由於在生產環境下,靜態文件是由部署在最前面的反向代理服務器(如Nginx)處理的,Node程序不須要處理靜態文件。而在開發環境下,咱們但願koa能順帶處理靜態文件,不然,就必須手動配置一個反向代理服務器,這樣會致使開發環境很是複雜。

7.編寫View

在編寫View的時候,很是有必要先編寫一個base.html做爲骨架,其餘模板都繼承自base.html,這樣,才能大大減小重複工做。

編寫HTML不在本教程的討論範圍以內。這裏咱們參考Bootstrap的官網簡單編寫了base.html

8.運行

一切順利的話,這個view-koa工程應該能夠順利運行。運行前,咱們再檢查一下app.js裏的middleware的順序:

第一個middleware是記錄URL以及頁面執行時間:

app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    var
        start = new Date().getTime(),
        execTime;
    await next();
    execTime = new Date().getTime() - start;
    ctx.response.set('X-Response-Time', `${execTime}ms`);
});

第二個middleware處理靜態文件:

if (! isProduction) {
    let staticFiles = require('./static-files');
    app.use(staticFiles('/static/', __dirname + '/static'));
}

第三個middleware解析POST請求:

app.use(bodyParser());

第四個middleware負責給ctx加上render()來使用Nunjucks:

app.use(templating('view', {
    noCache: !isProduction,
    watch: !isProduction
}));

最後一個middleware處理URL路由:

app.use(controller());

如今,在VS Code中運行代碼,不出意外的話,在瀏覽器輸入localhost:3000/,能夠看到首頁內容:

koa-index

直接在首頁登陸,若是輸入正確的Email和Password,進入登陸成功的頁面:

koa-signin-ok

若是輸入的Email和Password不正確,進入登陸失敗的頁面:

koa-signin-failed

怎麼判斷正確的Email和Password?目前咱們在signin.js中是這麼判斷的:

if (email === 'admin@example.com' && password === '123456') {
    ...
}

固然,真實的網站會根據用戶輸入的Email和Password去數據庫查詢並判斷登陸是否成功,不過這須要涉及到Node.js環境如何操做數據庫,咱們後面再討論。

9.擴展

注意到ctx.render內部渲染模板時,Model對象並非傳入的model變量,而是:

Object.assign({}, ctx.state || {}, model || {})

這個小技巧是爲了擴展。

首先,model || {}確保了即便傳入undefined,model也會變爲默認值{}Object.assign()會把除第一個參數外的其餘參數的全部屬性複製到第一個參數中。第二個參數是ctx.state || {},這個目的是爲了能把一些公共的變量放入ctx.state並傳給View。

例如,某個middleware負責檢查用戶權限,它能夠把當前用戶放入ctx.state中:

app.use(async (ctx, next) => {
    var user = tryGetUserFromCookie(ctx.request);
    if (user) {
        ctx.state.user = user;
        await next();
    } else {
        ctx.response.status = 403;
    }
});

這樣就沒有必要在每一個Controller的async函數中都把user變量放入model中。

 

文章來源:廖雪峯的官方網站,全部的示例我都在本機運行過,能夠到http://bijian1013.iteye.com/blog/2425085下載。

koa2的官方文檔資料詳見http://www.koacn.com/#contexthttps://koa.bootcss.com/#

相關文章
相關標籤/搜索