一.koa2入門javascript
首先,咱們建立一個目錄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
,便可看到效果:
還能夠直接用命令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函數,並傳入ctx
和next
參數。
咱們能夠對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.url
,ctx.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編寫處理函數。
爲了處理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/hello/koa
用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處理。
如今,咱們已經能夠處理不一樣的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);
確保每一個函數功能很是簡單,一眼能看明白,是代碼可維護的關鍵。
最後,咱們把掃描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有幾個特別重要的問題須要考慮:
對特殊字符要轉義,避免受到XSS攻擊。好比,若是變量name
的值不是小明
,而是小明<script>...</script>
,模板引擎輸出的HTML到了瀏覽器,就會自動執行惡意JavaScript代碼。
對不一樣類型的變量要格式化,好比,貨幣須要變成12,345.00
這樣的格式,日期須要變成2016-01-01
這樣的格式。
模板還須要能執行一些簡單邏輯,好比,要按條件輸出內容,須要if實現以下輸出:
{{ name }}同窗, {% if score >= 90 %} 成績優秀,應該獎勵 {% elif score >=60 %} 成績良好,繼續努力 {% else %} 不及格,建議回家打屁股 {% endif %}
因此,咱們須要一個功能強大的模板引擎,來完成頁面輸出的功能。
咱們選擇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)
方法,正好傳入view
和model
兩個參數,並返回字符串。
建立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 <script>alert("小明")</script></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
定義了三個可編輯的塊,分別命名爲header
、body
和footer
。子模板能夠有選擇地對塊進行從新定義:
{% 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: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:
咱們定義一個async函數處理首頁URL/
:
async (ctx, next) => { ctx.render('index.html', { title: 'Welcome' }); }
注意到koa並無在ctx
對象上提供render
方法,這裏咱們假設應該這麼使用,這樣,咱們在編寫Controller的時候,最後一步調用ctx.render(view, model)
就完成了頁面輸出。
咱們再定義一個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:
在編寫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。
咱們來編寫一個處理靜態文件的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官網搜索並查看其文檔:
最後,這個middleware使用起來也很簡單,在app.js
里加一行代碼:
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
注意:也能夠去npm搜索能用於koa2的處理靜態文件的包並直接使用。
集成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能順帶處理靜態文件,不然,就必須手動配置一個反向代理服務器,這樣會致使開發環境很是複雜。
在編寫View的時候,很是有必要先編寫一個base.html
做爲骨架,其餘模板都繼承自base.html
,這樣,才能大大減小重複工做。
編寫HTML不在本教程的討論範圍以內。這裏咱們參考Bootstrap的官網簡單編寫了base.html
。
一切順利的話,這個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/
,能夠看到首頁內容:
直接在首頁登陸,若是輸入正確的Email和Password,進入登陸成功的頁面:
若是輸入的Email和Password不正確,進入登陸失敗的頁面:
怎麼判斷正確的Email和Password?目前咱們在signin.js
中是這麼判斷的:
if (email === 'admin@example.com' && password === '123456') { ... }
固然,真實的網站會根據用戶輸入的Email和Password去數據庫查詢並判斷登陸是否成功,不過這須要涉及到Node.js環境如何操做數據庫,咱們後面再討論。
注意到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/#context,https://koa.bootcss.com/#。