初識AngularJS

Ant發表於2020-04-06

在使用了AngularJS重構團隊內部的平臺之後,一直想總結點什麼,這裡先說說學習和使用AngularJS的感受。AngularJS是一款開源的JavaScript MV*(MVW、MVVM、MVC)框架,目前由Google維護。AngularJS彌補了HTML在構建應用方面的不足,其通過使用識別符號(directives)結構,來擴充套件Web應用中的HTML詞彙,使開發者可以使用HTML來宣告動態內容,從而使得Web開發和測試工作變得更加容易。


AngularJS的創始人Misko是這樣來描述框架誕生的歷史的:AngularJS最初是作為一個編外專案(side project),當時我想去看看是否有可能讓Web設計師(非開發者)只使用HTML標籤來建立簡單的應用程式。隨著時間的推移,AngularJS演變成了一個全面的開發框架。AngularJS的設計理念是:構建UI應該是宣告式的,這樣的靈感來自於Misko在Adobe公司從事Flex方面工作的時候。


談談我個人使用的感受吧:最大的區別是對開發流程的影響,以往我想做一個Web應用,會先用純Html構造原型,所有的資料均是假資料,靜態的寫到Html檔案中,然後不斷的除錯CSS樣式。待頁面整體的展示讓我滿意之後,就開始Js與伺服器的互動開發,並初步把頁面的假資料替換掉,繫結成Js呼叫Ajax的返回。然而在使用到Table / List等資料載入的時候,我通常會在Js中來構造這些Dom元素。隨著應用的龐大和複雜,我發現我在Js中構造了大量的Dom元素,下面是一個例子:

function buildPersonHtml(data){
    var html = [];
    data = data[0];
    for(var id in data){
        var count = 0;
        if(data[id].length==0)
            continue;
        html[html.length] = '<div class="person alert alert-success">';
        html[html.length] = '<span><i class="icon-user"></i> '+id+' 的任務列表:</span>';
        html[html.length] = '<table class="table table-bordered">';
        html[html.length] = '<thead><tr><th>時間</th><th>功能點</th><th>詳細情況</th><th>完成進度</th><th>耗時</th><th>備註</th>';
        html[html.length] = '</tr></thead><tbody>';
        orderSubTask(data[id]);
        for(var i=0; i<data[id].length;i++){
            var subtask = data[id][i];
            if(subtask.progress != '100%')
                count ++;
            html[html.length] = '<tr>';
            if(subtask.status=='end')
                html[html.length] = '<td>開始: '+betterDate(subtask.createTime)+'<br/>結束: '+betterDate(subtask.endTime)+'</td>';
            else
                html[html.length] = '<td>開始: '+betterDate(subtask.createTime)+'</td>';
            html[html.length] = '<td>'+subtask.name+'</td>';
            html[html.length] = '<td>'+subtask.content.replace(new RegExp('\n','g'),'<br/>')+'</td>';
            html[html.length] = '<td>'+subtask.progress+'</td>';
            html[html.length] = '<td>'+subtask.time+'</td>';
            html[html.length] = '<td>'+subtask.note+'</td>';
            html[html.length] = '</tr>';
        }
        html[html.length] = '</tbody></table>';
        html[html.length] = '<span style="margin-top:-15px"><i class="icon-tasks"></i> 已完成任務:'+ (data[id].length-count)+
            '   <i class="icon-tasks"></i> 當前任務數:'+count+'</span>';
        html[html.length] = '</div>';
    }
    return html.join('');
}
隨著我構造這些Dom元素的Js程式碼越來越多,我的原型Html頁面的內容也就越來越少,少到最後很可能就只有一個header一個footer和一個空的div而已,其他所有內容,都是Ajax從伺服器讀取,拿到Json物件的返回之後,才開始構建Dom載入。


接下來,在使用AngularJS重構的時候,我發現頁面的原型設計好之後,基本上不需要做什麼改變,即所有的Dom元素,頁面上該看到的東西,都在你的Html中宣告出來了。僅僅看Html檔案,任何人都能知道這個頁面大概有哪些元素(表單、表格等各種UI控制元件),然後AngularJS用ng-model的方式讓你把這些Dom元素和資料進行了一種雙向繫結,即把你的資料(Data Model)宣告到頁面中。(我覺得這一步完全有可能由Web設計師來完成)下面的程式碼片段是一個例子:

