O Mvc e o Javascript – Na prática

Fala galera,

Eu tinha prometido uma implementação do Mvc na prática, e resolvi fazer isso através de um screencast, porque era mais fácil de explicar, mais fácil de demonstrar…

Fiz uma aplicação simples usando a API do Flickr, e fiz já considerando que conhecem o esquema Mvc que comentei no post anterior, para não perder muito tempo com explicações.

Ainda quero fazer um post mostrando a aplicação da galeria de imagens, mas vou precisar de um pouco mais de tempo para formulá-lo.

Perdoem a voz de ganso morto, mas é a voz que Deus me deu. =)

Qualquer dúvida, ou crítica, postem aqui. =P

Abraço.

Screencast : O Mvc e o Javascript – Na prática
Aprox. 15 min

Anúncios

O Mvc e o Javascript – A teoria

E aí galera, espero que tenham passado um bom Natal.

Faz um tempinho que postei sobre Mvc em Javascript e me lembro que tinha dito que o próximo post não ia demorar a sair, mas no final do ano, como sabem é uma correria pra tudo, estive muito ocupado e não tive tempo de continuar a nossa saga, então peço desculpas para todos.

Mas agora que estou na vagabundagem total, vou dar continuidade ao nosso assunto.

No último post, tratamos a parte server, mostrei que é possível desenvolver uma aplicação que funcione na parte server e servir um json para que funcione a parte Client sem nenhum retrabalho, lembram?

Neste post, gostaria de mostrar a implementação da Classe Mvc e como eu a utilizo numa aplicação simples em teoria.

A idéia que eu tenho do Mvc em Javascript é que deve se basear apenas na separação em três camadas como a própria definição deste Pattern, então eu não desenvolvi nada que gere arquivos numa estrutura definida ou que automatize testes como os frameworks mais conhecidos fazem. O que eu quero mostrar é uma forma de desenvolver nestas 3 camadas para facilitar a abstrair de maneira simples sistemas complexos que usem Ajax.

Vamos para o que interessa então. Abaixo está a classe Mvc, que na verdade não retorna um objeto como uma classe normalmente faz. Esta classe Mvc vai receber 3 singletons, Model, View e Controller, e vai estendê-los para que recebam métodos e propriedades que serão úteis no modelo de desenvolvimento que vamos seguir.

(function(namespace){

	var Interface = {
		actions : function(){			
			this . request = this.request || function(){
				throw new Error('Implemente o método request em sua classe/objeto')	
			}
			this . callback = this.callback || function(){
				throw new Error('Implemente o método callback em sua classe/objeto')
			}
		}
	}
	
	var Mvc = {
		
		Class : function( Model, View, Controller ){
			
			Mvc.Model.apply( Model )
			Mvc.View.apply( View )
			Mvc.Controller.apply( Controller, [Model] )
						
		},
		
		Model : function(){
			this.data = this.data || {}
		},
		
		View : function(){
			var pattern =  /\@\{(\w*)\}/g
			
			this.template = ''
			this.inject = function(json, template){
				var html = (template || this.template).replace(pattern, function(key){
					return json[key.slice(2, -1)]
				})
				return html
			}
			
		},
		
		Controller : function(Model){
			
			Interface.actions.apply(this)
			
			var self = this			
			var callback = this.callback
			
			this.action = new this.Action.Class
			
			this.callback = function(data){
				Model.data = data
				callback.call(self, data)
			} 
			
			this.run = function( json, url ){
				var url = url || location.href
				var result = null
					for (var key in json){
						var aux = key.replace(/\:(\w*)/, '([^&/#?]*)')
							if (result = url.match( aux )){
								result.shift()
								json[key].call(this, decodeURIComponent(result[0]))
							}
					}
			}
			
		},
		
		New :function(){ this.apply(null, arguments) }
		
	}
	
	namespace.Mvc = Mvc.Class
	namespace.Mvc.New = Mvc.New
	
})(window)

Pode ser que seja difícil de entender a primeira vista o que a classe faz apenas olhando para ela, mas com a prática, fica bem claro sua utilização.

Como eu havia comentado ela vai receber 3 singletons, a partir de agora vou montar passo a passo a estrutura de uma simples aplicação utilizando o Mvc seguindo a ordem de carregamento dos scripts. Então, carregada a classe Mvc, vamos seguir com a nossa estrutura.

Para facilitar a compreensão da separação das camadas, vou gerar vários arquivos para fazer sentido para mim. Você claro pode fazer tudo em apenas um arquivo, mas eu não encorajo fazer isso. Eu sempre acho melhor separar em vários arquivos e carregá-los todos usando um minify para fazer apenas uma requisição. Considerem neste post que estamos utilizando uma minify que chamará todos os arquivos js numa requisição só, bele?

Minha aplicação Ajax terá o nome de Home, porque vai ser carregada na Home do meu site.
Então, o primeiro arquivo a ser gerado é o home.model.js:

