หากพูดถึงความสามารถใหม่ๆ ของ html5 สิ่งที่ไม่ควรมองข้ามเลยก็คือ History API ถึงแม้ว่าหลายๆ คน อาจจะไม่ค่อยคุ้นเคยกับมัน แต่จริงๆ แล้ว เราอาจจะเคยใช้โดยที่ไม่รู้ตัวเลยก็ได้
History API ไม่ใช่ของใหม่
โดยปกติแล้ว web browsers จะมีการเก็บ “ประวัติการเข้าชมเว็บเพจ” ทุกครั้งที่มีการเปลี่ยนหน้าใหม่ history api ก็คือสิ่งที่ทำให้เราสามารถเข้าไปจัดการกับประวัติเหล่านั้นได้นั่นเอง
history api ประกอบไปด้วย methods(สั่งให้ทำ) และ properties(ดูข้อมูล) ต่างๆ ซึ่งในการจะใช้งานนั้น เราจะต้องทำผ่านทาง object ของ javascript ที่มีชื่อว่า window.history
Back & Forward (Method)
ทั้ง 2 methods นี้จะทำหน้าที่เหมือนกับการกดปุ่ม back และ forward ที่ web browsers
1 2 3 4 5 6 7 8 9 10 11 12 | <body> <button onclick="back()">Back</button> <button onclick="forward()">Forward</button> <script type="text/javascript"> function back () { window.history.back(); } function forward () { window.history.forward(); } </script> </body> |
Go (Method)
เราสามารถกำหนดจำนวนหน้าที่เราต้องการให้ back หรือ forward ได้ด้วย method go()
โดย parameter ที่เราจะต้องส่งให้ method นี้ก็คือ “จำนวนขั้น” ที่จะเดินหน้าหรือถอยหลัง หากจำนวนขั้นเป็นบวก จะถือว่าเป็นการ forward แต่ถ้าจำนวนขั้นเป็นลบก็จะถือเป็นการ back
1 2 3 4 5 6 7 8 9 | <body> <button onclick="go(-1)">Back</button> <button onclick="go(1)">Forward</button> <script type="text/javascript"> function go (steps) { window.history.go(steps); } </script> </body> |
Length (Property)
length
จะมีประโยชน์ในการหาจำนวนของ history ทั้งหมด ซึ่งก็คือจำนวนหน้าที่เราได้เคยเปิดไปนั่นเอง
1 2 3 4 5 6 7 8 9 | <body> <button onclick="backToFirstVisited()">Cancel</button> <script type="text/javascript"> function backToFirstVisited () { var moves = window.history.length - 1; window.history.go(-moves); } </script> </body> |
ปัญหา
เราจะเห็นว่า web application สมัยนี้ นิยมใช้ Ajax ในการโหลด content แบบ dynamic ข้อดีของวิธีนี้ก็คือ users จะสามารถได้รับ content ที่พวกเขาต้องการได้ทันทีโดยที่ไม่ต้องโหลดใหม่หมดทั้งหน้า แต่วิธีนี้ก็มีข้อเสียอยู่เหมือนกัน
สมมติเรากำลังอ่านข่าวดาราจากเว็บๆ หนึ่ง เราพบว่าที่ด้านล่างของเนื้อหาข่าวมี gallery อยู่ เมื่อเราลองกดดูรูปดาราไปเรื่อยๆ เราพบว่ารูปใหม่นั้นถูกโหลดเข้ามาใน container ของ gallery โดยที่เนื้อหาส่วนอื่นๆ ยังคงนิ่งเหมือนเดิม นี่คือ gallery ที่เขียนโดยใช้ Ajax
เมื่อดูไปสักพัก เราพบว่ารูปที่ 9 นั้น เป็นรูปดาราที่เราชอบ เราจึงอยากแชร์หน้านี้ไปให้เพื่อนๆ ดูบ้าง แต่พอเพื่อนๆ ได้ดูหน้าที่เราแชร์ไปนั้น พวกเขากลับพบว่ามันเป็นรูปที่ 1 ไม่ใช่รูปที่ 9 ทำไมถึงเป็นแบบนี้ ?
ที่เป็นเช่นนี้ก็เพราะว่าการใช้ Ajax นั้นเป็นเพียงเทคนิคที่จะทำให้ users รู้สึกว่าหน้านั้นๆ เปลี่ยนไป ทั้งๆ ที่จริงๆ แล้ว หน้านั้นยังคงอยู่กับที่ นั่นหมายความว่าหน้าที่ถูกโหลดเข้ามาด้วย Ajax นั้น ไม่ได้ถูกเพิ่มเข้าไปใน history แต่อย่างใด และแน่นอนว่า url ของหน้านั้นก็ยังคงเหมือนเดิม
History API ใน HTML5
เพื่อเป็นการแก้ไขปัญหาดังกล่าว html5 จึงได้เพิ่ม features ใหม่ๆ เข้ามาใน history api ดังนี้
pushState (Method)
method นี้จะเอาไว้เพิ่ม history entry ใหม่เข้ามาในรายการ history ทั้งหมด โดยปกติแล้ว entry ใหม่จะถูกเพิ่มเข้ามาก็ต่อเมื่อมีการเปลี่ยนหน้า แต่ใน html5 เราสามารถเพิ่มเองได้แล้ว โดย parameters ที่ใช้จะมีอยู่ 3 ตัว ด้วยกัน ดังนี้
- dataข้อมูลเกี่ยวกับ history นั้นๆ
- titleชื่อเรียก history นั้นๆ
- urlurl ที่เราต้องการจะให้แสดงที่ address bar
1 | window.history.pushState(data, title, url); /* เพิ่ม history entry ใหม่ */ |
replaceState (Method)
method นี้จะคล้ายกับ pushState
เลย แต่จะต่างกันตรงที่ replaceState
จะเป็นการ “แก้ไข” history ปัจจุบัน ไม่ใช่ “เพิ่ม” (การใช้ pushState
จะทำให้ window.history.length
มีค่าเพิ่มขึ้น ซึ่งต่างจากการใช้ replaceState
ที่ window.history.length
ยังคงมีค่าเท่าเดิม)
1 | window.history.replaceState(data, title, url); /* แก้ไข history entry ปัจจุบัน */ |
state (Property)
property นี้จะเอาไว้สำหรับเรียกดู data ของ history ปัจจุบัน ซึ่ง data นี้ก็คือ data ที่เราได้ใส่เข้าไปตอนใช้ method pushState
หรือ replaceState
นั่นเอง
1 | window.history.state; /* ดู data ของ history ปัจจุบัน */ |
popstate (Event)
popstate
นั้นเป็น event ที่จะ “ทำงาน” ทุกครั้งที่ users กดปุ่ม back หรือ forward หรือเมื่อมีการสั่งให้ method back()
, forward()
หรือ go()
ทำงาน
นอกจากนี้ เรายังสามารถเข้าไปดู data ของ history หลังจาก event popstate
ทำงานได้ โดยดูผ่าน property state
ของ event object ที่ส่งผ่านไปยัง callback function ลองดูตัวอย่างนี้
1 2 3 | window.addEventListener('popstate', function(event) { /* เมื่อ event popstate ทำงาน */ var state = event.state; /* ดู data ของ history หลังจาก event popstate ทำงานแล้ว */ }); |
จากโค้ดด้านบนจะเห็นว่าเราจะไม่ใช้ window.history.state
เพราะมันจะไปดึง data ของ history entry ปัจจุบันมาให้ แต่สิ่งเราต้องการจริงๆ แล้วก็คือ data ของ history entry ที่เรากำลังจะไปด้วย method back()
, forward()
หรือ go()
Workshop – History API
หลังจากที่เรารู้แล้วว่าแต่ละ methods, properties และ event ของ history api นั้นมีวิธีใช้งานอย่างไร เราก็มาลองลงมือทำกันเลยดีกว่า สมมติเราจะสร้างหน้า gallery ขึ้นมาสักหน้าหนึ่ง โค้ดของเราจะเป็นแบบนี้
HTML หน้ารวมรูปทั้งหมด (index.html)
1 2 3 4 5 6 7 8 9 | <nav class="gallery-list"> <ul> <li><a title="Image 1" href="1.html"><img class="thumbnail" src="img/1.jpg" alt="Image 1"></a></li> <li><a title="Image 2" href="2.html"><img class="thumbnail" src="img/2.jpg" alt="Image 2"></a></li> <li><a title="Image 3" href="3.html"><img class="thumbnail" src="img/3.jpg" alt="Image 3"></a></li> <li><a title="Image 4" href="4.html"><img class="thumbnail" src="img/4.jpg" alt="Image 4"></a></li> </ul> </nav> <section id="gallery-container"></section> <!-- ที่ container ยังไม่แสดงรูปใดๆ --> |
ในหน้ารวมรูปจะประกอบไปด้วยรูป thumbnails ทั้งหมด 4 รูปด้วยกัน ในแต่ละรูปจะมี link ที่จะพาไปยังหน้าสำหรับแสดงรูปนั้นๆ ส่วน container ของ gallery ให้เราปล่อยว่างไว้ก่อน
HTML หน้าแสดงรูปทั้ง 4 หน้า (1.html, 2.html, 3.html, 4.html)
1 2 3 4 5 6 7 8 9 10 11 | <nav class="gallery-list"> <ul> <li><a title="Image 1" href="1.html"><img class="thumbnail" src="img/1.jpg" alt="Image 1"></a></li> <li><a title="Image 2" href="2.html"><img class="thumbnail" src="img/2.jpg" alt="Image 2"></a></li> <li><a title="Image 3" href="3.html"><img class="thumbnail" src="img/3.jpg" alt="Image 3"></a></li> <li><a title="Image 4" href="4.html"><img class="thumbnail" src="img/4.jpg" alt="Image 4"></a></li> </ul> </nav> <section id="gallery-container"> <img src="img/1.jpg" alt=""> <!-- เปลี่ยน src ของรูปให้เป็น 1.jpg, 2.jpg, 3.jpg และ 4.jpg ตามลำดับ --> </section> |
หน้าแสดงรูปจะคล้ายกับหน้ารวมรูปเลย เพียงแต่เราจะต้องใส่ img element เพื่อที่จะแสดงรูปของหน้านั้นๆ เพิ่มเข้าไปใน container ของ gallery
เมื่อสร้างหน้ามาครบแล้ว ให้เราลองกดเปลี่ยนรูปดู เราจะพบว่ามันต้องโหลดใหม่หมดทั้งหน้า จริงๆ แล้ว ในส่วนของรูป thumbnails ด้านบนนั้นเหมือนกันทุกๆ หน้า เป็นไปได้มั้ยที่เราจะโหลดใหม่เฉพาะส่วน container ของ gallery ?
โหลด Content แบบ Dynamic ด้วย JavaScript
เนื่องจากเราไม่อยากให้ users ต้องโหลดใหม่หมดทั้งหน้า เราจึงเลือกใช้วิธีโหลด dynamic content ด้วย javascript แทนการสร้างหน้า html สำหรับแสดงแต่ละรูป เราจึงเขียนโค้ด javascript แบบนี้ (ขอใช้ jQuery เพื่อให้ง่ายต่อการทำความเข้าใจ)
1 2 3 4 5 6 7 8 9 | $(function(){ $('.gallery-list a').click(function(){ /* เมื่อกดดูรูป จะสั่งให้ทำอะไร? */ var title = $(this).attr('title'); /* ดึง title จาก attribute title ของ a */ var src = $(this).find('img').attr('src'); /* ดึงที่อยู่ของรูปจาก src ของ img */ var image = '<img src="' + src + '" alt="' + title + '">'; /* สร้าง img element ขึ้นมาจากข้อมูลที่ดึงมาได้ */ $('#gallery-container').html(image); /* นำ img element นั้น มาใส่เข้าไปใน container ของ gallery */ return false; /* ยกเลิกการเปลี่ยนหน้าจากการกด link */ }); }); |
เมื่อลองพรีวิวดู เราจะเห็นว่ารูปใหม่ถูกโหลดเข้ามาใน container โดยที่ไม่ต้องโหลดใหม่ทั้งหน้า แต่หากสังเกตดีๆ เราจะพบว่า url ของหน้านั้นยังคงเหมือนเดิมตลอด ไม่ได้เปลี่ยนไปตามรูป
ใช้ History API ในการเปลี่ยน URL ที่ Address Bar
เพื่อแก้ปัญหาดังกล่าว เราจึงใช้ pushState
เข้ามาช่วย “เปลี่ยน url แบบ manual” โดยก่อนจะใช้ pushState
ให้เราดึงข้อมูลที่เกี่ยวกับหน้าที่เรา “กำลังจะไป” มาให้ครบก่อน
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | $(function(){ $('.gallery-list a').click(function(){ /* เมื่อกดดูรูป จะสั่งให้ทำอะไร? */ var title = $(this).attr('title'); /* ดึง title จาก attribute title ของ a */ var href = $(this).attr('href'); /* ดึง href จาก attribute href ของ a */ var src = $(this).find('img').attr('src'); /* ดึงที่อยู่ของรูปจาก src ของ img */ var data = { /* เก็บข้อมูล title และ src ให้อยู่ในรูปแบบ object เพื่อจะนำไปใส่ตอนใช้ pushState */ title: title, src: src } getImage(data); /* สั่งให้ฟังก์ชัน getImage ทำงาน โดยรับ data ไปด้วย */ window.history.pushState(data, title, href); /* เพิ่ม history entry เข้าไป โดย href จะเป็น url ที่เราต้องการจะเปลี่ยนที่ address bar */ return false; /* ยกเลิกการเปลี่ยนหน้าจากการกด link */ }); }); function getImage(data){ /* ฟังก์ชันสำหรับแสดงรูปจากข้อมูล title และ src ที่ได้รับ */ var image = '<img src="'+data.src+'" alt="'+data.title+'">'; /* สร้าง img element ขึ้นมาจากข้อมูลได้รับ */ document.getElementById('gallery-container').innerHTML = image; /* นำ img element นั้น มาใส่เข้าไปใน container ของ gallery */ } |
จะเห็นว่าเราแยกโค้ดส่วนที่ไป “ดึงรูปมาแสดงใน container” ออกมาเป็นฟังก์ชัน getImage()
เพื่อจะได้สะดวกในการนำมาใช้ซ้ำ
รองรับการ Back, Forward โดยใช้ popstate
จากโค้ดก่อนหน้า หากมีการกดปุ่ม back หรือ forward ที่ web browser เราจะพบว่า url ที่ address bar สามารถแสดงผลได้อย่างถูกต้องแล้ว แต่ที่ container ของ gallery กลับว่างเปล่า ทำไมถึงเป็นเช่นนี้ ?
สาเหตุก็คือ pushState
นั้นมีหน้าที่แค่ “เก็บ” เพียงอย่างเดียว ไม่ใช่ “แสดง” เรานึกขึ้นได้ว่าการกดปุ่ม back หรือ forward ที่ web browser นั้นจะไปทำให้เกิด event popstate
ขึ้น เราจึงจะอาศัย event นี้ในการแสดงรูปที่ container ของ gallery ลองดูโค้ดต่อไปนี้
1 2 3 4 5 6 | window.addEventListener('popstate', function(event) { /* เมื่อ event popstate ทำงาน จะสั่งให้ทำอะไร? */ var state = event.state; /* ดึง data ของ history entry นั้นๆ */ if (state !== null) { /* ถ้า history entry นี้ มี data... */ getImage(state); /* สั่งให้ฟังก์ชัน getImage ทำงาน โดยรับ data ไปด้วย */ } }); |
เนื่องจากเราได้ “เก็บ” ข้อมูลทั้ง title และ src ของรูปเอาไว้ใน history เมื่อตอนที่เรา pushState
แล้ว เราจึงสามารถนำข้อมูลเหล่านั้นออกมาใช้เพื่อสั่งให้ฟังก์ชันสำหรับแสดงรูปทำงานได้ทันที
เพียงขั้นตอนเท่านี้ gallery ของเราก็จะสมบูรณ์แบบ users สามารถเลือกดูรูปภาพได้อย่างรวดเร็วโดยไม่ต้องโหลดหน้าใหม่ สามารถ bookmark หรือแชร์ url ของแต่ละรูปได้ และยังรองรับการกดปุ่ม back และ forward ที่ web browser อีกด้วย
History API ไม่ได้สร้างหน้าใหม่
จะเห็นว่า history api นั้นไม่ได้สร้างหน้าขึ้นมาใหม่แต่อย่างใด มันเป็นเพียง “เครื่องมือ” ที่ใช้จัดการกับประวัติการเข้าชมหน้าเว็บเท่านั้นเอง สมมติว่า users กดเลือกดูรูปที่ 4 แล้วต้องการจะแชร์ไปให้เพื่อนๆ ดูบ้าง url ที่แชร์ก็จะเป็นแบบนี้
1 | http://www.domain.com/4.html |
แน่นอนว่าการเข้า url ที่แชร์มาแบบตรงๆ จะทำให้โค้ด javascript ที่เราเขียนเพื่อใช้โหลด content แบบ dynamic ไม่ทำงาน เราจึงจะต้องเตรียมหน้าสำหรับแสดงรูปแบบปกติเอาไว้เสมอ (ซึ่งเราได้สร้างไว้ตั้งแต่ขั้นตอนแรกแล้ว)
Browser Compatibility + Polyfill
ข่าวดีก็คือ เราสามารถใช้ history api กับ web browsers ส่วนใหญ่ในปัจจุบันได้แล้ว แต่อาจมีปัญหาเล็กน้อยกับ Internet Explorer ที่จะรองรับตั้งแต่เวอร์ชัน 10 ขึ้นไป เราอาจใช้ polyfill อย่าง history.js เข้ามาช่วย โดย polyfill นี้จะใช้วิธี hash เป็น fallback หากพบว่า web browser นั้นยังไม่รองรับ history api
github ใช้ History API ในการ browse ไฟล์
เริ่มใช้ History API เสียแต่วันนี้ !
เราคงจะเห็นแล้วว่า history api นั้นมีประโยชน์มากแค่ไหน การหันมาใช้ history api นั้นจะต่างจากการเริ่มลองใช้ features อื่นๆ ตรงที่เราทำเพื่อ users ไม่ได้ทำเพื่อให้การเขียนของเราถูกต้องตามหลัก ซึ่งแค่เหตุผลนี้ก็เพียงพอแล้วที่เราควรจะลงมือเสียแต่วันนี้