前端開發中的MCRV模式

針對前端開發中基於ajax的複雜頁面開發所面臨的代碼規模大,難以組織和維護,代碼複用性、擴展性和適應性差等問題,本文嘗試以MVC思想爲基礎,結合Web前端開發中內容-結構-表現-行爲相分離的開發標準,提出一種將Web頁面代碼分爲視圖(View,頁面靜態部分,包括內容、結構、表現)、模型(Model,負責數據緩存、數據校驗與本地邏輯處理、發起ajax請求)、控制器(Controller,負責用戶和系統事件響應、模型和呈現器調度)、呈現器(Renderer,對視圖的渲染,控制器與事件的綁定、數據蒐集)的頁面開發新模式,並基於此模式提出了一個開發框架原型。

摘要

針對前端開發中基於ajax的複雜頁面開發所面臨的代碼規模大,難以組織和維護,代碼複用性、擴展性和適應性差等問題,本文嘗試以MVC思想爲基礎,結合Web前端開發中內容-結構-表現-行爲相分離的開發標準,提出一種將Web頁面代碼分爲視圖(View,頁面靜態部分,包括內容、結構、表現)、模型(Model,負責數據緩存、數據校驗與本地邏輯處理、發起ajax請求)、控制器(Controller,負責用戶和系統事件響應、模型和渲染器調度)、渲染器(Renderer,對視圖的渲染,控制器與事件的綁定、數據蒐集)的頁面開發新模式,並基於此模式提出了一個開發框架原型。

關鍵字:

MCRV設計模式 Javascript MVC Web開發標準

1.Web前端開發面臨的問題

早期的Web頁開發(Web前端開發)中,Web頁面較爲簡單,大多數Web頁面的功能僅限於用HTML和簡單樣式展示靜態信息,或向服務器發送數據,Web頁面與用戶的交互較少。隨着Web的發展,DHTML、CSS、javascript等技術出現,Web頁不再僅限於展示靜態信息,動態、交互成爲Web頁的主流功能之一。與此同時,Web頁代碼規模也變的較大,頁面中的HTML、CSS、Javascript等代碼往往混雜在一起,如何很好的組織這些代碼,使Web客戶端程序具有很好的結構,易於閱讀和維護,成爲Web前端開發人員面臨的一個難題。在實踐中,業界提出了內容(Content)-結構(Structure)-表現(Presentation)-行爲(Behavior)相分離的Web頁開發標準。在這種標準中,一個Web頁代碼可以分爲如下四個部分:

內容:頁面實際要傳達的真正信息,包含數據、文檔或者圖片等。
    結構:對內容的劃分,使內容更加具有邏輯性,易用性。類似頁面的標題、作者、章、節、段落和列表。
    表現:用來描述內容外觀,稱之爲“表現”,主要指CSS樣式。
    行爲:行爲就是對內容的交互及操作效果。行爲控制主要通過javascript實現。

內容-結構-表現-行爲(CSPB)開發標準對Web頁包含的的代碼進行了分類,使代碼各個部分得到很好的分離,使Web頁初步具有了良好的結構。

隨後,Web進入了嶄新的Web2.0時代,以Gmail爲代表的Web2.0應用大量出現,並且取得很大的成功。這類Web頁的典型特徵是用一個基於ajax技術的無刷新的頁面完成大量複雜的功能。此類頁面包括Web IM、Web Map、Gmail等應用,統稱爲one-page Web應用。同時,在企業級Web開發中,一個Web頁同時完成多個複雜功能的情況也越來越多。在這些複雜Web頁應用中, Javascript代碼是此類富客戶端應用程序的核心,負責與用戶的複雜交互和頁面展現。One-page Web頁面javascript代碼規模往往非常龐大,邏輯複雜,動則千行以上,有時甚至需要一個團隊來完成一個頁面的開發。如何合理組織大量的javascript代碼,使之具有良好的擴展性,能夠適應需求的變化,使代碼易於維護,是廣大Web前端開發人員面臨的一大挑戰。

