100 行程式碼實現的 JavaScript MVC 樣式框架

類別: IT

介紹

使用過 JavaScript框架(如 AngularJS, Backbone 或者Ember)的人都很熟悉在UI(使用者介面,前端)中mvc的工作機理。這些框架實現了MVC,使得在一個單頁面中實現根據需要變化檢視時更加輕鬆,而模型-檢視-控制器(mvc)的核心概念就是:處理傳入請求的控制器、顯示資訊的檢視、表示業務規則和資料訪問的模型。

因此,當需要建立這樣一個需要在單個頁面中實現切換出不同內容的應用時,我們通常選擇使用上述框架之一。但是,如果我們僅僅需要一個在一個url中實現檢視切換的框架,而不需要額外捆綁的功能的話,就不必使用象Angular和Ember等複雜的框架。本文就是嘗試使用簡單、有效方法來解決同樣的問題。

概念

應用中的程式碼利用urls中的“#”實現MVC模式的導航。應用以一個預設的url開始,基於雜湊值的程式碼載入應用檢視並且將物件-模型應用於檢視模板。

url格式像下面這樣:

http://Domain Name/index.html#/Route Name

檢視內容必須以{{Property-Name}}的方式繫結物件模型的值和屬性。程式碼會查詢這個專門的模板格式並且代替物件模型中的屬性值。

以ajax的方式非同步載入的檢視會被放置於頁面的佔位符中。檢視佔位符可以是任何的元素(理想的情況是div),但是它必須有一個專門的屬性,程式碼根據這個專門的屬性來定位它,這樣同樣有助於程式碼的實現。當url改變時,會重複這個場景,另外一個檢視被載入。聽起來很簡單吧!下面的流程圖解釋了在這個特定的實現中的訊息跳轉。

寫程式碼

我們以基本的模組設計模式開始,並且最終用門面設計模式的方式將我們的libs曝光於全域性範圍內。

; (function (w, d, undefined) { //rest of the code })(window, document);

我們需要將檢視元素儲存到一個變數中,這樣就可以多次使用。

var _viewElement = null; //element that will be used to render the view

我們需要一個預設的路由來應對url中沒有路由資訊的情況,這樣就預設的檢視就可以被載入而不是展示空白頁面。

var _defaultRoute = null;

現在我們來建立我們的主要MVC物件的構造方法。我們會把路由資訊儲存在“_routeMap”

var jsMvc = function () {
    //mapping object for the routes
    this._routeMap = {};
}

是時候建立路由物件了,我們會將路由、模板、控制器的資訊儲存在這個物件中。

var routeObj = function (c, r, t) {
    this.controller = c;
    this.route = r;
    this.template = t;
}

每一個url會有一個專門的路由物件routeObj.所有的這些物件都會被新增到_routeMap物件中,這樣我們後續就可以通過key-value的方式獲取它們。

為了新增路由資訊到MVC libs中,我們需要曝光libs中的一個方法。所以讓我們建立一個方法,這個方法可以被各自的控制器用來新增新路由。

jsMvc.prototype.AddRoute = function (controller, route, template) {
    this._routeMap[route] = new routeObj(controller, route, template);
}

方法AddRoute接收3個引數:控制器,路由和模板( contoller, route and template)。他們分別是:

controller:控制器的作用就是訪問特定的路線。

route:路由的路線。這個就是url中#後面的部分。

template:這是外部的html檔案,它作為這個路由的檢視被載入。現在我們的libs需要一個切入點來解析url,並且為相關聯的html模板頁面提供服務。為了完成這個,我們需要一個方法。

Initialize方法做如下的事情:

1)獲取檢視相關的元素的初始化。程式碼需要一個具有view屬性的元素,這樣可以被用來在HTML頁面中查詢:

2)設定預設的路由

3)驗證檢視元素是否合理

4)繫結視窗雜湊變更事件,當url不同雜湊值發生變更時檢視可以被及時更新

5)最後,啟動mvc

