老web換新枝----Sails.js移動設備的全新生產力(五)

自定義模型操作

      目前爲止,我們的進展非常順利,我們使用了 Sails 的默認路由來訪問或修改模型實例。這些默認設置(包含在 Sails Blueprint API 中)負責我們期望從 Web 或移動應用程序獲得的基本的創建(create)、讀取(read)、更新(update)和刪除(delete)功能。但是所有開發生產 HTTP API 的開發人員都會告訴您,簡單的 CRUD 只有這點能耐。即使您從這裏着手,也需要能夠定製基本路由與控制器的映射。

Blueprint API 對您很有幫助,但最終您需要某個更強大、靈活和可自定義(或同時具備這三種特性)的工具來構建您的客戶和用戶想要的應用程序。對於大部分開發週期,都可以使用藍圖路由來建立原型,然後將此框架替換爲自定義的路由和關聯的控制器。

除了 CRUD 路由,一個常規 Sails.js 安裝中已經預先定義了其他一些控制器-路由組合。但是,從很大程度上講,您往往想創建自己的映射來獲得所需的行爲。

映射覆雜的查詢

       在上一篇教程中,您將我們最開始擁有的博客 API 擴展爲了一個更龐大的內容管理系統(CMS)後端。儘管您目前構想的是讓這個應用程序爲一個網絡博客提供支持,但它還可以用於其他用途。該 RESTful API 可供幾乎任何想要獲取並顯式博客文章或 RSS 提要的前端應用程序訪問,而且它允許搜索查詢。擴展該 API 後得到了多種新模型類型,分別是 Author、Entry 和 Comment。每個 Entry 擁有一個 Author 和Comment,這些類型也鏈接回它們引用的 Entry。

       切換成 CMS 後,越來越明確地表明您希望能夠設置 Sails 的默認路由所不支持的操作。假設您想能夠發出複雜的查詢(比如針對一個由特定作者在特定日期後編寫的,按給定條件進行組織的文章),或者獲取與一個特定標籤對應的所有文章。您可能需要提供的不是採用原生 JSON 的格式,而是採用兼容 RSS 或 Atom 的 XML 格式的文章。在輸入端,您可能希望添加一個導入功能,使 CMS 能夠通過 RSS 或 Atom 提要獲取並存儲完整的現有博客。

實際上,您可以做很多事情,而且許多事情都是 Sails 默認不支持的。

控制器的作用

       在傳統 MVC 模式中,控制器(controller)定義模型(model)與其視圖(view)之間的交互。當某個模型的數據發生更改時,控制器會確保附加到該模型的每個視圖都會相應地更新。用戶在視圖內執行某項操作時,控制器也會獲得通知。如果該操作必須更改某個模型,控制器會向所有受影響的視圖發出通知。從架構角度講,模型、控制器和視圖都是在服務器上定義的。視圖通常是某種形式的 HTML 模板(理想情況下包含極少的代碼);模型是域類或對象;控制器是路由背後的代碼塊。

       在 HTTP API 中,三個應用程序組件之間的關係類似但不等同於 MVC。不同於 MVC 架構,HTTP API 的模型、視圖和控制器通常並不都包含在同一個服務器上。具體地講,視圖通常位於服務器外,會是一個單頁 Web 應用程序或移動應用程序的形式。

在架構上,HTTP API 或多或少與 MVC 應用程序有些類似:

· 模型 是通過線路交換的工件。(HTTP API 使用了一種專爲簡化傳輸而設計的模型,該模型有時稱爲 ViewModel。)

· 控制器 是 API 的動詞形式;它們的存在是爲“執行”某個操作而不是“成爲”某個東西。

在 HTTP API 中,當一個視圖將 HTTP 請求傳到控制器端點時,該請求直接依靠控制器來執行。控制器獲取通過 URL 或請求正文傳入的數據,對一個或多個現有模型執行某個操作(或創建新模型),並生成響應返回給視圖來進行更新。控制器的響應通常是 API 開發人員定義的 HTTP 狀態代碼和 JSON 主體的組合。