縱觀Web應用程序開發的歷史,Web後端開發(PHP、J2EE、ASP.NET)與Web客戶端開發的發展過程有一定的相似性,曾面臨類似的問題。最初,Web程序功能簡單,代碼較少時,所有的控制、邏輯、UI展現代碼混雜在一起;隨着Web發展,程序規模變大,按原有方式開發出的程序顯然不具有良好的結構,不利於大規模開發和後期維護,也不利於Web開發人員工作和角色的進一步細分,代碼複用性差,瓶頸開始出現。Web後端程序設計者的解決方法是進行代碼分類,將控制代碼與展現代碼開始分離, 於是Web後端開發從所謂的Model1發展到Model2;同時,傳統桌面程序設計中的MVC(模型-視圖-控制器)設計模式被引入,負責程序數據與邏輯計算的部分進一步分離出來,形成了Web後端開發中的MVC開發模式。MVC設計模式貫穿了軟件工程分而治之的思想,有效解決了Web後端程序設計中的代碼組織和複用問題。採用同一設計模式(MVC)也使代碼更容易被他人理解,保證代碼可靠性;它使Web開發工作可以細分爲業務邏輯開發和UI展現開發。因此,MVC也利於團隊開發。那麼,MVC設計思想是否也能應用在前端開發中解決相關問題呢?既然問題具有許多相似性,筆者沿着相同的解決思路來試圖解決前端開發中的代碼組織問題。

2.MVC設計模式

在提出本文提出的MCRV設計模式之前,有必要對其所基於的MVC設計模式進行闡述。

MVC這個概念很早之前就被人提起[1],它代表一種設計思想。MVC是Model-View-Conroller的縮寫,即模型-視圖-控制器。這種開發模式將一個應用程序分爲三個部分,模型(model)實現商業邏輯,提供數據;視圖(view)負責向用戶呈現界面和接受用戶交互;控制器(controller)則負責響應用戶交互請求,對用戶請求進行翻譯,根據不同的請求調用模型,執行商業邏輯。控制器本質上是一個調度器 (dispatcher),通過其包含的各個方法(action)來執行具體的商業邏輯。MVC各個部件以最小的耦合性協同工作,從而使程序具有良好的可擴展性和可維護性和複用性。

MVC設計模式最開始被用於桌面程序設計,典型地用於相同的數據需要不同的用戶交互界面的設計場景。在經典的MVC設計思想中,控制器負責響應用戶事件,根據事件類型和參數調用模型或改變視圖。每個模型對應一個或者多個視圖,當模型被控制器調用而得到改變時,模型向所有向其註冊過的視圖發送通知,視圖根據從模型的中得到的信息改變外觀。這種設計模式可以用圖1表示。可以看出經典MVC設計模式中Controller、Model 都可以改變視圖。

經典MVC

圖1 經典MVC

基於MVC模式的Web開發(後端)一般可以用圖2表示。在這種模式下,controller負責解析用戶瀏覽器請求的URL,根據URL自動調用controller中的不同的Action響應用戶請求。Action調用model,然後將model返回的數據填充到view中,view被返回給用戶瀏覽器。Web服務端開發中典型的採用MVC模式的框架有CakePHP、Structs、Spring等等。

Server端Web開發中的MVC

圖2 Server端Web開發中的MVC

相對於早期的經典MVC模式,Web開發中的MVC模式存在一些變化,因爲對於一個桌面應用程序而言,可以很方便地將視圖註冊給模型,當模型數據發生改變時,即時通知視圖頁面發生改變;而對於Web應用而言,即使將多個頁面註冊給一個模型,當模型發生變化時,模型無法主動發送消息給Web頁面(因爲Web應用都是基於請求/響應模式的),只有當用戶請求瀏覽該頁面時,控制器才負責調用模型數據來更新Web頁面。同時,Web開發中的MVC也比經典MVC具有更小的耦合性,因爲Model不再與View發生交互,因此程序的結構更加良好,使Web前端開發和業務邏輯開發相分離變的簡單。

對於具有複雜交互邏輯和豐富界面的程序來說,MVC已被實踐證明是一種行之有效的設計和開發思想。採用MVC設計模式的系統具有較好的結構,較低的耦合性,代碼具有很好的維護性,能夠適應複雜的業務邏輯和視圖展現變化。它也非常適合團隊開發,使開發人員分爲不同的角色,專注於自己負責部分的開發。

3.MCRV設計模式

綜上所述,MVC是解決具有複雜交互界面、代碼規模大的應用程序面臨代碼組織、複用問題的有效設計模式。基於此,本文嘗試基於MVC設計思想解決前端開發中的類似問題。

然而,在前端頁面開發中不能直接套用MVC設計模式。因爲,前端開發中的Web頁面包含了HTML、CSS、Javascript等多個種類的代碼,相對Web後端來說,Web頁面整體就是一個負責UI展現、用戶本地交互、發送服務請求的大View,與經典MVC及Web後端開發MVC模式中的View皆有不同。因此,需要具體問題具體分析。

