การเพิ่มความสามารถในการแยกโค้ดให้กับเว็บไซต์ WordPress ผ่าน PoP

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ เป็นที่ทราบกันดีอยู่แล้วว่าเว็บไซต์ WP นั้นไม่สะดวกที่จะใช้การแยกโค้ดผ่าน Webpack Leonardo นำสิ่งต่าง ๆ มาไว้ในมือของเขาเองและใช้การแยกโค้ดในเวอร์ชันของตัวเองสำหรับเฟรมเวิร์กโอเพนซอร์ซชื่อ PoP

ความเร็วเป็นหนึ่งในสิ่งสำคัญที่สุดสำหรับเว็บไซต์ทุกวันนี้ วิธีหนึ่งในการทำให้เว็บไซต์โหลดเร็วขึ้นคือโดยการแยกโค้ด: แบ่งแอปพลิเคชันออกเป็นชิ้น ๆ ที่สามารถโหลดได้ตามต้องการ — โหลดเฉพาะ JavaScript ที่จำเป็นซึ่งจำเป็นเท่านั้นและไม่มีอะไรอื่น เว็บไซต์ที่ใช้กรอบงาน JavaScript สามารถใช้การแยกโค้ดผ่าน Webpack ซึ่งเป็นชุดรวม JavaScript ยอดนิยมได้ทันที สำหรับเว็บไซต์ WordPress นั้นไม่ใช่เรื่องง่าย ประการแรก Webpack ไม่ได้ตั้งใจสร้างขึ้นเพื่อทำงานกับ WordPress ดังนั้นการตั้งค่าจึงต้องใช้วิธีแก้ปัญหาบางอย่าง ประการที่สอง ดูเหมือนว่าไม่มีเครื่องมือใดที่สามารถให้ความสามารถในการโหลดเนื้อหาตามความต้องการสำหรับ WordPress

เนื่องจากไม่มีโซลูชันที่เหมาะสมสำหรับ WordPress นี้ ฉันจึงตัดสินใจใช้เวอร์ชันการแยกโค้ดสำหรับ PoP ซึ่งเป็นเฟรมเวิร์กโอเพนซอร์สสำหรับสร้างเว็บไซต์ WordPress ที่ฉันสร้างขึ้นเอง เว็บไซต์ WordPress ที่ติดตั้ง PoP จะมีความสามารถในการแยกโค้ดโดยกำเนิด ดังนั้นจึงไม่จำเป็นต้องพึ่งพา Webpack หรือบันเดิลอื่นๆ ในบทความนี้ ผมจะแสดงให้คุณเห็นถึงวิธีการทำงาน โดยอธิบายว่าการตัดสินใจใดเกิดขึ้นจากแง่มุมต่างๆ ของสถาปัตยกรรมของเฟรมเวิร์ก ในท้ายที่สุด ฉันจะวิเคราะห์ประสิทธิภาพของเว็บไซต์ที่มีและไม่มีการแยกโค้ด รวมถึงประโยชน์และข้อเสียของการใช้การใช้งานแบบกำหนดเองกับบันเดิลภายนอก ฉันหวังว่าคุณจะสนุกกับการนั่ง!

การกำหนดกลยุทธ์

การแยกรหัสสามารถแบ่งออกกว้างๆ ได้เป็นสองขั้นตอนดังนี้:

  1. การคำนวณว่าต้องโหลดสินทรัพย์ใดสำหรับทุกเส้นทาง
  2. โหลดเนื้อหาเหล่านั้นตามต้องการแบบไดนามิก

เพื่อจัดการกับขั้นตอนแรก เราจะต้องสร้างแผนที่การพึ่งพาสินทรัพย์ ซึ่งรวมถึงทรัพย์สินทั้งหมดในแอปพลิเคชันของเรา ต้องเพิ่มเนื้อหาแบบเรียกซ้ำในแผนที่นี้ — ต้องเพิ่มการขึ้นต่อกันของการอ้างอิงด้วย จนกว่าจะไม่ต้องการเนื้อหาเพิ่มเติม จากนั้น เราสามารถคำนวณการพึ่งพาอาศัยทั้งหมดที่จำเป็นสำหรับเส้นทางหนึ่งๆ โดยผ่านแผนที่การพึ่งพาสินทรัพย์ โดยเริ่มจากจุดเริ่มต้นเส้นทาง (เช่น ไฟล์หรือชิ้นส่วนของรหัสที่เริ่มดำเนินการ) ไปจนถึงระดับสุดท้าย

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

เพื่อจัดการกับขั้นตอนที่ 2 เราสามารถคำนวณว่าเนื้อหาใดที่จำเป็นสำหรับ URL ที่ร้องขอทางฝั่งเซิร์ฟเวอร์ จากนั้นจึงส่งรายการทรัพย์สินที่จำเป็นในการตอบกลับ ซึ่งแอปพลิเคชันจะต้องโหลดเนื้อหาเหล่านั้น หรือ HTTP/ โดยตรง 2 ผลักดันทรัพยากรควบคู่ไปกับการตอบสนอง

อย่างไรก็ตาม วิธีแก้ปัญหาเหล่านี้ไม่เหมาะสม ในกรณีแรก แอปพลิเคชันจะต้องขอเนื้อหาทั้งหมดหลังจากการตอบกลับ ดังนั้นจะมีชุดคำขอไป-กลับเพิ่มเติมเพื่อดึงเนื้อหา และไม่สามารถสร้างมุมมองได้ก่อนที่จะโหลดทั้งหมด ส่งผลให้ ผู้ใช้ต้องรอ (ปัญหานี้บรรเทาลงได้ด้วยการเตรียมเนื้อหาทั้งหมดล่วงหน้าผ่านพนักงานบริการ ดังนั้นเวลารอจึงลดลง แต่เราไม่สามารถหลีกเลี่ยงการแยกวิเคราะห์สินทรัพย์ที่เกิดขึ้นหลังจากการตอบสนองกลับมาแล้วเท่านั้น) ในกรณีที่สอง เราอาจผลักดันเนื้อหาเดิมซ้ำๆ (เว้นแต่เราจะเพิ่มตรรกะพิเศษบางอย่าง เช่น เพื่อระบุว่าทรัพยากรใดที่เราโหลดผ่านคุกกี้แล้ว แต่สิ่งนี้จะเพิ่มความซับซ้อนที่ไม่ต้องการและบล็อกการตอบสนองจากการถูกแคช) และเรา ไม่สามารถให้บริการสินทรัพย์จาก CDN

ด้วยเหตุนี้ ฉันจึงตัดสินใจจัดการตรรกะนี้ในฝั่งไคลเอ็นต์ รายการของสินทรัพย์ที่จำเป็นสำหรับแต่ละเส้นทางจะพร้อมใช้งานสำหรับแอปพลิเคชันบนไคลเอนต์ เพื่อให้รู้อยู่แล้วว่าสินทรัพย์ใดที่จำเป็นสำหรับ URL ที่ร้องขอ การดำเนินการนี้แก้ไขปัญหาที่ระบุไว้ข้างต้น:

  • สามารถโหลดเนื้อหาได้ทันที ไม่ต้องรอการตอบกลับจากเซิร์ฟเวอร์ (เมื่อเราจับคู่สิ่งนี้กับพนักงานบริการ เราค่อนข้างมั่นใจว่าเมื่อการตอบสนองกลับมา ทรัพยากรทั้งหมดจะถูกโหลดและแยกวิเคราะห์ ดังนั้นจึงไม่มีเวลารอเพิ่มเติม)
  • แอปพลิเคชันรู้ว่าเนื้อหาใดโหลดไปแล้ว ดังนั้นจะไม่ขอเนื้อหาทั้งหมดที่จำเป็นสำหรับเส้นทางนั้น แต่จะขอเฉพาะเนื้อหาที่ยังไม่ได้โหลดเท่านั้น

ด้านลบในการแสดงรายการนี้ไปยังส่วนหน้าคืออาจใช้งานหนัก ขึ้นอยู่กับขนาดของเว็บไซต์ (เช่น จำนวนเส้นทางที่ให้บริการ) เราจำเป็นต้องหาวิธีโหลดโดยไม่เพิ่มเวลาในการโหลดที่รับรู้ของแอปพลิเคชัน เพิ่มเติมเกี่ยวกับเรื่องนี้ในภายหลัง