理論介紹得已經足夠多了,現在讓我們開始實現控制吧。

文檔:目前,對於錯誤場景的含義,HTTP API 設計中還沒有標準。錯誤編碼 500 究竟是表明請求未被處理,還是表明它已被處理,但由於執行請求的邏輯中的內部錯誤而失敗了?所以應該始終顯式地文檔化您的 HTTP API 中的錯誤編碼的含義。

· 視圖 顯示了通過 HTTP API 收到的數據。

創建一個控制器

      開始使用 Sails 控制器的最簡單方法是,創建一個返回靜態數據的控制器。在本例中,您將創建一個簡單的控制器,它返回 CMS API 的一個用戶友好的版本。這將是一個運行迅速且容易使用的 API,業務客戶可使用它測試服務器是否在正常運行。客戶還能夠大體瞭解任何最近的更改,比如對 API 的升級,並相應地調整其 Web 或移動前端。

可通過兩種方式在 Sails 中創建新控制器:可以使用 sails 命令生成框架,或者可以讓這些文件爲您生成控制器。在後一種情況下,生成器使用 Sails 約定規則所定義的命名系統來識別和放置文件,這些文件通常是空的。對於第一個控制器,我們將會使用生成器:

~$ sails generate controller System

info: Created a new controller ("System") at api/controllers/SystemController.js!

     控制器位於您的 Sails 項目的 api/controllers 目錄中。根據約定,它們擁有 controller 後綴。如果您在控制器名稱後附加額外的描述符,Sails 會假設這些是要在控制器上執行的操作(方法)。通過提前指定操作,您可以節省一些步驟;否則默認生成器會生成一個空的控制器,如這裏所示:

   /**

   * SystemController

   *

   * @description :: Server-side logic for managing the System

   * @help        :: See http://sailsjs.org/#!/documentation/concepts/Controllers

   */

  module.exports = {

  };

     運行一個稍微不同的命令——sails generate controller System version——會留下更多工作讓您處理:控制器位於您的 Sails 項目的 api/controllers 目錄中。根據約定,它們擁有 controller 後綴。如果您在控制器名稱後附加額外的描述符,Sails 會假設這些是要在控制器上執行的操作(方法)。通過提前指定操作,您可以節省一些步驟;否則默認生成器會生成一個空的控制器,如這裏所示:

  /**

   * SystemController

   *

   * @description :: Server-side logic for managing Systems

   * @help        :: See http://sailsjs.org/#!/documentation/concepts/Controllers

   */

  module.exports = {

    /**

     * `SystemController.version()`

     */

    version: function (req, res) {

      return res.json({

        todo: 'version() is not implemented yet!'

      });

    }

  };

      添加命令會對搭建的方法端點進行佈局,但您可以看到,這些命令目前未執行太多工作,僅返回有幫助的錯誤消息。使用該生成器創建已建立框架的控制器,最初可能很有幫助。隨着時間的推移,許多開發人員最終僅在自己最喜歡用的文本編輯器中執行 File|New。如果您決定這麼做,則需要正確地命名該文件和端點(例如 foocontroller.js 中的 FooController),並手動編寫導出的函數。這些方式都沒有對錯,所以請使用最適合您的方式。

實現第一個簡單的控制器非常容易:

  module.exports = {

   /**

   * `SystemController.version()`

   */

  version: function (req, res) {

    return res.json({

      version: '0.1'

    });

  }

}",

綁定和調用控制器

接下來,您希望綁定您的控制器,然後調用它。綁定(Binding)將控制器映射到一個路由;調用(invoking)向該路由發出合適的 HTTP 請求。基於此控制器的文件名和所調用方法的名稱,此控制器的默認路由將是 /System/version。

就個人而言,我不喜歡將“system”放在 URL 中,而是更喜歡使用“/version”。Sails 允許將控制器的調用綁定到選擇的任何路由,所以我們將它設置爲綁定到 /version。

您可以在 Sails 路由表中創建一個條目來設置控制器路由,該路由表存儲在 config/routes.js 中。只要您創建一個 Sails 應用程序,就會生成這個默認文件。除去所有註釋後,此文件幾乎是空的:

