自定義模型操作

? ? ? 目前為止,我們的進展非常順利,我們使用了?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:?"fred@flintstone.com"

????????}).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?文件中指定的參數是區分大小寫的,所以請確保您保持了一致的命名約定。大小寫差異是應用程序代碼中一種常見但不易察覺的錯誤來源。