;(function(namespace){
	
	var Model = {
		data : {},
		params  : {
			n : '',
			q : 2
		},		
		url : 'index.php'
	}
	
	namespace.Home = { Model :Model }
	
})(Mvc)

Vejam que já passei para meu escopo privado o objeto Mvc que já está criado, pois carregamos o Mvc.js. Para evitar ficar criando objetos globais no javascript, vou utilizar o objeto Mvc para guardar dados da minha aplicação, dados que fazem parte do sistema Mvc.

No caso acima, eu criei um singleton Model, que armazenará dados da requisição Ajax.

data carrega o resultado da requisição Ajax, não é necessário criar o data, porque a classe Mvc vai criar na hora de estender, mas acho uma boa prática deixar indicado no objeto todos as suas variáveis.

A Classe Mvc apenas precisa da variável data do Model, as outras variáveis pode ser dado o nome que quiser. Usei params para armazenar os parametros que serão passados na requisição e url, lógico, a url da requisição.

No final eu crio um namespace Home para armazenar este Model, pois o objeto está dentro de um escopo fechado e lógicamente está privado, não é possível acessar de fora. Portanto, a classe Mvc agora tem um Model armazenado dentro do namespace Home, sempre o Model é o responsável por iniciar este namespace.

O Model não vai receber nenhum método, porque seu papel é apenas armazenar dados.

O próximo arquivo a ser gerado é o home.view.js:

;(function(namespace){
	
	var View = {
		
		index : 
			'<p class="title">@{title}</p>' +
			'<div class="description">@{description}</div>',
			
		not_found :
			'<p>Resultado não encontrado</p>'
	}	
	
	namespace.Home.View = View
	
})(Mvc)

Este singleton é o responsável por armazenar o html que vai ser exibido na página após uma requisição, ele vai receber um método muito importante chamado .inject que será responsável por mesclar os dados dinâmicos da resposta da requisição com o template do View. Criei uma View index que mostrará o resultado do ajax e uma View not_found que mostrará uma mensagem de 404 caso nossa requisição não encontre dados.

De novo, passo o namespace do Mvc e crio uma nova variável View para o namespace Mvc.Home.

Então vamos para o próximo arquivo a ser gerado, que é o home.controller.js:

;(function(namespace, Model, View){
	
	var Controller = {

		request :function(url){
			$.getJSON( Model.url, Model.params, this.callback ) 
		},
		
		callback :function(){
			
		},
		/**
		 * @Actions
		 */
		Action :{
			
			Class :function(){
				
				this.home = function(){
					
				}
				
				this.not_found = function(){
					
				}

			}
		}
		
	}

	namespace.Home.Controller = Controller
	
})(Mvc, Mvc.Home.Model, Mvc.Home.View)

Vejam que passo Mvc.Home.Model e Mvc.Home.View que já existem, pois já carregamos a Model e View, e uso as variáveis Model e View nos parâmetros formais para que se um dia eu precisar alterar estes namespaces, não precise alterar os nomes das variáveis de dentro da função de escopo.

A classe controller é a mais complexa das 3, porque é onde estará a lógica que usará o Model e o View para retornar algo consistente. Esta classe precisa ter dois métodos obrigatórios, que é o .request e o .callback, como podem ver no Mvc.js, onde utilizo interface para garantir que a classe utilize estes métodos, caso contrário, gerará um erro dizendo para implementar estes métodos.

Isto porque estes métodos podem variar na sua implementação, e são obrigatórios porque considero no Mvc que um controller precisa requisitar algo e precisar ter um método de callback no retorno, mesmo que este callback não faça nada.

Além destes métodos, existe também dentro da Controller, uma outra Classe Action, que armazenará as actions que precisamos para nossa aplicação, será nelas que nossa lógica será desenvolvida, utilizando-se do Model e das Views. Criei ali duas actions, uma chamada home e outra chamada not_found, para quando uma requisição retornar um Model.data vazio, caia num resultado 404.

Após estendido, o nosso objeto Controller vai receber um método muito útil, que será utilizado o tempo todo para modificar os parâmetros do Model, este método é o .run .

Ele é um método sobrecarregado que recebe um json com rotas como primeiro parâmetro e recebe uma url como segundo parâmetro opcional, caso não passe esta url, a rota será executada a partir da url do navegador.

No final da função de escopo do singleton eu adiciono mais uma variável ao namespace Mvc.Home que é a Controller.

Assim, fechamos nosso namespace Mvc da Home, possuindo as três camadas: Mvc.Home.Model, Mvc.Home.View, Mvc.Home.Controller.

Dessa forma, não precisamos criar 3 variáveis globais para poder acessá-las de qualquer lugar, apenas as armazenamos dentro da variável global Mvc. =)

Enfim, faltam as rotas.
Então, geramos a home.routes.js:

;(function(namespace, Model, View, Controller){
	/**
	 * @Routes
	 */
	Controller.routes = {
		
		request :{
			'#/:page/' :function( page ){
				this.action[page] ?
					this.action[page]() :this.action['not_found']()
			}	
		},
		params :{
			'option1/:param' :function( param ){
				
			},
			
			'option2/:param' :function( param ){
				
			}
		}

	}
})(Mvc, Mvc.Home.Model, Mvc.Home.View, Mvc.Home.Controller)

Novamente passo os namespaces para a função de escopo, para me certificar de que vou poder enxergar o Model, o View e o Controller da home.

As rotas são executadas através do método .run() do Controller, e os seus métodos são executados no escopo da Controller, dá para ver isso ali na rota de request, onde eu acesso this.action que é uma propriedade da Controller. As rotas devem ficar na Controller.

Esta propriedade this.action é criada através daquela classe que criamos na home.controlller.js, como criamos duas actions, home e not_found, podemos resgatá-las de dentro das funções das rotas usando this.action.home, this.action.not_found ou utilizando a outra notação, this.action[‘home’] e this.action[‘not_found’]

As rotas vão o tempo todo modificar os parametros do Model, e elas que serão as responsáveis por chamar a action correta.

Eu separei as rotas em duas categorias, a request e a params. São nomes que eu preferi dar, não são nomes obrigatórios.
“request” é a rota que usarei para as requisições, ou seja, essa rota será executada pela controller caso encontre seus parametros na url do browser no momento do callback de uma requisição.

“params” será a rota que usarei para modificar os parametros do Model.

O método .run da Controller será o encarregado de percorrer estas rotas e separar seus parâmetros, é um método muito útil e muito poderoso, porque você pode usar qualquer separador que quiser e usar qualquer nome que desejar para identificar parametros. Ou seja, se eu quisesse procurar numa url querystrings ao invés do padrão de barras, poderia criar uma rota assim:

{
	'opcao1=:opcao' :function( op ){}
	'opcao2=:outraopcao' :function (outraop){}
}

Então, simplificando, o método .run pode percorrer a rota acima e procurar o valor de opcao1 e executar a função correspondente passando o valor para a mesma.
Por isso o método .run é tão poderoso, porque você pode escolher a os nomes dos parametros da sua rota. Eu usei :param na nossa rota, mas poderia usar qualquer palavra seguida de “:”.

Faltaria ainda um último singleton que é o Home propriamente dito, que é um objeto que fará toda a lógica da nossa página, e que apenas vai receber da nossa controller as views com o conteúdo dinâmico, além de iniciar a classe Mvc.

Este singleton não faz parte do Mvc, ele apenas será responsável por cuidar da parte dos eventos, e da apresentação geral do nosso site. Talvez vocês até já utilizam esse singleton como padrão de projeto nos seus sites:

;(function(namespace, Model, View, Controller){	

	var Home  = {

		initialize :function(){
			
		}, 
		
		display :function(){
			
		}
		
	}
	
	Mvc.New( Model, View, Controller )
	namespace.Home = Home
	
	$(function(){ Home.initialize() })
	
})(window, Mvc.Home.Model, Mvc.Home.View, Mvc.Home.Controller)

Este singleton está fora do Mvc, porque é um objeto que cuida da página em geral e consequentemente cuidará do resultado do ajax, tratado pela Controller.

Ele está fora do namespace Mvc, ele será um objeto global Home para poder ser usado de dentro de qualquer Action da Controller, por isso passo window como namespace para a função de escopo ali em baixo.

Eu também passo os namespaces contendo o Model, View, Controller e Routes da Home.

É nesse singleton que eu estendo os 3 singletons, a partir desta linha é que estes objetos serão verdadeiramente um Model, um View e um Controller reespectivamente.

É isso galera, neste post queria passar em teoria como fica estruturada a arquitetura do sistema no padrão Mvc, o assunto não é muito complexo mas é extenso, e é preciso escrever muito para poder explicar a lógica, o post acaba ficando muito grande. Por isso ainda vou postar mais dois capítulos desta saga.

Se você ainda não entendeu muito bem toda a lógica, tudo bem, a minha intenção não é que entenda como funciona tudo, mas de familiarizar com o padrão.

No próximo post, farei um sistema básico em Mvc funcionando, mostrando na tela o resultado de um ajax das imagens do Flickr. Aí será um pouco mais fácil de entender toda a lógica.

No último, implementarei em Mvc nossa galeria de imagens. Na verdade eu já fiz isso, e já está funcionando, mas preciso preparar um post bem explicativo de forma a ser fácil de entender o sistema todo. Gostaria de fazer um screencast, para ficar mais didático, menos teórico e mais prático, vamos ver se vou conseguir rsrsrs.

Um grande abraço galera e até o próximo post.