<table class="table table-bordered table-hover" ng-controller="MachineCtrl">
                      <tr>
                          <th ng-click="machineKey = 'host'; reverse = !reverse" class="order-th">主機名</th>
                          <th ng-click="machineKey = 'address'; reverse = !reverse" class="order-th">IP地址</th>
                          <th>配置</th>
                          <th>賬號</th><th>OS</th>
                          <th>軟體</th>
                          <th>備註</th>
                          <th ng-click="machineKey = 'owner'; reverse = !reverse" class="order-th">使用者</th>
                          <th>操作</th>
                      </tr>
                      <tr ng-repeat="machine in machines | filter:query | orderBy:machineKey:reverse">
                        <td class="nowrap">{{machine.host}}</td>
                        <td class="nowrap">{{machine.address}}</td>
                        <td>{{machine.hardware}}</td>
                        <td>{{machine.name}} / {{machine.password}}</td>
                        <td>{{machine.os}}</td>
                        <td>{{machine.env}}</td>
                        <td>{{machine.note}}</td>
                        <td class="nowrap machine-owner" id="{{machine._id}}_owner">
                            <span>{{machine.owner}}</span>
                            <input type="text" class="form-control owner-input" ng-keydown="bookMachine($event, machine._id)"/>
                            <a class="owner-btn" ng-click="toggleOfflineBook(offline._id)"><i class="fa fa-times fg-lg"></i></a>
                        </td>
                        <td class="nowrap" ng-class="machine.status=='maintenance'?'disable':'enable'">
                            <span ng-click="maintainMachine(machine._id)"><i class="fa fa-cog fa-spin fa-lg"></i> 維護中...</span>
                            <a title="訂閱" ng-click="toggleMachineBook(machine._id)"><i class="fa fa-pencil"></i></a> 
                            <a title="維護" ng-click="maintainMachine(machine._id)"><i class="fa fa-wrench"></i></a> 
                            <a title="編輯" ng-click="editMachine(machine._id)"><i class="fa fa-edit"></i></a> 
                            <a title="刪除" ng-click="deleteMachine(machine._id)"><i class="fa fa-trash-o"></i></a>     
                          </td>
                        </tr>
                </table>
可以看到我把一個table與叫machines的data model做了一個雙向繫結,而實際上machines就是一個Json陣列而已,當你用Js從服務端取到資料,填充了叫machines的Json陣列之後,這個table就會自動更新出來,每一行展示一個machine,每一個單元格展示machine上的某個屬性。所謂的雙向繫結,即一方產生變化的時候,另外一方就會同步更新。所以如果我在Js檔案中操作改變了這個machines陣列物件,例如刪除了其中一個元素那麼頁面table就會自動減少一行,更新了一個元素的屬性值,table也會自動更新。就感覺像頁面的宣告model是Js中Json物件的引用一樣,當Json物件發生任何變化,頁面的model引用也會隨之改變。


當具有這種特性之後,我的Js程式碼就可以專注對Model的管理和操作了,不需要再關心頁面的“資料”角色,頁面真正意義上充當了一個View的角色,我也不需要在Js中再去操作頁面上的資料和重構Dom元素之類的。(你可以選擇用jQuery.post,或者socket.io與伺服器通訊,在Js中維護Json物件的變化)當然,這種data-model binding並不是隨意的,而是分作用域的,某些model只在一定範圍的Html中可見。而控制這些作用域的就是ng-controller,即MVC中的C部分。


上面的Html中我將table宣告繫結了一個叫MachineCtrl的Controller,那麼machines這個model就只在這個table內可見。而Js中能操作它的也只有MachineCtrl這個Controller。我的Js關於MachineCtrl的程式碼片段如下:

/**
*   Machine Controller
**/
machineApp.controller('MachineCtrl', function($scope, socket){
    ...
    socket.on('machine list', function (data) {
        $scope.machines = data;
    });    
    $scope.editMachine = function(id){
        var entity = $scope.findMachine(id);
        var cloneEntity = cloneMachine(entity);
        loadMachine(cloneEntity,'test');
    }

    $scope.bookMachine = function(e, id){
        if(e.keyCode == 13){
            var input = $('#'+id+'_owner input');
            var owner = $.trim(input.val());

            var entity = $scope.findMachine(id);
            if(owner == entity.owner){
                error('重複訂閱,忽略此次訂閱!');
                $scope.toggleMachineBook(id);
                return;
            }else if(entity.owner.length !=0 && owner.length!=0){
                error('機器已經被訂閱,請先取消訂閱');
                return;
            }

            socket.emit('bookMachine',{'id': id, 'owner': owner});
            e.preventDefault();    
        }else if(e.keyCode == 27){
            $scope.toggleMachineBook(id);
            e.preventDefault(); 
        }
    }...
當我用socket.io從服務端取到machines的Json物件之後,直接複製到MachineCtrl的$scope.machines上,這樣就實現了與頁面UI宣告瞭MachineCtrl裡的machines的雙向繫結。之後我與伺服器互動,就在MachineCtrl中更新$scope.machines實現增刪改查,頁面就會自動響應更新。


總結:AngularJS確是一個很強大的Javascript前端MVC框架,如今的網際網路時代,對於富客戶端需求越來越旺盛,前端Js和Dom的互動越來越複雜,AngularJS很好的對它們進行了一種分層和維護管理。