มาพูดถึง... สถาปัตยกรรมกันไหม
เราจะพูดถึงหัวข้อที่สำคัญแต่ก็อาจทำให้เกิดความเข้าใจผิดได้ นั่นคือ สถาปัตยกรรมที่คุณใช้สำหรับเว็บแอป และโดยเฉพาะอย่างยิ่ง วิธีที่การตัดสินใจด้านสถาปัตยกรรมมีบทบาทเมื่อคุณสร้าง Progressive Web App
"สถาปัตยกรรม" อาจฟังดูคลุมเครือ และอาจไม่ชัดเจนในทันทีว่าเหตุใดเรื่องนี้จึง มีความสำคัญ วิธีหนึ่งในการพิจารณาสถาปัตยกรรมคือการถามคำถามต่อไปนี้กับตัวเอง เมื่อผู้ใช้เข้าชมหน้าเว็บในเว็บไซต์ของฉัน ระบบจะโหลด HTML ใด และเมื่อผู้ใช้เข้าชมหน้าอื่น ระบบจะโหลดอะไร
คำตอบของคำถามเหล่านั้นไม่ได้ตรงไปตรงมาเสมอไป และเมื่อคุณเริ่มคิดถึง Progressive Web App ก็อาจจะยิ่งซับซ้อนมากขึ้น ดังนั้นเป้าหมายของฉันคือการอธิบายสถาปัตยกรรมที่เป็นไปได้แบบหนึ่งที่ฉันพบว่ามีประสิทธิภาพ ตลอดบทความนี้ ฉันจะระบุการตัดสินใจที่ฉันทำเป็น "แนวทางของฉัน" ในการสร้าง Progressive Web App
คุณสามารถใช้วิธีของฉันเมื่อสร้าง PWA ของคุณเองได้ แต่ในขณะเดียวกัน ก็ยังมีทางเลือกอื่นๆ ที่ถูกต้องเสมอ เราหวังว่าการได้เห็นว่าชิ้นส่วนทั้งหมดทำงานร่วมกันอย่างไรจะสร้างแรงบันดาลใจให้คุณ และคุณจะรู้สึกมีอำนาจในการปรับแต่งให้เหมาะกับความต้องการของคุณ
PWA ของ Stack Overflow
ฉันได้สร้าง PWA ของ Stack Overflow เพื่อประกอบบทความนี้ ฉันใช้เวลาส่วนใหญ่ไปกับการอ่านและมีส่วนร่วมใน Stack Overflow และต้องการสร้างเว็บแอปที่จะช่วยให้เรียกดูคำถามที่พบบ่อยสำหรับหัวข้อหนึ่งๆ ได้ง่าย โดยสร้างขึ้นจาก Stack Exchange API สาธารณะ โดยเป็นโอเพนซอร์ส และคุณดูข้อมูลเพิ่มเติมได้โดยไปที่โปรเจ็กต์ GitHub
แอปแบบหลายหน้า (MPA)
ก่อนจะลงรายละเอียด เรามากำหนดคำศัพท์บางคำและอธิบายเทคโนโลยีพื้นฐานกันก่อน ก่อนอื่น ผมจะพูดถึงสิ่งที่ผมเรียกว่า "แอปแบบหลายหน้า" หรือ "MPA"
MPA เป็นชื่อที่ดูดีสำหรับสถาปัตยกรรมแบบดั้งเดิมที่ใช้มาตั้งแต่เริ่มมีเว็บ ทุกครั้งที่ผู้ใช้ไปยัง URL ใหม่ เบราว์เซอร์จะค่อยๆ แสดง HTML ที่เฉพาะเจาะจงสำหรับหน้านั้น ไม่มีการพยายาม รักษาสถานะของหน้าเว็บหรือเนื้อหาระหว่างการนำทาง ทุกครั้งที่คุณ เข้าชมหน้าใหม่ คุณจะเริ่มต้นใหม่
ซึ่งแตกต่างจากรูปแบบแอปหน้าเดียว (SPA) สำหรับการสร้างเว็บแอป ซึ่งเบราว์เซอร์จะเรียกใช้โค้ด JavaScript เพื่ออัปเดตหน้าที่มีอยู่เมื่อผู้ใช้เข้าชมส่วนใหม่ ทั้ง SPA และ MPA เป็นโมเดลที่ใช้ได้เหมือนกัน แต่ในโพสต์นี้ ฉันต้องการสำรวจแนวคิด PWA ในบริบทของแอปแบบหลายหน้า
รวดเร็วอย่างสม่ำเสมอ
คุณคงเคยได้ยินฉัน (และคนอื่นๆ อีกมากมาย) ใช้คำว่า "Progressive Web App" หรือ PWA คุณอาจคุ้นเคยกับข้อมูลพื้นฐานบางอย่างในส่วนอื่นๆ ของเว็บไซต์นี้อยู่แล้ว
คุณมอง PWA เป็นเว็บแอปที่มอบประสบการณ์การใช้งานชั้นยอด และสมควรที่จะอยู่บนหน้าจอหลักของผู้ใช้ คำย่อ "FIRE" ซึ่งย่อมาจาก Fast (รวดเร็ว) Integrated (ผสานรวม) Reliable (เชื่อถือได้) และ Engaging (น่าสนใจ) สรุป คุณลักษณะทั้งหมดที่ควรพิจารณาเมื่อสร้าง PWA
ในบทความนี้ ฉันจะมุ่งเน้นไปที่กลุ่มย่อยของแอตทริบิวต์เหล่านั้น ซึ่งได้แก่ รวดเร็ว และเชื่อถือได้
รวดเร็ว: แม้ว่า "รวดเร็ว" จะมีความหมายแตกต่างกันไปในบริบทต่างๆ แต่ฉันจะพูดถึง ประโยชน์ด้านความเร็วของการโหลดจากเครือข่ายให้น้อยที่สุด
เชื่อถือได้: แต่ความเร็วอย่างเดียวไม่เพียงพอ เว็บแอปควรมีความน่าเชื่อถือเพื่อให้ผู้ใช้รู้สึกเหมือนกำลังใช้ PWA โดยจะต้องมีความยืดหยุ่นมากพอที่จะโหลดเนื้อหาได้เสมอ แม้ว่าจะเป็นเพียงหน้าข้อผิดพลาดที่กำหนดเองก็ตาม ไม่ว่าเครือข่ายจะมีสถานะเป็นอย่างไร
รวดเร็วอย่างสม่ำเสมอ: สุดท้ายนี้ ฉันจะปรับคำจำกัดความของ PWA เล็กน้อยและดูว่า การสร้างสิ่งที่รวดเร็วอย่างสม่ำเสมอหมายความว่าอย่างไร การทำงานที่รวดเร็วและเชื่อถือได้เฉพาะเมื่อคุณอยู่ในเครือข่ายที่มีเวลาในการตอบสนองต่ำนั้นไม่เพียงพอ การมีความเร็วที่เชื่อถือได้ หมายความว่าเว็บแอปของคุณมีความเร็วที่สม่ำเสมอ ไม่ว่าสภาวะของเครือข่าย พื้นฐานจะเป็นอย่างไรก็ตาม
เทคโนโลยีที่เปิดใช้: Service Worker + Cache Storage API
PWA กำหนดมาตรฐานความเร็วและความยืดหยุ่นไว้สูง โชคดีที่แพลตฟอร์มเว็บ มีองค์ประกอบบางอย่างที่จะช่วยให้ประสิทธิภาพประเภทนี้เป็นจริงได้ เรากำลังพูดถึง Service Worker และ Cache Storage API
คุณสามารถสร้าง Service Worker ที่รอรับคำขอขาเข้า ส่งคำขอไปยังเครือข่าย และจัดเก็บสำเนาการตอบกลับเพื่อใช้ในอนาคตผ่าน Cache Storage API

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

