如何組織大型JavaScript應用中的代碼?
英文原文:Code Organization in Large AngularJS and JavaScript Applications
本文作者Cliff Meyers是一個前端工程師,熟悉HTML5、JavaScript、J2EE開發,他在開發過程中總結了自己在應對JavaScript應用越來越龐大情況下的文件結構,深得其他開發者認可。以下為CSDN編譯:
地板上堆放的衣服
首先,我們來看看angular-seed,它是AngularJS應用開發的官方入門項目,其文件結構是這樣的:
- css/
- img/
- js/
- app.js
- controllers.js
- directives.js
- filters.js
- services.js
- lib/
- partials/
看起來就像是把衣服按類型堆在地板上,一堆襪子、一堆內衣、一堆襯衫等等。你知道拐角的那堆襪子里有今天要穿的黑色羊毛襪,但你仍需要花上一段時間來尋找。
這種組織方式很凌亂。一旦你的代碼中存在6、7個甚至更多的控制器或者服務,文件管理就會變得難以處理——很難找到想要尋找的對象,源代碼控制中的文件也變更集變得難懂。
襪子抽屜
常見的JavaScript文件結構還有另一種形式,即按原型將文件分類。我們繼續用整理衣服來比喻——現在我們買了有很多抽屜的衣柜,打算將襪子放在其中一個抽屜里,內衣放在另一個抽屜,再把襯衫整齊地疊在第三個抽屜……
想象一下,我們正在開發一個簡單的電子商務網站,包括登陸流程、產品目錄以及購物車UI。同樣,我們將文件分為以下幾個原型:models(業務邏輯和狀態)、controllers以及services(HTTP/JSON端點加密),而按照Angular默認那樣非籠統地歸到“service”架構。因此我們的JavaScript目錄變成了這樣:
- controllers/
- LoginController.js
- RegistrationController.js
- ProductDetailController.js
- SearchResultsController.js
- directives.js
- filters.js
- models/
- CartModel.js
- ProductModel.js
- SearchResultsModel.js
- UserModel.js
- services/
- CartService.js
- UserService.js
- ProductService.js
不錯,現在已經可以通過樹形文件目錄或者IDE快捷鍵更方便地查找文件了,源代碼控制中的變更集(changeset)也能夠清楚地描述文件修改記錄。雖然已經獲得了極大的改進,但是仍有一定的局限性。
想象一下,你現在正在辦公室,突然發現明天有個商務出差,需要幾套干洗的衣服,因此給家里打電話告訴另一半把黑色和藍色的西裝交給清潔工,還有黑紋領帶配灰色襯衫、白襯衫配純黃領帶。如果你的另一半并不熟悉衣柜,又該如何從三條黃色的領帶中挑出你的正確需求?
模塊化
希望衣服的比喻沒有讓你覺得過于陳舊,下面舉一個實例:
- 你的搭檔是新來的開發者,他被要求去修補這個復雜應用中的一處bug。
- 他掃過這些文件夾,看到了controllers、models、services等文件夾整齊地排列著,但是他仍然不清楚對象間的依賴關系。
- 處于某些原因,他希望能夠重用部分代碼,這需要從各個文件夾中搜集相關文件,而且常常會遺漏某些文件夾中的對象。
信或不信,你確實很少會在新項目中重用很多代碼,但你很可能需要重用登陸系統這樣的整個模塊。所以,是不是按功能劃分文件會更好?下面的文件結構是以功能劃分后的應用結構:
- cart/
- CartModel.js
- CartService.js
- common/
- directives.js
- filters.js
- product/
- search/
- SearchResultsController.js
- SearchResultsModel.js
- ProductDetailController.js
- ProductModel.js
- ProductService.js
- search/
- user/
- LoginController.js
- RegistrationController.js
- UserModel.js
- UserService.js
雖然現實世界中有空間限制,難以隨意整理服裝,但是編程中類似的處理卻是零成本的。
現在即使是新來的開發者也能通過頂級文件夾的命名理解應用的功能,相同文件夾下的文件會存在互相依賴等關系,而且僅僅通過瀏覽文件組織結構就能輕易理解登錄、注冊等功能的原理。新的項目也可以通過復制粘貼來重用其中的代碼了。
使用AngularJS我們可以進一步將相關代碼組織為模塊:
var userModule = angular.module('userModule',[]); userModule.factory('userService', ['$http', function($http) { return new UserService($http); }]); userModule.factory('userModel', ['userService', function(userService) { return new UserModel(userService); }]); userModule.controller('loginController', ['$scope', 'userModel', LoginController]); userModule.controller('registrationController', ['$scope', 'userModel', RegistrationController]);
如果我們將UserModule.js文件放到user文件夾,它就成了這個模塊中使用到的對象的“manifest”,這也是適合RequireJS或者Browserify中放置某些加載指令的地方
如何處理通用代碼
每個應用都會有某些代碼廣泛使用在多個模塊中,我們常常使用名為“commom”或者“shared”的文件夾來存放這些功能代碼。又該如何處理這些通用代碼呢?
- 如果模塊中的對象需要直接訪問幾個“通用”對象,為這些對象提供幾個Facade(外觀模式)。這有助于減少每個對象的依賴者,而過多的關聯對象通常意味著糟糕的代碼結構。
- 如果“通用”模塊變得過于龐大,你需要將它按功能領域細分為多個子模塊。確保每個應用模塊只使用它需要的“通用”模塊,這即是SOLID中“接口隔離原則”的變種。
- 在根范圍($rootScope)添加實體,這樣子范圍也可以使用,適合多個控制器都依賴同一個對象(比如“PermissionsModel”)的情況。
- 在解耦兩個不明確互相引用的組件時,請使用事件。Angular中Scope對象的$emit、$broadcast以及$on方法使得這種方式變得現實。控制器能夠觸發一個事件來執行某些動作,然后再動作結束后收到相應地通知。