//Initialize the Mvc manager object to start functioning
jsMvc.prototype.Initialize = function () {
    var startMvcDelegate = startMvc.bind(this);

    //get the html element that will be used to render the view  
    _viewElement = d.querySelector('[view]');        
    if (!_viewElement) return; //do nothing if view element is not found    

    //Set the default route
    _defaultRoute = this._routeMap[Object.getOwnPropertyNames(this._routeMap)[0]];    

    //start the Mvc manager
    w.onhashchange = startMvcDelegate;
    startMvcDelegate();
}

在上面的程式碼中,我們從startMvc 方法中建立了一個代理方法startMvcDelegate 。當雜湊值變化時,這個代理都會被呼叫。下面就是當雜湊值變化時我們做的操作的先後順序:

1)獲取雜湊值

2)從雜湊中獲取路由值

3)從路由map物件_routeMap中獲取路由物件routeObj 

4)如果url中沒有路由資訊,需要獲取預設的路由物件

5)最後,呼叫跟這個路由有關的控制器並且為這個檢視元素的檢視提供服務

上面的所有步驟都被下面的startMvc方法所實現

//function to start the mvc support
function startMvc() {
    var pageHash = w.location.hash.replace('#', ''),
        routeName = null,
        routeObj = null;                
        
    routeName = pageHash.replace('/', ''); //get the name of the route from the hash        
    routeObj = this._routeMap[routeName]; //get the route object    

    //Set to default route object if no route found
    if (!routeObj)
        routeObj = _defaultRoute;
    
    loadTemplate(routeObj, _viewElement, pageHash); //fetch and set the view of the route
}

下一步,我們需要使用XML HTTP請求非同步載入合適的檢視。為此,我們會傳遞路由物件的值和檢視元素給方法loadTemplate。

//Function to load external html data
function loadTemplate(routeObject, view) {
    var xmlhttp;
    if (window.XMLHttpRequest) {
        // code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp = new XMLHttpRequest();
    }
    else {
        // code for IE6, IE5
        xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
    }
    xmlhttp.onreadystatechange = function () {
        if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
            loadView(routeObject, view, xmlhttp.responseText);
        }
    }
    xmlhttp.open('GET', routeObject.template, true);
    xmlhttp.send();
}

當前只剩載入檢視和將物件模型與檢視模板繫結了。我們會建立一個空的模型物件,然後傳遞與方法相關的模型來喚醒路由控制器。更新後的模型物件會與先前已經載入的XHR呼叫中的HTML模板繫結。

loadView 方法被用於呼叫控制器方法,以及準備模型物件。

replaceToken方法被用於與HTML模板一起繫結模型

//Function to load the view with the template
function loadView(routeObject, viewElement, viewHtml) {
    var model = {}; 

    //get the resultant model from the controller of the current route  
    routeObject.controller(model); 

    //bind the model with the view    
    viewHtml = replaceToken(viewHtml, model); 
    
    //load the view into the view element
    viewElement.innerHTML = viewHtml; 
}

function replaceToken(viewHtml, model) {
    var modelProps = Object.getOwnPropertyNames(model),
        
    modelProps.forEach(function (element, index, array) {
        viewHtml = viewHtml.replace('{{' + element + '}}', model[element]);
    });
    return viewHtml;
}

最後,我們將外掛曝光於js全域性範圍外

//attach the mvc object to the window
w['jsMvc'] = new jsMvc();

現在,是時候在我們單頁應用中使用這個MVC外掛。在下一個程式碼段中,下面這些會實現:

1)在web頁面中引入這個程式碼

2)用控制器新增路由資訊和檢視模板資訊

3)建立控制器功能

4)最後,初始化lib。

除了上面我們需要的連結讓我們導航到不同的路徑外,一個容器元素的檢視屬性包含著檢視模板html。

