近日來我有幸主導了一個典型的web app開發。該項目從產品層次來說是個典型的CRUD應用,故而我毫不猶豫地采用了grunt + boilerplate + angularjs + bootstrap + D3 + requirejs 的架構來實現它。angularjs早在去年6月份我就有所接觸,將它應用在實驗室項目的個別頁面中,11月份在新浪的時候也將其推薦給了所在雲事業部項目組。項目組老大程輝等人都是很有技術敏感性的人,大膽地采納了我的建議,將之應用於原本使用dojo開發的項目前端模塊上。然而,由於前端模塊很大,並且已經用dojo開發了很長時間了,故而angularjs一開始也是應用在一些頁面和子模塊上,至我離開新浪之前還完成全部的重構。雖然之后我也做過一些angularjs的插件之類的事,但終究沒有用angularjs完整地搞一個大型項目的經驗。故而這次實驗室交予我一整個項目,且不管所得利益幾何,對於一個對前端有愛的人而言,這是一次從頭開始完整地進行angularjs項目實戰的機會,我怎么可能放過。期間種種我都想用博客記錄下來,以備后用。
開發大型javascript應用和做幾個demo頁面玩一玩最大的區別在於,要對一開始的文件組織和模塊划分要有更清晰的認識。這項工作前期花了我較多的時間去處理。故而第一篇文章就寫模塊划分和目錄組織,這方面本人經驗不多,不足之處敬請指出。
項目需求和資源評估
沒有什么模塊划分是必須的和通用的,模塊划分都是在對項目需求、手頭資源(員工資源、時間資源和物質資源)有充分的評估,同時參考大量范型的基礎上作出決定。所以先大致介紹一下我接手的項目需求。這是一個典型的B/S架構的呈報系統軟件。要求有用戶-服務台-辦事人員-系統管理員四級的角色架構,針對不同的角色,顯示不同界面,每種界面內含子頁面大約20個不等,子頁面互相有重復。需求要求系統有足夠的穩定性,一般的並發性。前端要求有設計感,足夠美觀,快速響應,對一些特定統計內容有相應的可視化呈現。不需支持IE6。項目成型后,作為軟件安裝部署,二次開發可能性較小。人力資源方面,加上我干活的有三個,其中前端熟練工一個(俺),有一定開發經驗的一個,編寫代碼一般可視化程序操作優良踏實肯干的一個。時間資源大約3人*60個工作日。
根據項目需求可知,項目從用戶角色可划分為四個大模塊:用戶、服務台、辦事人員、系統管理員。大部分頁面邏輯較為單一,易於划分。對於重復子頁面可另建立通用模塊。考慮到人力方面,2/3是非熟練工,美工設計人員基本沒有(本來我是,但是現在的工作量不允許我做這個了),工程量大時間緊,故而要求將設計的工作量降到最低。使用的技術和模塊划分要盡量易上手,減少重復勞動,提高並發效率。由於前端人員較多,后台較為簡單,故而整體架構上,后台只須提供REST風格的API,將工作重心前移,前台負責主要邏輯模塊的實現。
前端初始架構設計
在確認了需求和資源評估之后,本人接下來的工作是在github上遍覽使用了angularjs的各種項目,以及google相關文檔。其中github上的幾個project給我了我較大啟發,它們分別是:
- https://github.com/angular/angular-seed
- https://github.com/zhangdiwaa/angular-coffee-AMD-seed
- https://github.com/elsom25/angular-requirejs-html5boilerplate-seed
- https://github.com/mz121star/NJBlog (一個使用 Mongodb+Nodejs+express+angularjs寫的博客程序)
- https://github.com/zhangdiwaa/ng-grid(一個基於angularjs寫的grid插件)
根據這些參考內容,我設計了初始的技術路線,如圖所示:
簡要說明一下:
web server使用的是node.js。同時配以自動化工具grunt做一些端到端測試、代碼壓縮和合並之類的工作。
framwork當然用的是angularjs做MVVM框架。同時用了一些UI小插件,如angular-ui-bootstrap, angular-ui和我自己寫的表格插件angrid(https://github.com/zhangdiwaa/anGrid)。因為表格插件對於本項目來說比較重要,這里采用自己寫的表格插件主要是為了便於定制(想當年為了定制dojo的表格插件改得要吐)。在模塊划分上,一開始是模仿angular-seed的方式,就用一個myApp作為程序入口點,direcvtives、filters、service作為文件夾,存放對應的js文件。
基礎的工具集采用jquery以及bootstrap。bootstrap這個東西的好處就在於可以快速地讓不懂設計的人也能快速開發出能看的界面,搞定不挑剔的用戶足夠了。何況國內還有很多軟件系統的界面遠不如bootstrap好看。這里bootstrap我們沒有使用其開發包而是直接使用它的工程包,並且摒棄了less。這樣做因為考慮到需求我們並不需要對bootstrap做多少定制化處理。直接使用原始設計,將樣式設計的工作量降到最低。less這個東西,只在需要非常頻繁變更樣式設計的時候方顯本事,其他時候還不如直接用CSS。
其他工具集主要是前端數據可視化工具包D3,D3的強大無需說明。另外把boilerplate單獨說一下是因為這東西我確實很喜歡,它是制作符合html5+CSS3規范的網頁程序的快捷方式
初始架構設計的問題1:angularjs沒有諸如AMD或者CMD的機制。
當我企圖以現有方案繼續的時候,發現在從myApp划分子模塊開始就進行不下去了。問題如下:angularjs沒有諸如AMD或者CMD的機制。從myApp開始,myApp所有的依賴都必須先行加載。雖然angularjs可以使用路由按需加載模板,但是控制器卻要先行加載(如下面的代碼所示)。
$route.when('/view1', {template: 'partials/partial1.html', controller: MyCtrl1});
我的項目里有可分4個角色模塊(用戶、服務台、辦事人員、系統管理員),對於一個入口點壓力已經很大了,每個模塊還有20多個頁面,難道所有的控制器、以來模塊都要全部加載嗎?這顯然不科學。必須按需加載。
於是我在網上搜了半天,首先找到的方法如下:
$routeProvider.when('/phones', { templateUrl: 'partials/phone-list.html', controller: PhoneListCtrl, resolve: PhoneListCtrl.resolve})
function PhoneListCtrl($scope) {//本身不用管,該怎么弄怎么弄}
PhoneListCtrl.resolve = { delay: function($q) {var delay = $q.defer(), load = function(){ $.getScript('/js/xxxxx.js',function(){ delay.resolve();});}; load();return delay.promise;}}
這個辦法相當於使用angularjs內置的ajax方法,延遲加載了js文件。然而此法並不好用。在每個控制函數后面都寫個延遲加載屬性方法顯得很笨,當然,你可以把它寫到prototype去或者改angularjs源碼,但那樣做很有可能是給另外兩個非熟練開發人員埋地雷。總之延遲加載controller問題多多,尤其是在顯示的時候,具體你試試就知道了。
於是我想到了require.js這個AMD工具。之所以用require.js而不是國產的seajs並非我歧視國產(事實上我上一個項目就用的是seajs),而是因為require.js文檔豐富,在github上有很多例程,適合教其他兩個開發人員使用。除此以外require.js的AMD模式跟dojo的AMD按需加載方式如出一轍,因此使用過dojo開發的我對require.js更有好感。於是我最終還是使用了require.js做AMD方式的按需加載。
更進一步地,github上還有人基於RequireJS寫了在angular路由時動態加載templete,controller,和directives的插件。在模塊較少較清晰的情況下,這樣已經夠用了。 https://github.com/matys84pl/angularjs-requirejs-lazy-controllers
初始架構設計的問題2:我們的程序真的只需要一個app做入口點嗎?
在angularjs的教程中,總是使用一個入口模塊(通常是叫myApp)來組織整個程序。不論是angular-seed,phonecat這樣的示例程序還是像NJBlog這樣的比較大的應用都是一個入口點。但是,我們的項目跟這些都不同,是一個比他們都大的多、復雜的多的項目。由於本項目中的角色之間功能是嚴格分離的,所以產生了四大角色模塊用戶、服務台、辦事人員、系統管理員,它們之間是不會串門的。所以這4個模塊完全可以用4個app做入口點來組織程序。最后再加上一個登陸模塊,做判定和跳轉即可。使用requirejs最怕的問題就是依賴關系太多從而產生混亂,而這樣划分5個入口的方式決定了每個app的依賴都不是太多,整個程序的功能邏輯一目了然。
雖然事實上,采用一個入口點,然后通過內部機制判斷,從而在各個模塊之間跳轉也是可行的,但是那樣程序復雜度就要高的多,還要給另外兩個搭檔解釋都要解釋半天,所以還是算了吧。
初始架構設計的問題3:按模塊組織文件
一開始的時候,我的文件目錄完全是照書抄書仿照angular-seed進行組織的。考慮到文件較多較復雜,於是就設置了幾個direcvtives、filters、service作為文件夾,存放對應的js文件。於是文件目錄看起來就像下面這個樣子:
- 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
但是這樣真的好用嗎?
看起來文件排列的是很整齊,但是叫我的搭檔來看,他依然不清楚這些對象的依賴關系。尤其當要重用某些模塊的時候,他必須從各個文件夾中搜集相關文件,而且常常會遺漏某些文件夾中的對象。
事實上,在快速開發中確實很少會在新項目中重用很多代碼(重復代碼倒有很多),但很可能需要重用登陸系統這樣的整個模塊。
所以,不如按照功能模塊去組織文件夾。最終,我的目錄是這么排列的:
- build/(工程目錄)
- css/
- img/
- js/
- appAdmin/ (獨立模塊,以app名字開頭,各app模塊內容近似)
- controller/ (相關的子模板的controller.js存放在這里)
- directives/ (相關的directive.js存放在這里)
- admin_app.js (app模型定義和路由配置文件)
- admin_main.js (requirejs的入口和配置文件)
- admin_services.js (app的相關服務配置文件)
- appCustomer/
- appHelpdesk/
- appRepairer/
- appLogin/
- common/ (通用模塊庫)
- angular_filter/ (一些通用的過濾器)
- common_plugin.js (一些非基於angular的但比較重要組件,例如console plugin)
- utils/ (其他組件)
- appAdmin/ (獨立模塊,以app名字開頭,各app模塊內容近似)
- lib/ (包含所有第三方類庫)
- templete/ (子模板文件夾,其內容按模塊類型分類)
- common/
- tplAdmin/
- tplCustomer/
- tplHelpdesk/
- tplRepairer/
- 404.html
- admin.html (admin模塊的入口html)
- customer.html (customer模塊的入口html)
- helpdesk.html (helpdesk模塊的入口html)
- login.html (login模塊的入口html)
- index.html (程序的總入口點,用以根據配置跳轉到各個模塊入口)
- repairer.html (repairer模塊的入口html)
- node_modules/ (grunt)
- src/ (未經grunt處理的源文件)
- test/ (端到端測試)
- gruntfile.js (grunt)
- human.txt
- package.json (grunt)
- README.md
顯然,這里我將js和templete里面的文件都按模塊划分為一個個子文件夾,在build的根目錄下留下了數個模塊入口點。這樣已經足夠清晰了。
還有更為激進的做法,就是將屬於同一個模塊的templete、css、js全放在一個模塊目錄下,我上一個項目就是這么做的。但是上一個項目未能清晰地划分通用模塊和功能模塊,造成了一定混亂。我暫時不好比較這兩種划分的優劣。至少以目前的划分,已經足夠可用了。
作者信息:張迪,中國傳媒大學醬油男,主要研究領域為網絡存儲,數據可視化。有3年前端開發經驗。個人愛好游泳,世界歷史,美術插畫,電腦CG。