65.9K
CodeProject 正在变化。 阅读更多。
Home

代码量不到 100 行的 JavaScript MVC 风格框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (7投票s)

2015年2月2日

MIT

7分钟阅读

viewsIcon

30381

downloadIcon

277

学习如何创建自己的 MVC 风格 JavaScript 框架。

引言

任何使用过AngularJS、Backbone或Ember等JavaScript框架的人都会熟悉UI上的MVC概念是如何工作的。这些框架让我们非常容易地在单页应用程序中根据URL实现自定义视图。事实上,这正是模型-视图-控制器术语的核心,即有一个控制器来处理传入的请求,一个视图来显示信息,以及模型来处理其他一切,从业务规则到数据访问。

考虑到以上信息,当我们想要创建一个需要在一个页面中切换视图的应用程序时,我们通常会使用上面提到的库/框架之一。但是,如果我们只想一个基于URL的视图切换框架,而不是像Angular和Ember那样捆绑的额外好处和功能呢?本文旨在解决这个特定需求,并提供一个既简单又有用的解决方案。

概念

该代码利用基于“#”的URL在应用程序中实现MVC风格的导航。应用程序以默认的“#”URL启动,根据哈希值,代码会加载适用的视图并将对象模型应用于视图的模板。

URL的格式如下:
http://域名/index.html#/路由名称

视图内容必须以{{属性名称}}的形式绑定对象模型的属性值。代码将查找这种特定的模板格式,然后用对象模型中包含的属性名称的值替换它们。

视图使用Ajax异步加载,并放置在HTML页面上的一个占位符中。视图占位符可以是任何元素(理想情况下应该是Div),但它必须有一个特定的属性,以便代码可以识别它,这也有助于实现非侵入性代码实现。当URL更改时,这个过程会重复,并加载另一个视图。听起来很简单,对吧!下面的流程图解释了这个特定实现中的信息流。

编写代码

我们将从基本的模块化设计模式开始,并在最后通过一个对象外观将我们的库暴露给全局作用域。

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

我们需要将view元素存储在一个变量中,以便可以多次使用它。

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 = {};
}

在此之后,是时候创建route对象了,我们将在其中存储关于routetemplatecontroller的信息。

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

对于每个不同的URL,都会有一个单独的路由对象routeObj。所有这些对象都将被添加到我们的_routeMap对象中,以便以后可以通过键值对关联来检索它们。

为了将路由添加到MVC库,我们需要从库外观暴露一个函数。所以,让我们创建一个函数来添加具有各自控制器的路由。

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

函数AddRoute接受三个参数;controllerroutetemplate。它们是:

  • controller:控制器函数的引用,每当访问此特定路由时都会调用该函数。
  • route:路由的路径。这仅仅是我们期望在URL的“#”符号之后的部分。
  • template:这是外部HTML文件,将作为此路由的视图加载。

现在,我们需要一个入口点来启动我们的库,以便解析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. 从路由映射对象_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 Request异步加载适当的视图。为此,我们将把路由对象的值和视图元素传递给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;
}

最后,我们将把插件暴露给JavaScript全局作用域的外部世界。

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

现在是时候在我们的单页应用程序中使用这个MVC插件了。在下一个代码片段中,发生了以下情况:

  1. 在网页中导入代码。
  2. 添加路由及其控制器和视图模板信息。
  3. 创建控制器函数。
  4. 最后,初始化库。

除上述内容外,我们还需要链接以便我们可以导航到不同的路由,以及一个带有view属性的容器元素来包含视图模板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>&nbsp;
   
        <a class="NavLink" href="index.html#/contact">Contact</a>&nbsp;

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

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

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

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

在上面的代码中,有一个针对Internet Explorer的条件注释。

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

如果IE版本低于9,则不支持function.bindObject.getOwnPropertyNamesArray.forEach等属性。因此,我们需要回退到IE 9以下浏览器支持的代码。

home.htmlcontact.htmladmin.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应用程序。

设置所需的属性,如别名、物理路径、应用程序池和用户凭据;然后单击确定

最后,打开Web应用程序内容并浏览您想要的HTML页面。

这是必要的,因为代码加载存储在外部文件中的视图,并且如果我们的代码不在专门的托管环境中运行,浏览器不允许这样做。作为替代方案,如果您正在运行Visual Studio,则右键单击HTML页面并选择“在浏览器中查看”。

浏览器支持

大多数现代浏览器都支持此代码,对于IE 8及更低版本,有一个单独的脚本,不幸的是,该脚本的代码行数远远超过100行。但是,它可能不是100%跨浏览器兼容的,如果您决定在项目中这样做,可能需要调整某些区域。

关注点

本例表明,对于非常具体的需求,我们确实不需要完整的库和框架。Web应用程序是资源密集型的,最好只使用所需的内容,而抛弃其余部分。

目前,这个代码没有太多可以做的。像Web服务调用、动态事件绑定等事情还不能完成。我将很快提供一个更新版本来支持更多功能。

历史

  • 2015年2月2日:初始版本
© . All rights reserved.