<!DOCTYPE html>
<html>
<head>
    <title>JavaScript Mvc</title>
    <script src="jsMvc.js"></script>
    <!--[if lt IE 9]> <script src="jsMvc-ie8.js"></script> <![endif]-->
    
    <style type="text/css">
        .NavLinkContainer {
            padding: 5px;
            background-color: lightyellow;
        }

        .NavLink {
            background-color:black;
            color: white;
            font-weight:800;
            text-decoration:none;
            padding:5px;
            border-radius:4px;
        }
            .NavLink:hover {
                background-color:gray;
            }
    </style>
</head>
<body>
    <h3>Navigation Links</h3>
    <div class="NavLinkContainer">
        <a class="NavLink" href="index.html#/home">Home</a> 
   
        <a class="NavLink" href="index.html#/contact">Contact</a> 

        <a class="NavLink" href="index.html#/admin">Admin</a> 
       
    </div>
    <br />
    <br />
    <h3>View</h3>
    <div view></div>
    <script>
        jsMvc.AddRoute(HomeController, &apos;home&apos;, &apos;Views/home.html&apos;);
        jsMvc.AddRoute(ContactController, &apos;contact&apos;, &apos;Views/contact.html&apos;);
        jsMvc.AddRoute(AdminController, &apos;admin&apos;, &apos;Views/admin.html&apos;);
        jsMvc.Initialize();

        function HomeController(model) {
            model.Message = &apos;Hello World&apos;;
        }

        function ContactController(model) {
            model.FirstName = "John";
            model.LastName = "Doe";
            model.Phone = &apos;555-123456&apos;;
        }

        function AdminController(model) {
            model.UserName = "John";
            model.Password = "MyPassword";
        }
    </script>
</body>
</html>


上面的程式碼有一段包含一個為IE的條件註釋。

<!--[if lt IE 9]> <script src="jsMvc-ie8.js"></script> <![endif]-->


如果IE的版本低於9,那麼function.bind,Object.getOwnPropertyNames和Array.forEach屬性將不會被支援。因此我們要通過判斷瀏覽器是否低於IE9來反饋程式碼是否支援。

其中的內容有home.html, contact.html 和 admin.html 請看下面:

home.html:

{{Message}}

contact.html:

{{FirstName}} {{LastName}}
<br />
{{Phone}}

admin.html:

<div style="padding:2px;margin:2px;text-align:left;">
    <label for="txtUserName">User Name</label>
    <input type="text" id="txtUserName" value="{{UserName}}" />
</div>
<div style="padding:2px;margin:2px;text-align:left;">
    <label for="txtPassword">Password</label>
    <input type="password" id="txtPassword" value="{{Password}}" />
</div>

完整的程式碼可以從給定的下載連結中得到。

如何執行程式碼

執行該程式碼比較簡單,需要在你喜歡的Web伺服器上建立一個Web應用,下面以IIS為例來說明。

首先在預設站點中新增一個Web應用.

然後設定必填資訊:別名,物理路徑,應用池,使用者認證資訊,點選OK。

最後定位到Web應用的內容目錄,瀏覽你想開啟的HTML頁面即可。

跑在伺服器裡是必要的,因為程式碼載入從儲存於外部檔案中的檢視,瀏覽器不會允許我們的程式碼在非宿主伺服器環境下執行。當然如果你使用Visual Studio那麼直接在目標html檔案上右鍵,選擇‘View In Browser’即可。

瀏覽器支援

大部分的現代瀏覽器都支援本程式碼。針對IE8及以下的瀏覽器,有一份單獨的程式碼來支援,但很不幸,這份程式碼遠多於100行。因此這程式碼不是百分百跨瀏覽器相容的,所以當你決定在專案中使用時需要對程式碼進行微調。

興趣點

This example demonstrates這個示例向我們展示了對於非常明確地需求來說,真沒必要全部使用js庫和框架來實現。Web應用是資源密集型的,最好只使用必要的程式碼而丟掉其他多餘部分。

目前的程式碼能做的就這些了。沒有諸如Web服務呼叫,動態事件繫結功能的。很快我會提供支援更多特性的升級版本。

100 行程式碼實現的 JavaScript MVC 樣式框架原文請看這裡