การหลีกเลี่ยงเครือข่ายทุกครั้งที่เป็นไปได้เป็นส่วนสำคัญในการมอบประสิทธิภาพที่รวดเร็วอย่างสม่ำเสมอ
JavaScript "ไอโซมอร์ฟิก"
อีกแนวคิดหนึ่งที่ฉันอยากจะพูดถึงคือสิ่งที่บางครั้งเรียกว่า JavaScript "ไอโซมอร์ฟิก" หรือ "สากล" กล่าวโดยย่อคือ แนวคิดที่ว่าโค้ด JavaScript เดียวกันสามารถแชร์ระหว่างสภาพแวดล้อมรันไทม์ที่แตกต่างกันได้ ตอนที่สร้าง PWA ฉันต้องการแชร์โค้ด JavaScript ระหว่างเซิร์ฟเวอร์แบ็กเอนด์กับ Service Worker
การแชร์โค้ดด้วยวิธีนี้มีแนวทางที่ถูกต้องมากมาย แต่แนวทางของฉันคือการใช้โมดูล ES เป็นซอร์สโค้ดที่แน่นอน
จากนั้นฉันก็แปลงและจัดกลุ่มโมดูลเหล่านั้นสำหรับ
เซิร์ฟเวอร์และ Service Worker โดยใช้การผสมผสานระหว่าง
Babel กับ Rollup ในโปรเจ็กต์ของฉัน
ไฟล์ที่มีนามสกุล .mjs
คือโค้ดที่อยู่ในโมดูล ES
เซิร์ฟเวอร์
เมื่อทราบแนวคิดและคำศัพท์เหล่านั้นแล้ว เรามาดูวิธีที่ฉันสร้าง PWA ของ Stack Overflow กัน เราจะเริ่มด้วยการพูดถึงเซิร์ฟเวอร์แบ็กเอนด์ และอธิบายว่าเซิร์ฟเวอร์นี้เข้ากับสถาปัตยกรรมโดยรวมได้อย่างไร
ฉันกำลังมองหาการผสมผสานระหว่างแบ็กเอนด์แบบไดนามิกกับโฮสติ้งแบบคงที่ และแนวทางของฉันคือการใช้แพลตฟอร์ม Firebase
Firebase Cloud Functions จะ สร้างสภาพแวดล้อมที่ใช้ Node โดยอัตโนมัติเมื่อมีคำขอขาเข้า และผสานรวมกับเฟรมเวิร์ก HTTP ของ Express ที่ได้รับความนิยม ซึ่งฉันคุ้นเคยอยู่แล้ว นอกจากนี้ ยังมีโฮสติ้งสำเร็จรูปสำหรับ ทรัพยากรแบบคงที่ทั้งหมดของเว็บไซต์ด้วย มาดูวิธีที่เซิร์ฟเวอร์จัดการคำขอ กัน
เมื่อเบราว์เซอร์ส่งคำขอไปยังส่วนต่างๆ ในเซิร์ฟเวอร์ของเรา คำขอจะผ่านขั้นตอนต่อไปนี้