首先對Web頁面中的HTML、CSS、Javascript等代碼進一步分析,明確它們的具體功能分類。根據業界提出的Web開發標準,Web頁首先被分爲了內容-結構-表現-行爲幾個部分。內容、結構、表現是頁面的靜態部分,主要負責UI的展現,用戶操作指令(鍵盤、鼠標)的接受,因此,內容、結構、表現屬於視圖 (View)的範疇。行爲主要就是javascript代碼,負責對用戶操作指令的響應。在複雜ajax應用中, javascript代碼的功能一般包括:響應用戶指令,執行數據驗證/處理、執行客戶端交互邏輯,向服務器發送ajax請求,接受並處理服務器返回的數據,根據數據改變UI(向頁面結構填充內容數據、變換樣式等)。基於模型-視圖-控制器分離的思想,Javascript代碼中的數據驗證/處理、本地業務邏輯計算、向服務器發請求獲取數據的功能,對應模型(Model)的功能;響應(翻譯)用戶操作指令,根據指令執行業務邏輯處理的功能,屬於控制器(Controller)的功能;而接受處理過的數據,根據數據修改頁面的內容/結構/樣式的功能既不屬於控制器的功範疇,也不屬於模型的功能範疇,這部分代碼因爲根據數據對View進行呈現(render),可以命名爲Renderer(渲染器呈現器)。

根據前述分析,本文提出了MCRV設計模式,如圖3所示。闡述如下:

M(Model):模型。完成數據驗證、數據處理,執行客戶端業務邏輯計算,或向服務器發起ajax請求調用服務端邏輯、接受返回的數據,將處理後的數據返回控制器。
   C(Controller):控制器。控制器響應View上的事件,根據事件調度執行模型的業務邏輯,從業務邏輯獲取返回數據,調度相應的渲染器(Render)來完成界面展現。在這個過程中控制器會有數據的傳遞:控制器調用模型中的邏輯時會傳送Renderer蒐集的數據(form表單各域的name/value、其他控制參數),模型執行邏輯後返回作爲執行結果的數據給控制器,控制器根據數據來調用渲染器(renderer)來完成界面呈現(rendering),呈現(rendering)就是修改頁面結構、內容和樣式的過程。數據傳遞過程可以用圖4表示。
   R(Renderer):渲染器(呈現器)。渲染器被控制器調用,接受從控制器傳遞的數據,完成對界面的具體渲染。渲染器也負責控件(widget)的初始化,及建立Controller與具體事件的對應關係,事件發生時負責蒐集View上的數據傳送到Controller。
   V(View):視圖。視圖是用戶最終看到的整個Web界面,由結構、內容、樣式(表現)等靜態內容共同構成。View由Renderer進行初始化渲染和修改。

MCRV開發模式

圖3:MCRV開發模式


數據傳遞過程

圖4:數據傳遞過程

可以看到在MCRV開發模式中,Controller處於控制中心的位置,Model完成具體的商業邏輯計算以及向後端發起ajax請求返回數據的功能。Controller與Model、Renderer之間的交互本質上數據交互的過程,它們之間存在着一個數據流,如圖4所示。因此,制定Controller、Model、Renderer之間的交互接口時,數據格式定義很重要。

4.基於MCRV設計模式的Demo

下面是一個用MCRV模式來開發的Demo頁面,頁面的功能是用表格展示和修改用戶信息。javascript使用了jQuery庫。頁面界面如圖5所示。頁面代碼在程序清單1中

基於MCRV設計模式的用戶管理Demo

圖5:基於MCRV設計模式的用戶管理Demo

程序清單1

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=7" />
        <title>MCRV 設計模式 Demo</title>
        <style type="text/css">
            /********表現*********/
            table{width:100%;border-collapse: collapse;}
            td{border: 1px solid black;padding: 2px;}
            #container {width:800px;margin:0px auto;}
            #tbUsers{margin:20px auto;}
            #tbUsers th{background-color: navy;color:white;text-align: center;vertical-align: middle;border:1px solid navy}
             #tbUsers td{text-align: center;}
            .editCaption{width:100px;text-align: right;}
            .buttonMargin{margin:0px 20px;}
            .buttonContainer{text-align: center;vertical-align: middle;height:50px}
        </style>
        <script src="/imgr?src=http%3A%2F%2Fwww.baiduux.com%2Fblog%2F2011%2F07%2F08%2Ffrontend-mcrv-design-pattern%2Fjquery-1.3.2.min.js&r=http%3A%2F%2Fwww.baiduux.com%2Fblog%2F2011%2F07%2F08%2Ffrontend-mcrv-design-pattern%2F" type="text/javascript" charset="utf-8"></script>
