Angular開發“云平臺控制臺”的實踐總結

來自: https://segmentfault.com/a/1190000004525913

前言

筆者目前在互聯網公司負責開發私有云平臺。云平臺控制臺,是一個典型的管控CRUD系統,用于管理各種IaaS資源。為了讓前端能達到仿客戶端體驗,同時保障代碼架構清晰規范,易維護,最終我們選擇了Angular(1.X)`作為云平臺控制臺的前端框架。本文主要圍繞Angular,介紹我們在開發控制臺過程中的點點滴滴。

1 為什么選擇Angular

1.1 輕松構建SPA(single page application,單頁面應用)

可以說,這是我們最終選擇了Angular的重要原因,如果你希望構建一個結構清晰,可維護,開發效率高,體驗好的單頁應用,Angular是相當不錯的一個框架。

單頁面應用的魅力

什么是單頁面應用?Single Page Application指一種基于web的應用或者網站,頁面永遠都是局部更新元素,而不是整頁刷新。當用戶點擊某個菜單或者按鍵時,不會跳轉到其他的頁面,前端會從后端獲取對應頁面的數據而不是html,之后在頁面中需要更新內容的地方,局部動態刷新,而如果是多頁網站,當用戶訪問不同的頁面時,服務器會直接返回一個html,然后瀏覽器直接將這個html展現給用戶。現在大部分云控制臺,都是單頁應用架構,單頁應用能帶來一種更近似客戶端,而不是網頁的體驗。單頁面應用網站,在體驗方面,具有如下優點:

  1. 做“頁面跳轉”時,永遠都是局部動態刷新,用戶不會感覺整個屏幕閃了一下,而是需要變化的區域做了局部刷新。例如兩個不同的頁面,假設頁面元素都是一樣的,只是元素中的文字不一樣(例如每個頁面都有一個面包屑,一排按鈕及一個表格,這幾個元素的布局也是一樣的),當用戶跳轉到另外一個頁面時,會看到整個頁面并沒有重新渲染,只是文字發生了變化。簡單地說,這有點類似你在使用一個app,永遠都是局部發生變化,你見過哪個app,當你點擊到不同的功能視圖時,整個屏幕會白屏閃一下的么?

  2. URL可收藏,可回退。如果瀏覽器的url一直不變,那還不能稱為真正的單頁應用。不同的模塊,不同的資源頁面,對應的url是不一樣的。當用戶跳轉到另外的功能時,會發現url會變成對應的url;通過回退鍵,也可以回到之前瀏覽的頁面。這個特性有什么用呢?如果url不會隨著功能而變化,當用戶刷新當前頁面時,就會回到之前的默認頁面,而不是預期的當前功能頁面,同樣的,url也不具備可收藏性,因為點開之前收藏的url,永遠都是網站的默認頁面。這對一些強交互的管控系統來說,體驗是不好的。

  3. 功能切換時快速流暢。快速流暢主要因為兩個原因:1、頁面都是局部刷新,從用戶感官來說更快;2、和后臺通信的內容,都是數據,而不是頁面模板,請求量更少;而傳統的網站,在訪問不同頁面時,服務端返回的是html,體積更大,而且還需要一直重復加載js、css文件。

  4. 因為網站的單頁化,可以更好地使用一些全局類的交互(做頁面切換時,可以一直保持不變的交互)。例如,頁面上需要顯示某次耗時操作的進度,例如上傳文件進度,耗時操作的當前狀態等,你可以在頁面最右側固定顯示進度。當用戶訪問單頁應用時,他會明白,當他點擊其他模塊時,這個右側的通知欄不會消失,會固定顯示。而如果是多頁網站,用戶則會困惑,擔心自己跳轉到其他頁面后,這個進度通知就會消失。

為什么Angular是開發單頁應用的利器

  1. SPA對于代碼分層,結構清晰有更高的要求,而Angular是一個MV**框架,其自身的約定,減少了我們寫出“一鍋粥”代碼的可能性(在下面討論“編寫更易維護的代碼”時會詳述)

  2. Angular的著名第三方組件 ui router ,是一個控制頁面路由的組件,它支持我們快速搭建單頁應用(Angular本身的路由功能也可以,但功能會稍微弱一些)

1.2 編寫更易維護的代碼

很多人經常會抱怨,不同水平的人湊在一起寫js,到最后項目經常就是一鍋粥,同一個js文件里面,各種各樣的邏輯都混在一起,要增刪一個功能,都簡直是惡夢。無規矩不成方圓。作為一個框架,Angular無疑能大大改善這種狀況,使得項目整體的分層明了,職責清晰。

關注點分離

關注點分離是Angular的一大特點。所謂關注點分離,指的是各個邏輯層職責清晰,例如,當你需要修改甚至替換展現層時,你無需去關注業務層是怎么實現的。在Angular中,服務層(Ajax 請求)- 業務層(Controller)- 展現層(HTML 模板)- 交互層(animation)這些都有對應的基礎組件。不同組件職責不同,你也很難將本屬于B組件的職責放到A組件上去實現。舉幾個例子:

  1. html及controller需要協同工作,但職責分明。視圖、交互層面的邏輯,只能放到html模板中,controller只能用于數據初始化,它沒有辦法去操作dom元素(不用jquery的話)。這一點非常重要,傳統的js代碼,經常會出現的情況,就是js里面會有大量dom操作的邏輯,同時還有大量數據操作相關的邏輯,這些邏輯耦合到一起,當需要單獨重構數據層或者視圖層時,都會捉襟見肘,同時,由于代碼量的迅速膨脹,維護起來也會很麻煩。

  2. 你無法將后臺通信邏輯放到controller中實現,而要放到factory中。后臺通信邏輯,一般要做成公用的。而由于controller之間是不能相互調用的,所以你也不可能將后臺通信邏輯放到其中一個controller,然后其他controller來調用這個controller暴露的接口。你唯一的辦法,就是將后臺通信邏輯放到factory或者service中。

  3. filter及directive看似都可以用于數據轉換,但實則不同。由于filter只能做數據格式化,不支持引入模板,所以公用的ui交互,涉及到dom元素或者需要引入html模板時時,你也只能通過directive來實現。

綜上所述,Angular項目,其展現層、交互層的邏輯,都是在html或者指令中,服務層(后臺通信),只適合出現在facoty(service)中,業務層,只能由controller來負責。這樣每層的邏輯都是輕薄的,而不是糾結在一起。如果你只是要優化展示邏輯,那你改改html就可以了,不用去管controller是怎么寫的。這一點我們有親身體會。項目開發過程中,我們重構了視覺效果,所有的html都要重寫。但是,在重構時,我們的controller、后臺通信(factory),filter基本都不用改,只要改html就行了。而如果項目是用jquery寫的,顯然是不可能做到這樣的,你需要重新為新的html增加一些可供jquery選擇器使用的class或id,然后你需要在js里面綁定事件,根據新的html、css的class等寫新的交互效果,而在Angular上,這些事情,有些不用做了(例如為了jquery為html元素新增class,id、在js中綁定事件),有些(例如交互效果)則只要改html就行,而不是改js。

Angular為我們省去的代碼量

代碼臃腫、繁多也是JS代碼混亂,難組織的原因之一。因此,實現同樣的功能,代碼量越少,抽象程度越高,在某種程度上意味著項目更方便維護。而能減少代碼量,也是Angular被推崇的一大優點。讓我們來看看,它是如何減少了代碼量:

  1. 首先,作為一個大而全的框架(雙刃劍,有利有弊),Angular提供的諸多特性,使我們可以更專注于業務代碼的編寫。

  2. 其次,Angular雙向數據綁定的特性,將我們從大量的綁定代碼中解放出來。和jquery對比,Angular不用為了選擇某個元素,而刻意為htm加上一些跟樣式無關的class,id;不用寫一堆從html元素中取值,設值的代碼;不用在js代碼中綁定事件;不用在js值發生變化時寫代碼去更新視圖html顯示的值。雙向數據綁定,讓我們告別很多簡單無趣的綁定事件、綁定值的代碼。

  3. directive、filter、factory等等,天然的就是一個個可以復用的組件,減少了冗余重復代碼。一些需要公用的邏輯,如果放在controller中,都會相當別扭,就這樣被Angular“逼著”,把公用邏輯都放到directive、filter、factory中去。

2 開發心得總結

2.1 我理解的Angular基礎組件

化繁為簡,幾大基礎組件的使用場景

首先我們需要理清Angular幾個組件的使用場景。Angular的一個毛病,就是新概念,新特性太多,新手一下子要了解這么多,學習曲線略陡。為了幫助大家理解,總結下我理解的幾大組件使用場景。

  1. 請求資源與數據緩存的東西放進Factory。Factory、Service本質上都是Provider的語法糖,兩者只是使用方式有所不同,建議大家直接都用Factory,免得兩個概念糾纏不清。

  2. 數據需要格式化的東西用filter處理。例如把status值轉化為中文值,把時間戳轉成時間字符串之類。

  3. 需要公用的dom操作,放在指令中去寫。另外,如果需要引入jquery組件,也可以寫個指令把jquery組件初始化代碼放進去。

  4. controller與視圖按照一對一的關系維護,在controller內主要初始化scope對象與在scope上添加方法(行為),為ViewModel做賦值。其他所有過程都不應該出現在 Controller 中。controller中不應該出現和頁面展示、交互相關的代碼。例如展示隱藏某些元素之類,這些應該是html模板或者指令負責。代碼越薄越好。

  5. 全局常量值放到constant

指令(directive)魔法

指令這個特性,用“魔法”一詞來形容它,都不為過。

解決的痛點

一言以蔽之,指令提供了一套前端組件化的方法及約定,這使得編寫,使用ui組件更加方便了。相對于jquery,它解決了以下痛點:

  1. 動態生成了html元素后,不用再手動去為其加上js特性。舉個栗子:html原生的checkbox框比較丑,在jquery時代,可以將checkbox替換成自定義的效果,如果是頁面一開始就有的checkbox,我們可以在 document.ready 的時候調用自定義checkbox的初始化方法。但是,如果這個checkbox是動態生成的,在每個動態生成checkbox的地方,我們都得去調用checkbox的初始化方法,相當麻煩。但用了Angular的指令,就不會有這個問題了,只要在模板的chceckbox中加上指令,不管這個模板是動態變化的還是靜態的,無需通過業務代碼來逐個調用初始化方法,呈現給用戶的,就已經是替換后的checkbox效果。

  2. 一個組件的html和js,是一個整體,而不是割裂的。基于jquery的ui組件,其引入方法,不少是這樣的,首先,要求你自己copy一段html,然后再調用初始化方法。而指令則支持定義對應的模板html,用戶在引入時,可能只要寫一個指令標簽,就會自動生成N行的html及綁定對應的js效果。當然,理論上jquery也能做到這樣,但是會比Angular的實現麻煩許多。

  3. 應用、移除ui特性時方便直觀。假設有這么一個需求,給一個普通輸入框增加輸入限制,只能輸入特定字符(如字母數字),寫好對應指令,只要給這個input輸入框加上這個指令標簽,就能馬上應用這個特性,之后要移除,只要把標簽去掉就好。相比之下,jquery就會麻煩多。jquery下,一般是通過元素選擇器來綁定js效果。應用時,你需要考慮給對應的輸入框指定一個合適的元素選擇器。移除特性時,你要考慮:1、有可能你在多個地方都加了相同的初始化代碼,這些js都需要移除;如果元素選擇器用的是class,得考慮是不是其他輸入框也有這個class,如果是,那么移除代碼時也會影響到其他輸入框。

技巧

  1. 如果你需要引入jquery組件,你可以寫一個指令,然后在其中初始化該組件。

  2. 要注意require參數中的值是駝峰的,在html中就得轉成對應的中劃線命名,例如有require參數phoneKey,那么html中應為phone-key="xxx"。雖然這個道理很淺顯,但經常一不小心就會弄錯了,然后發現在指令內部怎么著都拿不到require參數。

  3. 如果你在link中加了elm.bind('click'),當click回調函數中,作用域的值發生變化,記得調用scope.$apply(),否則值變化不會生效

2.2 文件、目錄約定

目錄結構

第三方庫、css、圖片放置到哪個目錄,不在本文討論范圍,這里略過。需要進一步說明的,是業務代碼目錄。

.
├── pages
|   ├── common
|   |   |── js
|   |   |── layout
|   |   └── template
|   ├── 模塊A,例如如firewall(云平臺中的防火墻模塊)
|   |   |── a.main.ctrl.js
|   |   |── a.main.html
|   |   |── a.create.ctrl.js
|   |   |── a.create.box.html
|   |   |── a.svr.js
|   |   └── a.fil.js
|   └── 模塊B,例如如disk(云平臺中的硬盤模塊)
└── index.html

我們將系統自身的js、html模板都放在pages目錄,其中

  • 子目錄common:放置公用的js及模板

  • 其他子目錄,以功能模塊名作為目錄名,然后將這個模塊相關的js及模板放在其中。這樣開發同個模塊功能時,可以方便地在html及對應js之間切換。有些代碼規范可能還會建議在模塊這一級目錄下,再根據Angular的幾大組件controller,filter,service等,創建不同的子目錄,例如模塊A/controller,模塊A/service之類,我們則將所有js及html放在同一級,這樣做主要有幾個原因:

  1. 我們的項目,平均每個模塊只有10個文件,特別是每個模塊一般只有一個filter及service,為了這一個文件創建一個目錄,顯得多此一舉。

  2. 通過文件名,已經可以很方便地區分不同的js組件類型及html模板類型,同時,由于IDE一般會按照文件名字母排序,所以相同功能的js及html會挨在一起,查找對應的模板或js代碼會方便很多。

文件名約定

這個約定對于Angular來說,特別重要。具體的約定是:

模塊名.功能.組件類型.文件后綴(如firewall.create.ctrl.js)

例如,為“防火墻”模塊開發“創建防火墻”的功能,它的controller,對應的js為:firewall.create.ctrl.js,對應的html模板為:firewall.create.ctrl.html。為了文件名書寫的方便,定義了組件的簡寫

  • controller -> ctrl

  • factory,service -> svr

  • filter -> fil

  • directive -> dire

這樣約定有兩個顯而易見的優點:

  1. 通過文件名,就能知道對應模塊、Angular基本組件類型、是模板html還是js

  2. 相同功能的js及html,會挨在一起(如果IDE是按照文件命名排序)

2.3 與后端服務器通信

根據后臺接口規范,結合Angular自身能力,我們做了一些封裝,使接口請求邏輯變得非常簡單。具體問題具體分析,先看看我們的后臺接口響應的標準格式是怎樣的,前端會按照這個接口返回格式做一些定制:

{
    errcode:0,
    errmsg:"",
    data:[
       {
            id:"test",
            name:"test"    
        }
    ]
}

我們項目服務端的標準響應是一個json,通過errcode描述此次請求的結果碼,通過errmsg描述出錯的原因(假如請求出錯的話),通過data返回正常數據。

出錯處理

當接口返回的errno!=0時,說明接口返回異常(系統異常或用戶輸入錯誤),這時我們希望能彈框提示用戶“出錯了”。顯然,如果在每個接口請求邏輯中,都去寫這個邏輯,會非常累贅,所幸Angular提供了攔截器的功能,我們只要寫一個攔截器,就可以對所有的異常返回做統一處理。

  • 首先,我們需要 顯式地拋出http錯誤 。因為當后臺邏輯出錯或者用戶輸入參數有誤時,返回的HTTP狀態碼都是200(這只是我們項目的約定),Angular并不會認為200是出錯的情況,因此,我們需要做點小動作。

         $httpProvider.defaults.transformResponse.push(function (responseData) {
               if (responseData.errno != 0) {
                   throw responseData;
               }
               ……
         });

Angular的 $httpProvider.defaults.transformResponse.push (下面簡稱 transformResponse )函數,可以統一處理所有的http響應。在這里,我們就通過它捕獲了所有errno!=0的請求,并往外拋一個exception

  • 接著,我們需要在 $httpProvider.interceptors 捕獲這個異常并彈框,代碼如下

       $httpProvider.interceptors.push(function () {
           return {
               responseError: function (response) {
                       if (response) {
                            if (response.hasOwnProperty("errmsg")) {
                               if (response.errno > 0) {
                                   alert(response.errmsg);
                               } else {
                                   alert("系統維護中,請稍候重試");
                               }
    
                           }
                           else {
                               if (response.status == 404) {
                                   alert("抱歉,后臺服務出錯,找不到對應的接口");
                               }
                               else {
                                   alert("抱歉,后臺服務出錯");
                               }
                           }
                       }
               }
           }
       });

有些人可能會問,為啥不直接在一開始的 transformResponse 函數中寫錯誤處理邏輯呢?這是因為,某些異常情況,并不會調用 transformResponse 邏輯,例如,當url不存在時,web容器默認返回的404'頁面',或者當程序出錯時,系統代碼未處理這個錯誤,web容器會返回默認的500'頁面'',這時均會直接進入interceptors的responseError中。因此,為了覆蓋所有的異常情況,需要在 transformResponse 中拋出異常,然后由responseError統一處理。

響應內容格式化

由于前端關心的數據,是放在響應內容的data屬性中。而另外兩個屬性errno、errmsg,當返回正常數據時,前端是不關心的,為了取數據時更加方便,可以進一步優化 transformResponse 中的處理。當errno==0時,都返回 responseData.data ,這樣,在業務邏輯里面,就可以直接使用data了,而不用取xxx.data

    $httpProvider.defaults.transformResponse.push(function (responseData) {
        if (responseData && responseData.hasOwnProperty("errno") && responseData.hasOwnProperty("errmsg") && responseData.hasOwnProperty("data")) {
            if (responseData.errno == 0) {
                if (Angular.isArray(responseData.data) || Angular.isObject(responseData.data)) {
                    return responseData.data;
                } else {
                    return responseData
                }
            }
            else {
                throw responseData;
            }
        }
        else {
            return responseData;
        }
    });

對 $resource 做進一步封裝

項目中每個模塊,都創建對應的svr文件,用于與后臺進行通信。例如"硬盤"模塊,對應DiskSvr,“防火墻“模塊,對應FirewallSvr。這樣劃分后,前端所有的后臺請求邏輯,都會很清晰易找。

在封裝后臺通信邏輯時,我們用到 $resource ,這是Angular自身的一個組件,當你的后臺接口符合RESTFul規范時,你可以很方便地使用 $resource 和后臺進行通信。而由于我們的后臺并不是完整的RESTFul實現,我們需要做一些簡單的封裝。

app.factory("DiskSvr", function () {

    var url = "schedule/disk";

    var customAPI = {
        clone: {
            method: "post"
        }
    }

    return getApi(url, customAPI);
});

//公用的,每個Svr都可以用
getApi = function (path, customAPI) {

    Angular.forEach(customAPI, function (value, key) {
        if (!value.url) {
            value.url = util.connectPath(path, key);
        } else {
            value.url = util.connectPath(path, value.url);
        }
    });

    //所有的
    var defaultAPI = {
        detail: {
            url: baseUrl + "/get"
        },
        delete: {
            url: baseUrl + "/delete",
            method: "delete"
        },
        create: {
            url: baseUrl + "/create",
            method: "post"
        },
        update: {
            url: baseUrl + "/update",
            method: "put"
        }
    }

    return $resource(path, {}, Angular.extend(defaultAPI, customAPI));
}

上面的代碼,主要做了這些事情:

  1. 為所有的svr注入了每個模塊接口都必備的,最基礎的增刪改查四個接口。這樣就無需在每個svr中加入這些接口。例如,在業務邏輯中調用DiskSvr.create(),就會用post請求調用 schedule/disk/create 接口。

  2. 簡化了新增接口的配置。新增一個接口,只要配置對應的http方法及名字即可。

  3. 通過引用 $resource 組件、進一步封裝及svr文件的約定,在業務controller中,不會看到任何的和后臺通信的基礎代碼,如果在這個controller中你需要和后臺通信,你只需注入響應的svr,然后調用對應方法即可。

注入請求header頭

在我們的項目中,約定了所有的請求都要在header中帶上一些相同的信息,這對Angular來說,是非常簡單的事情:執行以下代碼后,之后所有的http請求都會帶上名為project的header信息

$http.defaults.headers.common["project"] = "test";

2.4 controller間通信問題

controller調用

假設controllerA希望調用controllerB的某個函數,告訴同伴controller,我的某個你所關心的東西改變了,要怎么做呢?舉具體業務場景,有兩個controller,一個是主頁controller,另外主頁上有個彈框表單,這個彈框表單也有個controller,用戶成功提交了這個表單后,彈框controller需要告知主頁controller更新主頁上的某項數據。建議的做法,是用Angular的消息機制。例如,上面的例子中,彈框controller是主頁controller的子controller。那么彈框controller可以往上冒泡傳遞消息

$scope.$emit(EVENT.MODAL_SUCEESS, {});

其父controller去捕獲這個消息

 $rootScope.$on(EVENT.MODAL_SUCEESS, function (event, args) {
     //****
 });

這是一種很好的解耦辦法,假設這兩個controller是由兩個開發負責的,那么我開發我的controller,你開發你的,我不用去關心你那邊的邏輯。

數據共享

多個controller之間要共享數據,要怎么做呢?

最簡單但也不推薦的一個做法,就是把數據塞到rootscope中,但是,這就像js的全局變量,野蠻不好控制。

這里推薦下我們的做法:寫一個專門用于存儲、設置共享數據的共享數據facoty。在里面定義set方法,所有的共享變量,都需要經過set方法來設置。然后取數據則通過DATA變量獲取。

偽代碼如下:

app.factory('ShareSvr', function(){
    var shareData = {
        peopleNum
    }
    return {
        DATA:shareData,
        setPepleNum:function(num){
            shareData.peopleNum = num;
        }
    }
});

app.controller('TestController',function(ShareSvr){
    var self = this;
    this.DATA = ShareSvr.DATA;
}

這里并沒有要求DATA值只能通過get方法獲取,是為了之后在controller對應的視圖html中取值方便些。

2.5 第三方 庫/資源 推薦

  • ui router

    路由(route),幾乎所有的MVC(VM)框架都應該具有的特性,它是前端構建單頁面應用(SPA)必不可少的組成部分。相比原生的ngRouter,ui router功能更加強大,具備多視圖,嵌套路由等特性,可以解決路由大部分的應用場景。

  • ngDialog

    一個彈框控件,功能強大,我比較喜歡的地方,是它沒有寫死彈框的html、可以很方便地定義自己想要的彈框模板。例如,我們項目就通過它做了兩種彈框,一種是普通彈框,一種是側拉框(從屏幕右側滑出,占滿瀏覽器高度,寬度占滿一半屏幕(或者其他自定義寬度)。

  • Angular代碼規范

    1. https://github.com/johnpapa/angular-styleguide/blob/master/a1/i18n/zh-CN.md

    2. https://github.com/mgechev/Angularjs-style-guide/blob/master/README-zh-cn.md

  • Angular-filter

    提供了很多實用的filter,string類、math類,集合類等等

  • w5c-validator

    基于Angular.js原有的表單驗證,統一驗證規則和提示信息,在原有的基礎上擴展了一些錯誤提示的功能,讓大家不用在每個表單上寫一些提示信息的模板,專心的去實現業務邏輯。國人出品

  • ng-table

    輕量,功能強大的表格組件。可以很方便地修改表格的樣式、交互效果。

 本文由用戶 cxj1721 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!