5

 Như tôi đã đề cập vài ngày trước, tôi hiện đang trong quá trình đọc cuốn sách Mẫu thiết kế JavaScript của Addy Osman . (Lưu ý - trong mục blog trước đây của tôi, tôi đã liên kết với bản sao vật lý tại Amazon. Liên kết ở đây là phiên bản trực tuyến miễn phí. Tôi nghĩ rằng cuốn sách của anh ấy đáng để mua, nhưng bạn có thể thử trước khi mua!) Mẫu đầu tiên được mô tả trong cuốn sách và cuốn sách tôi sẽ nói hôm nay là mẫu Mô-đun.

Trước khi tôi bắt đầu, tôi muốn rõ ràng. Tôi đang viết điều này như một phương tiện để giúp tôi củng cố sự hiểu biết của mình. Tôi không phải là một chuyên gia. Tôi đang học. Tôi hy vọng sẽ phạm sai lầm, và tôi mong độc giả của tôi gọi tôi ra về nó. Nếu cuối cùng tất cả chúng ta cùng học một cái gì đó, thì tôi nghĩ rằng quá trình này có giá trị trong khi!

Addy mô tả mẫu Mô-đun như vậy:

Mẫu Mô-đun ban đầu được định nghĩa là một cách để cung cấp cả đóng gói riêng và công khai cho các lớp trong công nghệ phần mềm thông thường.

Trong JavaScript, mẫu Mô-đun được sử dụng để mô phỏng thêm khái niệm các lớp theo cách chúng ta có thể bao gồm cả các phương thức công khai / riêng tư và các biến trong một đối tượng, do đó che chắn các phần cụ thể khỏi phạm vi toàn cầu. Điều này dẫn đến kết quả là giảm khả năng tên hàm của chúng ta xung đột với các hàm khác được xác định trong các tập lệnh bổ sung trên trang.

Tôi nghĩ rằng điều này có ý nghĩa của chính nó, nhưng nó có thể giúp sao lưu một chút và xem xét tại sao nói chung đóng gói có thể là tốt. Nếu bạn chưa quen với JavaScript, có lẽ bạn đã xây dựng các trang web trông hơi giống thế này.

<html>
<head>
<title></title>
<script>
//stuff here
</script>
</head>

<body>
<!--awesome layout here -->
</body>
</html>

Bạn có thể bắt đầu thêm tính tương tác vào trang của mình bằng cách thêm một hàm JavaScript đơn giản:

<html>
<head>
<title></title>
<script>
function doSomething() {

}
</script>
</head>
 
<body>
<!--awesome layout here -->
</body>
</html>

Và sau đó dần dần thêm ngày càng nhiều ...

<html>
<head>
<title></title>
<script>
function doSomething() {
 
}

function doSomethingElse() {

}

function heyINeedThisToo() {

}
</script>
</head>
 
<body>
<!--awesome layout here -->
</body>
</html>

Cuối cùng, số lượng mã JavaScript bạn có thể đạt đến điểm mà bạn nhận ra có lẽ bạn nên lưu trữ nó trong tệp riêng của mình. Nhưng việc chuyển tất cả mã đó vào một tệp riêng biệt không nhất thiết phải giúp đỡ. Sau một thời gian, bạn bắt đầu thấy mình gặp khó khăn trong việc tìm kiếm các chức năng phù hợp. Bạn có thể có mã liên quan đến tính năng X bên cạnh tính năng Y, theo sau là một thứ khác liên quan đến tính năng X. Có thể hơn là bạn tạo nhiều tệp JavaScript. Một cho X, một cho Y. Nhưng như Addy ám chỉ ở trên, bạn có nguy cơ các tên hàm ghi đè lên nhau.

Hãy xem xét một ứng dụng web có giao diện với cả Facebook và Twitter. Bạn viết một chức năng cho Twitter có tên getLatest lấy các Tweets mới nhất về Paris Hilton. (Và tại sao bạn không làm điều đó?) Sau đó, bạn viết mã để nhận các cập nhật trạng thái mới nhất từ ​​bạn bè của bạn trên Facebook. Chúng ta nên gọi nó là gì? Ồ vâng - getLatest!