</head>
<body>
    <!-------結構--------->
    <div id="container">
        <table id="tbUsers">
            <thead><th>id</th><th>姓名</th><th>年齡</th><th>修改</th></thead>
            <tbody/>
        </table>
        <div style="display: none;" id="dvEditPanel">
            <form id="frmModify" name="frmModify">
                <table>
                <tr>
                    <td class="editCaption"> id: </td>
                    <td><span id="spID"></span></td>
                </tr>
                <tr>
                    <td class="editCaption"> 姓名: </td>
                    <td><input type="text" size="20" id="txtName"/></td>
                </tr>
                <tr>
                    <td class="editCaption"> 年齡: </td>
                    <td><input type="text" size="20" id="txtAge"/></td>
                </tr>
                <tr>
                    <td colspan="2"  class="buttonContainer">
                        <button id="btnSubmitModify" class="buttonMargin" type="button">提交</button>
                        <button id="btnCancelModify" class="buttonMargin" type="button">取消</button>
                    </td>
                </tr>
              </table>
            </form> 

        </div>
    </div>
    <script>
        /***************************行爲********************************/ 

        var UserManagerMCR;
        $(function()
        {
            UserManagerMCR=new MCR(UserController,UserModel,UserRenderer);
        }); 

        /*
         * MCR 三元組
         */
        function MCR(Controller,Model,Renderer)
        {
            this.controller=new Controller();
            this.model=new Model();
            this.renderer=new Renderer();
            this.controller.model=this.model;
            this.controller.renderer=this.renderer;
            this.model.controller=this.controller;
            this.renderer.controller=this.controller;
            if(typeof this.model.init=="function")
            {
                this.model.init();
            }
            if(typeof this.renderer.init=="function")
            {
                this.renderer.init();
            }
            if(typeof this.controller.init=="function")
            {
                this.controller.init();
            }
        } 

        /*
         * 控制器
         */
        function UserController()
        {
           this.init=function()
            {
                this.initUserList();
            } 

            this.initUserList=function()
            {
                var list=this.model.getUserList();
                this.renderer.renderUserList(list);
            } 

            this.beginModify=function(data)
            {
                var user=this.model.getUserByID(data.id);
                this.renderer.showModifyUI(user);
            } 

            //提交修改
            this.submitModify=function(user)
            {
                var result=this.model.modifyUser(user);
                if(result.success)
                {
                    var list=this.model.getUserList();
                    this.renderer.renderUIWhenSubmitModifySuccess(list);
                }
                else
                {
                    alert(result.msg);
                }
            } 

            //取消修改
            this.cancelModify=function()
            {
                this.renderer.hideModifyUI();
            } 

        } 

        /*
         * 模型
         */
        function UserModel()
        {
            //模擬的數據,實際應用中經常從服務器獲取 

           this.init=function()
           {
              this.data = [
                {id:0,name:"John",age:22},
                {id:1,name:"Tom",age:30},
                {id:2,name:"Tony",age:25}
               ];
           } 

            //獲得用戶數據列表
            this.getUserList=function()
            {
                //todo ,可能ajax從後端返回
                return this.data;
            } 

            //獲得用戶數據
            this.getUserByID=function(id)
            {
                var ix;
                $.each(this.data,function(i,item){if(item["id"]==id ) { ix=i; return false;}});
                return this.data[ix];
            } 

            //修改用戶數據
            this.modifyUser=function(user)
            {
                var result={success:true,msg:"修改成功"};
                //todo,驗證參數user
                //todo,修改用戶數據
                $.each(this.data,function(i,item)
                {
                    if(item["id"]==user["id"])
                    {
                        item["name"]=user["name"];
                        item["age"]=user["age"]
                        return false;
                    }
                });
                return result;
            } 

        } 

        /*
         * 渲染器
         */
        function UserRenderer()
        {
            this.init=function()
            {
                 var me=this;
                 $("#btnSubmitModify").click(function()
                 {
                     var user={id:$("#spID").text(),name:$("#txtName").val(),age:$("#txtAge").val()};
                     me.controller.submitModify(user);
                 });
                 $("#btnCancelModify").click(function()
                 {
                     me.controller.cancelModify();
                 });
                 $("#tbUsers .modify").live("click",function()
                 {
                     var id=$(this).attr("uid");
                     me.controller.beginModify({"id":id});
                 });
            } 

            this.renderUserList=function(list)
            {
                var htm=[];
                for(var ix=0;ix<list.length;ix++)
                {
                    htm.push("<tr><td>" +list[ix]["id"]+"</td>" +"<td>"+list[ix]["name"]+"<td>"+list[ix]["age"]+"</td>"
                        +"<td>"+"<a class='modify' href="javascript:void(0)" uid='"+list[ix]["id"]+"'>修改</a></td>"+"</tr>");
                }
                $("#tbUsers").children("tbody").html(htm.join(""));
            } 

            this.showModifyUI=function(user)
            {
                $("#dvEditPanel").show();
                $("#spID").text(user["id"]);
                $("#txtName").val(user["name"]);
                $("#txtAge").val(user["age"]);
            } 

            this.hideModifyUI=function()
            {
                document.frmModify.reset();
                $("#dvEditPanel").hide();
            } 

            this.renderUIWhenSubmitModifySuccess=function(list)
            {
                this.hideModifyUI();
                this.renderUserList(list);
            }
        }
    </script>
