คนเชิดหุ่นกระบอกและวิธีเสนอตัวเลือก
Puppeteer เป็นไลบรารีการทำงานอัตโนมัติของเบราว์เซอร์สำหรับโหนด โดยให้คุณควบคุมเบราว์เซอร์โดยใช้ JavaScript API ที่ทันสมัยและไม่ซับซ้อน
แน่นอนว่างานที่สำคัญที่สุดของเบราว์เซอร์คือการเรียกดูหน้าเว็บ การทำให้งานนี้ทำงานอัตโนมัตินั้นมีส่วนสำคัญในการทำให้การโต้ตอบกับหน้าเว็บเป็นแบบอัตโนมัติ
ใน Puppeteer จะทราบได้จากการค้นหาองค์ประกอบ DOM โดยใช้ตัวเลือกตามสตริง และดำเนินการต่างๆ เช่น คลิกหรือพิมพ์ข้อความในองค์ประกอบ ตัวอย่างเช่น สคริปต์ที่เปิด developer.google.com แล้วพบช่องค้นหา และการค้นหา puppetaria
อาจมีลักษณะดังนี้
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://developers.google.com/', { waitUntil: 'load' });
// Find the search box using a suitable CSS selector.
const search = await page.$('devsite-search > form > div.devsite-search-container');
// Click to expand search box and focus it.
await search.click();
// Enter search string and press Enter.
await search.type('puppetaria');
await search.press('Enter');
})();
วิธีที่ระบบระบุองค์ประกอบโดยใช้ตัวเลือกการค้นหาจึงเป็นองค์ประกอบสำคัญในประสบการณ์การใช้งาน Puppeteer ก่อนหน้านี้ ตัวเลือกใน Puppeteer ยังจำกัดอยู่เพียงตัวเลือก CSS และ XPath ซึ่งแม้ว่าจะมีประสิทธิภาพมาก แต่ก็อาจมีข้อเสียต่อการโต้ตอบกับเบราว์เซอร์ในสคริปต์ได้
ตัวเลือกไวยากรณ์เทียบกับความหมาย
ตัวเลือก CSS มีลักษณะเป็นไวยากรณ์ จะมีการเชื่อมโยงอย่างมากกับการทำงานภายในของการแสดงข้อความต้นไม้ DOM ในลักษณะที่เป็นการอ้างอิงรหัสและชื่อคลาสจาก DOM ด้วยเหตุนี้ จึงมีเครื่องมือที่เป็นส่วนประกอบสำหรับนักพัฒนาเว็บเพื่อใช้แก้ไขหรือเพิ่มสไตล์ให้กับองค์ประกอบในหน้าเว็บ แต่นักพัฒนาซอฟต์แวร์จะมีสิทธิ์ควบคุมหน้าเว็บและโครงสร้าง DOM ของหน้าเว็บอย่างสมบูรณ์
ในทางกลับกัน สคริปต์ Puppeteer เป็นผู้สังเกตการณ์ภายนอกของหน้าเว็บ ดังนั้นเมื่อมีการใช้ตัวเลือก CSS ในบริบทนี้ ก็จะมีสมมติฐานแอบแฝงเกี่ยวกับวิธีใช้งานหน้าเว็บซึ่งสคริปต์ Puppeteer ไม่สามารถควบคุมได้
ผลกระทบก็คือ สคริปต์ดังกล่าวมีความเปราะบางและมีความเสี่ยงต่อการเปลี่ยนแปลงซอร์สโค้ด ตัวอย่างเช่น สมมติว่าหนึ่งใช้สคริปต์ Puppeteer สำหรับการทดสอบอัตโนมัติสำหรับเว็บแอปพลิเคชันที่มีโหนด <button>Submit</button>
เป็นโหนดย่อยที่ 3 ขององค์ประกอบ body
ข้อมูลโค้ดหนึ่งจากกรอบการทดสอบอาจมีลักษณะดังนี้
const button = await page.$('body:nth-child(3)'); // problematic selector
await button.click();
ตรงนี้เราจะใช้ตัวเลือก 'body:nth-child(3)'
เพื่อค้นหาปุ่มส่ง แต่ปุ่มนี้จะผูกกับหน้าเว็บเวอร์ชันนี้ทุกประการ หากมีการเพิ่มองค์ประกอบเหนือปุ่มในภายหลัง ตัวเลือกนี้จะใช้งานไม่ได้อีกต่อไป
นี่ไม่ใช่ข่าวสำหรับบรรดานักเขียนทดสอบ ผู้ใช้ Puppeteer ได้พยายามเลือกตัวเลือกที่เหมาะสำหรับการเปลี่ยนแปลงดังกล่าวอยู่แล้ว เรามอบเครื่องมือใหม่ในภารกิจนี้ให้แก่ผู้ใช้ด้วย Puppetaria
ขณะนี้ Puppeteer จะจัดส่งโดยใช้เครื่องจัดการการค้นหาทางเลือกโดยอิงตามการค้นหาแผนผังการช่วยเหลือพิเศษแทนที่จะใช้ตัวเลือก CSS ปรัชญาพื้นฐานในที่นี้คือ หากองค์ประกอบที่เป็นรูปธรรมที่เราต้องการเลือกไม่มีการเปลี่ยนแปลง โหนดการช่วยเหลือพิเศษที่เกี่ยวข้องก็ไม่ควรเปลี่ยนแปลงเช่นกัน
เราตั้งชื่อตัวเลือกดังกล่าวว่า "ตัวเลือก ARIA" และรองรับการค้นหาชื่อที่เข้าถึงได้ที่คำนวณแล้ว และบทบาทสำหรับโครงสร้างการช่วยเหลือพิเศษ เมื่อเทียบกับตัวเลือก CSS คุณสมบัติเหล่านี้เป็นไปตามความหมาย ค่าเหล่านี้ไม่ได้เชื่อมโยงกับคุณสมบัติด้านไวยากรณ์ของ DOM แต่จะใช้ตัวบอกวิธีการสังเกตหน้าเว็บผ่านเทคโนโลยีความช่วยเหลือพิเศษ เช่น โปรแกรมอ่านหน้าจอ
ในตัวอย่างสคริปต์ทดสอบข้างต้น เราสามารถใช้ตัวเลือก aria/Submit[role="button"]
เพื่อเลือกปุ่มที่ต้องการแทน โดย Submit
หมายถึงชื่อที่เข้าถึงได้ขององค์ประกอบ:
const button = await page.$('aria/Submit[role="button"]');
await button.click();
ตอนนี้หากเราตัดสินใจเปลี่ยนเนื้อหาข้อความของปุ่มจาก Submit
เป็น Done
ในภายหลัง การทดสอบก็จะล้มเหลวอีกครั้ง แต่ในกรณีนี้เป็นไปตามที่ต้องการ การเปลี่ยนชื่อของปุ่มจะทำให้เราเปลี่ยนเนื้อหาของหน้าเว็บ ไม่ใช่การนำเสนอด้วยภาพหรือโครงสร้างของปุ่มใน DOM การทดสอบของเราควรเตือนเราเกี่ยวกับการเปลี่ยนแปลงดังกล่าวเพื่อให้แน่ใจว่าการเปลี่ยนแปลงเป็นไปโดยตั้งใจ
กลับไปที่ตัวอย่างที่ใหญ่กว่าด้วยแถบค้นหา เราสามารถใช้ประโยชน์จากเครื่องจัดการ aria
ใหม่และแทนที่
const search = await page.$('devsite-search > form > div.devsite-search-container');
กับ
const search = await page.$('aria/Open search[role="button"]');
เพื่อค้นหาแถบค้นหา
โดยทั่วไป เราเชื่อว่าการใช้ตัวเลือก ARIA ดังกล่าวสามารถมอบประโยชน์ต่อไปนี้ให้แก่ผู้ใช้ Puppeteer ได้
- ทำให้ตัวเลือกในสคริปต์ทดสอบมีความยืดหยุ่นมากขึ้นต่อการเปลี่ยนแปลงซอร์สโค้ด
- ทำให้สคริปต์การทดสอบอ่านง่ายขึ้น (ชื่อที่เข้าถึงได้คือตัวบ่งชี้ความหมาย)
- กระตุ้นแนวทางปฏิบัติที่ดีในการกำหนดพร็อพเพอร์ตี้การช่วยเหลือพิเศษให้กับองค์ประกอบ
ส่วนที่เหลือของบทความนี้จะอธิบายรายละเอียดเกี่ยวกับวิธีที่เราดำเนินโครงการ Puppetaria
ขั้นตอนการออกแบบ
ข้อมูลเบื้องต้น
ดังที่ได้กล่าวไปข้างต้น เราต้องการเปิดใช้องค์ประกอบการค้นหาตามชื่อและบทบาทที่เข้าถึงได้ รายการเหล่านี้เป็นคุณสมบัติของแผนผังการช่วยเหลือพิเศษ ซึ่งเป็นคู่กับแผนผัง DOM ปกติที่อุปกรณ์ เช่น โปรแกรมอ่านหน้าจอใช้เพื่อแสดงหน้าเว็บ
เมื่อดูข้อมูลจำเพาะของการคำนวณชื่อที่เข้าถึงได้ เห็นได้ชัดเจนว่าการคำนวณชื่อขององค์ประกอบหนึ่งๆ เป็นงานที่ไม่สำคัญ ดังนั้นตั้งแต่ต้น เราจึงตัดสินใจว่าต้องการนำโครงสร้างพื้นฐานที่มีอยู่ของ Chromium มาใช้ใหม่
วิธีที่เราใช้แคมเปญนี้
แม้แต่การจำกัดตัวเองให้ใช้โครงสร้างการช่วยเหลือพิเศษของ Chromium เราก็มีวิธีใช้งานการค้นหา ARIA ใน Puppeteer อยู่ 2-3 วิธี หากต้องการทราบเหตุผล เรามาดูวิธีที่ Puppeteer ควบคุมเบราว์เซอร์กันก่อน
เบราว์เซอร์แสดงอินเทอร์เฟซการแก้ไขข้อบกพร่องผ่านโปรโตคอลที่เรียกว่า Chrome DevTools Protocol (CDP) ระบบจะแสดงฟังก์ชัน เช่น "โหลดหน้าเว็บซ้ำ" หรือ "เรียกใช้ JavaScript ส่วนนี้ในหน้าเว็บและส่งผลลัพธ์กลับมา" ผ่านอินเทอร์เฟซที่เข้าใจได้โดยไม่จำเป็นต้องเข้าใจภาษาที่พูด
ทั้งฟรอนท์เอนด์ของเครื่องมือสำหรับนักพัฒนาเว็บและ Puppeteer ต่างก็ใช้ CDP เพื่อสื่อสารกับเบราว์เซอร์ ในการใช้คำสั่ง CDP จะต้องมีโครงสร้างพื้นฐานของเครื่องมือสำหรับนักพัฒนาเว็บอยู่ในคอมโพเนนต์ทั้งหมดของ Chrome เช่น ในเบราว์เซอร์ ในโหมดแสดงภาพ และอื่นๆ CDP จะดูแลการกำหนดเส้นทางคำสั่งไปยังที่ที่ถูกต้อง
การทำงานของการเชิดหุ่น เช่น การค้นหา การคลิก และการประเมินนิพจน์จะทำได้โดยใช้ประโยชน์จากคำสั่ง CDP เช่น Runtime.evaluate
ซึ่งจะประเมิน JavaScript โดยตรงในบริบทหน้าเว็บและส่งผลลัพธ์กลับไป การดำเนินการอื่นๆ ของ Puppeteer เช่น การจำลองภาวะบกพร่องในการมองเห็นสี การถ่ายภาพหน้าจอ หรือการจับภาพการติดตามจะใช้ CDP เพื่อสื่อสารกับกระบวนการแสดงผล Blink โดยตรง
ซึ่งทำให้เรามี 2 เส้นทางในการใช้ฟังก์ชันการค้นหา ได้แก่ เราสามารถทำสิ่งต่อไปนี้
- เขียนตรรกะการค้นหาของเราใน JavaScript และแทรกสิ่งนั้นลงในหน้าเว็บโดยใช้
Runtime.evaluate
หรือ - ใช้ปลายทาง CDP ที่เข้าถึงและค้นหาแผนผังการช่วยเหลือพิเศษได้โดยตรงในกระบวนการ Blink
เราใช้ต้นแบบ 3 แบบ ดังนี้
- JS DOM Traversal - โดยอิงตามการแทรก JavaScript ลงในหน้าเว็บ
- การส่งผ่าน Puppeteer AXTree - อิงจากการใช้การเข้าถึง CDP ที่มีอยู่ไปยังแผนผังการช่วยเหลือพิเศษ
- การข้ามผ่าน DOM ของ CDP - การใช้ปลายทาง CDP ใหม่ที่สร้างขึ้นสำหรับวัตถุประสงค์ในการค้นหาแผนผังการช่วยเหลือพิเศษ
การส่งผ่าน JS DOM
ต้นแบบนี้จะข้ามผ่าน DOM โดยสมบูรณ์และใช้ element.computedName
และ element.computedRole
ซึ่งกั้นไว้ในแฟล็กการเปิดตัว ComputedAccessibilityInfo
เพื่อดึงข้อมูลชื่อและบทบาทของแต่ละองค์ประกอบระหว่างการข้ามผ่าน
การส่งผ่านของ Puppeteer AXTree
ในส่วนนี้ เราจะเรียกข้อมูลโครงสร้างการช่วยเหลือพิเศษทั้งหมดผ่าน CDP และข้ามแผนผังใน Puppeteer จากนั้นโหนดการช่วยเหลือพิเศษที่ได้จะแมปกับโหนด DOM
การส่งผ่าน DOM ของ CDP
สำหรับต้นแบบนี้ เราใช้ปลายทาง CDP ใหม่เพื่อค้นหาโครงสร้างการช่วยเหลือพิเศษโดยเฉพาะ ด้วยวิธีนี้ การค้นหาอาจเกิดขึ้นในส่วนหลังผ่านการใช้งาน C++ แทนในบริบทของหน้าเว็บผ่าน JavaScript
การเปรียบเทียบการทดสอบหน่วย
รูปต่อไปนี้เปรียบเทียบรันไทม์ทั้งหมดของการค้นหาองค์ประกอบ 4 รายการ 1,000 ครั้งสำหรับต้นแบบทั้ง 3 ตัว การเปรียบเทียบได้ดำเนินการในการกำหนดค่า 3 แบบที่แตกต่างกันโดยแบ่งตามขนาดหน้าเว็บ และมีการเปิดใช้การแคชองค์ประกอบการช่วยเหลือพิเศษหรือไม่
เห็นได้ค่อนข้างชัดเจนว่ากลไกการค้นหาที่ใช้ CDP กับอีก 2 กลไกการค้นหามีความแตกต่างอย่างมากระหว่างกลไกการค้นหาที่ใช้เพียง Puppeteer เท่านั้น และผลต่างสัมพัทธ์ก็ดูเพิ่มขึ้นอย่างมากเมื่อกำหนดขนาดหน้าเว็บ ค่อนข้างน่าสนใจที่เห็นว่าต้นแบบ JS DOM Traversal ตอบสนองต่อการแคชการเข้าถึงได้เป็นอย่างดี เมื่อปิดใช้การแคช ระบบจะคำนวณโครงสร้างการช่วยเหลือพิเศษตามคำขอ และทิ้งแผนผังหลังการโต้ตอบแต่ละครั้งหากโดเมนถูกปิดใช้ การเปิดใช้โดเมนจะทำให้ Chromium แคชโครงสร้างที่คำนวณแล้วแทน
สำหรับการข้ามผ่าน JS DOM เราจะขอชื่อและบทบาทที่เข้าถึงได้สำหรับทุกองค์ประกอบระหว่างการข้ามผ่าน ดังนั้นหากมีการปิดใช้การแคช Chromium จะคำนวณและทิ้งโครงสร้างการช่วยเหลือพิเศษสำหรับทุกองค์ประกอบที่เราเข้าชม ในทางกลับกัน สำหรับวิธีการที่ใช้ CDP โครงสร้างดังกล่าวจะถูกยกเลิกระหว่างการเรียกไปยัง CDP แต่ละครั้งเท่านั้น กล่าวคือ สำหรับทุกคำค้นหา วิธีการเหล่านี้ยังได้ประโยชน์จากการเปิดใช้แคชด้วย เนื่องจากโครงสร้างการช่วยเหลือพิเศษจะคงอยู่ตลอดการเรียก CDP แต่การเพิ่มประสิทธิภาพจึงน้อยกว่า
แม้ว่าการเปิดใช้การแคชจะดูเหมาะกับการใช้งานในส่วนนี้ แต่ก็ยังมีค่าใช้จ่ายในการใช้หน่วยความจำเพิ่มเติม สคริปต์นี้อาจทำให้เกิดปัญหาสำหรับสคริปต์ Puppeteer เช่น บันทึกไฟล์การติดตาม เราจึงตัดสินใจที่จะไม่เปิดใช้การแคชแผนผังการช่วยเหลือพิเศษโดยค่าเริ่มต้น ผู้ใช้สามารถเปิดการแคชด้วยตนเองโดยเปิดใช้โดเมนการช่วยเหลือพิเศษของ CDP
การเปรียบเทียบชุดทดสอบของเครื่องมือสำหรับนักพัฒนาเว็บ
การเปรียบเทียบก่อนหน้านี้แสดงให้เห็นว่าการใช้กลไกการค้นหาของเราที่เลเยอร์ CDP ช่วยเพิ่มประสิทธิภาพในสถานการณ์การทดสอบหน่วยทางคลินิก
ในการดูว่ามีความแตกต่างชัดเจนพอที่จะสังเกตได้ในสถานการณ์ที่สมจริงมากขึ้นในการใช้งานชุดทดสอบเต็มรูปแบบหรือไม่ เราได้แพตช์ชุดทดสอบแบบต้นทางถึงปลายทางในเครื่องมือสำหรับนักพัฒนาเว็บเพื่อใช้ประโยชน์จากต้นแบบที่ใช้ JavaScript และ CDP และเปรียบเทียบรันไทม์ ในการเปรียบเทียบนี้ เราเปลี่ยนตัวเลือกทั้งหมด 43 รายการจาก [aria-label=…]
เป็นเครื่องจัดการการค้นหาที่กำหนดเอง aria/…
ซึ่งต่อมาเราจะทำได้โดยการนำมาใช้กับแต่ละต้นแบบ
ตัวเลือกบางตัวมีการใช้หลายครั้งในสคริปต์ทดสอบ ดังนั้น จำนวนการดำเนินการจริงของตัวแฮนเดิลการค้นหา aria
คือ 113 ครั้งต่อการเรียกใช้ชุดโปรแกรม จำนวนการเลือกคำค้นหาทั้งหมดคือ 2, 253 รายการ ดังนั้นการค้นหาเพียงบางส่วนจึงเกิดขึ้นผ่านต้นแบบ
ดังที่เห็นในภาพด้านบน รันไทม์ทั้งหมดมีความแตกต่างกันอย่างชัดเจน ข้อมูลไม่ชัดเจนมากจนไม่สามารถสรุปข้อมูลได้อย่างเฉพาะเจาะจง แต่เห็นได้ชัดเจนว่าช่องว่างด้านประสิทธิภาพระหว่างต้นแบบทั้งสองแสดงให้เห็นในสถานการณ์นี้เช่นกัน
ปลายทาง CDP ใหม่
จากการเปรียบเทียบข้างต้น และเนื่องจากวิธีการแบบอิงตาม Flag เปิดตัวนั้นไม่เป็นที่ต้องการโดยทั่วไป เราจึงตัดสินใจเดินหน้าใช้คำสั่ง CDP ใหม่เพื่อค้นหาโครงสร้างการช่วยเหลือพิเศษ เอาล่ะ เราต้องหาอินเทอร์เฟซของอุปกรณ์ปลายทางใหม่นี้กัน
สำหรับกรณีการใช้งานของเราใน Puppeteer เราต้องใช้ปลายทางที่ใช้สิ่งที่เรียกกันว่า RemoteObjectIds
เป็นอาร์กิวเมนต์ และหากต้องการให้เราค้นหาองค์ประกอบ DOM ที่เกี่ยวข้องได้ในภายหลัง ควรแสดงรายการออบเจ็กต์ที่มี backendNodeIds
สำหรับองค์ประกอบ DOM
ดังที่เห็นในแผนภูมิด้านล่าง เราได้ลองหลายวิธีที่พึงพอใจกับอินเทอร์เฟซนี้ จากข้อมูลนี้ เราพบว่าขนาดของออบเจ็กต์ที่แสดงผล เช่น เราแสดงโหนดการช่วยเหลือพิเศษแบบสมบูรณ์หรือไม่ หรือเพียง backendNodeIds
เท่านั้นที่ทำให้ไม่เห็นความแตกต่าง ในทางกลับกัน เราพบว่าการใช้ NextInPreOrderIncludingIgnored
ที่มีอยู่เป็นตัวเลือกที่ไม่ดีในการใช้ตรรกะการข้ามผ่านที่นี่ เนื่องจากทำให้ช้าลงอย่างมาก
สรุปทุกอย่าง
หลังจากที่มีปลายทาง CDP แล้ว เราจึงติดตั้งใช้งานตัวแฮนเดิลการค้นหาในฝั่ง Puppeteer สิ่งที่ต้องปรับปรุงก็คือการปรับโครงสร้างโค้ดการจัดการการค้นหาใหม่เพื่อให้การค้นหาแก้ปัญหาผ่าน CDP ได้โดยตรง แทนที่จะต้องค้นหาผ่าน JavaScript ที่ประเมินในบริบทของหน้า
ขั้นตอนถัดไปคือ
เครื่องจัดการ aria
ใหม่ที่มาพร้อมกับ Puppeteer v5.4.0 เป็นตัวแฮนเดิลการค้นหาในตัว เราตื่นเต้นที่จะได้เห็นว่าผู้ใช้นำการเปลี่ยนแปลงนี้ไปใช้ในสคริปต์การทดสอบอย่างไร และเราอดใจรอไม่ไหวที่จะได้ฟังแนวคิดของคุณว่าจะพัฒนาให้มีประโยชน์มากขึ้นได้อย่างไรบ้าง
ดาวน์โหลดเวอร์ชันตัวอย่าง
ลองใช้ Chrome Canary, Dev หรือ เบต้า เป็นเบราว์เซอร์เริ่มต้นสำหรับการพัฒนา ช่องทางพรีวิวเหล่านี้จะทำให้คุณเข้าถึงฟีเจอร์ล่าสุดของเครื่องมือสำหรับนักพัฒนาเว็บ ทดสอบ API แพลตฟอร์มเว็บที่ล้ำสมัย และพบปัญหาในเว็บไซต์ก่อนผู้ใช้
ติดต่อทีม Chrome DevTools
ใช้ตัวเลือกต่อไปนี้เพื่อพูดคุยเกี่ยวกับฟีเจอร์ใหม่และการเปลี่ยนแปลงในโพสต์ หรืออื่นๆ ที่เกี่ยวข้องกับเครื่องมือสำหรับนักพัฒนาเว็บ
- ส่งข้อเสนอแนะหรือความคิดเห็นถึงเราทาง crbug.com
- รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บโดยใช้ตัวเลือกเพิ่มเติม
> ความช่วยเหลือ > รายงานปัญหาเกี่ยวกับเครื่องมือสำหรับนักพัฒนาเว็บในเครื่องมือสำหรับนักพัฒนาเว็บ
- ทวีตที่ @ChromeDevTools
- แสดงความคิดเห็นว่ามีอะไรใหม่ในวิดีโอ YouTube เครื่องมือสำหรับนักพัฒนาเว็บ หรือวิดีโอ YouTube สำหรับเครื่องมือสำหรับนักพัฒนาเว็บ