有些同学反映说,需要看太多的篇章才能明白如何使用JavaScriptMVC来开发,可不可以 用一篇把主要用到技术介绍一下,这样就可以快速入门,并且可以快速用到开发项目的。 这篇文章就是这个目的,下面我们来讲述如何快速开发。 也就是我们习惯的的开发,自己创建项目,模块等。
不过不管怎样,我们都需要下载JavaScriptMVC包,下载它,然后把它解压到我们的项目中。 解压完JavaScriptMVC包后,我们看到4个文件夹,文件列表如下:
documentjs - documentation engine funcunit - testing app jquery - jquery and jQueryMX plugins steal - dependency management js - JS command line for Linux/Mac js.bat - JS command line for Windows
注意:这个库文件夹,我们统称为根目录。
获取JavaScriptMVC运行
JMVC使用steal来管理。Steal加载脚本。像JMVC中使用到特性$.Controller和$.View,steal是这样加载的:
steal('jquery/controller','jquery/view/ejs',function(){ //code that uses controller and view goes here })
在使用steal之前,你需要把steal脚本添加到你的页面中。在根目录下创建一个todos文件夹,
然后在这个文件夹下面 创建一个todos.html和todos.js文件:
ROOT\ documentjs\ jquery\ funcunit\ steal\ todos\ todos.js todos.html
修改todos.html文件,把steal.js和todos.js脚本加载进来:
<!DOCTYPE html> <html> <head></head> <body> <ul id='todos'></ul> <input id='editor'/> <script type='text/javascript' src='../steal/steal.js?todos/todos.js'> </script> </body> </html>
在浏览器中打开这个页面,然后使用debug工具可以看到steal.js和todos.js已经加载进来。
Steal steal([paths])
Steal是使用来加载脚本,样式,甚至CoffeeScript,LESS和模板到你的程序中。当然这个只是Steal其中的一个特性。 Path是假设相对于根目录的。这就意味下述加载jquery/class/class.js文件是没有问题的:
steal('jquery/class/class.js');
你可以使用./来加载相对于目前文件夹的文件:
steal('./helpers.js')
Steal也支持CSS,下述就是加载todos/todos.css:
steal('./todos.css')
因为加载像jquery/class/class.js这种路径太普遍,如果你没有提供后缀名为.js的文件,Steal将会加载把文件夹名称加.js后缀的文件。 像下述也是加载jquery/class/class.js文件:
steal('jquery/class')
Steal是一个异步的载入器,所以你不能这样写:
steal('jquery/class') $.Class
应该是这样:
steal('jquery/class', function(){ $.Class })
对于这个程序,我们将加载jQueryMX的常用插件。添加最终结果如下述:
steal('jquery/class', 'jquery/model', 'jquery/dom/fixture', 'jquery/view/ejs', 'jquery/controller', 'jquery/controller/route', function($){ })
下面讲述的都当我们开发todos程序时需要使用到的各个插件。
$.Class
$.class([name],[staticProperties],[prototypeProps])
从构造函数可以看出$.Class是使用来创建一个对象的。
$.Controller和$.Model都使用到它。
创建自定义类,调用$.Class,然后给传递以下参数:
1、name 类名
2、staticProperties 静态属性
3、prototypeProperties 成员属性
$.Class使用的原型链,所以子类很容易扩展它,如下:
steal('jquery/class', function(){ $.Class("Todo",{ init : function(){}, author : function(){ ... }, coordinates : function(){ ... }, allowedToEdit: function(account) { return true; } }); Todo('PrivateTodo',{ allowedToEdit: function(account) { return account.owns(this); } }) });
$.Class还提供了在父类中使用简短的a_super方法可以调用父类的方法:
var SecureNote = Todo({ allowedToEdit: function(account) { return this._super(account) && this.isSecure(); } })
构造器/初始化 new Class(arg1,arg2)
当一个类的构造函数被调用时,$.Class将实例化并且调用init函数,它接收参数。
$.Class('Todo',{ init : function(text) { this.text = text }, read : function(){ console.log(this.text); } }) var todo = new Todo("Hello World"); todo.read()
注:在init执行前还有一个setup方法,setup能使用来改变参数,然后再传递给init方法。
Model $.Model(name,staticProperties,prototypeProperties)
数据模型对于任何程序都是主要的。它包含数据和逻辑为一体。 你用你的专用方法去继承
$.Model $.Model提供了一个Set方法管理变化。 创建一个数据模型,调用$.Model,传递以下参数:
1、name 类名
2、staticProperties 静态属性,包含findAll,findOne,create,update,destroy属性
3、prototypeProperties 成员属性 下述就是使用$.Model来创建一个数据模型:
steal('jquery/class', 'jquery/controller', 'jquery/model', 'jquery/view/ejs', 'jquery/dom/fixture', function($){ $.Model('Todo',{ findAll : "GET /todos", findOne : "GET /todos/{id}", create : "POST /todos", update : "PUT /todos/{id}", destroy : "DELETE /todos/{id}" }, {}) });
注:在你的浏览器中试试下述的指令。
new $.Model(attributes)
创建一个todo实例:
var todo = new Todo({name: "do the dishes"});
attr model.attr(name,[value])
$.Model.prototype.attr 可以读取和设置数据模型实例中属性。
todo.attr('name') //-> "do the dishes" todo.attr('name', "wash the dishes" ); todo.attr() //-> {name: "wash the dishes"} todo.attr({name: "did the dishes"});
与服务端交互
Model使用静态函数findAll,findOne,create,update和destroy函数去创建,检索,更新和删除数据模型在服务端。 现在你可以在Todo中调用这些函数与服务端做些数据变化的交互了。 执行下面这句代码:
Todo.findAll({});
我们可以在浏览器的Debug工具看到一个GET/todos请求。
如果你的服务端没有这个/todos服务,这个请求会出错。为了没有后台,我们也可以正常开发, JavaScriptMVC给我们提供了一个$.fixture,它可以模拟服务端的服务。
$.fixture
$.fixture(url,fixture(original,settings,headers))
Fixtures需要一个模拟请求的特定URL。Fixture的回调函数需要的参数如下:
1、original 原文
2、settings Ajax设置
3、headers 请求头部 它把返回的参数数组传递给jQuery的Ajax传送完成回调系统:
return [ 200, "success", {json: []}, {} ];
像: 模拟todo服务,在下述代码中添加回调函数:
// our list of todos var TODOS = [ {id: 1, name: "wake up"}, {id: 2, name: "take out trash"}, {id: 3, name: "do dishes"} ]; // findAll $.fixture("GET /todos", function(){ return [TODOS] }); // findOne $.fixture("GET /todos/{id}", function(orig){ return TODOS[(+orig.data.id)-1]; }) // create var id= 4; $.fixture("POST /todos", function(){ return {id: (id++)} }) // update $.fixture("PUT /todos/{id}", function(){ return {}; }) // destroy $.fixture("DELETE /todos/{id}", function(){ return {}; })
现在你可以使用Model的Ajax方法去执行增删查改todos了。
findAll findAll(params,success(todos),error())
findAll返回多个todos:
Todo.findAll({}, function( todos ) { console.log( todos ); })
findOne findOne(params,success(todo),error())
findOne返回单个todo:
Todo.findOne({}, function( todo ) { console.log( todo ); })
save todo.save(success(todo),error())
Save 可以创建或者修改实例,如果实例已经存在则是修改,否则是创建。 创建一个todo实例,然后调用它的save方法,即可实现在服务端创建一个Todo:
var todo = new Todo({name: "mow lawn"}) todo.save(function(todo){ console.log( todo ); })
在上面已经创建Todo实例的基础上,我们修改它的属性值,然后再执行save方法,那么它就是修改服务端的todo。
var todo = new Todo({name: "mow lawn"}); todo.save( function(todo){ console.log("created", todo ); todo.attr("name", "mow my lawn") todo.save( function( todo ) { console.log("updated", todo ); }) })
destroy todo.destroy(success(todo),error())
Destroy删除服务端的一条记录。
var todo = new Todo({name: "mow lawn"}); todo.save( function(todo){ console.log("created", todo ); todo.destroy( function( todo ) { console.log("destroyed", todo ); }) })
bind todo.bind(event,handler(ev,todo))
监听数据模型的变化是MVC结构的相关内容。Model让你可以给一个模型实例或者所有模型实例绑定变化监听。 例如,你可以监听一个数据模型实例的创建:
var todo = new Todo({name: "mow lawn"}); todo.bind('created', function(ev, todo){ console.log("created", todo ); }) todo.save()
你可以给这个数据模型绑定创建监听:
Todo.bind('created', function(ev, todo){ console.log("created", todo ); })
Model产生下述的事件:
1、created - 监听创建事件
2、updated - 监听修改事件
3、destroyed - 监听删除事件
$.fn.model $(el).model([modelInstance]) 给HTML元素绑定数据模型实例是非常有用的,它可以让我们在后续操作该模型提供方便。 jQuery.fn.model插件就是用来给一个HTML元素设置和获取一个数据模型实例的。 设置实例:
var li = $('<li>').model( new Todo({id: 5}) ) .appendTo("#todos");
这将给<li>元素的class添加"todo todo_5"样式。那我们就可以通过<li>获取这个实例了:
li.model().id //-> 5
elements todo.elements([context])
通过数据模型实例获取对应的HTML元素。Context元素让我们在这个元素内检索数据模型实例对应的HTML元素。
todo.elements('#todos');
View $.View(idOrUrl,data)
使用$.View很容易用JS模板创建HTML。通过它:
1、可以根据脚本标签的ID来使用这个模板在内容。
2、把数据传递给该模板 接着它返回的就是模板被渲染后的结果。例如,下述给todos.html添加模板:
<script type='text/ejs' id='todosEJS'> <% for(var i = 0; i < this.length; i++ ){ %> <li><%= this[i].name %></li> <% } %> </script>
用一个todos列表渲染它:
Todo.findAll( {}, function( todos ){ console.log( $.View( 'todosEJS', todos ) ); });
$.View也可以通过URL来做为一个模板位置。创建一个todos/todos.ejs文件,它的内容如下:
<% for(var i = 0; i < this.length; i++ ){ %> <li><%= this[i].name %></li> <% } %>
渲染它:
Todo.findAll( {}, function( todos ){ console.log( $.View( 'todos.ejs', todos ) ); });
$.View可以使用任何的模板语言,像JAML,TMPL,Mustache等,和给它们加强以下功能:
1、可以通过脚本标签或者文件加载模板
2、用jQuery修饰符使用模板,像HTML
3、模板缓存
4、支持延迟
5、压缩模板
Modifiers - el.modifier(idOrUrl,data)
$.View重写了jQuery的HTML修饰符after,append,before,html,prepend,replaceWidth,和 text,允许你这样写:
Todo.findAll( {}, function( todos ){ $('#todos').html( 'todos.ejs', todos ); });
想要看到上面代码执行的结果,要确保todos.html中有一个#todos的元素:
<ul id='todos'></ul>
延迟
$.Model的Ajax方法返回一个延迟对象。$.View接受延迟对象,
$('#todos').html('todos.ejs', Todo.findAll() )
这个语句表示直到Todo.findAll发送Ajax请求完成的结果传递给todos.ejs模板,最后渲染模板。
Hookup
$.View.hookup可以实现在模板元素执行回调。这些回调函数都是模板已经插入到DOM中后被调用。 你可以在元素中这样调用jQuery方法:
<li <%= ($el) -> $el.fadeIn() %> style='display:none'> <%= this[i].name %> </li>
在我们现在代码,添加一个返回带箭头函数语法的神奇的标签(<%= %>) ,这个参数传递给函数将封装元素。 如果我们想把Model添加到EJS模板中的元素上。修改todos.ejs:
<% $.each(this, function(i, todo){ %> <li <%= ($el) -> $el.model(todo) %>> <%= todo.name %> <a href="javascript://" class='destroy'>X</a> </li> <% }) %>
又或者修改成如下:
<% $.each(this, function(i, todo){ %> <li <%= todo %>> <%= todo.name %> <a href="javascript://" class='destroy'>X</a> </li> <% }) %>
它们的功能都是一样,那么为什么这段代码也执行了回调函数el.model(todo)呢,那里因为我们EJS提供了默认的$.View.hookup方法, 如果以上面这种编写,它会自动调用$.fn.model来给元素绑定模型。
Controller
$.Controller(name,classProps,prototypeProps)$.Controller创建可组织的,释放内存泄漏,快速执行,标准的jQuery控件。它使用来创建UI控件像tabs,grids和菜单, 让我们开发一个基本的todos控件,它能显示todos列表和删除。在todos.js添加下述代码:
$.Controller("Todos",{ "init" : function( element , options ){ this.element.html('todos.ejs', Todo.findAll() ) } })
我们把这个控件创建在#todos元素上。
new Todos('#todos', {});
init $.Controller.prototype.init(element,options)
Init是当一个新控制器实例被创建后被调用。它接收2个参数:
1、element 这个元素需要传递给控制器。它可以是一个JQuery元素,一个原始元素,或者一个CSS选择器。在控制器实例中this.element指的 就是这个元素。
2、options 这个参数传递给一个新的控制器,扩展控制静态属性defaults。那么在控制器我们就可以 使用this.options来使用这些参数。 它可以传递其它一些参数给新控制器。例如:
$.Controller("Todos", { defaults : {template: 'todos.ejs'} }, { "init" : function( element , options ){ element.html(options.template, Todo.findAll() ) } }) new Todos( document.body.firstElementChild ); new Todos( $('#todos'), {template: 'specialTodos.ejs'})
element this.element
this.element指的就是被控制器封装后的元素。
options this.options
this.options 是传递给控制的第二个参数它合并到控制器的静态default属性中。
监听事件
控制器自动绑定属性方法,它看起来像事件句柄。 监听<li>元素的点击事件:
$.Controller("Todos",{ "init" : function( element , options ){ this.element.html('todos.ejs', Todo.findAll() ) }, "li click" : function(li, event){ console.log("You clicked", li.text() ) // let other controls know what happened li.trigger('selected'); } })
当一个<li>元素被点击后,"li click"将被调用。 控制器使用事件委托,所以你能给<li>元素添加事件而不需要重复绑定事件句柄。 当点击X链接后,会删除一条todo记录。
$.Controller("Todos",{ "init" : function( element , options ){ this.element.html('todos.ejs', Todo.findAll() ) }, "li click" : function(li){ li.trigger('selected', li.model() ); }, "li .destroy click" : function(el, ev){ // get the li element that has the model var li = el.closest('.todo'); // get the model var todo = li.model() //destroy it todo.destroy(function(){ // remove the element li.remove(); }); } })
事件句柄模板第一部分 - 事件
通过点位符来实现定制事件行为。下述就是允许我们定制销毁一个todo定制事件:
$.Controller("Todos",{ "init" : function( element , options ){ ... }, "li click" : function(li){ ... }, "li .destroy {destroyEvent}" : function(el, ev){ // previous destroy code here } }) // create Todos with this.options.destroyEvent new Todos("#todos",{destroyEvent: "mouseenter"})
控制中的{name}的值是从控制器的this.options或者Window中检索到的,然后替换它。 下面的代码实现的定制事件就是从Window中检索替换的:
$.Controller("Todos",{ "init" : function( element , options ){ ... }, "li click" : function(li){ ... }, "li .destroy {Events.destroy}" : function(el, ev){ // previous destroy code here } }) // Events config Events = {destroy: "click"}; // Events.destroy is looked up on the window. new Todos("#todos")
事件句柄前面的选择器也可以模板化。
事件句柄模板第二部分 - 对象
控制器也可以使用事件句柄模板给一个对象绑定事件。这个对于避免内存泄漏非常关键,所以在MVC程序会经常碰到。
如果这个{name}是一个对象,这个对象将被绑定。 例如,下述是点击窗口会出现一个tooltip的监听:
$.Controller("Tooltip",{ "{window} click" : function(el, ev){ // hide only if we clicked outside the tooltip if(! this.element.has(ev.target ) { this.element.remove(); } } }) // create a Tooltip new Tooltip( $('<div>INFO</div>').appendTo(el) )
当我们想监听一个数据模型更新时,就可以使用事件句柄模板来监听数据模型的变化。
$.Controller("Todos",{ "init" : function( element , options ){ this.element.html('todos.ejs', Todo.findAll() ) }, "li click" : function(li){ li.trigger('selected', li.model() ); }, "li .destroy click" : function(el, ev){ el.closest('.todo') .model() .destroy(); ev.stopPropagation(); }, "{Todo} destroyed" : function(Todo, ev, destroyedTodo){ destroyedTodo.elements(this.element) .remove(); }, "{Todo} updated" : function(Todo, ev, updatedTodo){ updatedTodo.elements(this.element) .replaceWith('todos.ejs',[updatedTodo]); } }) new Todos("#todos");
这样,代码看来非常好看,非常符合MVC模型的要求。
destroy controller.destroy()
$.Controller.prototype.destroy将释放HTML元素的控制器的事件句柄。但是不会删除这个HTML元素。
var todo = new Todos("#todos") todo.destroy();
当一个绑定有控制器的HTML元素从页面上销毁掉,那么绑定到这个HTML元素上的控制器的destroy自动会被调用。
new Todos("#todos") $("#todos").remove();
注:如果程序使用事件句柄模板在HTML中的Body元素上,那么执行$(document.body).empty()将把所有数据释放。
update controller.update(options)
$.Controller.prototype.update 更新控制器的this.options和重新绑定所有事件句柄。 当你想监听一个特别的模型时非常有用:
$.Controller('Editor',{ update : function(options){ this._super(options) this.setName(); }, // a helper that sets the value of the input // to the todo's name setName : function(){ this.element.val(this.options.todo.name); }, // listen for changes in the todo // and update the input "{todo} updated" : function(){ this.setName(); }, // when the input changes // update the todo instance "change" : function(){ var todo = this.options.todo todo.attr('name',this.element.val() ) todo.save(); } }); var todo1= new Todo({id: 6, name: "trash"}), todo2 = new Todo({id: 6, name: "dishes"}); // create the editor; var editor = new Editor("#editor"); // show the first todo editor.update({todo: todo1}) // switch it to the second todo editor.update({todo: todo2});
注:因为我们重写了update,我们必须调用_super。
路由
$.route是JavaScriptMVC路由功能的核心。它是一个观察者对象,监听window.location.hash的变化。它是一个非常完善的路由行为,这个指南有很复杂。但是,它也可以使用到比较基础的用例中。在控制器中使用"route"事件来监听路由:
$.Controller("Routing",{ "route" : function(){ // matches empty hash, #, or #! }, "todos/:id route" : function(data){ // matches routes like #!todos/5 } }) // create routing controller new Routing(document.body);
上面代码表达的意思:名为"route"的函数是当没有路由数据时被调用。而"todos/:id route"是有像{id: 6}这样的数据时被调用。我们可以通过改变路由数据来更新路由:
$.route.attr('id','6') // location.hash = #!todos/6
或者自己设置hash的值:
var hash = $.route.url({id: 7}) // #!todos/7 location.hash = hash;
下面升级一点让Routing控制器去监听".todo selected" 事件和更新路由。当路由更新后,它从服务端检索到的Todo数据更新到对应的编辑控件中。
$.Controller("Routing",{ init : function(){ this.editor = new Editor("#editor") new Todos("#todos"); }, // the index page "route" : function(){ $("#editor").hide(); }, "todos/:id route" : function(data){ $("#editor").show(); Todo.findOne(data, $.proxy(function(todo){ this.editor.update({todo: todo}); }, this)) }, ".todo selected" : function(el, ev, todo){ $.route.attr('id',todo.id); } }); // create routing controller new Routing(document.body);
FuncUnit
JavaScriptMVC使用FuncUnit来测试。FuncUnit提供编写功能测试的API,它能模拟鼠标和键盘动作。创建一个FuncUnit测试:
1、创建一个测试文件,它引入funcunit模块。
2、创建一个funcunit.html页面加载你的测试文件。
在todos目录下,新建一个fununit.html文件,并且添加以下HTML:
<html> <head> <link rel="stylesheet" type="text/css" href="../funcunit/qunit/qunit.css" /> <script type='text/javascript' src='../steal/steal.js?todos/funcunit.js'></script> </head> <body> <h1 id="qunit-header">Todos Test Suite</h1> <h2 id="qunit-banner"></h2> <div id="qunit-testrunner-toolbar"></div> <h2 id="qunit-userAgent"></h2> <div id="test-content"></div> <ol id="qunit-tests"></ol> <div id="qunit-test-area"></div> </body> </html>
现在新建fununit.js文件并且添加以下代码:
steal('funcunit', function(){ module('todos') test('todos test', function(){ ok(true, "the test loads"); }) })
在浏览器中打开fununit.html。一个测试用例通过。
编写一个测试
使用S.open是通知测试打开这个todos页面。
S.open("//todos/todos.html");
这个页面一打开,我们选择第一个todo点击它:
S(".todo:first").click();
S是复制jQuery的$作法添加到FuncUnit的API中。这个编辑输入框将出现。什么等待命令告诉FuncUnit将等待:
S("#editor").val("wake up", "First Todo added correctly");
第2个参数是一个断言信息。如下述一样,在Steal回调函数中替换成下面的代码:
module('todos', { setup: function(){ S.open("//todos/todos.html"); } }) test('open first todo', function(){ S(".todo:first").click(); S("#editor").val("wake up", "First Todo added correctly"); })
刷新funcunit.html页面。你将看到在一个独立的窗口打开页面和执行这个测试。自动化 上面这些测试也可以自动化,通过控制台执行下面的命令:
./js funcunit/run selenium todos/funcunit.html