เซิร์ฟเวอร์จะกำหนดเส้นทางคำขอตาม URL และใช้ตรรกะการสร้างเทมเพลตเพื่อ สร้างเอกสาร HTML ที่สมบูรณ์ ฉันใช้ข้อมูลที่ได้จาก API ของ Stack Exchange ร่วมกับส่วน HTML บางส่วนที่เซิร์ฟเวอร์จัดเก็บไว้ในเครื่อง เมื่อ Service Worker ทราบวิธีตอบกลับแล้ว ก็จะเริ่มสตรีม HTML กลับไปยังเว็บแอปของเราได้
มี 2 ส่วนในภาพนี้ที่ควรสำรวจในรายละเอียดเพิ่มเติม ได้แก่ การกำหนดเส้นทาง และการสร้างเทมเพลต
การกำหนดเส้นทาง
เมื่อพูดถึงการกำหนดเส้นทาง แนวทางของฉันคือการใช้ไวยากรณ์การกำหนดเส้นทางดั้งเดิมของเฟรมเวิร์ก Express มีความยืดหยุ่น เพียงพอที่จะจับคู่ค่าต่อท้ายของ URL แบบง่าย รวมถึง URL ที่มีพารามิเตอร์เป็น ส่วนหนึ่งของเส้นทาง ในที่นี้ ฉันสร้างการจับคู่ ระหว่างชื่อเส้นทางกับรูปแบบ Express พื้นฐานเพื่อใช้ในการจับคู่
const routes = new Map([
['about', '/about'],
['questions', '/questions/:questionId'],
['index&
#39;, '/'],
]);
export default routes;
จากนั้นฉันจะอ้างอิงการแมปนี้ได้โดยตรงจากโค้ดของเซิร์ฟเวอร์ เมื่อมีรูปแบบ Express ที่ตรงกัน ตัวแฮนเดิลที่เหมาะสมจะตอบกลับด้วยตรรกะการสร้างเทมเพลตที่เฉพาะเจาะจงกับเส้นทางที่ตรงกัน
import routes from './lib/routes.mjs';
app.get(routes.get('index'), as>ync (req, res) = {
// Templa
ting logic.
});
การใช้เทมเพลตฝั่งเซิร์ฟเวอร์
แล้วตรรกะการสร้างเทมเพลตมีลักษณะอย่างไร ฉันจึงเลือกใช้วิธี ที่นำส่วนย่อย HTML บางส่วนมาต่อกันตามลำดับทีละส่วน โมเดลนี้ เหมาะกับการสตรีมเป็นอย่างยิ่ง
เซิร์ฟเวอร์จะส่งบอยเลอร์เพลต HTML เริ่มต้นกลับมาทันที และเบราว์เซอร์ จะแสดงผลหน้าเว็บบางส่วนนั้นได้ทันที ขณะที่เซิร์ฟเวอร์รวบรวม แหล่งข้อมูลที่เหลือ เซิร์ฟเวอร์จะสตรีมแหล่งข้อมูลเหล่านั้นไปยังเบราว์เซอร์จนกว่าเอกสารจะเสร็จสมบูรณ์
หากต้องการดูว่าฉันหมายถึงอะไร ให้ดูโค้ด Express สำหรับ เส้นทางหนึ่งของเรา
app.get(routes.get('index'), async (req>, res) = {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end
();
});
การใช้เมธอด write()
ของออบเจ็กต์ response
และการอ้างอิงเทมเพลตบางส่วนที่จัดเก็บไว้ในเครื่อง
ช่วยให้ฉันเริ่มสตรีมการตอบกลับได้ทันที
โดยไม่ต้องบล็อกแหล่งข้อมูลภายนอก เบราว์เซอร์จะใช้ HTML เริ่มต้นนี้
และแสดงอินเทอร์เฟซที่มีความหมายและข้อความการโหลดทันที
ส่วนถัดไปของหน้าเว็บใช้ข้อมูลจาก Stack Exchange API การรับข้อมูลดังกล่าวหมายความว่าเซิร์ฟเวอร์ของเราต้องส่งคำขอเครือข่าย เว็บแอปจะแสดงสิ่งอื่นไม่ได้ จนกว่าจะได้รับการตอบกลับและประมวลผล แต่ผู้ใช้จะไม่ต้องมอง หน้าจอว่างเปล่าขณะรอ
เมื่อเว็บแอปได้รับการตอบกลับจาก Stack Exchange API แล้ว แอปจะเรียกใช้ ฟังก์ชันการสร้างเทมเพลตที่กำหนดเองเพื่อแปลข้อมูลจาก API เป็น HTML ที่ เกี่ยวข้อง
ภาษาเทมเพลต
การใช้เทมเพลตอาจเป็นหัวข้อที่ถกเถียงกันอย่างน่าประหลาดใจ และสิ่งที่ฉันเลือกใช้เป็นเพียง แนวทางหนึ่งในหลายๆ แนวทาง คุณจะต้องแทนที่โซลูชันของคุณเอง โดยเฉพาะ หากคุณมีความเชื่อมโยงเดิมกับเฟรมเวิร์กการสร้างเทมเพลตที่มีอยู่
สิ่งที่เหมาะกับกรณีการใช้งานของฉันคือการใช้ template literals ของ JavaScript โดยมีตรรกะบางอย่างที่แยกออกมาเป็นฟังก์ชันตัวช่วย ข้อดีอย่างหนึ่งของการสร้าง MPA คือคุณไม่ต้องติดตามการอัปเดตสถานะและ แสดงผล HTML อีกครั้ง ดังนั้นแนวทางพื้นฐานที่สร้าง HTML แบบคงที่จึงเหมาะกับ ฉัน
ดังนั้นต่อไปนี้คือตัวอย่างวิธีที่ฉันใช้เทมเพลตส่วน HTML แบบไดนามิกของดัชนีเว็บแอป เช่นเดียวกับเส้นทางของฉัน ตรรกะการสร้างเทมเพลตจะจัดเก็บไว้ในโมดูล ES ซึ่งสามารถนำเข้าไปยังทั้งเซิร์ฟเวอร์และ Service Worker ได้
export function index(tag, items) {
const title = `<h3>Top "${escape(tag)}"< Qu>estions/h3`;
cons<t form = `form me>tho<d=&qu>ot;GET".../form`;
const questionCards = i>tems
.map(item =
questionCard({
id: item.question_id,
title: item.title,
})
)
.join('&<#39;);
const que>stions = `div id<=&qu>ot;questions"${questionCards}/div`;
return title + form + questions;
}
ฟังก์ชันเทมเพลตเหล่านี้เป็น JavaScript ล้วนๆ และการแยกตรรกะออกเป็นฟังก์ชันตัวช่วยขนาดเล็กเมื่อเหมาะสมจะเป็นประโยชน์ ในที่นี้ ฉันจะส่งแต่ละรายการที่แสดงผลในการตอบกลับของ API ไปยังฟังก์ชันดังกล่าว ซึ่งจะสร้างองค์ประกอบ HTML มาตรฐานโดยตั้งค่าแอตทริบิวต์ที่เหมาะสมทั้งหมด
function questionCard({id, title}) {
return `<a class="card"
href="/questions/${id}"
data-cache-url=>"${<qu>estio
nUrl(id)}"${title}/a`;
}
สิ่งที่ควรทราบเป็นพิเศษคือแอตทริบิวต์ข้อมูล
ที่ฉันเพิ่มลงในแต่ละลิงก์ data-cache-url
ซึ่งตั้งค่าเป็น URL ของ Stack Exchange API
ที่ฉันต้องใช้เพื่อแสดงคำถามที่เกี่ยวข้อง โปรดทราบว่า ฉันจะกลับมาดูอีกครั้งในภายหลัง
กลับไปที่ตัวแฮนเดิลเส้นทาง เมื่อสร้างเทมเพลตเสร็จแล้ว ฉันจะสตรีมส่วนสุดท้ายของ HTML ของหน้าเว็บไปยัง เบราว์เซอร์และสิ้นสุดสตรีม ซึ่งเป็นคิวที่บอกเบราว์เซอร์ว่าการแสดงผลแบบค่อยๆ เป็นค่อยๆ ไปเสร็จสมบูรณ์แล้ว
app.get(routes.get('index'), async (req>, res) = {
res.write(headPartial + navbarPartial);
const tag = req.query.tag || DEFAULT_TAG;
const data = await requestData(...);
res.write(templates.index(tag, data.items));
res.write(footPartial);
res.end
();
});
นั่นคือการทัวร์สั้นๆ เกี่ยวกับการตั้งค่าเซิร์ฟเวอร์ของฉัน ผู้ใช้ที่เข้าชมเว็บแอปของฉันเป็นครั้งแรกจะได้รับการตอบกลับจากเซิร์ฟเวอร์เสมอ แต่เมื่อผู้เข้าชมกลับมาที่เว็บแอปของฉัน Service Worker จะเริ่มตอบกลับ มาดูรายละเอียดกัน
Service Worker

แผนภาพนี้อาจดูคุ้นเคย เนื่องจากมีหลายส่วนที่เหมือนกับที่ฉันเคยกล่าวถึงก่อนหน้านี้ แต่มีการจัดเรียงที่แตกต่างกันเล็กน้อย มาดู ขั้นตอนการส่งคำขอโดยคำนึงถึง Service Worker กัน
Service Worker จะจัดการคำขอไปยัง URL ที่ระบุ และใช้ตรรกะการกำหนดเส้นทางและการสร้างเทมเพลตเพื่อพิจารณาวิธีตอบสนองเช่นเดียวกับที่เซิร์ฟเวอร์ของฉันทำ
แนวทางนี้เหมือนกับก่อนหน้า แต่ใช้ Primitive ระดับต่ำที่แตกต่างกัน เช่น fetch()
และ Cache Storage API ฉันใช้แหล่งข้อมูลเหล่านั้นเพื่อสร้างการตอบกลับ HTML ซึ่ง Service Worker
ส่งกลับไปยังเว็บแอป
Workbox
แทนที่จะเริ่มต้นจากศูนย์ด้วยองค์ประกอบพื้นฐานระดับต่ำ ฉันจะสร้าง Service Worker โดยใช้ชุดไลบรารีระดับสูงที่เรียกว่า Workbox ซึ่งเป็นรากฐานที่มั่นคงสำหรับตรรกะการแคช การกำหนดเส้นทาง และการสร้างการตอบกลับของ Service Worker
การกำหนดเส้นทาง
เช่นเดียวกับโค้ดฝั่งเซิร์ฟเวอร์ของฉัน Service Worker ต้องรู้วิธีจับคู่คำขอขาเข้ากับตรรกะการตอบกลับที่เหมาะสม
แนวทางของฉันคือการแปล
เส้นทาง Express แต่ละเส้นทางเป็นนิพจน์ทั่วไปที่สอดคล้องกัน
โดยใช้ประโยชน์จากไลบรารีที่มีประโยชน์ชื่อ
regexparam
เมื่อแปลแล้ว ฉันจะใช้ประโยชน์จากการรองรับการกำหนดเส้นทางนิพจน์ทั่วไปใน Workbox ได้
หลังจากนำเข้าโมดูลที่มีนิพจน์ทั่วไปแล้ว ฉันจะลงทะเบียนนิพจน์ทั่วไปแต่ละรายการกับเราเตอร์ของ Workbox ภายในแต่ละเส้นทาง ฉันสามารถระบุ ตรรกะการสร้างเทมเพลตที่กำหนดเองเพื่อสร้างการตอบกลับได้ การใช้เทมเพลตใน Service Worker มีความซับซ้อนกว่าในเซิร์ฟเวอร์แบ็กเอนด์ของฉันเล็กน้อย แต่ Workbox ช่วยลด ภาระหนักๆ ได้มาก
import regExpRoutes from './regexp-routes.mjs';
workbox.routing.registerRoute(
regExpRoutes.get('index')
// Templ
ating logic.
);
การแคชชิ้นงานแบบคงที่
ส่วนสำคัญอย่างหนึ่งของการสร้างเทมเพลตคือการตรวจสอบว่าเทมเพลต HTML บางส่วนของฉันพร้อมใช้งานในเครื่องผ่าน Cache Storage API และเป็นเวอร์ชันล่าสุดเมื่อฉันทำการเปลี่ยนแปลงในเว็บแอป การบำรุงรักษาแคชอาจเกิดข้อผิดพลาดได้เมื่อทำด้วยตนเอง ดังนั้นฉันจึงใช้ Workbox เพื่อจัดการการแคชล่วงหน้าเป็นส่วนหนึ่งของกระบวนการบิลด์
ฉันบอก Workbox ว่าจะแคช URL ใดล่วงหน้าโดยใช้ไฟล์กำหนดค่า ซึ่งชี้ไปยังไดเรกทอรีที่มีชิ้นงานในเครื่องทั้งหมดพร้อมชุด รูปแบบที่จะจับคู่ CLI ของ Workbox จะอ่านไฟล์นี้โดยอัตโนมัติ ซึ่งจะเรียกใช้ทุกครั้งที่ฉันสร้างเว็บไซต์ใหม่
module.exports = {
globDirectory: 'build',
globPatterns: ['**/*.{html,js,svg}'],
// Othe
r options...
};
Workbox จะถ่ายภาพรวมของเนื้อหาแต่ละไฟล์ และแทรกรายการ URL และการแก้ไขเหล่านั้นลงในไฟล์ Service Worker สุดท้ายโดยอัตโนมัติ ตอนนี้ Workbox มีทุกอย่างที่จำเป็นเพื่อให้ไฟล์ที่แคชล่วงหน้าพร้อมใช้งานเสมอและเป็นเวอร์ชันล่าสุด ผลลัพธ์คือไฟล์ service-worker.js
ที่มีข้อมูล
คล้ายกับตัวอย่างต่อไปนี้
workbox.precaching.precacheAndRoute([
{
url: 'partials/about.html',
revision: '518747aad9d7e',
},
{
url: 'partials/foot.html',
revision: '69bf746
a9ecc6',
},
// etc.
]);
สำหรับผู้ที่ใช้กระบวนการบิลด์ที่ซับซ้อนมากขึ้น Workbox มีทั้งwebpack
ปลั๊กอินและโมดูลโหนดทั่วไป นอกเหนือจากอินเทอร์เฟซบรรทัดคำสั่ง
สตรีมมิง
จากนั้นฉันต้องการให้ Service Worker สตรีม HTML บางส่วนที่แคชไว้ล่วงหน้ากลับไปยัง เว็บแอปทันที นี่เป็นส่วนสำคัญของการเป็น "รวดเร็วอย่างสม่ำเสมอ" ซึ่งหมายความว่าฉันจะเห็นสิ่งที่น่าสนใจบนหน้าจอได้ทันที โชคดีที่การใช้ Streams API ภายใน Service Worker ของเราทำให้ทำเช่นนั้นได้
ตอนนี้คุณอาจเคยได้ยินเกี่ยวกับ Streams API มาก่อน Jake Archibald เพื่อนร่วมงานของฉันชื่นชมฟีเจอร์นี้มาหลายปีแล้ว เขาคาดการณ์อย่างกล้าหาญว่าปี 2016 จะเป็นปีแห่ง สตรีมบนเว็บ และวันนี้ Streams API ก็ยังคงยอดเยี่ยมเหมือนกับเมื่อ 2 ปีที่แล้ว แต่มีข้อแตกต่างที่สำคัญ
แม้ว่าในตอนนั้นจะมีเพียง Chrome เท่านั้นที่รองรับ Streams แต่ตอนนี้ API ของ Streams รองรับการใช้งานในวงกว้างมากขึ้น โดยรวมแล้วเรื่องนี้เป็นเรื่องดี และหากมีโค้ดสำรองที่เหมาะสม ก็ไม่มีอะไร มาขัดขวางไม่ให้คุณใช้สตรีมใน Service Worker ได้ในวันนี้
แต่ก็อาจมีสิ่งหนึ่งที่ทำให้คุณหยุดชะงัก นั่นก็คือการทำความเข้าใจ ว่า Streams API ทำงานอย่างไร ซึ่งจะแสดงชุดองค์ประกอบพื้นฐานที่มีประสิทธิภาพมาก และนักพัฒนาซอฟต์แวร์ที่คุ้นเคยกับการใช้ชุดองค์ประกอบพื้นฐานนี้จะสร้างโฟลว์ข้อมูลที่ซับซ้อนได้ เช่น โฟลว์ต่อไปนี้
const stream = new ReadableStream({
pull(controller) {
return sources[0]
.then(r => r.read())
.then(result => {
if (result.done) {
sources.shift();
if (sources.length === 0) return controller.close();
return this.pull(controller);
} else {
controller.enqueue(result.value);
}
});
},
});
แต่การทำความเข้าใจผลกระทบทั้งหมดของโค้ดนี้อาจไม่เหมาะกับทุกคน มาพูดถึงแนวทางของฉันในการสตรีม Service Worker กันดีกว่าแทนที่จะวิเคราะห์ตรรกะนี้
ฉันใช้ Wrapper ระดับสูงตัวใหม่ล่าสุด
workbox-streams
ซึ่งช่วยให้ฉันส่งผ่านข้อมูลในแหล่งที่มาของการสตรีมแบบผสมได้ ทั้งจากแคชและ
ข้อมูลรันไทม์ที่อาจมาจากเครือข่าย Workbox จะดูแล
การประสานงานแหล่งข้อมูลแต่ละแหล่งและรวมเข้าด้วยกันเป็นคำตอบแบบสตรีมมิงเดียว
นอกจากนี้ Workbox ยังตรวจหาโดยอัตโนมัติว่า Streams API ได้รับการรองรับหรือไม่ และเมื่อไม่ได้รับการรองรับ ก็จะสร้างการตอบกลับแบบไม่สตรีมที่เทียบเท่า ซึ่งหมายความว่าคุณไม่ต้องกังวลเรื่องการเขียนฟอลแบ็ก เนื่องจากสตรีมจะเข้าใกล้การรองรับเบราว์เซอร์ 100% มากขึ้นเรื่อยๆ
การแคชรันไทม์
มาดูกันว่าService Worker ของฉันจัดการข้อมูลรันไทม์จาก Stack Exchange API อย่างไร ฉันใช้การรองรับในตัวของ Workbox สำหรับกลยุทธ์การแคชแบบล้าสมัยขณะตรวจสอบซ้ำ พร้อมกับการหมดอายุเพื่อให้แน่ใจว่าพื้นที่เก็บข้อมูลของเว็บแอปจะไม่เพิ่มขึ้น อย่างไม่จำกัด
ฉันตั้งค่ากลยุทธ์ 2 อย่างใน Workbox เพื่อจัดการแหล่งที่มาต่างๆ ที่จะ ประกอบกันเป็นคำตอบแบบสตรีม Workbox ช่วยให้เราทำสิ่งที่ต้องใช้โค้ดที่เขียนด้วยมือหลายร้อยบรรทัดได้ด้วยการเรียกใช้ฟังก์ชันและการกำหนดค่าเพียงไม่กี่ครั้ง
const cacheStrategy = workbox.strategies.cacheFirst({
cacheName: workbox.core.cacheNames.precache,
});
const apiStrategy = workbox.strategies.staleWhileRevalidate({
cacheName: API_CACHE_NAME,
plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});
กลยุทธ์แรกจะอ่านข้อมูลที่แคชไว้ล่วงหน้า เช่น เทมเพลต HTML บางส่วน
ส่วนอีกกลยุทธ์หนึ่งจะใช้ตรรกะการแคช stale-while-revalidate พร้อมกับการหมดอายุของแคชที่ไม่ได้ใช้มานานที่สุดเมื่อมีรายการถึง 50 รายการ
ตอนนี้เมื่อมีกลยุทธ์เหล่านั้นแล้ว สิ่งที่เหลืออยู่ก็คือบอก Workbox วิธีใช้กลยุทธ์เหล่านั้นเพื่อสร้างการตอบกลับแบบสตรีมมิงที่สมบูรณ์ ฉันส่งอาร์เรย์ ของแหล่งที่มาเป็นฟังก์ชัน และระบบจะเรียกใช้ฟังก์ชันแต่ละรายการ ทันที Workbox จะนำผลลัพธ์จากแต่ละแหล่งที่มาและสตรีมไปยังเว็บแอปตามลำดับ โดยจะหน่วงเวลาเฉพาะในกรณีที่ฟังก์ชันถัดไปในอาร์เรย์ยังไม่เสร็จสมบูรณ์
workbox.streams.strategy([
() => cacheStrategy.makeRequest({request: '/head.html'})>,
() = cacheStrategy.makeRequest({request: '/navbar.html'}),
async >({event, url}) = {
const tag = url.searchParams.get('tag') || DEFAULT_TAG;
const listResponse = await apiStrategy.makeRequest(...);
const data = await listResponse.json();
return templates.index(tag, >data.items);
},
() = cacheStrategy.makeRequest({reque
st: '/foot.html'}),
]);
แหล่งที่มา 2 รายการแรกคือเทมเพลตบางส่วนที่แคชไว้ล่วงหน้าซึ่งอ่านจาก Cache Storage API โดยตรง จึงพร้อมใช้งานทันทีเสมอ ซึ่งจะช่วยให้มั่นใจได้ว่าการติดตั้งใช้งาน Service Worker ของเราจะตอบสนองต่อคำขอได้อย่างรวดเร็วและเชื่อถือได้ เช่นเดียวกับโค้ดฝั่งเซิร์ฟเวอร์ของฉัน
ฟังก์ชันแหล่งข้อมูลถัดไปจะดึงข้อมูลจาก Stack Exchange API และประมวลผล การตอบกลับเป็น HTML ที่เว็บแอปคาดหวัง
กลยุทธ์ "ล้าสมัยขณะตรวจสอบซ้ำ " หมายความว่าหากฉันมีคำตอบที่แคชไว้ก่อนหน้านี้สำหรับการเรียก API นี้ ฉันจะสตรีมคำตอบนั้นไปยังหน้าเว็บได้ทันที ขณะเดียวกันก็จะอัปเดตรายการแคช"ในเบื้องหลัง" สำหรับครั้งถัดไปที่มีการขอ
สุดท้าย ฉันจะสตรีมสำเนาส่วนท้ายที่แคชไว้และปิดแท็ก HTML สุดท้าย เพื่อทำให้การตอบกลับเสร็จสมบูรณ์
การแชร์รหัสจะช่วยให้ทุกอย่างซิงค์กัน
คุณจะเห็นว่าโค้ด Service Worker บางส่วนดูคุ้นเคย HTML บางส่วนและตรรกะการสร้างเทมเพลตที่ใช้โดย Service Worker ของฉันจะเหมือนกับสิ่งที่แฮนเดิลฝั่งเซิร์ฟเวอร์ของฉันใช้ การแชร์โค้ดนี้ช่วยให้มั่นใจได้ว่าผู้ใช้จะได้รับ ประสบการณ์การใช้งานที่สอดคล้องกัน ไม่ว่าผู้ใช้จะเข้าชมเว็บแอปของฉันเป็นครั้งแรก หรือกลับมาที่หน้าที่ Service Worker แสดงผล นั่นคือข้อดีของ JavaScript แบบไอโซมอร์ฟิก
การเพิ่มประสิทธิภาพแบบไดนามิกและแบบต่อเนื่อง
ฉันได้อธิบายทั้งเซิร์ฟเวอร์และ Service Worker สำหรับ PWA ของฉันแล้ว แต่ยังเหลือ ตรรกะอีกเล็กน้อยที่ต้องกล่าวถึง นั่นคือมี JavaScript จำนวนเล็กน้อย ที่ทำงานในแต่ละหน้าของฉันหลังจากที่สตรีมเข้ามาจนครบ
โค้ดนี้จะช่วยปรับปรุงประสบการณ์ของผู้ใช้ทีละน้อย แต่ก็ไม่ได้สำคัญมากนัก เว็บแอปจะยังคงทำงานได้แม้ว่าจะไม่ได้เรียกใช้โค้ดนี้
ข้อมูลเมตาของหน้า
แอปของฉันใช้ JavaScript ฝั่งไคลเอ็นต์เพื่ออัปเดตข้อมูลเมตาของหน้าเว็บตามการตอบกลับของ API เนื่องจากฉันใช้ HTML ที่แคชไว้ในตอนต้นเหมือนกันสำหรับแต่ละหน้า เว็บแอปจึงมีแท็กทั่วไปในส่วนหัวของเอกสาร แต่ด้วยการประสานงานระหว่างเทมเพลตและโค้ดฝั่งไคลเอ็นต์ ฉันจึงอัปเดตชื่อของหน้าต่างได้โดยใช้ข้อมูลเมตาเฉพาะหน้า
ในส่วนของโค้ดเทมเพลต แนวทางของฉันคือการใส่แท็กสคริปต์ที่มีสตริงที่หลีกเลี่ยงอย่างเหมาะสม
const metadataScript = `<script>
self._title = '${escape(item.title)<}';>
/s
cript`;
จากนั้นเมื่อหน้าเว็บโหลดแล้ว ฉันจะ อ่านสตริงนั้นและอัปเดตชื่อเอกสาร
if (self._title) {
document.title = unescape(self._title);
}
หากมีข้อมูลเมตาอื่นๆ ที่เฉพาะเจาะจงของหน้าเว็บที่คุณต้องการอัปเดตในเว็บแอปของคุณเอง คุณสามารถใช้วิธีเดียวกันนี้ได้
UX ออฟไลน์
การเพิ่มประสิทธิภาพแบบค่อยเป็นค่อยไปอีกอย่างที่ฉันเพิ่มเข้าไปใช้เพื่อดึงดูดความสนใจมาที่ ความสามารถแบบออฟไลน์ ฉันสร้าง PWA ที่เชื่อถือได้ และต้องการให้ผู้ใช้ทราบว่า เมื่อออฟไลน์ ผู้ใช้จะยังโหลดหน้าเว็บที่เคยเข้าชมก่อนหน้านี้ได้
ก่อนอื่น ฉันใช้ Cache Storage API เพื่อรับรายการคำขอ API ทั้งหมดที่แคชไว้ก่อนหน้านี้ และแปลงเป็นรายการ URL
คุณยังจำแอตทริบิวต์ข้อมูลพิเศษที่ฉันพูดถึงได้ไหม ซึ่งแต่ละแอตทริบิวต์มี URL สำหรับคำขอ API ที่จำเป็นต่อการแสดงคำถาม ฉันสามารถอ้างอิงแอตทริบิวต์ข้อมูลเหล่านั้นกับรายการ URL ที่แคชไว้ และสร้างอาร์เรย์ของลิงก์คำถามทั้งหมดที่ไม่ตรงกัน
เมื่อเบราว์เซอร์เข้าสู่สถานะออฟไลน์ ฉันจะวนซ้ำใน รายการลิงก์ที่ไม่ได้แคช และทำให้ลิงก์ที่ใช้ไม่ได้จางลง โปรดทราบว่า นี่เป็นเพียงคำแนะนำด้วยภาพสำหรับผู้ใช้เกี่ยวกับสิ่งที่ควรคาดหวังจากหน้าเว็บเหล่านั้น ฉันไม่ได้ปิดใช้ลิงก์หรือป้องกันไม่ให้ผู้ใช้ ไปยังส่วนต่างๆ
const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);
const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filte>r(card = {
return !cachedUrls.includes(card.dataset.cacheUrl);
});
const offlineHandle>r = () = {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '0.3';
}
};
const onli>neHandler = () = {
for (const uncachedCard of uncachedCards) {
uncachedCard.style.opacity = '1.0';
}
};
window.addEventListener('online', onlineHandler);
window.addEventListe
ner('offline', offlineHandler);
ข้อผิดพลาดที่พบบ่อย
ตอนนี้ฉันได้อธิบายแนวทางในการสร้าง PWA แบบหลายหน้าแล้ว มีหลายปัจจัยที่คุณจะต้องพิจารณาเมื่อคิดค้นแนวทางของตัวเอง และคุณอาจเลือกแตกต่างจากที่ฉันเลือก ความยืดหยุ่นดังกล่าวเป็นข้อดีอย่างหนึ่งของการสร้างเว็บไซต์
ข้อผิดพลาดที่พบบ่อยบางอย่างที่คุณอาจพบเมื่อตัดสินใจด้านสถาปัตยกรรมด้วยตนเอง และฉันอยากช่วยให้คุณไม่ต้องเจ็บปวดกับเรื่องนี้
อย่าแคช HTML แบบเต็ม
เราไม่แนะนำให้จัดเก็บเอกสาร HTML ทั้งหมดไว้ในแคช ประการหนึ่งคือเป็นการสิ้นเปลืองพื้นที่ หากเว็บแอปใช้โครงสร้าง HTML พื้นฐานเดียวกันสำหรับแต่ละหน้า คุณจะต้องจัดเก็บสำเนาของมาร์กอัปเดียวกันซ้ำๆ
ที่สำคัญกว่านั้นคือ หากคุณใช้การเปลี่ยนแปลงกับโครงสร้าง HTML ที่แชร์ของเว็บไซต์ หน้าเว็บที่แคชไว้ก่อนหน้านี้ทุกหน้าจะยังคงใช้เลย์เอาต์เก่า ลองนึกถึงความหงุดหงิดของผู้เข้าชมที่กลับมาซึ่งเห็นทั้งหน้าเว็บเก่าและหน้าเว็บใหม่
ความคลาดเคลื่อนของเซิร์ฟเวอร์ / Service Worker
ข้อควรระวังอีกอย่างที่ควรหลีกเลี่ยงคือการที่เซิร์ฟเวอร์และ Service Worker ไม่ซิงค์กัน แนวทางของฉันคือการใช้ JavaScript แบบ Isomorphic เพื่อให้โค้ดเดียวกัน ทํางานได้ทั้ง 2 ที่ แต่การทำเช่นนั้นอาจเป็นไปไม่ได้เสมอไป โดยขึ้นอยู่กับสถาปัตยกรรมเซิร์ฟเวอร์ที่มีอยู่
ไม่ว่าคุณจะตัดสินใจเลือกสถาปัตยกรรมแบบใด คุณควรมีกลยุทธ์ในการเรียกใช้โค้ดการกำหนดเส้นทางและการสร้างเทมเพลตที่เทียบเท่าในเซิร์ฟเวอร์และ Service Worker
สถานการณ์ที่เลวร้ายที่สุด
เลย์เอาต์ / การออกแบบไม่สอดคล้องกัน
จะเกิดอะไรขึ้นหากคุณไม่สนใจข้อควรระวังเหล่านั้น แน่นอนว่าอาจเกิดความล้มเหลวได้หลายรูปแบบ แต่กรณีที่แย่ที่สุดคือผู้ใช้ที่กลับมาเข้าชมหน้าเว็บที่แคชไว้ซึ่งมีเลย์เอาต์ที่ล้าสมัยมาก อาจเป็นหน้าเว็บที่มีข้อความส่วนหัวที่ล้าสมัย หรือใช้ชื่อคลาส CSS ที่ไม่ถูกต้องอีกต่อไป
สถานการณ์ที่เลวร้ายที่สุด: การกำหนดเส้นทางขัดข้อง
หรือผู้ใช้อาจเห็น URL ที่เซิร์ฟเวอร์ของคุณจัดการ แต่ไม่ใช่ Service Worker เว็บไซต์ที่มีเลย์เอาต์ที่ล้าสมัยและไม่มีทางไปต่อไม่ใช่ PWA ที่เชื่อถือได้
เคล็ดลับเพื่อความสำเร็จ
แต่คุณไม่ได้อยู่คนเดียว เคล็ดลับต่อไปนี้จะช่วยให้คุณหลีกเลี่ยงข้อผิดพลาดเหล่านั้นได้
ใช้ไลบรารีการกำหนดเส้นทางและการสร้างเทมเพลตที่มีการติดตั้งใช้งานหลายภาษา
ลองใช้ไลบรารีการกำหนดเส้นทางและการสร้างเทมเพลตที่มีการใช้งาน JavaScript เราทราบดีว่านักพัฒนาซอฟต์แวร์บางรายอาจไม่มีเวลา ย้ายข้อมูลออกจากเว็บเซิร์ฟเวอร์และภาษาเทมเพลตปัจจุบัน
แต่เฟรมเวิร์กการกำหนดเส้นทางและการสร้างเทมเพลตยอดนิยมหลายรายการมีการใช้งานในหลายภาษา หากพบไลบรารีที่ใช้ได้กับ JavaScript และภาษาของเซิร์ฟเวอร์ปัจจุบัน คุณก็เข้าใกล้การซิงค์ Service Worker และเซิร์ฟเวอร์มากขึ้นอีกขั้น
เลือกเทมเพลตแบบลำดับมากกว่าแบบซ้อนกัน
จากนั้น ฉันขอแนะนำให้ใช้ชุดเทมเพลตแบบต่อเนื่องที่สามารถสตรีมได้ทีละรายการ คุณสามารถใช้ตรรกะการสร้างเทมเพลตที่ซับซ้อนมากขึ้นในส่วนท้ายๆ ของหน้าเว็บได้ ตราบใดที่คุณสตรีมส่วนแรกของ HTML ได้เร็วที่สุด
แคชทั้งเนื้อหาแบบคงที่และแบบไดนามิกใน Service Worker
คุณควรแคชล่วงหน้าสำหรับทรัพยากรแบบคงที่ที่สำคัญทั้งหมดของเว็บไซต์เพื่อให้ได้ประสิทธิภาพสูงสุด นอกจากนี้ คุณควรตั้งค่าตรรกะการแคชรันไทม์เพื่อจัดการเนื้อหาแบบไดนามิก เช่น คำขอ API การใช้ Workbox หมายความว่าคุณ สามารถสร้างต่อยอดจากกลยุทธ์ที่ผ่านการทดสอบมาอย่างดีและพร้อมใช้งานจริงแทนที่จะ ติดตั้งใช้งานทั้งหมดตั้งแต่ต้น
บล็อกในเครือข่ายเมื่อจำเป็นจริงๆ เท่านั้น
และที่เกี่ยวข้องกับเรื่องนี้ คุณควรบล็อกในเครือข่ายเฉพาะในกรณีที่ไม่สามารถ สตรีมคำตอบจากแคชได้ การแสดงการตอบกลับ API ที่แคชไว้ทันทีมักจะช่วยให้ผู้ใช้ได้รับประสบการณ์ที่ดีกว่าการรอข้อมูลล่าสุด