odule.exports.routes = {

  /***************************************************************************

  *                                                                          *

  * Make the view located at `views/homepage.ejs` (or `views/homepage.jade`, *

  * etc. depending on your default view engine) your home page.              *

  *                                                                          *

  * (Alternatively, remove this and add an `index.html` file in your         *

  * `assets` directory)                                                      *

  *                                                                          *

  ***************************************************************************/

  '/': {

    view: 'homepage'

  }

};


大體上講,config/routes.js 包含一組路由(routes)和目標(targets)。路由是相對 URL,目標是您希望 Sails 調用的對象。默認路由是 / URL 模式,它調出 Sails 的默認主頁。如果您願意的話,還可以將此默認路由替換爲一個常規 HTML 頁面。這樣,該 HTML 將位於您的 assets 目錄中,就在我們將一直使用的 api 目錄旁邊。

升級 CMS 的 HTML 對於像 React.js 這樣的單頁應用程序框架可能是一種有趣的練習,但這裏的目的是添加一個 /version 路由,並讓它指向 SystemController.version 方法。爲此,您需要向 config.js 所導入的 JSON 對象添加額外的一行代碼:

module.exports.routes = {

  'get /version': 'SystemController.version'

};

Blueprint API 中的控制器

將此路由保存到 config.js 文件中後,向 /version 發出 HTTP GET 請求會發回您之前設置的相同 JSON 結果。

創建模型時,Sails 的工具會自動爲該模型創建一個控制器。所以,即使您沒有打算創建它們,您的每個方法也已經有一個控制器:AuthorController、EntryController 等。

爲了進一步熟悉 Sails 控制器和路由,我們假設您希望 AuthorController 持有一個方法,該方法返回您 CMS 中所有作者的簡歷的聚合列表。該實現非常簡單——只需要獲取所有 Authors,提取它們的簡歷,並傳回該列表:

  module.exports = {

  bios: function(req, res) {

    Author.find({})

      .then(function (authors) {

        console.log("authors = ",authors);

        var bs = [];

        authors.forEach(function (author) {

          bs.push({

            name: author.fullName,

            bio: author.bio

          });

        });

        res.json(bs);

      })

      .catch(function (err) {

        console.log(err);

        res.status(500)

          .json({ error: err });

      });

  }

};

測試狀態如果您使用了默認的 /author/bios 路由,您的 routes.js 中甚至不需要特殊條目。在這種情況下,默認條目就夠用了(如果您不這麼認爲,您知道如何更改它),所以我們暫時保留默認路由。

測試狀態

您可能發現種子控制器(seed controller)對一些測試很有用。這種控制器將數據庫初始化爲一種已知狀態,就象這樣:

module.exports = {

    run: function(req, res) {

        Author.create({

            fullName: "Fred Flintstone",

            bio: "Lives in Bedrock, blogs in cyberspace",

            username: "fredf",

            email: "[email protected]"

        }).exec(function (err, author) {

            Entry.create({

                title: "Hello",

                body: "Yabba dabba doo!",

                author: author

            }).exec(function (err, created) {

                Entry.create({

                    title: "Quit",

                    body: "Mr Slate is a jerk",

                    author: author.id

                }).exec(function (err, created) {

                    return res.send("Database seeded");

                });

            });

        });

    }

};

   在這種情況下,該已知狀態被綁定到控制器的默認路由 /seed/run。您還可以爲不同的測試和/或開發場景設置不同的種子方法(seed method)。對於所關注的路由,可使用 curl 命令將數據庫設置爲特定狀態。但需要確保您在生產代碼中禁用或刪除了這些路由。

管理控制器輸入:

現在,您已擁有一個沒有輸入的非常簡單的控制器。但是,控制器通常需要從調用方獲取輸入。有三種類型的控制器輸入:

1. 請求正文中發送的表單參數。這是通過 Web 接受輸入的傳統機制。

