动态

详情 返回 返回

Koa1技術分享 - 动态 详情

寫在前面

  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後面加上要安裝的中間件名稱和相應的參數即可。

一、使用Koa搭建Web項目流程

1、Koa項目創建
  個人認為不管任何框架,Web項目搭建必需的幾個方面,頁面、中間件、路由、會話和存儲、日誌、靜態文件指定,以及錯誤的處理。當然,網站開發不止這些東西,還有許多主題,比如實時通訊,搜索引擎架構,權限控制,郵件優先隊列,日誌記錄分析,對Web開發還剛剛入門屬於菜鳥級別,這裏就不做深入的討論了。瞭解Express框架的小夥伴一定知道Express的部署過程,不管是通過express-generator生成還是WebStorm等編譯器直接創建,它的目錄結構大概是這樣的:

|——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框架就搭建好了,非常的的輕量級,它的目錄結構如下:

    |——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 語法。

2、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/

  最後呢,如果有需要,要把日誌進行存儲。
3、Koa的錯誤處理
  Koa 有 error 事件,當發生錯誤時,可以通過該事件,對錯誤進行統一的處理。

var Koa = require('koa');
var app = Koa();
app.on('error', function(err,ctx){
    console.log(err);
});   
app.listen(3000);

  上面這段代碼在如果捕獲到錯誤,頁面會打印出 “Internal Server Error” (這是Koa對錯誤的默認處理)。這個錯誤我們在綜合監控系統中也經常見到,那麼我們顯然無法根據這條日誌得到什麼信息

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使用方法:

    var onerror = require('Koa-onerror');
    onerror(app);

4、Koa靜態文件指定
  Koa靜態文件指定中間件Koa-static,npm install Koa-static之後就可以使用Koa-static負責託管 Koa 應用內的靜態資源。映射了靜態文件目錄,引用的時候直接去該目錄下尋找資源,會減少一些消耗。(不知道講的準確不準確,只是個人的理解)指定public為靜態文件目錄的代碼如下:

    var staticServer = require('koa-static');
    var path = require('path');
    app.use(staticServer(path.join(__dirname,'public')));

5、ejs模板的使用
  渲染頁面需要一種模板,這裏選擇風格接近html的ejs模板。npm install Koa-ejs後就可以在Koa框架中使用ejs模版。

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

6、Koa路由設置
  Koa個極簡的web框架,簡單到連路由模塊都沒有配備。自己手寫路由是這樣的:

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

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

二、異步編程

1、異步流程控制
  異步編程對 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 進行了封裝。
2、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
3、Koa 中間件機制實現原理
使用 Koa 的同學一定會有如下疑問:

  1. Koa 的中間件機制是如何實現?
  2. 為什麼中間件必須是 generator function?
  3. next 實參指向是什麼?為什麼可以通過 yield next 可以執行下一個中間件?
  4. 為什麼中間件從上到下執行完後,可以從下到上執行 yield next 後的邏輯?

  通過實現簡單的 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中涉及但本次沒有講的問題

1、Koa中的cookie和session(後續詳細講解)
  web應用程序都離不開cookie和session的使用,是因為Http是一種無狀態性的協議。保存用户狀態信息的一種方法或手段,Session 與 Cookie 的作用都是為了保持訪問用户與後端服務器的交互狀態。
2、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");
         }
      });

Add a new 评论

Some HTML is okay.