Tất nhiên, đây không phải là thứ bạn không thể xử lý, nhưng vấn đề là, các mẫu thiết kế được xây dựng cho mục đích duy nhất là giúp bạn giải quyết vấn đề. Trong trường hợp này, mẫu mô-đun là một ví dụ về giải pháp giúp bạn tổ chức mã của mình. Hãy xem xét một ví dụ đầy đủ về điều này.

Tôi đã tạo một ứng dụng web đơn giản cho phép người dùng ghi nhật ký. Hiện tại chức năng rất đơn giản - bạn có thể xem các mục trong quá khứ của mình, viết các mục mới và xem một mục cụ thể. Ứng dụng web này sẽ sử dụng WebQuery, không hoạt động trong Firefox hoặc IE! Đây là chủ ý và tôi dự định sẽ giải quyết vấn đề này trong một mục blog sau. Bạn có thể giới thiệu bản này (một lần nữa, vui lòng sử dụng Chrome và một lần nữa, hãy nhớ rằng có một lý do khiến tôi không xử lý nhiều trình duyệt) bằng cách vào đây:

http://www.raymondcamden.com/demos/2013/mar/22/v1/

Mặc dù bạn có thể tự xem nguồn, tôi nghĩ rằng có thể có ích khi chia sẻ tệp JavaScript chính tại đây. Cảnh báo, đây là một khối mã lớn. Đó là loại điểm. Đừng không đọc từng dòng ở đây.


var db;
var mainView;

$(document).ready(function() {
  //create a new instance of our Diary and listen for it to complete it's setup
	setupDiary(startApp);
});

function dbErrorHandler(e) {
	console.log('DB Error');
	console.dir(e);
}

function setupDiary(callback) {

	//First, setup the database
	db = window.openDatabase("diary", 1, "diary", 1000000);
	db.transaction(initDB, dbErrorHandler, callback);

}

function initDB(t) {
	t.executeSql('create table if not exists diary(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, image TEXT, published DATE)');
}

/*
Main application handler. At this point my database is setup and I can start listening for events.
*/

function startApp() {
	console.log('startApp');

	mainView = $("#mainView");

	//Load the main view
	pageLoad("main.html");
	
	//Always listen for home click
	$(document).on("touchend", ".homeButton", function(e) {
		e.preventDefault();
		pageLoad("main.html");
	});


}

function pageLoad(u) {
	console.log("load "+u);
	//convert url params into an ob
	var data = {};
	if(u.indexOf("?") >= 0) {
		var qs = u.split("?")[1];
		var parts = qs.split("&");
		for(var i=0, len=parts.length; i<len; i++) {
			var bits = parts[i].split("=");
			data[bits[0]] = bits[1];
		};
	}
	$.get(u,function(res,code) {
		mainView.html(res);
		
		var evt = document.createEvent('CustomEvent');
		evt.initCustomEvent("pageload",true,true,data);
		var page = $("div", mainView);
		page[0].dispatchEvent(evt);
		
	});
}

//Utility to convert record sets into array of obs
function fixResults(res) {
	var result = [];
	for(var i=0, len=res.rows.length; i<len; i++) {
		var row = res.rows.item(i);
		result.push(row);
	}
	return result;
}

function getDiaryEntries(start,callback) {
	console.log('Running getEntries');
	if(arguments.length === 1) callback = arguments[0];

	db.transaction(
		function(t) {
			t.executeSql('select id, title, body, image, published from diary order by published desc',[],
				function(t,results) {
					callback(fixResults(results));
				},dbErrorHandler);
		}, dbErrorHandler);

}

$(document).on("pageload", "#mainPage", function(e) {
	getDiaryEntries(function(data) {
		console.log('getEntries');
		var s = "";
		for(var i=0, len=data.length; i<len; i++) {
			s += "<div data-id='"+data[i].id+"'>" + data[i].title + "</div>";
		}
		$("#entryList").html(s);

		//Listen for add clicks
		$("#addEntryBtn").on("touchend", function(e) {
			e.preventDefault();
			pageLoad("add.html");
		});

		//Listen for entry clicks
		$("#entryList div").on("touchend", function(e) { 
			e.preventDefault();
			var id = $(this).data("id");
			pageLoad("entry.html?id="+id);
		});

	});

});