</body>
</html>

5.基於MCRV模式的開發框架原型

5.1基本設計思想

框架是在一個特定的問題領域內,應用程序的部分設計與實現[2]。框架與設計模式通常是精密結合的。框架規定了應用的體系結構,使基於特定設計模式的開發能夠複用公共代碼;反之,框架強調設計複用,框架設計也基本上使用了設計模式,掌握了框架的設計模式可以快速掌握基於框架的應用程序開發。因此,使用MCRV設計模式進行前端開發時最好有一個與之配合的javascript框架。
基本設計考慮:

 Model、Controller、Renderer組成一個三元組MCR,一個MCR三元組中Controller對象、Model對象和Renderer對象是唯一的。

 Model、Controller、Renderer可以初始化和銷燬

 Controller、Model、View在需要時可以透明地引用其他對象

 數據與邏輯分離,支持數據本地存儲

 一個Web頁面可以有多個MCR組,即可以有多個Controller、Model、Renderer三元組分別完成不同的控制、邏輯、展現。這爲一個頁面邏輯和交互非常複雜時,拆分爲多個模塊,由多人開發提供了支持。

5.2 基本對象及其關係

下面是一個基於上述考慮的基於MCRV設計模式的javascript框架原型(以下簡稱MCRV框架)。
框架的基本對象:MCR、Model、Controller、Renderer、Cache。如圖6所示。

圖6 基於MCRV模式的框架原型

圖6 基於MCRV模式的框架原型

 MCR:控制器(Controller)對象、模型對象(Model)和渲染器對象(Renderer)的複合對象。

-Dispose()方法:執行MCR銷燬操作,避免javascript內存泄露;在頁面unload時自動調用;這個方法自動調用Model、Controller和Renderer的dispose()方法

-model屬性:包含的模型

-controller屬性:包含的控制器
-renderer屬性:包含的渲染器

 Model:模型對象。

-init()方法:執行模型初始化

-dispose()方法:執行對象銷燬,釋放資源

-cahce屬性:數據的緩存。

 Controller:控制器對象

-init()方法:執行控制器初始化

-dispose()方法:執行對象銷燬,釋放資源

-model屬性:調度的模型

-renderer屬性:控制的渲染器

 Renderer:渲染器對象

-init()方法:執行渲染器初始化

-dispose()方法:執行對象銷燬,釋放資源

-controller屬性:對controller的引用

 Cache:基於key-value的緩存對象

-get()方法:基於鍵值獲取緩存數據

-set()方法:設置緩存數據

-remove()方法:刪除對應某個鍵值的緩存

-size():緩存數量

5.3 MCRV框架與其他web其他部分的關係

基於MCRV模式的開發框架與Web頁面開發其他組成部分的關係可以用圖7表示。MCRV框架搭建起了js應用程序整體的結構,提供了應用程序上下文環境。MCRV框架可以與js組件庫、css基礎樣式庫和js業務邏輯組件一起作爲應用程序構建的基礎,並且它們之間沒有依賴關係。儘管應用程序可以在js基礎庫(如jQuery)上開發,但是MCRV框架不依賴這些庫。

圖7 基於MCRV模式的開發框架與其他Web頁其他部分的關係

圖7 基於MCRV模式的開發框架與其他Web頁其他部分的關係

6.結論

實踐表明,MCRV設計模式能夠有效解決複雜ajax開發中面臨的問題,可以使Web頁面代碼結構良好,降低javascript代碼的耦合性,提高複用性、適應性和靈活性,使Web頁更加易於維護和重構。

參考文獻

[1]. Trygve Reenskaug Taskon.Working with objects in the user interfaces.

[2]. J. Van Gurp, J. Bosch.Design, Implementation and Evolution of Object Oriented Frameworks Concepts and Guidelines.pdf.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章