เมื่อทำการตัดสินใจเหล่านี้แล้ว เราก็สามารถดำเนินการออกแบบและใช้การแยกโค้ดในแอปพลิเคชันได้ เพื่อให้เข้าใจได้ง่ายขึ้น จึงมีการแบ่งขั้นตอนดังนี้

  1. การทำความเข้าใจสถาปัตยกรรมของแอปพลิเคชัน
  2. การทำแผนที่การพึ่งพาสินทรัพย์
  3. รายชื่อเส้นทางการสมัครทั้งหมด
  4. การสร้างรายการที่กำหนดสินทรัพย์ที่จำเป็นสำหรับแต่ละเส้นทาง
  5. กำลังโหลดเนื้อหาแบบไดนามิก
  6. การใช้การเพิ่มประสิทธิภาพ

มาเข้าเรื่องกันเลย!

0. การทำความเข้าใจสถาปัตยกรรมของแอปพลิเคชัน

เราจะต้องแมปความสัมพันธ์ของทรัพย์สินทั้งหมดเข้าด้วยกัน มาดูลักษณะเฉพาะของสถาปัตยกรรม PoP เพื่อออกแบบโซลูชันที่เหมาะสมที่สุดเพื่อให้บรรลุเป้าหมายนี้

PoP เป็นเลเยอร์ที่ล้อมรอบ WordPress ทำให้เราใช้ WordPress เป็น CMS ที่ขับเคลื่อนแอปพลิเคชัน แต่ยังมีเฟรมเวิร์ก JavaScript ที่กำหนดเองเพื่อแสดงเนื้อหาบนฝั่งไคลเอ็นต์เพื่อสร้างเว็บไซต์แบบไดนามิก มันกำหนดองค์ประกอบการสร้างของหน้าเว็บใหม่: ในขณะที่ WordPress นั้นใช้แนวคิดของเทมเพลตแบบลำดับชั้นที่สร้าง HTML (เช่น single.php , home.php และ archive.php ) PoP ขึ้นอยู่กับแนวคิดของ “โมดูล ” ซึ่งเป็นฟังก์ชันอะตอมมิกหรือองค์ประกอบของโมดูลอื่นๆ การสร้างแอปพลิเคชัน PoP นั้นคล้ายกับการเล่นกับ LEGO — การซ้อนโมดูลทับกันหรือห่อหุ้มแต่ละส่วนเข้าด้วยกัน ซึ่งท้ายที่สุดแล้วจะสร้างโครงสร้างที่ซับซ้อนมากขึ้น นอกจากนี้ยังสามารถคิดได้ว่าเป็นการนำการออกแบบอะตอมของแบรดฟรอสต์มาใช้และดูเหมือนว่านี้:

ตัวอย่างขนาดใหญ่

โมดูลสามารถจัดกลุ่มเป็นเอนทิตีที่มีลำดับสูงกว่า กล่าวคือ: บล็อก, blockGroups, pageSections และ topLevels เอนทิตีเหล่านี้เป็นโมดูลเช่นกัน เพียงมีคุณสมบัติและความรับผิดชอบเพิ่มเติม และมีส่วนประกอบซึ่งกันและกันตามสถาปัตยกรรมจากบนลงล่างอย่างเคร่งครัด ซึ่งแต่ละโมดูลสามารถดูและเปลี่ยนแปลงคุณสมบัติของโมดูลภายในทั้งหมดได้ ความสัมพันธ์ระหว่างโมดูลมีลักษณะดังนี้:

  • 1 ระดับบนสุดมี N ส่วนหน้า
  • 1 pageSection มี N บล็อกหรือ blockGroups
  • 1 blockGroup มี N บล็อกหรือ blockGroups
  • 1 บล็อกประกอบด้วยโมดูล N
  • 1 โมดูลประกอบด้วย N โมดูล ad infinitum

การรันโค้ด JavaScript ใน PoP

PoP สร้าง HTML แบบไดนามิกโดยเริ่มต้นที่ระดับ pageSection ทำซ้ำผ่านโมดูลทั้งหมดด้านล่าง แสดงผลแต่ละโมดูลผ่านเทมเพลต Handlebars ที่กำหนดไว้ล่วงหน้าของโมดูล และสุดท้ายเพิ่มองค์ประกอบที่สร้างขึ้นใหม่ที่เกี่ยวข้องของโมดูลลงใน DOM เมื่อดำเนินการเสร็จแล้ว จะเรียกใช้ฟังก์ชัน JavaScript กับฟังก์ชันเหล่านี้ ซึ่งกำหนดไว้ล่วงหน้าตามโมดูลทีละโมดูล

PoP แตกต่างจากเฟรมเวิร์ก JavaScript (เช่น React และ AngularJS) เนื่องจากโฟลว์ของแอปพลิเคชันไม่ได้เกิดขึ้นที่ไคลเอนต์ แต่ยังคงได้รับการกำหนดค่าไว้ที่ส่วนหลัง ภายในการกำหนดค่าของโมดูล (ซึ่งมีการเข้ารหัสไว้ในอ็อบเจ็กต์ PHP) ได้รับอิทธิพลจากตะขอแอ็คชันของ WordPress PoP ใช้รูปแบบการเผยแพร่ - สมัครสมาชิก:

  1. แต่ละโมดูลกำหนดว่าฟังก์ชัน JavaScript ใดที่ต้องทำงานบนองค์ประกอบ DOM ที่สร้างขึ้นใหม่ที่เกี่ยวข้อง โดยไม่จำเป็นต้องรู้ล่วงหน้าว่าโค้ดนี้จะเรียกใช้งานอะไรหรือจะมาจากไหน
  2. ออบเจ็กต์ JavaScript ต้องลงทะเบียนฟังก์ชัน JavaScript ที่พวกเขานำไปใช้
  3. สุดท้าย บนรันไทม์ PoP จะคำนวณว่าอ็อบเจ็กต์ JavaScript ใดต้องรันฟังก์ชัน JavaScript และเรียกใช้อย่างเหมาะสม

ตัวอย่างเช่น ผ่านอ็อบเจ็กต์ PHP ที่สอดคล้องกัน โมดูลปฏิทินระบุว่าต้องการฟังก์ชัน calendar เพื่อดำเนินการกับองค์ประกอบ DOM ดังนี้:

 class CalendarModule { function get_jsmethods() { $methods = parent::get_jsmethods(); $this->add_jsmethod($methods, 'calendar'); return $methods; } ... }

จากนั้น ออบเจ็กต์ JavaScript — ในกรณีนี้คือ popFullCalendar — ประกาศว่าได้ใช้งานฟังก์ชัน calendar แล้ว สิ่งนี้ทำได้โดยเรียก popJSLibraryManager.register :

 window.popFullCalendar = { calendar : function(elements) { ... } }; popJSLibraryManager.register(popFullCalendar, ['calendar', ...]);

ในที่สุด popJSLibraryManager จะทำการจับคู่กับสิ่งที่รันโค้ดใด อนุญาตให้ออบเจ็กต์ JavaScript ลงทะเบียนว่าใช้ฟังก์ชันใดบ้าง และจัดเตรียมวิธีการเรียกใช้ฟังก์ชันเฉพาะจากออบเจ็กต์ JavaScript ที่สมัครรับข้อมูลทั้งหมด:

 window.popJSLibraryManager = { libraries: [], methods: {}, register : function(library, methods) { this.libraries.push(library); for (var i = 0; i < methods.length; i++) { var method = methods[i]; this.methods[method] = this.methods[method] || []; this.methods[method].push(library); } }, execute : function(method, elements) { var libraries = this.methods[method] || []; for (var i = 0; i < libraries.length; i++) { var library = libraries[i]; library[method](elements); } } }

หลังจากเพิ่มองค์ประกอบปฏิทินใหม่ลงใน DOM ซึ่งมี ID เป็น calendar-293 แล้ว PoP จะดำเนินการฟังก์ชันต่อไปนี้:

 popJSLibraryManager.execute("calendar", document.getElementById("calendar-293"));

จุดเริ่มต้น

สำหรับ PoP จุดเริ่มต้นในการรันโค้ด JavaScript คือบรรทัดนี้ที่ส่วนท้ายของเอาต์พุต HTML:

 <script type="text/javascript">popManager.init();</script>