function fixResult(res) {
	if(res.rows.length) {
		return res.rows.item(0);
	} else return {};
}

function getEntry(id, callback) {

	db.transaction(
		function(t) {
			t.executeSql('select id, title, body, image, published from diary where id = ?', [id],
				function(t, results) {
					callback(fixResult(results));
				}, dbErrorHandler);
			}, dbErrorHandler);

}

$(document).on("pageload", "#entryPage", function(e) {

	getEntry(Number(e.detail.id), function(ob) {
		var content = "<h2>" + ob.title + "</h2>";
		content += "Written "+dtFormat(ob.published) + "<br/><br/>";
		content += ob.body;
		$("#entryDisplay").html(content);
	});
});

function saveEntry(data, callback) {
	db.transaction(
		function(t) {
			t.executeSql('insert into diary(title,body,published) values(?,?,?)', [data.title, data.body, new Date().getTime()],
			function() { 
				callback();
			}, dbErrorHandler);
		}, dbErrorHandler);
}

$(document).on("pageload", "#addPage", function(e) {

	$("#addEntrySubmit").on("touchstart", function(e) {
		e.preventDefault();
		//grab the values
		var title = $("#entryTitle").val();
		var body = $("#entryBody").val();
		//store!
		saveEntry({title:title,body:body}, function() {
			pageLoad("main.html");
		});
		
	});
});


function dtFormat(input) {
   if(!input) return "";
	input = new Date(input);
	var res = (input.getMonth()+1) + "/" + input.getDate() + "/" + input.getFullYear() + " ";
	var hour = input.getHours()+1;
	var ampm = "AM";
	if(hour === 12) ampm = "PM";
	if(hour > 12){
		hour-=12;
		ampm = "PM";
	}
	var minute = input.getMinutes()+1;
	if(minute < 10) minute = "0" + minute;
	res += hour + ":" + minute + " " + ampm;
	return res;
}

Những gì bạn thấy là làm thế nào tôi thường mã. Ứng dụng của tôi bắt đầu bằng cách chờ DOM tải. Sau đó, nó cần phải thiết lập một cơ sở dữ liệu. Tiếp theo, nó cần liệt kê chúng ra. Và như vậy. Nếu bạn đọc "xuống" tệp JavaScript, bạn có thể thấy các hàm liên quan đến thao tác DOM, các hàm xử lý "kiến trúc trang đơn" của tôi và các hàm thực hiện crap cơ sở dữ liệu. Khi tôi đang làm việc ở trang một, tôi tập trung vào danh sách. Khi tôi làm việc trên biểu mẫu thêm, tôi đã làm việc về hỗ trợ viết dữ liệu. Về cơ bản, khi tôi chuyển vào ứng dụng, tôi chỉ cần lần lượt thực hiện các chức năng.

Những công việc này. Nhưng - chú ruột của tôi đang nói với tôi rằng tập tin này rất lộn xộn. Khi tôi nhìn thấy một cái gì đó sai, tôi không chắc chắn nơi để tìm vì mọi thứ được trộn lẫn với nhau. Trong đoạn văn trên, tôi đã mô tả hầu hết các chức năng của mình là rơi vào ba lĩnh vực chính. Tôi đã quyết định rằng tôi muốn lấy công cụ cơ sở dữ liệu và tóm tắt nó thành một mô-đun.

Cấu trúc cơ bản của mã sử dụng mẫu Mô-đun có thể được định nghĩa như sau:

var someModule = (function() {
 
 
 
}());

Hãy giơ tay nếu bạn thấy cú pháp đó khó hiểu. Tôi biết tôi làm được. Bây giờ nó có ý nghĩa với tôi. Nhưng trong một thời gian dài, nó chỉ cảm thấy ... kỳ lạ. Tôi đã có một khối tinh thần chấp nhận hình thức của mã này trong một thời gian dài . Tôi không xấu hổ khi thừa nhận nó.