2. 通過請求正文中的 JSON 對象發送的輸入數據。此概念與表單參數相同,但發送的內容類型爲application/json,而不是 form/multipart-form-data。客戶端通常更容易生成輸入數據,而且服務器也更容易使用。

3. 通過參數指定並通過 URL 路由中的佔位符發送的輸入。請求某個特定作者的文章時,您通常希望在 URL 自身中傳遞作者標識符。一個示例是 /author/1/entries,其中的“1”是作者的唯一標識符。這樣,您就保留了博客文章包含在作者資源中的外觀,即使這些文章在物理上未與該作者存儲在一起。(示例應用程序就是如此,Entry 對象存儲在一個與 Author 對象不同的集合或表中。)

表單參數屬於傳統的 Express 樣式 request.getParam() 調用的領域,已在其他地方具有明確規定。而且 HTTP API 也不經常使用表單參數,所以我們暫時放棄該方法。第二種方法非常適合 CMS 應用程序。

獲取輸入

捕獲通過 JSON 對象發送的值通常很簡單,只需使用一個 request.body.field。如果輸入是一個位於 JSON 最高層級的數組,您則可以使用 request.body[idx].field。

首先,您將創建一個端點,它將返回 CMS 數據庫中所有 Entry 對象的 RSS XML 提要。(在實際的系統中,需要限制此數字來支持分頁模式,比如返回最近的 20 篇文章,但我們暫時保持簡單即可。)命名您的控制器(我在下面選擇了 FeedController),並在它之上放置一個 RSS 方法,使默認路由(/feed/rss)變得有意義:

var generateRSS = function(entries) {

  var rss = 

    '<rss version="2.0">' +

    '<channel>' + 

    '<title>SailsBlog</title>';

  // Items

  entries.forEach(function (entry) {

    rss += '<item>' +

'<title>' + entry.title + '</title>' +

      '<description>' + entry.body + '</description>' +

      '</item>';

  });

  // Closing  rss += '</channel>' +

    '</rss>';

  return rss;

}

module.exports = {

  rss: function (req, res) {

    Entry.find({})

      .then(function (entries) {

        var rss = generateRSS(entries);

        res.type("rss");

        res.status(200);

        res.send(rss);

      })

      .catch(function (err) {

        console.log(err);

        res.status(500)

          .json({ error: err });

      });

    return res;

  }

};

現在,這是一個很小的提要,但是,如果您想將提要限制到一個或多個特定作者,該怎麼辦?在這種情況下,您需要設置一個新路由(/author/{id}/rss)。新路由將獲取 URL 中傳遞的標識符,然後使用它限制查詢,僅查找給定作者編寫的文章。該 RSS 方法的剩餘部分基本相同。

看看該方法在代碼中的效果。首先,FeedController 獲取一個針對每個作者的 RSS 方法,就像之前一樣:

module.exports = {

  rss: // as before

  authorRss: function(req, res) {

    Entry.find({ 'author' : req.param("authorID") })

      .then(function (entries) {

        var rss = generateRSS(entries);

        res.type("rss");

        res.status(200);

        res.send(rss);

      })

      .catch(function (err) {

        console.log(err);

        res.status(500)

          .json({ error: err });

      });

   

    return res;

  }

};

不同的是上面是一個查詢,它現在被限制爲僅查找所有具有某個作者 ID 的 Entry 對象。請注意 HTTP 請求中包含的參數 authorID 的指令。authorID(在本例中爲傳入的 URL 模式的第三部分)的映射在routes.js 文件中指定,如下所示:

var generateRss = // as before

module.exports = {

  rss: // as before

  authorRss: function(req, res) {

    Entry.find({ 'author' : req.param("authorID") })

      .then(function (entries) {

        var rss = generateRSS(entries);

        res.type("rss");

        res.status(200);

        res.send(rss);

      })

      .catch(function (err) {

        console.log(err);

        res.status(500)

          .json({ error: err });

      });

    return res;

  }

};

routes 文件中指定的參數是區分大小寫的,所以請確保您保持了一致的命名約定。大小寫差異是應用程序代碼中一種常見但不易察覺的錯誤來源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章