Koa使用了ES6規範的generator和異步編程是一個更輕量級Web開發的框架,Koa 的先天優點在於 generator。因爲是我我的的分享交流,因此Node基礎、ES6標準、Web開發基礎以及Koa的"Hello World"程序都不在討論,但願各位小夥伴提出意見和指導。
PS:Koa 內核中沒有捆綁任何中間件,但不用擔憂,Koa 擁有極其強悍的拓展性,正文全部中間件均可以在npm官網下載安裝,但國內域名安裝會有一些限制,提供一個國內鏡像安裝方法,速度很是快,在直接npm模塊失敗的時候很是好用,使用npm --registry=http://registry.npmjs.org install XXXXX –XX 命令安裝,只須要在install後面加上要安裝的中間件名稱和相應的參數便可。 css
一鍵生成koa/koa2項目:
一、 npm install -g koa-generator 2、新建項目目錄 koa mytest (koa1項目) koa2 koa2test (koa2項目) 3、進入目錄 cd koa2test 4、安裝依賴 npm install 五、運行 npm start
一、Koa項目建立
我的認爲無論任何框架,Web項目搭建必需的幾個方面,頁面、中間件、路由、會話和存儲、日誌、靜態文件指定,以及錯誤的處理。固然,網站開發不止這些東西,還有許多主題,好比實時通信,搜索引擎架構,權限控制,郵件優先隊列,日誌記錄分析,對Web開發還剛剛入門屬於菜鳥級別,這裏就不作深刻的討論了。瞭解Express框架的小夥伴必定知道Express的部署過程,無論是經過express-generator生成仍是WebStorm等編譯器直接建立,它的目錄結構大概是這樣的:html
|——app.js |——bin |——node_modules |——package.json |——public |——routes |——views
*app.js,是程序啓動文件
*bin,存放執行程序
*node_modules,存放項目依賴庫
*package.json,是配置和一些相關信息
*public,存放靜態文件(css,js,img)
*routes,存放路由文件
*views,存放前臺頁面文件
這些結構基本包含了上述提到的Web項目搭建的要素,可是目前相似express-generator的Koa部署工具Koa-generator(非官方)並不完善而且我的測試存在些許錯誤。其實Koa-generator也是仿造上述express-generator生成的目錄,既然這樣還不如手動建立目錄來的爽快(generator-k是另外一款生成器,用上去感受還行),在根目錄新建app.js做爲程序的啓動文件,建立三個文件夾分別命名public、routes和views,最後新建package.json文件存放你的項目的一些信息。完成這些建立以後,用npm命令安裝Koa,這樣的話一個基本的Koa框架就搭建好了,很是的的輕量級,它的目錄結構以下:node
|——app.js |——node_modules |——public | |——img | |——css | |——js | |——routes | |——index.js | |——user.Js | |——views | |——_layout.html | |——index.html | |——package.json Koa項目運行:node --harmony app.js 必須加 --harmony ,這樣纔會支持 ES6 語法。
二、Koa日誌
日誌是項目error調試和平常維護的基本手段,Koa有日誌模塊Koa-logger,npm install Koa-logger後使用app.use(logger());命令程序就會在控制檯自動打印日誌,固然若是你對Koa-logger的風格不滿意或者想要看到更多得信息也能夠本身編輯代碼實現有本身風格的日誌打印。
例如:程序員
auto map route -> [get]/authority/saveAddUser/ auto map route -> [get]/authority/searchUserInfo/ auto map route -> [get]/authority/updateUser/ auto map route -> [get]/authority/deletedUser/ auto map route -> [get]/authority/getSelectValues/ auto map route -> [get]/authority/saveAuthority/
最後呢,若是有須要,要把日誌進行存儲。
三、Koa的錯誤處理
Koa 有 error 事件,當發生錯誤時,能夠經過該事件,對錯誤進行統一的處理。es6
var Koa = require('koa'); var app = Koa(); app.on('error', function(err,ctx){ console.log(err); }); app.listen(3000);
上面這段代碼在若是捕獲到錯誤,頁面會打印出 「Internal Server Error」 (這是Koa對錯誤的默認處理)。這個錯誤咱們在綜合監控系統中也常常見到,那麼咱們顯然沒法根據這條日誌獲得什麼信息web
TypeError: Cannot read property 'split' of undefined at Object.Home.index (d:\test\route\home.js:143:31) at GeneratorFunctionPrototype.next (native) at Object.dispatch (d:\test\node_modules\koa-router\lib\router.js:97:44) at GeneratorFunctionPrototype.next (native)
這些錯誤信息是怎麼報出來的的呢,實際上是Koa-onerror 中間件,它優化錯誤信息,根據這些錯誤信息就能更好的捕獲到錯誤。 Koa-onerror使用方法:redis
var onerror = require('Koa-onerror'); onerror(app);
四、Koa靜態文件指定
Koa靜態文件指定中間件Koa-static,npm install Koa-static以後就可使用Koa-static負責託管 Koa 應用內的靜態資源。映射了靜態文件目錄,引用的時候直接去該目錄下尋找資源,會減小一些消耗。(不知道講的準確不許確,只是我的的理解)指定public爲靜態文件目錄的代碼以下:sql
var staticServer = require('koa-static'); var path = require('path'); app.use(staticServer(path.join(__dirname,'public')));
五、ejs模板的使用
渲染頁面須要一種模板,這裏選擇風格接近html的ejs模板。npm install Koa-ejs後就能夠在Koa框架中使用ejs模版。mongodb
var render = require('koa-ejs'); render(app, { root: path.join(__dirname, 'views'), layout: '__layout', viewExt: 'html', cache: false, debug: true }); app.use(function *(){ yield this.render('index',{layout:false}); });
六、Koa路由設置
Koa個極簡的web框架,簡單到連路由模塊都沒有配備。本身手寫路由是這樣的:shell
app.use(function *(){ //我是首頁 if(this.path==='/'){ } });
使用更增強大的路由中間件,Koa中設置路由通常安裝Koa-router,Koa-router支持五種方法
router.get() router.post() router.put() router.del() router.patch()
GET方法舉例:
var app = require('koa')(); var Router = require('koa-router'); var myRouter = new Router(); myRouter.get('/', function *(next) { yield this.render('index',{layout:false}); }); app.use(myRouter.routes()); app.listen(3000);
Koa-router 擁有豐富的 api 細節,用好這些 api ,可讓頁面代碼更爲優雅與可維護。 接收query參數
http://localhost:3000/?a=1(條件) index.js var router = require('koa-router')(); router .get('/',function *(next){ console.log(this.query); yield this.render('index',{layout:false}); }) .get('/home',function *(ctx,next){ ctx.render('home'); }); //ctx爲Koa2.0中支持 ... ... module.exports = router; 控制檯打印: <-- GET /?a=1 { a: '1' } { a: '1' } 接收params參數 http://localhost:3000/users/123(參數) router.get('/user/:id', function *(next) { console.log(this.params.id); });
param() 用於封裝參數處理中間件,當訪問 /detail/:id 路由時,會先執行 param() 定義的 generator function 邏輯。函數的第一個是路由參數的值,next 是中間件流程關鍵標識變量。
yield next;
表示執行下一個中間件。
app.param('id',function *(id,next){ this.id = Number(id); if ( typeof this.id != 'number') return this.status = 404; yield next; }).get('/detail/:id', function *(next) { //我是詳情頁面 var id = this.id; //123 this.body = id; });
七、Koa中間件
Koa的中間件很像Express的中間件,也是對HTTP請求進行處理的函數,可是必須是一個Generator函數即 function *(){} 語法,否則會報錯。能夠這麼說,Nodejs的Web程序中任何請求和響應都是中間件在操做。
app .use(logger()) //日誌中間件 .use(serve(__dirname + '/public')) //靜態文件指定中間件 .use(router.routes()) //路由中間件 .use(router.allowedMethods()); //路由中間件
app.use 加載用於處理http請求的middleware(中間件),當一個請求來的時候,會依次被這些 middlewares處理。執行的順序是你定義的順序。中間件的執行順序規則是相似「棧」的結構,全部須要執行的中間件都被一個一個放入「棧」中,當沒有遇到next()的時候,「棧」裏邊的這些中間件被逆序執行。
app.use(function *(next){ this; // is the Context this.request; // is a Koa Request this.response; // is a Koa Response });
說明:
•this是上下文
•*表明es6裏的generator
http模型裏的請求和響應
•this.request
•this.response
app.use() 究竟發生了什麼難以想象的化學反應呢? 其實 app.use() 就幹了一件事,就是將中間件放入一個數組,真正執行邏輯的是:app.listen(3000); Koa 的 listen() 除了指定了 http 服務的端口號外,還會啓動 http server,等價於:
var http = require('http'); http.createServer(app.callback()).listen(3000);
後面這種繁瑣的形式有什麼用呢?
一個典型的場景是啓動 https 服務,默認 app.listen(); 是啓動 http 服務,啓動 https 服務就須要:
var https = require('https'); https.createServer(app.callback()).listen(3000);
一、異步流程控制
異步編程對 JavaScript 語言過重要。JavaScript 只有一根線程,若是沒有異步編程,根本無法用,非卡死不可。
之前,異步編程的方法,大概有下面四種。
回調函數
事件監聽
發佈/訂閱
Promise 對象
JavaScript 語言對異步編程的實現,就是回調函數。所謂回調函數,就是把任務的第二段單獨寫在一個函數裏面,等到從新執行這個任務的時候,就直接調用這個函數。它的英語名字 callback,直譯過來就是"從新調用"。 讀取文件進行處理,是這樣寫的。
fs.readFile('/etc/passwd', function (err, data) { if (err) throw err; console.log(data); });
上面代碼中,readFile 函數的第二個參數,就是回調函數,也就是任務的第二段。等到操做系統返回了 /etc/passwd 這個文件之後,回調函數纔會執行。回調函數自己並無問題,它的問題出如今多個回調函數嵌套。假定讀取A文件以後,再讀取B文件,代碼以下。
fs.readFile(fileA, function (err, data) { fs.readFile(fileB, function (err, data) { // ... }); });
不難想象,若是依次讀取多個文件,就會出現多重嵌套。代碼不是縱向發展,而是橫向發展,很快就會亂成一團,沒法管理。這種狀況就稱爲"回調函數噩夢"(callback hell)。Promise就是爲了解決這個問題而提出的。它不是新的語法功能,而是一種新的寫法,容許將回調函數的橫向加載,改爲縱向加載。採用Promise,連續讀取多個文件,寫法以下。
var readFile = require('fs-readfile-promise'); readFile(fileA) .then(function(data){ console.log(data.toString()); }) .then(function(){ return readFile(fileB); }) .then(function(data){ console.log(data.toString()); }) .catch(function(err) { console.log(err); });
上面代碼中,我使用了 fs-readfile-promise 模塊,它的做用就是返回一個 Promise 版本的 readFile 函數。Promise 提供 then 方法加載回調函數,catch方法捕捉執行過程當中拋出的錯誤。能夠看到,Promise 的寫法只是回調函數的改進,使用then方法之後,異步任務的兩段執行看得更清楚了,除此之外,並沒有新意。
Promise 的最大問題是代碼冗餘,原來的任務被Promise 包裝了一下,無論什麼操做,一眼看去都是一堆 then,原來的語義變得很不清楚。
那麼,有沒有更好的寫法呢?
ECMAScript 6 (簡稱 ES6 )做爲下一代 JavaScript 語言,將 JavaScript 異步編程帶入了一個全新的階段。異步編程的語法目標,就是怎樣讓它更像同步編程。
Koa 的先天優點在於 generator。
generator指的是
function* xxx(){ }
是es6裏的寫法。
var r = 3; function* infinite_ap(a) { for( var i = 0; i < 3 ; i++) { a = a + r ; yield a; } } var sum = infinite_ap(5); console.log(sum.next()); // returns { value : 8, done : false } console.log(sum.next()); // returns { value : 11, done: false } console.log(sum.next()); // returns { value : 14, done: false } console.log(sum.next()); //return { value: undefined, done: true }
yield語句就是暫停標誌,next方法遇到yield,就會暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回對象的value屬性的值。當下一次調用next方法時,再繼續往下執行,直到遇到下一個yield語句。若是沒有再遇到新的yield語句,就一直運行到函數結束,將return語句後面的表達式的值,做爲value屬性的值,若是該函數沒有return語句,則value屬性的值爲undefined。當第一次調用 sum.next() 時 返回的a變量值是5 + 3,同理第二次調用 sum.next() ,a變量值是8 +3,知道循環執行結束,返回done:true標識。你們有沒有發現個問題,Koa 中 generator 的用法與上述 demo 演示的用法有很是大得差別,那是由於 Koa 中的 generator 使用了 co 進行了封裝。
二、co的使用
Ps:(這裏只是簡單介紹,後續能夠做爲一個專題來說)
co 函數庫是著名程序員 TJ Holowaychuk 於2013年6月發佈的一個小工具,用於 Generator 函數的自動執行。
好比,有一個 Generator 函數,用於依次讀取兩個文件。
var gen = function* (){ var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
co 函數庫可讓你不用編寫 Generator 函數的執行器。
var co = require('co'); co(gen);
上面代碼中,Generator 函數只要傳入 co 函數,就會自動執行。
co 函數返回一個 Promise 對象,所以能夠用 then 方法添加回調函數。
co(gen).then(function (){ console.log('Generator 函數執行完成'); })
上面代碼中,等到 Generator 函數執行結束,就會輸出一行提示。
爲何 co 能夠自動執行 Generator 函數?
前面文章說過,Generator 函數就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。
兩種方法能夠作到這一點。
(1)回調函數。將異步操做包裝成 Thunk 函數,在回調函數裏面交回執行權。
(2)Promise 對象。將異步操做包裝成 Promise 對象,用 then 方法交回執行權。
co 函數庫其實就是將兩種自動執行器(Thunk 函數和 Promise 對象),包裝成一個庫。使用 co 的前提條件是,Generator 函數的 yield 命令後面,只能是 Thunk 函數或 Promise 對象。
參考:http://www.ruanyifeng.com/blog/2015/05/co.html
三、Koa 中間件機制實現原理
使用 Koa 的同窗必定會有以下疑問:
經過實現簡單的 Koa 框架(剝離除中間件外全部的邏輯)來解答上述問題,這個框架的名字叫 SimpleKoa:
var co = require('co'); function SimpleKoa(){ this.middlewares = []; } SimpleKoa.prototype = { //注入箇中間件 use: function(gf){ this.middlewares.push(gf); }, //執行中間件 listen: function(){ this._run(); }, _run: function(){ var ctx = this; var middlewares = ctx.middlewares; return co(function *(){ var prev = null; var i = middlewares.length; //從最後一箇中間件到第一個中間件的順序開始遍歷 while (i--) { //實際Koa的ctx應該指向server的上下文,這裏作了簡化 //prev 將前面一箇中間件傳遞給當前中間件 prev = middlewares[i].call(ctx, prev); } //執行第一個中間件 yield prev; })(); } };
寫個 demo 印證下中間件執行順序:
var app = new SimpleKoa(); app.use(function *(next){ this.body = '1'; yield next; this.body += '5'; console.log(this.body); }); app.use(function *(next){ this.body += '2'; yield next; this.body += '4'; }); app.use(function *(next){ this.body += '3'; }); app.listen();
執行後控制檯輸出:123456,對照 Koa 中間件執行順序,徹底一致!寥寥幾行代碼,咱們就實現了 Koa 的中間件機制!這就是 co 的魔力。
一、Koa中的cookie和session(後續詳細講解)
web應用程序都離不開cookie和session的使用,是由於Http是一種無狀態性的協議。保存用戶狀態信息的一種方法或手段,Session 與 Cookie 的做用都是爲了保持訪問用戶與後端服務器的交互狀態。
二、Koa中nosql(後續技術分享會詳細講解)
mongodb是一個基於文檔的非關係型數據庫,全部數據是從磁盤上進行讀寫的,其優點在於查詢功能比較強大,能存儲海量數據。
redis是內存型數據庫,數據保存在內存中,經過tcp直接存取,優點是速度快,併發高,缺點是數據類型有限,查詢功能不強,通常用做緩存。它由C語言實現的,與 NodeJS工做原理近似,一樣以單線程異步的方式工做,先讀寫內存再異步同步到磁盤,讀寫速度上比MongoDB有巨大的提高,當併發達到必定程度時,便可考慮使用Redis來緩存數據和持久化Session。
var mongoose = require('mongoose'); // 引入 mongoose 模塊 mongoose.connect('mongodb://localhost/blog'); // 而後鏈接對應的數據庫:mongodb://localhost/test // 其中,前面那個 mongodb 是 protocol scheme 的名稱;localhost 是 mongod 所在的地址; // 端口號省略則默認鏈接 27017;blog是數據庫的名稱 // mongodb 中不須要創建數據庫,當你須要鏈接的數據庫不存在時,會自動建立一個出來。 module.exports = mongoose; // 導出 mongoose 模塊 var mongoose = require('../modules/db'); // 引入 mongoose 模塊 var User = mongoose.model('User',{ name: {type: String, match: /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/}, password: String }); //建立了一個名爲 User 的 model var user1 = new User({name:'12345@qqqqqq.com'}); user1.password = 'a5201314'; user1.save(function(err){ if(err){ console.log("save error"); } });