Đối với tôi, thật hữu ích nếu tôi sao lưu một chút và nhìn nó như vậy:

var someModule = (
 

 
);

Được rồi, cái đó có lý. Về cơ bản, tôi đang nói biến someModule bằng với bất cứ điều gì trong cái quái được thực hiện trong ngoặc đơn của tôi. Ok, vậy chuyện gì đang xảy ra ở đó ...

function() {



}()

Ok, vì vậy chúng tôi đang tạo ra một chức năng chạy nó. Ngay. Ok, vì vậy bất cứ điều gì hàm trả về - đó là những gì sẽ được truyền cho someModule. Đây là nơi mọi thứ trở nên thú vị. Hãy đặt một số mã trong mô-đun này.

var someModule = (function() {

  //Credit Addy Osmani: http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript
  var counter = 0;

  return {

    incrementCounter: function () {
      return counter++;
    },

    resetCounter: function () {
      console.log( "counter value prior to reset: " + counter );
      counter = 0;
    }
  };
 
 
}());

Mã cho mô-đun này xuất phát từ ví dụ của Addy. Biến, bộ đếm, là một biến riêng. Tại sao? Bởi vì nó không thực sự được trả về từ hàm. Thay vào đó, chúng tôi trả về một đối tượng bằng chữ có chứa hai hàm. Các hàm này có quyền truy cập vào bộ đếm biến, bởi vì, khi tạo, chúng nằm trong cùng một phạm vi. (Lưu ý: Câu cuối cùng này có thể không mô tả chính xác tình huống.) Kết quả của chức năng này là đối tượng theo nghĩa đen. Mô-đun của tôi chứa hai chức năng và một biến dữ liệu riêng tư.

Điều tuyệt vời là - tôi không còn phải lo lắng về sự va chạm chức năng. Tôi có thể đặt tên cho chức năng của mình bất cứ điều gì tôi rất vui lòng. Tôi cũng có thể tạo các chức năng riêng tư và ẩn chúng khỏi việc thực hiện. Có thể có các chức năng tiện ích có ý nghĩa cho mô-đun của tôi nhưng không có ý nghĩa ở bất kỳ nơi nào khác. Tôi có thể nhét chúng vào đó và giấu chúng đi!

Vì vậy, như tôi đã nói, tôi muốn loại bỏ các khía cạnh cơ sở dữ liệu của mã của tôi. Tôi đã tạo một tệp mới, diary.js và tạo mô-đun này.

var diaryModule = (function() {
  
	var db;
	
	function initDB(t) {
		t.executeSql('create table if not exists diary(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, image TEXT, published DATE)');
	}
	
	function dbErrorHandler(e) {
		console.log('DB Error');
		console.dir(e);
	}
	
	//Utility to convert record sets into array of obs
	function fixResults(res) {
		var result = [];
		for(var i=0, len=res.rows.length; i<len; i++) {
			var row = res.rows.item(i);
			result.push(row);
		}
		return result;
	}
	
	//I'm a lot like fixResults, but I'm only used in the context of expecting one row, so I return an ob, not an array
	function fixResult(res) {
		if(res.rows.length) {
			return res.rows.item(0);
		} else return {};
	}

	return {
	
		setup:function(callback) {
			db = window.openDatabase("diary", 1, "diary", 1000000);
			db.transaction(initDB, dbErrorHandler, callback);
		},
		getEntries:function(start,callback) {
			console.log('Running getEntries');
			if(arguments.length === 1) callback = arguments[0];
			
			db.transaction(
				function(t) {
					t.executeSql('select id, title, body, image, published from diary order by published desc',[],
						function(t,results) {
							callback(fixResults(results));
						},dbErrorHandler);
				}, dbErrorHandler);
		
		},
		getEntry:function(id, callback) {
			db.transaction(
				function(t) {
					t.executeSql('select id, title, body, image, published from diary where id = ?', [id],
						function(t, results) {
							callback(fixResult(results));
						}, dbErrorHandler);
					}, dbErrorHandler);
		
		},
		saveEntry:function(data, callback) {
			db.transaction(
				function(t) {
					t.executeSql('insert into diary(title,body,published) values(?,?,?)', [data.title, data.body, new Date().getTime()],
					function() { 
						callback();
					}, dbErrorHandler);
				}, dbErrorHandler);
		}


		
	}
	
}());

