Salt JS Kullanılarak MVC Uygulamak
Şimdi herhangi bir üçüncü parti JS kütüphanesi kullanmadan, sadece JS kullanarak MVC yapmaya çalışalım. Önce arayüzün görünümüne bakalım:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Game</title> <style type="text/css"> @import url('css/bootstrap.css'); @import url('css/bootstrap-theme.css'); </style> <script type="text/javascript" src="js/lib/jquery-1.11.3.js"></script> <script type="text/javascript" src="js/lib/bootstrap.js"></script> <script type="text/javascript" src="game.js"></script> <script type="text/javascript" src="controller.js"></script> </head> <body> <p /> <div class="container" role="main"> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">Number Guessing Game</h3> </div> <div class="panel-body"> <div class="form-group"> <label for="tries">Tries</label> <span id="tries" class="badge"></span> <div class="progress"> <div class="progress-bar progress-bar-striped" id="pbTries" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="7" style="width: 0%"></div> </div> </div> <div class="form-group"> <label class="active">Counter</label> <span id="counter" class="badge"></span> <div class="progress"> <div class="progress-bar progress-bar-danger" id="pbCounter" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="25" style="width: 0%"></div> </div> </div> <div class="form-group"> <label for="guess">Guess:</label> <input type="text" id="guess"class="form-control" /> <button id="playButton" class="btn btn-success">Play</button> </div> <div id="validationMessageContainer" class="alert alert-danger" role="alert"> <strong id="validationMessage"></strong> </div> </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">History</h3> </div> <div class="panel-body"> <table id="history" class="table table-stripe"> <thead> <tr> <th>Guess</th> <th>Message</th> </tr> </thead> <tbody id="moves"> </tbody> </table> </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">Game Statistics</h3> </div> <div class="panel-body"> <div class="form-group"> <label class="active">Wins</label> <span id="wins" class="badge"></span> </div> <div class="form-group"> <label class="active">Average Win Time</label> <span id="avgWinTime" class="badge" data-bind="text: "></span> <div class="progress"> <div class="progress-bar progress-bar-danger" id="pbAvgWinTime" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="25" style="width: 0%"></div>
</div> </div>
</div> </div> </div> </body> </html>
var emptyElement = function(element) { var node = element; while (element.hasChildNodes()) { if (node.hasChildNodes()) { node = node.lastChild; } else { node = node.parentNode; node.removeChild(node.lastChild); } } }; var View = function(document){ var self= this; self.document= document; self.guess= self.document.getElementById("guess"); self.tries= self.document.getElementById("tries"); self.pbTries= self.document.getElementById("pbTries"); self.counter= self.document.getElementById("counter"); self.pbCounter= self.document.getElementById("pbCounter"); self.wins= self.document.getElementById("wins"); self.avgWinTime= self.document.getElementById("avgWinTime"); self.pbAvgWinTime= self.document.getElementById("pbAvgWinTime"); self.validationMessageContainer= self.document.getElementById("validationMessageContainer"); self.validationMessage= self.document.getElementById("validationMessage"); self.playButton= self.document.getElementById("playButton"); self.history= self.document.getElementById('history').getElementsByTagName('tbody')[0]; }; var Controller = function(model,view){ var self= this; self.view= view; self.model= model; self.updateCounterView = function(){ self.model.counter--; self.view.counter.innerHTML= self.model.counter; if (self.model.counter<=0){ self.model.init(); self.model.history.push(new Move('Time is out!','Game over!')); self.updateView(); } self.view.pbCounter.setAttribute('ariavalue',self.model.counter); self.view.pbCounter.setAttribute('style',"width: "+self.model.counter*4+"%"); }; self.updateStatisticsView = function(){ self.view.wins.innerHTML= self.model.statistics.wins; self.view.avgWinTime.innerHTML= self.model.statistics.avgWinTime; self.view.pbAvgWinTime.setAttribute('ariavalue',self.model.statistics.avgWinTime); self.view.pbAvgWinTime.setAttribute('style',"width: "+self.model.statistics.avgWinTime*4+"%"); }; self.updateHistoryTable = function() { emptyElement(self.view.history); for (i in self.model.history) { newRow = self.view.history.insertRow(self.view.history.rows.length); cellGuess = newRow.insertCell(0); cellMessage = newRow.insertCell(1); move = self.model.history[i]; cellGuess.appendChild(document.createTextNode(move.guess)); cellMessage.appendChild(document.createTextNode(move.message)); } }; self.updateTries = function(){ if (self.model.tries>0){ self.view.tries.innerHTML= self.model.tries; } else{ self.view.tries.innerHTML= 'New game'; } self.view.pbTries.setAttribute('ariavalue',self.model.tries) self.view.pbTries.setAttribute('style',"width: "+((self.model.tries*100)/7)+"%") }; self.updateValidationView = function(){ self.view.validationMessage.innerHTML= self.model.validationMessage; if (self.model.validationMessage.length>0){ self.view.validationMessageContainer.style.visibility = "visible"; } else { self.view.validationMessageContainer.style.visibility = "hidden"; } }; self.updateView = function(){ self.updateHistoryTable(); self.updateTries(); self.updateValidationView(); self.updateStatisticsView(); }; self.clickPlay = function(){ self.model.play(self.view.guess.value); self.updateView(); } self.init = function() { self.model.init(); self.updateView(); setInterval(self.updateCounterView, 1000); self.view.playButton.onclick = self.clickPlay; }; }; var Application = function() { var self= this; self.run = function(){ self.view= new View(document); self.model= new GameViewModel(); self.controller= new Controller(self.model,self.view); self.controller.init(); }; }; var app= new Application(); window.onload= app.run;
'use strict'; class Move { constructor (guess,message){ this.guess= guess; this.message= message; } }; class GameStatistics { constructor (){ this.wins= 0; this.totalWinTime= 0; this.avgWinTime= 0; } }; class GameViewModel { constructor() { this.tries = 0; this.secret= 0; this.history = []; this.counter=25; this.validationMessage= ""; this.statistics= new GameStatistics(); } init (){ this.history= []; this.tries= 0; this.secret= Math.floor(Math.random()*100)+1; this.counter=25; this.validationMessage= ""; } play (guess){ this.validationMessage= ""; if (isNaN(guess)){ this.validationMessage= "Enter a valid integer!"; return false; } guess = Number(guess); if (guess<1 || guess >100){ this.validationMessage= "Enter an integer between 1 and 100!"; return false; } for (let i=0;i<this.history.length;++i){ let move= this.history[i]; if (guess==move.guess){ this.validationMessage= "You have already played "+guess+"!"; return false; } } if (this.secret==guess){ this.statistics.wins++; this.statistics.totalWinTime += 25 - this.counter; this.statistics.avgWinTime = this.statistics.totalWinTime / this.statistics.wins ; this.init(); this.history.push(new Move(guess,"You win!")); return true; } let evaluation = "Pick smaller!"; if (guess < this.secret){ evaluation = "Pick larger!"; } this.history.push(new Move(guess,evaluation)); this.tries++; if (this.tries>=7){ this.init(); this.history.push(new Move('You have 7 tries.',"Game over!")); } return true; } }
JS ve jQuery Kullanılarak MVC Uygulamak
DOM API'yi kullanarak dokümanı değiştirmek epey vaktimizi aldı. Şimdi bu işi jQuery kütüphanesine yıkalım. game.js içinde modelimiz yer alıyor. Bir önceki çözümde kullandığımız model sınıflarında bir değişiklik yok, aynısını kullanacağız. Tüm değişiklik denetçinin yer aldığı controller.js dosyasında yapacağız. Daha önce salt DOM API'yi kullanarak görünümde yaptığımız değişiklikleri şimdi jQuery kütüphanesi kullanarak gerçekleştiriyoruz:
var View = function(document){ var self= this; self.document= document; self.guess= $("#guess"); self.tries= $("#tries"); self.pbTries= $("#pbTries"); self.counter= $("#counter"); self.pbCounter= $("#pbCounter"); self.wins= $("#wins"); self.avgWinTime= $("#avgWinTime"); self.pbAvgWinTime= $("#pbAvgWinTime"); self.validationMessageContainer= $("#validationMessageContainer"); self.validationMessage= $("#validationMessage"); self.playButton= $("#playButton"); self.history= $('#history'); }; var Controller = function(model,view){ var self= this; self.view= view; self.model= model; self.updateCounterView = function(){ self.model.counter--; self.view.counter.text(self.model.counter); if (self.model.counter<=0){ self.model.init(); self.model.history.push(new Move('Time is out!','Game over!')); self.updateView(); } self.view.pbCounter.attr('ariavalue',self.model.counter); self.view.pbCounter.attr('style',"width: "+self.model.counter*4+"%"); }; self.updateStatisticsView = function(){ self.view.wins.text(self.model.statistics.wins); self.view.avgWinTime.text(self.model.statistics.avgWinTime); self.view.pbAvgWinTime.attr('ariavalue',self.model.statistics.avgWinTime); self.view.pbAvgWinTime.attr('style',"width: "+self.model.statistics.avgWinTime*4+"%"); }; self.updateHistoryTable = function() { self.view.history.empty(); for (i in model.history){ move= model.history[i]; self.view.history.append("<tr><td>"+move.guess+"</td><td>"+move.message+"</td></tr>"); } }; self.updateTries = function(){ if (self.model.tries>0){ self.view.tries.text(self.model.tries); } else{ self.view.tries.text('New game'); } self.view.pbTries.attr('ariavalue',self.model.tries) self.view.pbTries.attr('style',"width: "+((self.model.tries*100)/7)+"%") }; self.updateValidationView = function(){ self.view.validationMessage.text(self.model.validationMessage); if (self.model.validationMessage.length>0){ self.view.validationMessageContainer.show(); } else { self.view.validationMessageContainer.hide(); } }; self.updateView = function(){ self.updateHistoryTable(); self.updateTries(); self.updateValidationView(); self.updateStatisticsView(); }; self.clickPlay = function(){ self.model.play(self.view.guess.val()); self.updateView(); } self.init = function() { self.model.init(); self.updateView(); setInterval(self.updateCounterView, 1000); self.view.playButton.click(self.clickPlay); }; }; var Application = function() { var self= this; self.run = function(){ self.view= new View(document); self.model= new GameViewModel(); self.controller= new Controller(self.model,self.view); self.controller.init(); }; }; var app= new Application(); $(document).ready(app.run);
Şimdi denetçiyi ortadan kaldıracak bir çözüm inceleyelim. Amacımız View değiştiğinde otomatik olarak ViewModel'i değiştirecek, ViewModel değiştiğinde ise View'i otomatik olarak değiştirecek bir çözüm oluşturmaktır. Knockout.js bunu, ViewModel ile View arasında çift yönlü bağlantı tanımlayarak gerçekleştirmemize yardımcı oluyor. Knockout.js'de View ile ViewModel arasında üç tür bağlantı kurulabilir:
1. ko.observable()
Eğer bağlanacak değişken skaler bir değişken ise bu değişken ko.observable() çağrısı ile yaratılır.
2. ko.observableArray()
Eğer bağlanacak değişken bir dizi ise bu değişken ko.observableArray() çağrısı ile yaratılır.
3. ko.computed()
Eğer bağlanacak değer, diğer "observable" değişkenlerden hesaplanarak oluşturulacak ise bu durumda ko.computed() çağrısı ile bir fonksiyon olarak tanımlanır.
Yukarıdaki örnek uygulamadaki ViewModel'i şimdi bu "observable" yapıları kullanarak yeniden tanımlayalım:
var Profile = { TIME_LIMIT: 60, INIT_GUESS: 50, MAX_TRIES: 7 }; var Move = function(guess,message) { var self= this; self.guess= guess; self.message= message; }; var GameViewModel = function() { var self= this; self.tries = ko.observable(0); self.secret= ko.observable(0); self.guess= ko.observable(Profile.INIT_GUESS); self.history = ko.observableArray([]); self.counter= ko.observable(Profile.TIME_LIMIT); self.wins= ko.observable(0); self.totalWinTime= ko.observable(0); self.avgWinTime= ko.computed(function(){ var ratio= self.totalWinTime()/self.wins(); if (isNaN(ratio)) return 0; return ratio; }); self.pbTries = ko.computed(function(){ return 100*self.tries()/Profile.MAX_TRIES+'%'; }); self.pbCounter = ko.computed(function(){ return (100*self.counter())/Profile.TIME_LIMIT + '%'; }); self.pbAvgWinTime = ko.computed(function(){ return (100*self.avgWinTime())/Profile.TIME_LIMIT + '%'; }); setInterval(function(){ self.counter(self.counter()-1); }, 1000); self.init = function(){ self.history([]); self.tries(0); self.secret(Math.floor(Math.random()*100)+1); self.counter(Profile.TIME_LIMIT); } self.timeOut= ko.computed(function(){ if (self.counter()<=0){ self.init(); self.history.push(new Move("Time is out!","Game is over!")); } }) ; self.play = function(){ if (self.secret()==self.guess()){ self.wins(self.wins()+1); self.totalWinTime(Profile.TIME_LIMIT+self.totalWinTime()-self.counter()); self.init(); self.history.push(new Move(self.guess(),"You win!")); return; } else { if (self.guess() < self.secret()){ move = new Move(self.guess(),"Pick larger!"); } else { move = new Move(self.guess(),"Pick smaller!"); } self.history.push(move); } self.tries(self.tries()+1); if (self.tries()>=7){ self.init(); self.history.push(new Move('7 tries',"Game over!")); } } }
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Game</title> <style type="text/css"> @import url('css/bootstrap.css'); @import url('css/bootstrap-theme.css'); </style> <script type="text/javascript" src="js/lib/jquery-1.11.3.js"></script> <script type="text/javascript" src="js/lib/bootstrap.js"></script> <script type="text/javascript" src="js/lib/knockout-3.4.0.js"></script> <script type="text/javascript" src="game.js"></script> <script type="text/javascript"> var vm = new GameViewModel(); $(document).ready(function() { vm.init(); ko.applyBindings(vm); }); </script> </head> <body> <p /> <div class="container" role="main"> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">Number Guessing Game</h3> </div> <div class="panel-body"> <div class="form-group"> <label for="tries">Tries</label> <span id="tries" class="badge" data-bind="text: tries"></span> <div class="progress"> <div class="progress-bar progress-bar-striped" id="pbTries" role="progressbar" aria-valuenow="0" aria-valuemin="0" data-bind="attr: { 'aria-valuemin': tries }, style: { width: pbTries}" aria-valuemax="7" style="width: 0%"></div> </div> </div> <div class="form-group"> <label class="active">Counter</label> <span id="counter" class="badge" data-bind="text: counter"></span> <div class="progress"> <div class="progress-bar progress-bar-danger" id="pbCounter" role="progressbar" data-bind="attr: { 'aria-valuemin': counter }, style: { width: pbCounter}" aria-valuenow="0" aria-valuemin="0" aria-valuemax="25" style="width: 0%"></div> </div> </div> <div class="form-group"> <label for="guess">Guess:</label> <input type="text" id="guess" data-bind="value: guess" class="form-control" /> <button id="playButton" data-bind="click: play" class="btn btn-success">Play</button> </div> </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">History</h3> </div> <div class="panel-body"> <table id="history" class="table table-stripe"> <thead> <tr> <th>Guess</th> <th>Message</th> </tr> </thead> <tbody id="moves" data-bind="foreach: history"> <tr> <td data-bind="text: guess"></td> <td data-bind="text: message"></td> </tr> </tbody> </table> </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">Game Statistics</h3> </div> <div class="panel-body"> <div class="form-group"> <label class="active">Wins</label> <span id="wins" class="badge" data-bind="text: wins"></span> </div> <div class="form-group"> <label class="active">Average Win Time</label> <span id="avgWinTime" class="badge" data-bind="text: "></span> <div class="progress"> <div class="progress-bar progress-bar-danger" id="pbAvgWinTime" role="progressbar" data-bind="attr: { 'aria-valuemin': avgWinTime }, style: { width: pbAvgWinTime}" aria-valuenow="0" aria-valuemin="0" aria-valuemax="25" style="width: 0%"></div> </div> </div> </div> </div> </div> </body> </html>
Angular.js ile MVC Uygulamak
İstemci tarafta MVC uygulamak ile ilgili diğer bir çatı Angular.js. Knockout.js'de olduğu gibi Angular.js'de de modeli ön yüze Angular.js'in yönergelerini (=directive) kullanarak bağlıyoruz. Önce denetçi koduna bir bakalım:
game.js:
game.js:
var Profile = { TIME_LIMIT: 60, INIT_GUESS: 50, MAX_TRIES: 7 }; var Move = function(guess, message) { var self = this; self.guess = guess; self.message = message; } var game = angular.module("game", []).controller("gameController", function($scope,$interval) { var self = this; self.guess = 0; self.counter = Profile.TIME_LIMIT; self.secret = Math.floor(Math.random() * 100) + 1; self.history = new Array(); self.tries = 0; self.avgWinTime= 0.0; self.totalWinTime=0.0; self.wins= 0; self.init = function() { self.tries = 0; self.counter = Profile.TIME_LIMIT; self.history = new Array(); self.secret = Math.floor(Math.random() * 100) + 1; self.pbTries= {'width': '0%'}; self.pbCounter= {'width': '100%'}; self.pbAvgWinTime= {'width': '0%'}; } $interval(function(){ self.pbCounter= { 'width' : ((self.counter * 100) / Profile.TIME_LIMIT) + '%' }; if (self.counter == 0) { self.init(); } else { self.counter--; } }, 1000); self.play = function() { self.tries++; self.pbTries= { 'width' : ((self.tries * 100) / Profile.MAX_TRIES) + '%' }; if (self.tries > 7) { self.init(); self.history.push(new Move("You have 7 moves!", "Game is over!")); } else if (self.secret == self.guess) { self.wins++; self.totalWinTime = self.totalWinTime + 60 - self.counter ; self.avgWinTime = self.totalWinTime / self.wins; var moves= self.tries; self.init(); self.history.push(new Move("Congratulations!", "You win in "+moves+" moves!")); self.pbAvgWinTime= { 'width' : ((self.avgWinTime * 100) / Profile.TIME_LIMIT) + '%' }; } else { message = "Pick larger"; if (self.guess > self.secret) message = "Pick smaller"; self.history.push(new Move(self.guess, message)); } } } );
<!DOCTYPE html> <html ng-app="game"> <head> <meta charset="UTF-8"> <title>Game</title> <style type="text/css"> @import url('css/bootstrap.css'); @import url('css/bootstrap-theme.css'); </style> <script type="text/javascript" src="js/lib/jquery-1.11.3.js"></script> <script type="text/javascript" src="js/lib/bootstrap.js"></script> <script type="text/javascript" src="js/lib/angular.min.js"></script> <script type="text/javascript" src="game.js"></script> </head> <body ng-controller="gameController as ctrl"> <p /> <div class="container" role="main"> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">Number Guessing Game</h3> </div> <div class="panel-body"> <div class="form-group"> <label for="tries">Tries</label> <span id="tries" class="badge">{{ctrl.tries}}</span> <div class="progress"> <div class="progress-bar progress-bar-striped" id="pbTries" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="7" ng-style="ctrl.pbTries"></div> </div> </div> <div class="form-group"> <label class="active">Counter</label> <span id="counter" class="badge">{{ctrl.counter}}</span> <div class="progress"> <div class="progress-bar progress-bar-danger" id="pbCounter" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="60" ng-style="ctrl.pbCounter"></div> </div> </div> <div class="form-group"> <label for="guess">Guess:</label> <input type="text" id="guess" ng-model="ctrl.guess" class="form-control" /> <button id="playButton" ng-click="ctrl.play()" class="btn btn-success">Play</button> </div> </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">History</h3> </div> <div class="panel-body"> <table id="history" class="table table-stripe"> <thead> <tr> <th>Guess</th> <th>Message</th> </tr> </thead> <tbody id="moves" ng-repeat="move in ctrl.history"> <tr> <td>{{move.guess}}</td> <td>{{move.message}}</td> </tr> </tbody> </table> </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">Game Statistics</h3> </div> <div class="panel-body"> <div class="form-group"> <label class="active">Wins</label> <span id="wins" class="badge">{{ctrl.wins}}</span> </div> <div class="form-group"> <label class="active">Average Win Time</label> <span id="avgWinTime" class="badge">{{ctrl.avgWinTime}}</span> <div class="progress"> <div class="progress-bar progress-bar-danger" id="pbAvgWinTime" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="25" ng-style="ctrl.pbAvgWinTime"></div> </div> </div> </div> </div> </div> </body> </html>
Emeğinize saygı duyuyorum teşekürler. Knockout yerine react veya angulardan bahsetseydiniz çok güzel olurdu.
ReplyDeleteYazıya uygulamanın Angular.js versiyonunu ekledim. React.js burada farklı bir konuma sahip. Sunumu bileşen tabanlı bir yaklaşımla oluşturmaya çalışıyor. Knockout.js, Angular.js, Backbone.js gibi MV* çözümlerinin yerini alabilecek, onlara rakip bir yaklaşım değil. Başka bir yazıda React.js'i ele alacağım.
ReplyDeleteTeşekürler Diliyorum.
ReplyDelete