Son beş yılda, masaüstü uygulama geliştirirken yoğun olarak kullanılan MVC kalıbını, bu sefer Web tarayıcıda çalışan arayüzler oluşturmak için kullanan, çok sayıda JS çatısının doğduğuna tanık olduk. Bu çatılar arasında, basitliği ve sadeliği ile öne çıkan bir çatı bulunuyor: KnockoutJS. KnockoutJS, Model-View-ViewModel (MVVM) kalıbını kullanıyor. Bu kalıpta Model sunucu tarafta, genellikle veri tabanında yer alır. View bileşenini HTML5 ve CSS3 kullanarak oluşturuyoruz. ViewModel ise arayüzde kullanıcıya sunmak istediğimiz veriyi modeller. View ile onu besleyen ViewModel arasındaki bağlantıyı ise HTML takılarına iliştirdiğimiz data-bind özniteliğini kullanarak bildirimsel (=declarative) olarak yapıyoruz. data-bind, HTML5 standardında tanımlı bir öznitelik (=attribute) değil. Ancak HTML5'in amacı Web tarayıcısını, uygulama geliştirebileceğimiz bir platforma dönüştürmektir. Bu nedenle KnockoutJS gibi çatıların ihtiyaçlarına uygun olarak yeni takı (=tag) ve öznitelik tanımlamalarına izin verir. KnockoutJS'de MVC'de deki Controller kodlamaya ihtiyaç bulunmaz. Controller, data-bind özniteliği ile tanımlanan bağın çift taraflı (2-way binding) çalışmasını sağlar ve göz önünde bulunmaz.
Bu yazıda Knockout.js'deki çift taraflı bağlantı ele alınacaktır. Çift taraflı bağlantıyı çalışabilmek için örnek bir uygulama geliştireceğiz: Mastermind. Bu bir oyun. Bilgisayar n basamaklı, basamakları biri birinden farklı bir sayı seçer ve oyuncu bu sayıyı tahmin etmeye çalışır. n oyunun seviyesi. Başlangıç seviyesi ise 3. Oyunun başında bilgisayar 3 basamaklı, basamakları biri birinden bir sayı seçer. Bilgisayar kullanıcının tahminini değerlendirir ve bir sonraki tahmini daha iyi yapabilmesi için geri bildirimde bulunur. Her doğru basamakta bulduğu rakam için + ve yanlış basamakta bulduğu rakam için - kodlama yapar. Örneğin, bilgisayarın rastgele tuttuğu sayı 549 olsun. Kullanıcının 759 tahmini için bilgisayar 11, 579 tahmini için 2 ve 957 tahmini için ise 2 geri bildiriminde bulunur. Oyuncu, 549 tahmini ile bu seviyedeki sayıyı bildiğinde, oyunun seviyesi bir artar. Şimdi bilgisayar 4 basamaklı, basamakları biri birinden farklı rastgele bir sayı seçer. Oyuncunun amacı şimdi dört basamaklı olan bu sayıyı tahmin etmektir. Oyun en son sekizinci seviye için oynanır. Oyuncunun, bu seviyedeki sayıyı doğru tahmin etmesi ile oyun sona erer. Bu oyun için ViewModel aşağıdaki gibi olabilir:
var GameViewModel = function() { var self = this; self.guess = 123 ; self.gameLevel = 3; self.tries = 0; self.history = []; self.secret = createSecret(self.gameLevel); }
<!-- Game Board --> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">Mastermind</h3> </div> <div class="panel-body"> <div class="form-group"> <label for="gameLevel">Game Level:</label> <span></span> </div> <div class="form-group"> <label for="tries">Tries:</label> <span></span> </div> <div class="form-group"> <label for="tries">Time:</label> <span></span> </div> <div class="form-group"> <label for="guess">Guess:</label> <span></span> </div> <div class="form-group"> <button class="btn btn-success">Play</button> </div> </div> </div> <!-- Moves --> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">Moves</h3> </div> <div class="panel-body"> <table class="table table-striped"> <thead> <tr> <th>Guess</th> <th>Evaluation</th> </tr> </thead> <tbody> <tr> <td></td> <td></td> </tr> </tbody> </table> </div> </div>
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 ViewModel'i şimdi bu "observable" yapıları kullanarak yeniden tanımlayalım:
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 ViewModel'i şimdi bu "observable" yapıları kullanarak yeniden tanımlayalım:
var GameViewModel = function() { var self = this; self.guess = ko.observable(); self.gameLevel = ko.observable(3); self.tries = ko.observable(0); self.history = ko.observableArray([]); self.secret = createSecret(self.gameLevel()); }
<!-- Game Board --> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">Mastermind</h3> </div> <div class="panel-body"> <div class="form-group"> <label for="gameLevel">Game Level:</label> <span data-bind="text: gameLevel"></span> </div> <div class="form-group"> <label for="tries">Tries:</label> <span data-bind="text: tries"></span> </div> <div class="form-group"> <label for="guess">Guess:</label> <span data-bind="value: guess"></span> </div> <div class="form-group"> <button>Play</button> </div> </div> </div> <!-- Moves --> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">Moves</h3> </div> <div class="panel-body"> <table class="table table-striped"> <thead> <tr> <th>Guess</th> <th>Evaluation</th> </tr> </thead> <tbody> <tr> <td></td> <td></td> </tr> </tbody> </table> </div> </div>
self.play = function() { var move = evaluateGuess(self.guess(), self.secret, self.gameLevel()); self.tries(self.tries() + 1); if (move.eval.win) { self.gameLevel(self.gameLevel() + 1); self.init(); } self.history.push(move); }
<div class="form-group"> <button data-bind="click: play">Play</button> </div>
viewModel = new GameViewModel(); $(document).ready(function() { ko.applyBindings(viewModel); });
<!-- Moves --> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">Moves</h3> </div> <div class="panel-body"> <table class="table table-striped"> <thead> <tr> <th>Guess</th> <th>Evaluation</th> </tr> </thead> <tbody data-bind="foreach: history"> <tr> <td data-bind="text: guess"></td> <td data-bind="visible: eval.win"> <span class="label label-success" data-bind="visible: eval.win">You win!</span> </td> <td data-bind="visible: !eval.win"> <span class="label label-success"
data-bind="visible: eval.perfectMatch>0, text: eval.perfectMatch"></span> <span class="label label-danger"
data-bind="visible: eval.partialMatch>0, text: eval.partialMatch"></span> <span class="label label-info"
data-bind="visible: eval.perfectMatch==0 && eval.partialMatch==0">No Match</span> </td> </tr> </tbody> </table> </div> </div>
Burada dikkat edilmesi gereken önemli bir nokta bulunuyor. Değişkenleri observable olarak tanımladığımızda, onlar artık basit birer öznitelik olmaktan çıkıyorlar. Değerlerine ulaşmak ya da değerlerini değiştirmek için fonksiyon çağırır gibi çağırmamız gerekir. Örneğin tries'ın değerini bir arttırmak için aşağıdaki ifadeyi yazmamız gerekir:
self.tries( self.tries() + 1 );
<div class="alert alert-danger" role="alert" data-bind="visible: validationMessage().length>0">
Burada validationMessage, ViewModel içinde ko.observable() olarak tanımlı bir alandır.
Şimdi oyundaki süreler ile ilgili bazı istatistikler toplamak isteyelim: kronometre, her bir seviyede hamle sayısı, tamamlama süresi, ortalama hamle süresi gibi. Bu kez bağlanacak değerlerin hesaplanarak getirilmesi gerekiyor. Bu durumda üçüncü bağlantı şeklini kullanacağız, ko.computed():self.average = ko.computed(function(){ if (self.tries() == 0) return self.duration(); return self.duration()/ self.tries(); }); self.totalTime = ko.computed(function() { if (self.statistics().length == 0) return "No game played yet!"; tt = 0.0; for (i in self.statistics()) { stat = self.statistics()[i]; tt += Number(stat.time); } return tt; }); self.averageTime = ko.computed(function() { if (self.statistics().length == 0) return "No game played yet!"; return self.totalTime() / Number(self.statistics().length); });
Şimdi, View ve ViewModel'in tamamına bakalım:
index.html:
<!doctype html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <style type="text/css"> @import url('css/bootstrap.css'); @import url('css/bootstrap-theme.css'); </style> <script type="text/javascript" src="js/lib/jquery.js"></script> <script type="text/javascript" src="js/lib/knockout.js"></script> <script type="text/javascript" src="js/lib/bootstrap.js"></script> <script type="text/javascript" src="js/mastermind.js"></script> <title>MasterMind</title> </head> <body> <div class="container" role="main"> <!-- Game Board --> <div class="panel panel-primary"> <div class="panel-heading"> <h3 class="panel-title">Mastermind</h3> </div> <div class="panel-body"> <div class="form-group"> <label for="gameLevel">Game Level:</label> <span data-bind="text: gameLevel"></span> </div> <div class="form-group"> <label for="tries">Tries:</label> <span data-bind="text: tries"></span> </div> <div class="form-group"> <label for="tries">Time:</label> <span class="label label-danger" data-bind="text: duration"></span> </div> <div class="form-group"> <label for="tries">Average time:</label> <span class="label label-danger" data-bind="text: average"></span> </div> <div class="form-group"> <label for="guess">Guess:</label> <input type="text" data-bind="value: guess" class="form-control" id="guess" /> </div> <div class="form-group"> <button class="btn btn-success" data-bind="click: play">Play</button> </div> </div> </div> <!-- Moves --> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">Moves</h3> </div> <div class="panel-body"> <table class="table table-striped"> <thead> <tr> <th>Guess</th> <th>Evaluation</th> </tr> </thead> <tbody data-bind="foreach: history"> <tr> <td data-bind="text: guess"></td> <td data-bind="visible: eval.win"> <span class="label label-success"
data-bind="visible: eval.win">You win!</span> </td> <td data-bind="visible: !eval.win"> <span class="label label-success"
data-bind="visible: eval.perfectMatch>0, text: eval.perfectMatch"></span> <span class="label label-danger"
data-bind="visible: eval.partialMatch>0, text: eval.partialMatch"></span> <span class="label label-info"
data-bind="visible: eval.perfectMatch==0 && eval.partialMatch==0">No Match</span> </td> </tr> </tbody> </table> </div> <div class="alert alert-danger" role="alert"
data-bind="visible: validationMessage().length>0"> <strong data-bind="text: validationMessage"></strong> </div> </div> <!-- Statistics --> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">Statistics</h3> </div> <div class="panel-body"> <table class="table table-striped"> <thead> <tr> <th>Level</th> <th>Tries</th> <th>Time (seconds)</th> <th>Average Move Time (seconds)</th> </tr> </thead> <tbody data-bind="foreach: statistics"> <tr> <td data-bind="text: level"></td> <td data-bind="text: tries"></td> <td data-bind="text: time"></td> <td data-bind="text: average"></td> </tr> </tbody> </table> </div> </div> <div class="alert alert-danger" role="alert"> <strong>Total game time:</strong>
<span data-bind="text: totalTime"></span> </div> <div class="alert alert-info" role="alert"> <strong>Average game level time:</strong>
<span data-bind="text: averageTime"></span> </div> </div> </body> </html>
mastermind.js:
function countDistinct(value){ val= new String(value); hash = {}; for (i = 0; i < val.length; ++i) { c= val.charAt(i); hash[c]= true; } return Object.keys(hash).length; } function getTimeDifference(startTime,endTime){ return Math.round((endTime.getTime() - startTime.getTime()) / 1000); } function getTimeDifference(startTime){ return Math.round((new Date().getTime() - startTime.getTime()) / 1000); } function getRandomDigit(min, max) { return Math.floor((Math.random() * (max - min + 1))) + min; } function createSecret(numberOfDigits) { var numbers = new Array(); var candidate = new Number(); numbers[0] = getRandomDigit(1, 9); for (i = 1; i < numberOfDigits; ++i) { do { candidate = getRandomDigit(0, 9); } while (numbers.indexOf(candidate) > -1); numbers[i] = candidate; } number = 0; for (i = 0; i < numberOfDigits; ++i) number = 10 * number + numbers[i]; return number; } var GameLevelStat = function(level, tries, time, average) { var self = this; self.level = level; self.tries = tries; self.time = time; self.average = average; } var Move = function(guess, eval) { var self = this; self.guess = guess; self.eval = eval; }; function Evaluation(perfectMatch, partialMatch, win) { var self = this; self.perfectMatch = Number(perfectMatch); self.partialMatch = Number(partialMatch); self.win = win; }; function evaluateGuess(guess, secret, level) { if (guess == secret) { return new Move(guess, new Evaluation(level, 0, true)); } secret = new String(secret); guess = new String(guess); var perfectMatch = 0; var partialMatch = 0; for (i = 0; i < secret.length; ++i) { for (j = 0; j < guess.length; ++j) { if (secret.charAt(i) == guess.charAt(j)) { if (i == j) ++perfectMatch; else ++partialMatch; } } } return new Move(guess, new Evaluation(perfectMatch, partialMatch, false)); }; var GameViewModel = function() { var self = this; self.guess = ko.observable(); self.gameLevel = ko.observable(3); self.start = new Date(); self.duration = ko.observable(0); self.tries = ko.observable(0); self.history = ko.observableArray([]); self.statistics = ko.observableArray([]); self.secret = createSecret(self.gameLevel()); self.validationMessage = ko.observable(""); self.average = ko.computed(function(){ if (self.tries() == 0) return self.duration(); return self.duration()/ self.tries(); }); self.totalTime = ko.computed(function() { if (self.statistics().length == 0) return "No game played yet!"; tt = 0.0; for (i in self.statistics()) { stat = self.statistics()[i]; tt += Number(stat.time); } return tt; }); self.averageTime = ko.computed(function() { if (self.statistics().length == 0) return "No game played yet!"; return self.totalTime() / Number(self.statistics().length); }); self.validateGuess= function(){ self.validationMessage(""); guess = new String(self.guess()); pattern= new RegExp('\^\\d{'+self.gameLevel()+'\}\$'); if (!pattern.test(guess)){ self.validationMessage("You must enter "+self.gameLevel()+"-digit integer!"); return false; } if (countDistinct(guess) != self.gameLevel()){ self.validationMessage("You have entered repeated digits!"); return false; } for (i in self.history()){ move= self.history()[i]; if (self.guess()==move.guess){ self.validationMessage("You have already entered "+self.guess()+"!"); return false; } } return true; } self.init = function() { self.history([]); self.tries(0); self.start = new Date(); self.secret = createSecret(self.gameLevel()); setInterval(function() { now = new Date(); self.duration( getTimeDifference(self.start) ); }, 1000); }; self.play = function() { if (self.validateGuess()){ var move = evaluateGuess(self.guess(), self.secret, self.gameLevel()); self.tries(self.tries() + 1); if (move.eval.win) { self.statistics.push(new GameLevelStat(self.gameLevel(),
self.tries(), self.duration(), self.duration()/self.tries())); self.gameLevel(self.gameLevel() + 1); self.init(); } self.history.push(move); } } }; viewModel = new GameViewModel(); $(document).ready(function() { ko.applyBindings(viewModel); viewModel.init(); });
4. seviyede oyun ekranı |
Tekrar eden basamaklar için sistemin uyarı verdiği ekran |
Kullanıcının hamleleri ve bilgisayarın geri bildirim ekranı |
Projenin tamamına bu bağlantıdan ulaşabilirsiniz. Knockout.js ve diğer MV* JS çatıları ile ilgili daha detaylı bilgi almak isterseniz Client-side and Server-side Javascript eğitimini öneririm.
No comments:
Post a Comment