Bây giờ tôi đã có một "gói" mà tệp JavaScript chính của tôi có thể sử dụng. Hãy nhìn vào đó.

var mainView;

$(document).ready(function() {

  //create a new instance of our Diary and listen for it to complete it's setup
	diaryModule.setup(startApp);
});

/*
Main application handler. At this point my database is setup and I can start listening for events.
*/

function startApp() {
	console.log('startApp');

	mainView = $("#mainView");

	//Load the main view
	pageLoad("main.html");
	
	//Always listen for home click
	$(document).on("touchend", ".homeButton", function(e) {
		e.preventDefault();
		pageLoad("main.html");
	});


}

function pageLoad(u) {
	console.log("load "+u);
	//convert url params into an ob
	var data = {};
	if(u.indexOf("?") >= 0) {
		var qs = u.split("?")[1];
		var parts = qs.split("&");
		for(var i=0, len=parts.length; i<len; i++) {
			var bits = parts[i].split("=");
			data[bits[0]] = bits[1];
		};
	}
	$.get(u,function(res,code) {
		mainView.html(res);
		
		var evt = document.createEvent('CustomEvent');
		evt.initCustomEvent("pageload",true,true,data);
		var page = $("div", mainView);
		page[0].dispatchEvent(evt);
		
	});
}

$(document).on("pageload", "#mainPage", function(e) {
	diaryModule.getEntries(function(data) {
		console.log('getEntries');
		var s = "";
		for(var i=0, len=data.length; i<len; i++) {
			s += "<div data-id='"+data[i].id+"'>" + data[i].title + "</div>";
		}
		$("#entryList").html(s);

		//Listen for add clicks
		$("#addEntryBtn").on("touchend", function(e) {
			e.preventDefault();
			pageLoad("add.html");
		});

		//Listen for entry clicks
		$("#entryList div").on("touchend", function(e) { 
			e.preventDefault();
			var id = $(this).data("id");
			pageLoad("entry.html?id="+id);
		});

	});

});

$(document).on("pageload", "#entryPage", function(e) {

	diaryModule.getEntry(Number(e.detail.id), function(ob) {
		var content = "<h2>" + ob.title + "</h2>";
		content += "Written "+dtFormat(ob.published) + "<br/><br/>";
		content += ob.body;
		$("#entryDisplay").html(content);
	});
});

$(document).on("pageload", "#addPage", function(e) {

	$("#addEntrySubmit").on("touchstart", function(e) {
		e.preventDefault();
		//grab the values
		var title = $("#entryTitle").val();
		var body = $("#entryBody").val();
		//store!
		diaryModule.saveEntry({title:title,body:body}, function() {
			pageLoad("main.html");
		});
		
	});
});


function dtFormat(input) {
    if(!input) return "";
	input = new Date(input);
    var res = (input.getMonth()+1) + "/" + input.getDate() + "/" + input.getFullYear() + " ";
    var hour = input.getHours()+1;
    var ampm = "AM";
	if(hour === 12) ampm = "PM";
    if(hour > 12){
        hour-=12;
        ampm = "PM";
    }
    var minute = input.getMinutes()+1;
    if(minute < 10) minute = "0" + minute;
    res += hour + ":" + minute + " " + ampm;
    return res;
}

Bạn có thể thấy rằng tất cả các chức năng cơ sở dữ liệu đã biến mất. Bây giờ tôi truy cập chúng qua diaryModule.whthing. Tôi tin rằng tôi đã cắt ra khoảng 60 dòng mã, khoảng một phần ba, nhưng những gì còn lại sẽ tập trung hơn. (Tôi cũng có thể loại bỏ các chức năng được sử dụng để hỗ trợ kiến ​​trúc trang đơn của mình.)

Bạn có thể chạy phiên bản này tại đây: http://www.raymondcamden.com/demos/2013/mar/22/v2

Vì vậy, câu hỏi? Ý kiến? Rants? :)



|