popManager.init() เริ่มต้นเฟรมเวิร์กส่วนหน้าก่อน จากนั้นจึงเรียกใช้ฟังก์ชัน JavaScript ที่กำหนดโดยโมดูลที่แสดงผลทั้งหมด ดังที่อธิบายไว้ข้างต้น ด้านล่างนี้คือรูปแบบง่ายๆ ของฟังก์ชันนี้ (โค้ดต้นฉบับอยู่บน GitHub) โดยการเรียกใช้ popJSLibraryManager.execute('pageSectionInitialized', pageSection) และ popJSLibraryManager.execute('documentInitialized') ออบเจ็กต์ JavaScript ทั้งหมดที่ใช้ฟังก์ชันเหล่านั้น ( pageSectionInitialized และ documentInitialized ) จะดำเนินการ

 (function($){ window.popManager = { // The configuration for all the modules (including pageSections and blocks) in the application configuration : {...}, init : function() { var that = this; $.each(this.configuration, function(pageSectionId, configuration) { // Obtain the pageSection element in the DOM from the ID var pageSection = $('#'+pageSectionId); // Run all required JavaScript methods on it this.runJSMethods(pageSection, configuration); // Trigger an event marking the block as initialized popJSLibraryManager.execute('pageSectionInitialized', pageSection); }); // Trigger an event marking the document as initialized popJSLibraryManager.execute('documentInitialized'); }, ... }; })(jQuery);

ฟังก์ชัน runJSMethods เรียกใช้งานเมธอด JavaScript ที่กำหนดไว้สำหรับแต่ละโมดูล โดยเริ่มจาก pageSection ซึ่งเป็นโมดูลบนสุด และจากนั้นจึงลงบรรทัดสำหรับบล็อกภายในและโมดูลภายในทั้งหมด:

 (function($){ window.popManager = { ... runJSMethods : function(pageSection, configuration) { // Initialize the heap with "modules", starting from the top one, and recursively iterate over its inner modules var heap = [pageSection.data('module')], i; while (heap.length > 0) { // Get the first element of the heap var module = heap.pop(); // The configuration for that module contains which JavaScript methods to execute, and which are the module's inner modules var moduleConfiguration = configuration[module]; // The list of all JavaScript functions that must be executed on the module's newly created DOM elements var jsMethods = moduleConfiguration['js-methods']; // Get all of the elements added to the DOM for that module, which have been stored in JavaScript object `popJSRuntimeManager` upon creation var elements = popJSRuntimeManager.getDOMElements(module); // Iterate through all of the JavaScript methods and execute them, passing the elements as argument for (i = 0; i < jsMethods.length; i++) { popJSLibraryManager.execute(jsMethods[i], elements); } // Finally, add the inner-modules to the heap heap = heap.concat(moduleConfiguration['inner-modules']); } }, }; })(jQuery);

โดยสรุป การเรียกใช้ JavaScript ใน PoP นั้นเชื่อมโยงกันอย่างหลวมๆ: แทนที่จะมีการพึ่งพาแบบถาวร เราเรียกใช้ฟังก์ชัน JavaScript ผ่าน hook ที่อ็อบเจ็กต์ JavaScript ใดๆ สามารถสมัครรับข้อมูลได้

หน้าเว็บและ API

เว็บไซต์ PoP เป็น API ที่บริโภคเอง ใน PoP ไม่มีความแตกต่างระหว่างหน้าเว็บกับ API: แต่ละ URL ส่งคืนหน้าเว็บโดยค่าเริ่มต้น และเพียงแค่เพิ่มพารามิเตอร์ output=json ก็จะส่งคืน API แทน (เช่น getpop.org/en/ เป็น หน้าเว็บและ getpop.org/en/?output=json คือ API) API ใช้สำหรับการแสดงเนื้อหาแบบไดนามิกใน PoP ดังนั้นเมื่อคลิกลิงก์ไปยังหน้าอื่น API คือสิ่งที่ได้รับการร้องขอ เพราะเมื่อนั้นเฟรมของเว็บไซต์จะโหลดแล้ว (เช่น การนำทางด้านบนและด้านข้าง) — จากนั้นชุดทรัพยากรที่จำเป็นสำหรับโหมด API จะ เป็นส่วนย่อยของสิ่งนั้นจากหน้าเว็บ เราจะต้องคำนึงถึงสิ่งนี้เมื่อคำนวณการขึ้นต่อกันของเส้นทาง: การโหลดเส้นทางเมื่อโหลดเว็บไซต์ครั้งแรกหรือโหลดแบบไดนามิกโดยการคลิกที่ลิงก์จะสร้างชุดเนื้อหาที่จำเป็นต่างกัน

นี่คือแง่มุมที่สำคัญที่สุดของ PoP ที่จะกำหนดการออกแบบและการใช้งานการแยกโค้ด ดำเนินการในขั้นตอนต่อไป

1. การทำแผนที่การพึ่งพาสินทรัพย์

เราสามารถเพิ่มไฟล์การกำหนดค่าสำหรับไฟล์ JavaScript แต่ละไฟล์ โดยให้รายละเอียดการพึ่งพาที่ชัดเจน อย่างไรก็ตาม โค้ดนี้จะทำซ้ำและคงความสม่ำเสมอได้ยาก โซลูชันที่สะอาดกว่าคือเก็บไฟล์ JavaScript ไว้เป็นแหล่งความจริงเพียงแหล่งเดียว แยกโค้ดจากภายใน จากนั้นวิเคราะห์โค้ดนี้เพื่อสร้างการขึ้นต่อกัน

ข้อมูลเมตาที่เรากำลังมองหาในซอร์สไฟล์ JavaScript เพื่อให้สามารถสร้างการแมปใหม่ได้มีดังต่อไปนี้:

  • การเรียกใช้เมธอดภายใน เช่น this.runJSMethods(...) ;
  • การเรียกใช้เมธอดภายนอก เช่น popJSRuntimeManager.getDOMElements(...) ;
  • การเกิดขึ้นทั้งหมดของ popJSLibraryManager.execute(...) ซึ่งเรียกใช้ฟังก์ชัน JavaScript ในวัตถุทั้งหมดที่นำไปใช้
  • เกิดขึ้นทั้งหมดของ popJSLibraryManager.register(...) เพื่อรับว่าอ็อบเจกต์ JavaScript ใดใช้เมธอด JavaScript ใด

เราจะใช้ jParser และ jTokenizer เพื่อสร้างโทเค็นไฟล์ซอร์ส JavaScript ของเราใน PHP และแยกข้อมูลเมตาดังนี้:

  • การเรียกเมธอดภายใน (เช่น this.runJSMethods ) ถูกอนุมานเมื่อค้นหาลำดับต่อไปนี้: โทเค็ . this หรือ that + + โทเค็นอื่น ๆ ซึ่งเป็นชื่อของวิธีการภายใน ( runJSMethods )
  • การเรียกเมธอดภายนอก (เช่น popJSRuntimeManager.getDOMElements ) จะถูกอนุมานเมื่อค้นหาลำดับต่อไปนี้: โทเค็นที่รวมอยู่ในรายการของออบเจกต์ JavaScript ทั้งหมดในแอปพลิเคชันของเรา (เราต้องการรายการนี้ล่วงหน้า ในกรณีนี้ จะมีอ็อบเจ็กต์ popJSRuntimeManager ) + . + โทเค็นอื่น ๆ ซึ่งเป็นชื่อของวิธีการภายนอก ( getDOMElements )
  • เมื่อใดก็ตามที่เราพบ popJSLibraryManager.execute("someFunctionName") เราจะอนุมานว่าเมธอด Javascript เป็น someFunctionName
  • เมื่อใดก็ตามที่เราพบ popJSLibraryManager.register(someJSObject, ["someFunctionName1", "someFunctionName2"]) เราอนุมานว่า Javascript object someJSObject ใช้วิธี someFunctionName1 , someFunctionName2

ฉันได้ใช้สคริปต์แล้ว แต่จะไม่อธิบายที่นี่ (ยาวเกินไปไม่ได้เพิ่มมูลค่ามากนัก แต่สามารถพบได้ในที่เก็บของ PoP) สคริปต์ซึ่งทำงานเมื่อมีการร้องขอหน้าภายในบนเซิร์ฟเวอร์การพัฒนาของเว็บไซต์ (วิธีการที่ฉันได้เขียนเกี่ยวกับในบทความก่อนหน้าเกี่ยวกับพนักงานบริการ) จะสร้างไฟล์การแมปและเก็บไว้ในเซิร์ฟเวอร์ ฉันได้เตรียมตัวอย่างไฟล์การแมปที่สร้างขึ้น เป็นไฟล์ JSON อย่างง่าย ซึ่งมีแอตทริบิวต์ต่อไปนี้:

  • internalMethodCalls
    สำหรับแต่ละออบเจ็กต์ JavaScript แสดงรายการการพึ่งพาจากฟังก์ชันภายในระหว่างกัน
  • externalMethodCalls
    สำหรับออบเจ็กต์ JavaScript แต่ละรายการ ให้แสดงรายการการพึ่งพาจากฟังก์ชันภายในไปยังฟังก์ชันจากออบเจ็กต์ JavaScript อื่นๆ
  • publicMethods
    แสดงรายการเมธอดที่ลงทะเบียนทั้งหมด และสำหรับแต่ละเมธอดที่ออบเจ็กต์ JavaScript นำไปใช้
  • methodExecutions
    สำหรับแต่ละอ็อบเจ็กต์ JavaScript และแต่ละฟังก์ชันภายใน ให้แสดงรายการเมธอดทั้งหมดที่ดำเนินการผ่าน popJSLibraryManager.execute('someMethodName')

โปรดทราบว่าผลลัพธ์ยังไม่ได้แมปการพึ่งพาสินทรัพย์ แต่เป็นแมปการพึ่งพาวัตถุ JavaScript จากแผนที่นี้ เราสามารถสร้าง เมื่อใดก็ตามที่ฟังก์ชันจากบางอ็อบเจ็กต์ถูกเรียกใช้งาน สิ่งที่อ็อบเจกต์อื่น ๆ จะต้องใช้ก็จำเป็นเช่นกัน เรายังคงต้องกำหนดค่าออบเจกต์ JavaScript ที่บรรจุอยู่ในแต่ละแอสเซท สำหรับสินทรัพย์ทั้งหมด (ในสคริปต์ jTokenizer ออบเจกต์ JavaScript คือโทเค็นที่เรากำลังมองหาเพื่อระบุการเรียกเมธอดภายนอก ดังนั้นข้อมูลนี้เป็นอินพุตของสคริปต์และสามารถ ไม่สามารถรับได้จากไฟล์ต้นฉบับเอง) สิ่งนี้ทำได้ผ่านออบเจ็กต์ ResourceLoaderProcessor PHP เช่น resourceloader-processor.php

สุดท้าย ด้วยการรวมแผนที่และการกำหนดค่า เราจะสามารถคำนวณสินทรัพย์ที่จำเป็นทั้งหมดสำหรับทุกเส้นทางในแอปพลิเคชัน

2. รายการเส้นทางการสมัครทั้งหมด

เราจำเป็นต้องระบุเส้นทางทั้งหมดที่มีอยู่ในใบสมัครของเรา สำหรับเว็บไซต์ WordPress รายการนี้เริ่มต้นด้วย URL จากแต่ละลำดับชั้นของเทมเพลต สิ่งที่นำมาใช้สำหรับ PoP คือ:

  • หน้าแรก: https://getpop.org/en/
  • ผู้เขียน: https://getpop.org/en/u/leo/
  • เดี่ยว: https://getpop.org/en/blog/new-feature-code-splitting/
  • แท็ก: https://getpop.org/en/tags/internet/
  • หน้า: https://getpop.org/en/philosophy/
  • หมวดหมู่: https://getpop.org/en/blog/ (หมวดหมู่นี้ใช้งานจริงเป็นเพจ เพื่อลบ category/ ออกจากเส้นทาง URL)
  • 404: https://getpop.org/en/this-page-does-not-exist/

สำหรับแต่ละลำดับชั้นเหล่านี้ เราต้องได้รับเส้นทางทั้งหมดที่สร้างการกำหนดค่าที่ไม่ซ้ำกัน (กล่าวคือ ซึ่งจะต้องมีชุดสินทรัพย์ที่ไม่ซ้ำกัน) ในกรณีของ PoP เรามีดังต่อไปนี้:

  • หน้าแรกและ 404 เป็นเอกลักษณ์
  • หน้าแท็กมีการกำหนดค่าเดียวกันเสมอสำหรับแท็กใดๆ ดังนั้น URL เดียวสำหรับแท็กใดๆ ก็เพียงพอแล้ว
  • โพสต์เดี่ยวขึ้นอยู่กับประเภทของโพสต์ (เช่น "กิจกรรม" หรือ "โพสต์") และหมวดหมู่หลักของโพสต์ (เช่น "บล็อก" หรือ "บทความ") จากนั้น เราต้องการ URL สำหรับแต่ละชุดค่าผสมเหล่านี้
  • การกำหนดค่าหน้าประเภทขึ้นอยู่กับประเภท ดังนั้น เราต้องการ URL ของทุกหมวดหมู่ของโพสต์
  • หน้าผู้เขียนขึ้นอยู่กับบทบาทของผู้เขียน ("บุคคล" "องค์กร" หรือ "ชุมชน") ดังนั้น เราจึงต้องการ URL สำหรับผู้แต่งสามคน ซึ่งแต่ละคนมีบทบาทอย่างใดอย่างหนึ่งเหล่านี้
  • แต่ละหน้าสามารถมีการกำหนดค่าของตัวเองได้ ("เข้าสู่ระบบ" "ติดต่อเรา" "ภารกิจของเรา" ฯลฯ) ดังนั้น ต้องเพิ่ม URL ของหน้าทั้งหมดในรายการ

อย่างที่เราเห็น รายการค่อนข้างยาวอยู่แล้ว นอกจากนี้ แอปพลิเคชันของเราอาจเพิ่มพารามิเตอร์ให้กับ URL ที่เปลี่ยนการกำหนดค่า ซึ่งอาจเปลี่ยนแปลงด้วยว่าเนื้อหาใดที่จำเป็น ตัวอย่างเช่น PoP เสนอให้เพิ่มพารามิเตอร์ URL ต่อไปนี้:

  • แท็บ ( ?tab=… ) เพื่อแสดงข้อมูลที่เกี่ยวข้อง: https://getpop.org/en/blog/new-feature-code-splitting/?tab=authors;
  • format ( ?format=… ) เพื่อเปลี่ยนวิธีการแสดงข้อมูล: https://getpop.org/en/blog/?format=list;
  • target ( ?target=… ) เพื่อเปิดหน้าในหน้าอื่นส่วน: https://getpop.org/en/add-post/?target=addons

เส้นทางเริ่มต้นบางเส้นทางสามารถมีพารามิเตอร์ด้านบนได้หนึ่ง สอง หรือสามตัว ทำให้เกิดชุดค่าผสมที่หลากหลาย:

  • โพสต์เดียว: https://getpop.org/en/blog/new-feature-code-splitting/
  • ผู้เขียนโพสต์เดียว: https://getpop.org/en/blog/new-feature-code-splitting/?tab=authors
  • ผู้เขียนโพสต์เดียวตามรายการ: https://getpop.org/en/blog/new-feature-code-splitting/?tab=authors&format=list
  • ผู้เขียนโพสต์เดียวเป็นรายการในหน้าต่างโมดอล: https://getpop.org/en/blog/new-feature-code-splitting/?tab=authors&format=list&target=modals

โดยสรุป สำหรับ PoP เส้นทางที่เป็นไปได้ทั้งหมดจะเป็นการรวมกันของรายการต่อไปนี้:

  • เส้นทางลำดับชั้นของเทมเพลตเริ่มต้นทั้งหมด
  • ค่าต่าง ๆ ทั้งหมดที่ลำดับชั้นจะสร้างคอนฟิกูเรชันที่แตกต่างกัน
  • แท็บที่เป็นไปได้ทั้งหมดสำหรับแต่ละลำดับชั้น (ลำดับชั้นที่แตกต่างกันอาจมีค่าแท็บที่แตกต่างกัน: โพสต์เดียวสามารถมีแท็บ "ผู้เขียน" และ "การตอบกลับ" ในขณะที่ผู้เขียนสามารถมีแท็บ "โพสต์" และ "ผู้ติดตาม");
  • รูปแบบที่เป็นไปได้ทั้งหมดสำหรับแต่ละแท็บ (แท็บที่แตกต่างกันอาจใช้รูปแบบที่แตกต่างกัน: แท็บ "ผู้เขียน" อาจมีรูปแบบ "แผนที่" แต่แท็บ "ตอบกลับ" อาจไม่มี)
  • เป้าหมายที่เป็นไปได้ทั้งหมดที่ระบุหน้าส่วนที่อาจแสดงแต่ละเส้นทาง (ในขณะที่สามารถสร้างโพสต์ได้ในส่วนหลักหรือในหน้าต่างลอย หน้า "แบ่งปันกับเพื่อนของคุณ" อาจถูกตั้งค่าให้เปิดในหน้าต่างโมดอล)

ดังนั้น สำหรับแอปพลิเคชันที่ซับซ้อนเล็กน้อย การสร้างรายการด้วยเส้นทางทั้งหมดไม่สามารถทำได้ด้วยตนเอง เราต้องสร้างสคริปต์เพื่อดึงข้อมูลนี้ออกจากฐานข้อมูล จัดการมัน และสุดท้าย ส่งออกข้อมูลในรูปแบบที่จำเป็น สคริปต์นี้จะได้รับหมวดหมู่ของโพสต์ทั้งหมด ซึ่งเราสามารถสร้างรายการ URL ของหน้าหมวดหมู่ต่างๆ ได้ จากนั้นสำหรับแต่ละหมวดหมู่ ให้ค้นหาฐานข้อมูลสำหรับโพสต์ใดๆ ที่อยู่ภายใต้เดียวกัน ซึ่งจะทำให้สร้าง URL สำหรับรายการเดียว โพสต์ในทุกหมวด เป็นต้น มีสคริปต์ฉบับเต็มโดยเริ่มจาก function get_resources() ซึ่งจะทำให้ hooks ถูกใช้งานโดยแต่ละกรณีของลำดับชั้น

3. การสร้างรายการที่กำหนดสินทรัพย์ที่จำเป็นสำหรับแต่ละเส้นทาง

ถึงตอนนี้ เรามีแผนที่การพึ่งพาสินทรัพย์และรายการเส้นทางทั้งหมดในแอปพลิเคชัน ถึงเวลาแล้วที่จะรวมสองสิ่งนี้เข้าด้วยกันและสร้างรายการที่ระบุ สำหรับแต่ละเส้นทาง ว่าจำเป็นต้องมีสินทรัพย์ใด

ในการสร้างรายการนี้ เราใช้ขั้นตอนต่อไปนี้:

  1. สร้างรายการที่มีเมธอด JavaScript ทั้งหมดที่จะดำเนินการสำหรับทุกเส้นทาง:
    คำนวณโมดูลของเส้นทาง จากนั้นรับการกำหนดค่าสำหรับแต่ละโมดูล จากนั้นแยกการกำหนดค่าที่ฟังก์ชัน JavaScript ของโมดูลจำเป็นต้องดำเนินการ และเพิ่มเข้าด้วยกัน
  2. ถัดไป สำรวจแมปการพึ่งพาสินทรัพย์สำหรับฟังก์ชัน JavaScript แต่ละรายการ รวบรวมรายการการพึ่งพาที่จำเป็นทั้งหมด และเพิ่มทั้งหมดเข้าด้วยกัน
  3. สุดท้าย เพิ่มเทมเพลตแฮนด์บาร์ที่จำเป็นในการแสดงแต่ละโมดูลภายในเส้นทางนั้น

นอกจากนี้ ตามที่ระบุไว้ก่อนหน้านี้ แต่ละ URL มีหน้าเว็บและโหมด API ดังนั้น เราจำเป็นต้องเรียกใช้ขั้นตอนข้างต้นสองครั้ง หนึ่งครั้งสำหรับแต่ละโหมด (เช่น เมื่อเพิ่มพารามิเตอร์ output=json ให้กับ URL ซึ่งแสดงถึงเส้นทางสำหรับโหมด API และเมื่อไม่เปลี่ยนแปลง URL สำหรับโหมดหน้าเว็บ) จากนั้นเราจะสร้างรายการสองรายการ ซึ่งจะมีประโยชน์ต่างกัน:

  1. รายการโหมดหน้าเว็บจะถูกใช้เมื่อเริ่มโหลดเว็บไซต์ เพื่อให้สคริปต์ที่เกี่ยวข้องสำหรับเส้นทางนั้นรวมอยู่ในการตอบสนอง HTML เริ่มต้น รายการนี้จะถูกเก็บไว้ในเซิร์ฟเวอร์
  2. รายการโหมด API จะใช้เมื่อโหลดหน้าบนเว็บไซต์แบบไดนามิก รายการนี้จะถูกโหลดบนไคลเอนต์ เพื่อให้แอปพลิเคชันสามารถคำนวณว่าต้องโหลดเนื้อหาเพิ่มเติมใด ตามความต้องการ เมื่อคลิกลิงก์

ตรรกะจำนวนมากได้ถูกนำไปใช้โดยเริ่มจาก function add_resources_from_settingsprocessors($fetching_json, ...) (คุณสามารถหาได้ในที่เก็บ) พารามิเตอร์ $fetching_json แยกความแตกต่างระหว่างโหมดหน้าเว็บ ( false ) และ API ( true )

เมื่อเรียกใช้สคริปต์สำหรับโหมดหน้าเว็บ สคริปต์จะส่งเอาต์พุต resourceloader-bundle-mapping.json ซึ่งเป็นอ็อบเจ็กต์ JSON ที่มีคุณสมบัติดังต่อไปนี้:

  • bundle-ids
    นี่คือคอลเล็กชันของทรัพยากรสูงสุดสี่รายการ (ชื่อของพวกเขาถูกจัดการสำหรับสภาพแวดล้อมการผลิต: eq => handlebars , er => handlebars-helpers ฯลฯ ) จัดกลุ่มภายใต้ ID บันเดิล
  • bundlegroup-ids
    นี่คือคอลเล็กชันของ bundle-ids BundleGroup แต่ละชุดแสดงถึงชุดทรัพยากรที่ไม่ซ้ำกัน
  • key-ids
    นี่คือการจับคู่ระหว่างเส้นทาง (แสดงโดยแฮช ซึ่งระบุชุดของแอตทริบิวต์ทั้งหมดที่ทำให้เส้นทางไม่ซ้ำกัน) และกลุ่มบันเดิลที่เกี่ยวข้อง

ดังที่สังเกตได้ การทำแผนที่ระหว่างเส้นทางกับทรัพยากรนั้นไม่ตรง แทนที่จะจับ key-ids กับรายการทรัพยากร จะจับคู่กับบันเดิลกรุ๊ปที่ไม่ซ้ำกัน ซึ่งเป็นรายการของบัน bundles ล และมีเพียงแต่ละบันเดิลเท่านั้นที่เป็นรายการของ resources (สูงสุดสี่องค์ประกอบต่อบันเดิล) ทำไมมันทำอย่างนี้? นี้ทำหน้าที่สองวัตถุประสงค์:

  1. ช่วยให้เราสามารถระบุทรัพยากรทั้งหมดภายใต้กลุ่มบันเดิลที่ไม่ซ้ำกัน ดังนั้น แทนที่จะรวมทรัพยากรทั้งหมดในการตอบกลับ HTML เราสามารถรวมเนื้อหา JavaScript ที่ไม่ซ้ำกัน ซึ่งเป็นไฟล์ BundleGroup ที่เกี่ยวข้องแทน ซึ่งจะรวมกลุ่มภายในทรัพยากรที่เกี่ยวข้องทั้งหมด สิ่งนี้มีประโยชน์เมื่อให้บริการอุปกรณ์ที่ยังไม่รองรับ HTTP/2 และยังช่วยเพิ่มเวลาในการโหลด เนื่องจาก Gzip การรวมไฟล์เพียงไฟล์เดียวจะมีประสิทธิภาพมากกว่าการบีบอัดไฟล์ที่เป็นส่วนประกอบด้วยตัวเองแล้วรวมเข้าด้วยกัน อีกทางหนึ่ง เราสามารถโหลดชุดของบันเดิลแทนชุดบันเดิลเฉพาะ ซึ่งเป็นการประนีประนอมระหว่างทรัพยากรและกลุ่มบันเดิล (การโหลดบันเดิลนั้นช้ากว่ากลุ่มบันเดิลเนื่องจาก Gzip'ing แต่จะมีประสิทธิภาพมากกว่าหากเกิดการใช้งานไม่ได้บ่อยครั้ง ดังนั้น เรา จะดาวน์โหลดเฉพาะบันเดิลที่อัปเดตแล้วไม่ใช่ทั้งกลุ่มบันเดิล) สคริปต์สำหรับการรวมทรัพยากรทั้งหมดไว้ในบันเดิลและบันเดิลกรุ๊ปมีอยู่ใน filegenerator-bundles.php และ filegenerator-bundlegroups.php
  2. การแบ่งชุดของทรัพยากรออกเป็นบันเดิลช่วยให้เราระบุรูปแบบทั่วไปได้ (เช่น การระบุชุดของทรัพยากรสี่อย่างที่ใช้ร่วมกันระหว่างหลายเส้นทาง) ส่งผลให้เส้นทางต่างๆ เชื่อมโยงไปยังบันเดิลเดียวกันได้ เป็นผลให้รายการที่สร้างขึ้นจะมีขนาดที่เล็กกว่า นี่อาจไม่เป็นประโยชน์อย่างมากสำหรับรายการหน้าเว็บซึ่งอยู่บนเซิร์ฟเวอร์ แต่เหมาะสำหรับรายการ API ซึ่งจะถูกโหลดบนไคลเอ็นต์ ดังที่เราเห็นในภายหลัง

เมื่อเรียกใช้สคริปต์สำหรับโหมด API สคริปต์จะส่งออกไฟล์ resources.js ด้วยคุณสมบัติดังต่อไปนี้:

  • บัน bundles ลและ bundle-groups มีจุดประสงค์เดียวกับที่ระบุไว้สำหรับโหมดหน้าเว็บ
  • keys ยังใช้เพื่อจุดประสงค์เดียวกับ key-ids สำหรับโหมดหน้าเว็บ อย่างไรก็ตาม แทนที่จะมีแฮชเป็นกุญแจสำคัญในการแสดงเส้นทาง มันเป็นการรวมแอตทริบิวต์ทั้งหมดที่ทำให้เส้นทางไม่ซ้ำกัน — ในกรณีของเรา รูปแบบ ( f ), แท็บ ( t ) และเป้าหมาย ( r )
  • source เป็นไฟล์ sources สำหรับแต่ละทรัพยากร
  • types คือ CSS หรือ JavaScript สำหรับแต่ละทรัพยากร (แม้ว่าเพื่อความเรียบง่าย เราไม่ได้กล่าวถึงในบทความนี้ว่าทรัพยากร JavaScript อาจตั้งค่าทรัพยากร CSS เป็นการพึ่งพา และโมดูลอาจโหลดเนื้อหา CSS ของตนเอง โดยใช้กลยุทธ์การโหลด CSS แบบก้าวหน้า ).
  • resources จะรวบรวมบันเดิลกรุ๊ปที่ต้องโหลดสำหรับแต่ละลำดับชั้น
  • ordered-load-resources ประกอบด้วยทรัพยากรที่ต้องโหลดตามลำดับ เพื่อป้องกันไม่ให้สคริปต์ถูกโหลดก่อนสคริปต์ที่ขึ้นต่อกัน (โดยค่าเริ่มต้น สคริปต์จะไม่ตรงกัน)

เราจะสำรวจวิธีใช้ไฟล์นี้ในหัวข้อถัดไป

4. การโหลดสินทรัพย์แบบไดนามิก

ตามที่ระบุไว้ รายการ API จะถูกโหลดบนไคลเอนต์ เพื่อให้เราสามารถเริ่มโหลดเนื้อหาที่จำเป็นสำหรับเส้นทางได้ทันทีหลังจากที่ผู้ใช้คลิกที่ลิงค์

กำลังโหลดสคริปต์การทำแผนที่

ไฟล์ JavaScript ที่สร้างขึ้นพร้อมรายการทรัพยากรสำหรับเส้นทางทั้งหมดในแอปพลิเคชันนั้นไม่เบา ในกรณีนี้มันออกมาที่ 85 KB (ซึ่งได้รับการปรับให้เหมาะสม โดยแยกชื่อทรัพยากรและสร้างบันเดิลเพื่อระบุรูปแบบทั่วไปในเส้นทางต่างๆ) . เวลาในการแยกวิเคราะห์ไม่ควรเป็นปัญหาคอขวดที่ใหญ่ เนื่องจากการแยกวิเคราะห์ JSON นั้นเร็วกว่าการแยกวิเคราะห์ JavaScript สำหรับข้อมูลเดียวกันถึง 10 เท่า อย่างไรก็ตาม ขนาดเป็นปัญหาจากการถ่ายโอนเครือข่าย เราจึงต้องโหลดสคริปต์นี้ในลักษณะที่ไม่ส่งผลต่อเวลาในการโหลดของแอปพลิเคชันที่รับรู้ หรือให้ผู้ใช้รอ

วิธีแก้ปัญหาที่ฉันใช้คือ precache ไฟล์นี้โดยใช้พนักงานบริการ โหลดโดยใช้ defer เพื่อไม่ให้บล็อกเธรดหลักขณะดำเนินการตามวิธี JavaScript ที่สำคัญ จากนั้นแสดงข้อความแจ้งเตือนทางเลือกหากผู้ใช้คลิกที่ลิงก์ ก่อนที่สคริปต์จะโหลด: “เว็บไซต์ยังคงโหลดอยู่ โปรดรอสักครู่เพื่อคลิกลิงก์” สิ่งนี้ทำได้โดยการเพิ่ม div คงที่โดยมีคลาสของหน้าจอการ loadingscreen วางทับทุกอย่างในขณะที่สคริปต์กำลังโหลด จากนั้นเพิ่มข้อความแจ้งเตือนด้วยคลาสของ notificationmsg ภายใน div และ CSS สองสามบรรทัดเหล่านี้:

 .loadingscreen > .notificationmsg { display: none; } .loadingscreen:focus > .notificationmsg, .loadingscreen:active > .notificationmsg { display: block; }

อีกวิธีหนึ่งคือแบ่งไฟล์นี้ออกเป็นหลาย ๆ ไฟล์แล้วโหลดไปเรื่อย ๆ ตามต้องการ (กลยุทธ์ที่ฉันได้เขียนไว้แล้ว) นอกจากนี้ ไฟล์ขนาด 85 KB ยังรวมเส้นทางที่เป็นไปได้ ทั้งหมด ในแอปพลิเคชัน รวมถึงเส้นทางต่างๆ เช่น “ประกาศของผู้เขียน แสดงในภาพขนาดย่อ แสดงในหน้าต่างโมดอล” ซึ่งอาจเข้าถึงได้เพียงครั้งเดียวในพระจันทร์สีน้ำเงิน หากมี เส้นทางที่เข้าถึงได้เป็นส่วนใหญ่มีเพียงไม่กี่เส้นทาง (หน้าแรก หน้าเดียว ผู้แต่ง แท็ก และหน้าทั้งหมด ทั้งหมดไม่มีแอตทริบิวต์พิเศษ) ซึ่งควรสร้างไฟล์ที่เล็กกว่ามากในบริเวณใกล้เคียง 30 KB

การรับเส้นทางจาก URL ที่ร้องขอ

เราต้องสามารถระบุเส้นทางจาก URL ที่ร้องขอได้ ตัวอย่างเช่น:

  • https://getpop.org/en/u/leo/ แผนที่เส้นทาง “ผู้แต่ง”
  • https://getpop.org/en/u/leo/?tab=followers แมปไปยังเส้นทาง “ผู้ติดตามของผู้เขียน”,
  • https://getpop.org/en/tags/internet/ แมปไปยังเส้นทาง “แท็ก”
  • https://getpop.org/en/tags/ แผนที่ไปยังเส้นทาง “หน้า /tags/ ”,
  • และอื่นๆ

เพื่อให้บรรลุสิ่งนี้ เราจะต้องประเมิน URL และอนุมานจากองค์ประกอบที่ทำให้เส้นทางไม่ซ้ำกัน: ลำดับชั้นและแอตทริบิวต์ทั้งหมด (รูปแบบ แท็บ และเป้าหมาย) การระบุแอตทริบิวต์ไม่มีปัญหา เนื่องจากเป็นพารามิเตอร์ใน URL ความท้าทายเพียงอย่างเดียวคือการอนุมานลำดับชั้น (บ้าน ผู้แต่ง หน้าเดียว หรือแท็ก) จาก URL โดยจับคู่ URL กับรูปแบบต่างๆ ตัวอย่างเช่น,

  • สิ่งที่ขึ้นต้นด้วย https://getpop.org/en/u/ เป็นผู้แต่ง
  • สิ่งที่ขึ้นต้นด้วยแต่ไม่ใช่ https://getpop.org/en/tags/ ก็คือแท็ก หากเป็น https://getpop.org/en/tags/ กันแน่ แสดงว่าเป็นหน้า
  • และอื่นๆ.

ฟังก์ชันด้านล่าง ซึ่งเริ่มใช้งานตั้งแต่บรรทัด 321 ของ resourceloader.js จะต้องได้รับการกำหนดค่าด้วยรูปแบบสำหรับลำดับชั้นเหล่านี้ทั้งหมด ก่อนอื่นจะตรวจสอบว่าไม่มีเส้นทางย่อยใน URL หรือไม่ ซึ่งในกรณีนี้คือ "บ้าน" จากนั้นจะตรวจสอบทีละรายการเพื่อให้ตรงกับลำดับชั้นสำหรับ "ผู้แต่ง" "แท็ก" และ "โสด" หากไม่ประสบผลสำเร็จกับกรณีใดกรณีหนึ่ง แสดงว่าเป็นกรณีเริ่มต้น ซึ่งก็คือ “หน้า”:

 window.popResourceLoader = { // The config will be populated externally, using a config.js file, generated by a script config : {}, getPath : function(url) { var parser = document.createElement('a'); parser.href = url; return parser.pathname; }, getHierarchy : function(url) { var path = this.getPath(url); if (!path) { return 'home'; } var config = this.config; if (path.startsWith(config.paths.author) && path != config.paths.author) { return 'author'; } if (path.startsWith(config.paths.tag) && path != config.paths.tag) { return 'tag'; } // We must also check that this path is, itself, not a potential page (https://getpop.org/en/posts/articles/ is "page", but https://getpop.org/en/posts/this-is-a-post/ is "single") if (config.paths.single.indexOf(path) === -1 && config.paths.single.some(function(single_path) { return path.startsWith(single_path) && path != single_path;})) { return 'single'; } return 'page'; }, ... };

เนื่องจากข้อมูลที่จำเป็นทั้งหมดอยู่ในฐานข้อมูลแล้ว (หมวดหมู่ทั้งหมด หน้าทากทั้งหมด ฯลฯ) เราจะเรียกใช้สคริปต์เพื่อสร้างไฟล์การกำหนดค่านี้โดยอัตโนมัติในสภาพแวดล้อมการพัฒนาหรือการแสดงละคร The implemented script is resourceloader-config.php, which produces config.js with the URL patterns for the hierarchies “author”, “tag” and “single”, under the key “paths”:

 popResourceLoader.config = { "paths": { "author": "u/", "tag": "tags/", "single": ["posts/articles/", "posts/announcements/", ...] }, ... };

Loading Resources for the Route

Once we have identified the route, we can obtain the required assets from the generated JavaScript file under the key “resources”, which looks like this:

 config.resources = { "home": { "1": [1, 110, ...], "2": [2, 111, ...], ... }, "author": { "7": [6, 114, ...], "8": [7, 114, ...], ... }, "tag": { "119": [66, 127, ...], "120": [66, 127, ...], ... }, "single": { "posts/": { "7": [190, 142, ...], "3": [190, 142, ...], ... }, "events/": { "7": [213, 389, ...], "3": [213, 389, ...], ... }, ... }, "page": { "log-in/": { "3": [233, 115, ...] }, "log-out/": { "3": [234, 115, ...] }, "add-post/": { "3": [239, 398, ...] }, "posts/": { "120": [268, 127, ...], "122": [268, 127, ...], ... }, ... } };

At the first level, we have the hierarchy (home, author, tag, single or page). Hierarchies are divided into two groups: those that have only one set of resources (home, author and tag), and those that have a specific subpath (page permalink for the pages, custom post type or category for the single). Finally, at the last level, for each key ID (which represents a unique combination of the possible values of “format”, “tab” and “target”, stored under “keys”), we have an array of two elements: [JS bundleGroup ID, CSS bundleGroup ID], plus additional bundleGroup IDs if executing progressive booting (JS bundleGroups to be loaded as "async" or "defer" are bundled separately; this will be explained in the optimizations section below).

Please note: For the single hierarchy, we have different configurations depending on the custom post type. This can be reflected in the subpath indicated above (for example, events and posts ) because this information is in the URL (for example, https://getpop.org/en/posts/the-winners-of-climate-change-techno-fixes/ and https://getpop.org/en/events/debate-post-fork/ ), so that, when clicking on a link, we will know the corresponding post type and can thus infer the corresponding route. However, this is not the case with the author hierarchy. As indicated earlier, an author may have three different configurations, depending on the user role ( individual , organization or community ); however, in this file, we've defined only one configuration for the author hierarchy, not three. That is because we are not able to tell from the URL what is the role of the author: user leo (under https://getpop.org/en/u/leo/ ) is an individual, whereas user pop (under https://getpop.org/en/u/pop/ ) is a community; however, their URLs have the same pattern. If we could instead have the URLs https://getpop.org/en/u/individuals/leo/ and https://getpop.org/en/u/communities/pop/ , then we could add a configuration for each user role. However, I've found no way to achieve this in WordPress. As a consequence, only for the API mode, we must merge the three routes (individuals, organizations and communities) into one, which will have all of the resources for the three cases; and clicking on the link for user leo will also load the resources for organizations and communities, even if we don't need them.

Finally, when a URL is requested, we obtain its route, from which we obtain the bundleGroup IDs (for both JavaScript and CSS assets). From each bundleGroup, we find the corresponding bundles under bundlegroups . Then, for each bundle, we obtain all resources under the key bundles . Finally, we identify which assets have not yet been loaded, and we load them by getting their source, which is stored under the key sources . The whole logic is coded starting from line 472 in resourceloader.js.

And with that, we have implemented code-splitting for our application! From now on, we can get better loading times by applying optimizations. Let's tackle that next.

5. Applying Optimizations

The objective is to load as little code as possible, as delayed as possible, and to cache as much of it as possible. Let's explore how to do this.

Splitting Up the Code Into Smaller Units

A single JavaScript asset may implement several functions (by calling popJSLibraryManager.register ), yet maybe only one of those functions is actually needed by the route. Thus, it makes sense to split up the asset into several subassets, implementing a single function on each of them, and extracting all common code from all of the functions into yet another asset, depended upon by all of them.

For instance, in the past, there was a unique file, waypoints.js , that implemented the functions waypointsFetchMore , waypointsTheater and a few more. However, in most cases, only the function waypointsFetchMore was needed, so I was loading the code for the function waypointsTheater unnecessarily. Then, I split up waypoints.js into the following assets:

  • waypoints.js, with all common code and implementing no public functions;
  • waypoints-fetchmore.js, which implements just the public function waypointsFetchMore ;
  • waypoints-theater.js, which implements just the public function waypointsTheater .

Evaluating how to split the files is a manual job. Luckily, there is a tool that greatly eases the task: Chrome Developer Tools' “Coverage” tab, which displays in red those portions of JavaScript code that have not been invoked:

ตัวอย่างขนาดใหญ่
ตัวอย่างขนาดใหญ่

By using this tool, we can better understand how to split our JavaScript files into more granular units, thus reducing the amount of unneeded code that is loaded.

Integration With Service Workers

By precaching all of the resources using service workers, we can be pretty sure that, by the time the response is back from the server, all of the required assets will have been loaded and parsed. I wrote an article on Smashing Magazine on how to accomplish this.

Progressive Booting

PoP's architecture plays very nice with the concept of loading assets in different stages. When defining the JavaScript methods to execute on each module (by doing $this->add_jsmethod($methods, 'calendar') ), these can be set as either critical or non-critical . By default, all methods are set as non-critical, and critical methods must be explicitly defined by the developer, by adding an extra parameter: $this->add_jsmethod($methods, 'calendar', 'critical') . Then, we will be able to load scripts immediately for critical functions, and wait until the page is loaded to load non-critical functions, the JavaScript files of which are loaded using defer .

 (function($){ window.popManager = { init : function() { var that = this; $.each(this.configuration, function(pageSectionId, configuration) { ... this.runJSMethods(pageSection, configuration, 'critical'); ... }); window.addEventListener('load', function() { $.each(this.configuration, function(pageSectionId, configuration) { ... this.runJSMethods(pageSection, configuration, 'non-critical'); ... }); }); ... }, ... }; })(jQuery);

The gains from progressive booting are major: The JavaScript engine needs not spend time parsing non-critical JavaScript initially, when a quick response to the user is most important, and overall reduces the time to interactive.

Testing And Analizying Performance Gains

We can use https://getpop.org/en/, a PoP website, for testing purposes. When loading the home page, opening Chrome Developer Tools' “Elements” tab and searching for “defer”, it shows 4 occurrences. Thanks to progressive booting, that is 4 bundleGroup JavaScript files containing the contents of 57 Javascript files with non-critical methods that could wait until the website finished loading to be loaded:

ตัวอย่างขนาดใหญ่

If we now switch to the “Network” tab and click on a link, we can see which assets get loaded. For instance, click on the link “Application/UX Features” on the left side. Filtering by JavaScript, we see it loaded 38 files, including JavaScript libraries and Handlebars templates. Filtering by CSS, we see it loaded 9 files. These 47 files have all been loaded on demand:

ตัวอย่างขนาดใหญ่

Let's check whether the loading time got boosted. We can use WebPagetest to measure the application with and without code-splitting, and calculate the difference.

  • Without code-splitting: testing URL, WebPagetest results
ตัวอย่างขนาดใหญ่
  • With code-splitting, loading resources: testing URL, WebPagetest Results
ตัวอย่างขนาดใหญ่
  • With code-splitting, loading a bundleGroup: testing URL, WebPagetest Results
ตัวอย่างขนาดใหญ่

We can see that when loading the app bundle with all resources or when doing code-splitting and loading resources, there is not so much gain. However, when doing code-splitting and loading a bundleGroup, the gains are significant: 1.7 seconds in loading time, 500 milliseconds to the first meaningful paint, and 1 second to interactive.

Conclusion: Is It Worth It?

You might be thinking, Is it worth it all this trouble? Let's analyze the advantages and disadvantages of implementing our own code-splitting features.

ข้อเสีย

  • เราต้องรักษาไว้
    หากเราเพิ่งใช้ Webpack เราสามารถพึ่งพาชุมชนของตนเพื่อให้ซอฟต์แวร์ทันสมัยอยู่เสมอและอาจได้รับประโยชน์จากระบบนิเวศของปลั๊กอิน
  • สคริปต์ใช้เวลาในการรัน
    เว็บไซต์ PoP Agenda Urbana มีเส้นทางที่แตกต่างกัน 304 เส้นทาง ซึ่งสร้างแหล่งข้อมูลที่ไม่ซ้ำกัน 422 ชุด สำหรับเว็บไซต์นี้ การเรียกใช้สคริปต์ที่สร้างแผนผังการพึ่งพาสินทรัพย์ โดยใช้ MacBook Pro จากปี 2555 ใช้เวลาประมาณ 8 นาที และการเรียกใช้สคริปต์ที่สร้างรายการด้วยทรัพยากรทั้งหมด และสร้างไฟล์บันเดิลและบันเดิลกรุ๊ปใช้เวลา 15 นาที . นั่นเป็นเวลามากเกินพอที่จะไปดื่มกาแฟ!
  • มันต้องมีสภาพแวดล้อมการแสดงละคร
    หากเราต้องรอประมาณ 25 นาทีเพื่อเรียกใช้สคริปต์ เราก็ไม่สามารถรันสคริปต์นี้ในเวอร์ชันที่ใช้งานจริงได้ เราจำเป็นต้องมีสภาพแวดล้อมการแสดงละครที่มีการกำหนดค่าเดียวกันกับระบบที่ใช้งานจริงทุกประการ
  • มีการเพิ่มรหัสพิเศษในเว็บไซต์ เพื่อการจัดการเท่านั้น
    โค้ดขนาด 85 KB ไม่ทำงานโดยตัวมันเอง แต่เพียงใช้โค้ดเพื่อจัดการโค้ดอื่นๆ
  • เพิ่มความซับซ้อน
    สิ่งนี้หลีกเลี่ยงไม่ได้ในทุกกรณี หากเราต้องการแบ่งทรัพย์สินของเราออกเป็นหน่วยย่อยๆ Webpack จะเพิ่มความซับซ้อนให้กับแอปพลิเคชันด้วย

ข้อดี

  • ทำงานร่วมกับเวิร์ดเพรส
    Webpack ไม่สามารถใช้งานกับ WordPress ได้ตั้งแต่เริ่มต้น และเพื่อให้ใช้งานได้ต้องมีวิธีแก้ปัญหา โซลูชันนี้ใช้งานได้ทันทีสำหรับ WordPress (ตราบใดที่ติดตั้ง PoP)
  • สามารถปรับขนาดและขยายได้
    ขนาดและความซับซ้อนของแอปพลิเคชันสามารถเติบโตได้โดยไม่มีขอบเขต เนื่องจากไฟล์ JavaScript ถูกโหลดตามต้องการ
  • รองรับ Gutenberg (หรือที่รู้จักในชื่อ WordPress ของวันพรุ่งนี้)
    เนื่องจากช่วยให้เราโหลดเฟรมเวิร์ก JavaScript ได้ตามต้องการ จึงรองรับบล็อกของ Gutenberg (เรียกว่า Gutenblocks) ซึ่งคาดว่าจะได้รับการเข้ารหัสในเฟรมเวิร์กที่นักพัฒนาเลือก โดยอาจต้องใช้เฟรมเวิร์กที่แตกต่างกันสำหรับแอปพลิเคชันเดียวกัน
  • มันสะดวก
    เครื่องมือสร้างจะดูแลการสร้างไฟล์การกำหนดค่า นอกจากการรอคอยแล้ว เรายังไม่ต้องใช้ความพยายามพิเศษใดๆ จากเราอีกด้วย
  • ทำให้การเพิ่มประสิทธิภาพเป็นเรื่องง่าย
    ปัจจุบัน หากปลั๊กอิน WordPress ต้องการเลือกโหลดเนื้อหา JavaScript มันจะใช้เงื่อนไขมากมายเพื่อตรวจสอบว่า ID ของเพจถูกต้องหรือไม่ ด้วยเครื่องมือนี้ ไม่จำเป็นต้องทำเช่นนั้น กระบวนการนี้เป็นไปโดยอัตโนมัติ
  • แอปพลิเคชันจะโหลดเร็วขึ้น
    นี่คือเหตุผลทั้งหมดว่าทำไมเราจึงเขียนโค้ดเครื่องมือนี้
  • มันต้องมีสภาพแวดล้อมการแสดงละคร
    ผลข้างเคียงที่เป็นบวกคือความน่าเชื่อถือที่เพิ่มขึ้น: เราจะไม่เรียกใช้สคริปต์ในการผลิต ดังนั้นเราจะไม่ทำลายสิ่งใดที่นั่น กระบวนการปรับใช้จะไม่ล้มเหลวจากพฤติกรรมที่ไม่คาดคิด และผู้พัฒนาจะถูกบังคับให้ทดสอบแอปพลิเคชันโดยใช้การกำหนดค่าเดียวกันกับในการผลิต
  • มันถูกปรับแต่งให้เข้ากับแอปพลิเคชันของเรา
    ไม่มีค่าใช้จ่ายหรือวิธีแก้ไขปัญหาชั่วคราว สิ่งที่เราได้รับคือสิ่งที่เราต้องการอย่างแท้จริง โดยอิงจากสถาปัตยกรรมที่เรากำลังใช้งานอยู่

โดยสรุป: ใช่ มันคุ้มค่า เพราะตอนนี้เราสามารถใช้โหลดเนื้อหาตามความต้องการบนเว็บไซต์ WordPress ของเราและทำให้โหลดเร็วขึ้น

แหล่งข้อมูลเพิ่มเติม

  • Webpack รวมถึง “”Code Splitting” guide
  • “Better Webpack Builds” (วิดีโอ), K. Adam White
    การรวม Webpack กับ WordPress
  • “กูเทนเบิร์กและเวิร์ดเพรสแห่งอนาคต” มอร์เทน แรนด์-เฮนดริกเซ่น, WP Tavern
  • “WordPress สำรวจ JavaScript Framework-Agnostic Approach เพื่อสร้าง Gutenberg Blocks” Sarah Gooding, WP Tavern