代码量不到 100 行的 JavaScript MVC 风格框架
学习如何创建自己的 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
对象了,我们将在其中存储关于route
、template
和controller
的信息。
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
接受三个参数;controller
、route
和template
。它们是:
controller
:控制器函数的引用,每当访问此特定路由时都会调用该函数。route
:路由的路径。这仅仅是我们期望在URL的“#”符号之后的部分。template
:这是外部HTML文件,将作为此路由的视图加载。
现在,我们需要一个入口点来启动我们的库,以便解析URL并将关联的HTML模板提供给页面。为此,我们需要一个函数。Initialize
函数执行以下操作:
- 初始获取视图元素的引用。代码期望一个带有
view
属性的元素,以便在HTML页面中搜索它。 - 设置默认路由。
- 验证视图元素。
- 绑定窗口哈希更改事件,以便在URL中的哈希值不同时能够正确更新视图。
- 最后,启动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
。每次哈希值更改时都会调用此委托。以下是每次哈希值更改时需要执行的步骤序列:
- 获取哈希值。
- 从哈希值中获取路由值。
- 从路由映射对象
_routeMap
中获取路由对象routeObj
。 - 如果URL中没有找到路由,则获取默认路由对象。
- 最后,调用与路由关联的控制器,并在视图元素中提供所需的视图。
所有上述步骤都包含在以下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插件了。在下一个代码片段中,发生了以下情况:
- 在网页中导入代码。
- 添加路由及其控制器和视图模板信息。
- 创建控制器函数。
- 最后,初始化库。
除上述内容外,我们还需要链接以便我们可以导航到不同的路由,以及一个带有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>
<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, '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.bind
、Object.getOwnPropertyNames
和Array.forEach
等属性。因此,我们需要回退到IE 9以下浏览器支持的代码。
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应用程序。
设置所需的属性,如别名、物理路径、应用程序池和用户凭据;然后单击确定。
最后,打开Web应用程序内容并浏览您想要的HTML页面。
这是必要的,因为代码加载存储在外部文件中的视图,并且如果我们的代码不在专门的托管环境中运行,浏览器不允许这样做。作为替代方案,如果您正在运行Visual Studio,则右键单击HTML页面并选择“在浏览器中查看”。
浏览器支持
大多数现代浏览器都支持此代码,对于IE 8及更低版本,有一个单独的脚本,不幸的是,该脚本的代码行数远远超过100行。但是,它可能不是100%跨浏览器兼容的,如果您决定在项目中这样做,可能需要调整某些区域。
关注点
本例表明,对于非常具体的需求,我们确实不需要完整的库和框架。Web应用程序是资源密集型的,最好只使用所需的内容,而抛弃其余部分。
目前,这个代码没有太多可以做的。像Web服务调用、动态事件绑定等事情还不能完成。我将很快提供一个更新版本来支持更多功能。
历史
- 2015年2月2日:初始版本