ดูวิธีนําเข้าและรวมชิ้นงานประเภทต่างๆ จาก JavaScript
สมมติว่าคุณกําลังทําเว็บแอป ในกรณีนี้ คุณอาจต้องจัดการกับทั้งโมดูล JavaScript และทรัพยากรอื่นๆ อีกมากมาย เช่น Web Worker (ซึ่งเป็น JavaScript เช่นกัน แต่ไม่ได้เป็นส่วนหนึ่งของกราฟโมดูลปกติ) รูปภาพ สไตล์ชีต แบบอักษร โมดูล WebAssembly และอื่นๆ
คุณสามารถใส่การอ้างอิงทรัพยากรบางส่วนเหล่านั้นใน HTML ได้โดยตรง แต่โดยปกติแล้ว ทรัพยากรเหล่านี้จะเชื่อมโยงกับคอมโพเนนต์ที่นํากลับมาใช้ซ้ำได้ เช่น สไตล์ชีตสำหรับเมนูแบบเลื่อนลงที่กำหนดเองซึ่งเชื่อมโยงกับส่วน JavaScript, รูปภาพไอคอนที่เชื่อมโยงกับคอมโพเนนต์แถบเครื่องมือ หรือโมดูล WebAssembly ที่เชื่อมโยงกับกาว JavaScript ในกรณีเหล่านี้ การอ้างอิงทรัพยากรจากโมดูล JavaScript โดยตรงและโหลดแบบไดนามิกเมื่อ (หรือหาก) โหลดคอมโพเนนต์ที่เกี่ยวข้องจะสะดวกกว่า
อย่างไรก็ตาม โปรเจ็กต์ขนาดใหญ่ส่วนใหญ่มีระบบบิลด์ที่เพิ่มประสิทธิภาพและจัดระเบียบเนื้อหาเพิ่มเติม เช่น การรวมและการทำให้เล็กลง ไม่สามารถเรียกใช้โค้ดและคาดเดาผลลัพธ์ของการดำเนินการได้ รวมถึงไม่สามารถเรียกใช้สตริงที่แท้จริงที่เป็นไปได้ทั้งหมดใน JavaScript และคาดเดาว่า URL นั้นเป็น URL ของทรัพยากรหรือไม่ คุณจึงต้องทําให้เครื่องมือ "เห็น" ชิ้นงานแบบไดนามิกที่โหลดโดยคอมโพเนนต์ JavaScript และรวมไว้ในบิลด์
การนําเข้าที่กําหนดเองในเครื่องมือรวม
แนวทางหนึ่งที่พบบ่อยคือการใช้ไวยากรณ์การนําเข้าแบบคงที่ซ้ำ ในเครื่องมือจัดกลุ่มบางรายการ ระบบอาจตรวจหารูปแบบโดยอัตโนมัติจากนามสกุลไฟล์ ขณะที่เครื่องมือจัดกลุ่มอื่นๆ อาจอนุญาตให้ปลั๊กอินใช้รูปแบบ URL ที่กําหนดเอง เช่น ในตัวอย่างต่อไปนี้
// regular JavaScript import
import { loadImg } from './utils.js';
// special "URL imports" for assets
import imageUrl from 'asset-url:./image.png';
import wasmUrl from 'asset-url:./module.wasm';
import workerUrl from 'js-url:./worker.js';
loadImg(imageUrl);
WebAssembly.instantiateStreaming(fetch(wasmUrl));
new Worker(workerUrl);
เมื่อปลั๊กอินเครื่องมือจัดกลุ่มพบการนําเข้าที่มีนามสกุลที่รู้จักหรือรูปแบบที่กําหนดเองอย่างชัดเจน (asset-url:
และ js-url:
ในตัวอย่างด้านบน) ปลั๊กอินจะเพิ่มชิ้นงานที่อ้างอิงลงในกราฟการบิลด์ คัดลอกไปยังปลายทางสุดท้าย เพิ่มประสิทธิภาพที่เหมาะกับประเภทของชิ้นงาน และแสดงผล URL สุดท้ายที่จะนําไปใช้ระหว่างรันไทม์
ข้อดีของแนวทางนี้คือ การใช้ไวยากรณ์การนําเข้า JavaScript ซ้ำจะรับประกันว่า URL ทั้งหมดเป็นแบบคงที่และสัมพันธ์กับไฟล์ปัจจุบัน ซึ่งทําให้ระบบบิลด์ค้นหา Dependency ดังกล่าวได้ง่าย
แต่ก็มีข้อเสียที่สำคัญอย่างหนึ่งคือ โค้ดดังกล่าวจะใช้งานในเบราว์เซอร์โดยตรงไม่ได้ เนื่องจากเบราว์เซอร์ไม่ทราบวิธีจัดการรูปแบบการนําเข้าหรือส่วนขยายที่กําหนดเองเหล่านั้น วิธีนี้อาจไม่มีปัญหาหากคุณควบคุมโค้ดทั้งหมดและใช้เครื่องมือรวมสำหรับการพัฒนาอยู่แล้ว แต่การใช้โมดูล JavaScript ในเบราว์เซอร์โดยตรง (อย่างน้อยก็ในระหว่างการพัฒนา) เพื่อลดความยุ่งยากนั้นเป็นเรื่องปกติมากขึ้นเรื่อยๆ ผู้ที่กำลังทำงานกับเดโมขนาดเล็กอาจไม่จําเป็นต้องใช้เครื่องมือจัดกลุ่มเลยแม้แต่ในเวอร์ชันที่ใช้งานจริง
รูปแบบสากลสําหรับเบราว์เซอร์และเครื่องมือรวม
หากทํางานกับคอมโพเนนต์ที่นํากลับมาใช้ใหม่ได้ คุณจะต้องการใช้งานคอมโพเนนต์ดังกล่าวในทั้ง 2 สภาพแวดล้อม ไม่ว่าจะเป็นการใช้งานในเบราว์เซอร์โดยตรงหรือสร้างไว้ล่วงหน้าเป็นส่วนหนึ่งของแอปขนาดใหญ่ เครื่องมือจัดกลุ่มสมัยใหม่ส่วนใหญ่รองรับการดำเนินการนี้โดยยอมรับรูปแบบต่อไปนี้ในโมดูล JavaScript
new URL('./relative-path', import.meta.url)
เครื่องมือจะตรวจหารูปแบบนี้ได้แบบคงที่ ราวกับว่าเป็นไวยากรณ์พิเศษ แต่รูปแบบนี้เป็นนิพจน์ JavaScript ที่ถูกต้องซึ่งทำงานในเบราว์เซอร์โดยตรงได้ด้วย
เมื่อใช้รูปแบบนี้ ตัวอย่างข้างต้นจะเขียนใหม่ได้ดังนี้
// regular JavaScript import
import { loadImg } from './utils.js';
loadImg(new URL('./image.png', import.meta.url));
WebAssembly.instantiateStreaming(
fetch(new URL('./module.wasm', import.meta.url)),
{ /* … */ }
);
new Worker(new URL('./worker.js', import.meta.url));
หลักการทำงาน มาดูรายละเอียดกัน ตัวสร้าง new URL(...)
ใช้ URL แบบสัมพัทธ์เป็นอาร์กิวเมนต์แรกและแก้ไข URL นั้นเทียบกับ URL แบบสัมบูรณ์ที่ระบุเป็นอาร์กิวเมนต์ที่ 2 ในกรณีของเรา อาร์กิวเมนต์ที่ 2 คือ import.meta.url
ซึ่งให้ URL ของโมดูล JavaScript ปัจจุบัน ดังนั้นอาร์กิวเมนต์ที่ 1 อาจเป็นเส้นทางใดก็ได้ที่สัมพันธ์กับ URL ดังกล่าว
ซึ่งมีข้อดีข้อเสียคล้ายกับการนําเข้าแบบไดนามิก แม้ว่าคุณจะใช้ import(...)
กับนิพจน์ที่กำหนดเองได้ เช่น import(someUrl)
แต่เครื่องมือสร้างแพ็กเกจจะดำเนินการกับรูปแบบที่มี URL แบบคงที่ import('./some-static-url.js')
เป็นกรณีพิเศษเพื่อประมวลผลข้อมูลที่ต้องพึ่งพาซึ่งทราบ ณ เวลาคอมไพล์ แต่แยกออกเป็นกลุ่มของตัวเองซึ่งโหลดแบบไดนามิก
ในทํานองเดียวกัน คุณสามารถใช้ new URL(...)
กับนิพจน์ที่กำหนดเองได้ เช่น new URL(relativeUrl, customAbsoluteBase)
แต่รูปแบบ new URL('...', import.meta.url)
เป็นสัญญาณที่ชัดเจนสำหรับเครื่องมือสร้างแพ็กเกจให้ประมวลผลล่วงหน้าและรวมไฟล์ที่ต้องพึ่งพาไว้กับ JavaScript หลัก
URL แบบสัมพัทธ์ที่คลุมเครือ
คุณอาจสงสัยว่าทำไมเครื่องมือจัดกลุ่มจึงไม่ตรวจหารูปแบบทั่วไปอื่นๆ เช่น fetch('./module.wasm')
ที่ไม่มี new URL
Wrapper
สาเหตุคือคําขอแบบไดนามิกจะได้รับการแก้ไขตามเอกสารนั้นๆ ไม่ใช่ไฟล์ JavaScript ปัจจุบัน ซึ่งแตกต่างจากคำสั่งการนําเข้า สมมติว่าคุณมีโครงสร้างต่อไปนี้
index.html
:
html <script src="src/main.js" type="module"></script>
src/
main.js
module.wasm
หากต้องการโหลด module.wasm
จาก main.js
คุณอาจต้องการใช้เส้นทางแบบสัมพัทธ์ เช่น fetch('./module.wasm')
อย่างไรก็ตาม fetch
จะไม่ทราบว่า URL ของไฟล์ JavaScript ที่ใช้งานอยู่คืออะไร แต่จะใช้ URL ที่เกี่ยวข้องกับเอกสารแทน ด้วยเหตุนี้ fetch('./module.wasm')
จึงพยายามโหลด http://example.com/module.wasm
แทน http://example.com/src/module.wasm
ที่ต้องการและดำเนินการไม่สำเร็จ (หรือแย่กว่านั้นคือโหลดทรัพยากรอื่นโดยที่คุณไม่รู้ตัว)
การรวม URL แบบสัมพัทธ์ไว้ใน new URL('...', import.meta.url)
จะช่วยหลีกเลี่ยงปัญหานี้และรับประกันว่า URL ที่ระบุจะได้รับการแก้ไขโดยสัมพันธ์กับ URL ของโมดูล JavaScript ปัจจุบัน (import.meta.url
) ก่อนที่จะส่งต่อไปยังโปรแกรมโหลด
แทนที่ fetch('./module.wasm')
ด้วย fetch(new URL('./module.wasm', import.meta.url))
แล้วระบบจะโหลดโมดูล WebAssembly ที่คาดไว้ได้สำเร็จ รวมถึงช่วยให้เครื่องมือรวมไฟล์มีวิธีค้นหาเส้นทางแบบสัมพัทธ์เหล่านั้นในระหว่างเวลาสร้างด้วย
การรองรับเครื่องมือ
เครื่องมือรวม
เครื่องมือรวมต่อไปนี้รองรับรูปแบบ new URL
อยู่แล้ว
- Webpack v5
- Rollup (ทำได้ผ่านปลั๊กอิน @web/rollup-plugin-import-meta-assets สำหรับชิ้นงานทั่วไปและ @surma/rollup-plugin-off-main-thread สำหรับ Workers โดยเฉพาะ)
- Parcel v2 (เบต้า)
- Vite
WebAssembly
เมื่อทํางานกับ WebAssembly โดยทั่วไปคุณจะไม่ได้โหลดโมดูล Wasm ด้วยตนเอง แต่จะใช้การนําเข้ากาว JavaScript ที่เครื่องมือทางเทคนิคสร้างขึ้นแทน เครื่องมือต่อไปนี้สามารถส่งออกรูปแบบ new URL(...)
ที่อธิบายไว้ให้คุณได้
C/C++ ผ่าน Emscripten
เมื่อใช้ Emscripten คุณสามารถขอให้ Emscripten แสดงผลกาว JavaScript เป็นโมดูล ES6 แทนสคริปต์ปกติผ่านตัวเลือกใดตัวเลือกหนึ่งต่อไปนี้
$ emcc input.cpp -o output.mjs
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6
เมื่อใช้ตัวเลือกนี้ เอาต์พุตจะใช้รูปแบบ new URL(..., import.meta.url)
อยู่เบื้องหลังเพื่อให้เครื่องมือจัดกลุ่มพบไฟล์ Wasm ที่เชื่อมโยงโดยอัตโนมัติ
คุณยังใช้ตัวเลือกนี้กับเธรด WebAssembly ได้ด้วยโดยเพิ่ม Flag -pthread
$ emcc input.cpp -o output.mjs -pthread
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6 -pthread
ในกรณีนี้ Web Worker ที่สร้างขึ้นจะรวมอยู่ในลักษณะเดียวกัน และเครื่องมือจัดกลุ่มและเบราว์เซอร์จะค้นพบได้เช่นกัน
Rust ผ่าน wasm-pack / wasm-bindgen
wasm-pack ซึ่งเป็นเครื่องมือทางเทคนิคหลักของ Rust สําหรับ WebAssembly ก็มีโหมดเอาต์พุตหลายโหมดด้วย
โดยค่าเริ่มต้น เครื่องมือนี้จะสร้างโมดูล JavaScript ที่ใช้ข้อเสนอการผสานรวม ESM ของ WebAssembly ขณะเขียนบทความนี้ ข้อเสนอนี้ยังอยู่ระหว่างการทดสอบ และเอาต์พุตจะใช้งานได้เมื่อรวมกับ Webpack เท่านั้น
แต่คุณสามารถขอให้ wasm-pack แสดงผลโมดูล ES6 ที่เข้ากันได้กับเบราว์เซอร์แทนได้ผ่าน --target web
$ wasm-pack build --target web
เอาต์พุตจะใช้รูปแบบ new URL(..., import.meta.url)
ที่อธิบายไว้ และเครื่องมือรวมไฟล์จะค้นพบไฟล์ Wasm โดยอัตโนมัติด้วย
หากต้องการใช้เธรด WebAssembly กับ Rust การดำเนินการจะซับซ้อนขึ้นเล็กน้อย ดูข้อมูลเพิ่มเติมได้ในส่วนที่เกี่ยวข้องของคู่มือ
สรุปสั้นๆ คือ คุณใช้ API ของชุดข้อความแบบกำหนดเองไม่ได้ แต่หากใช้ Rayon คุณสามารถรวมเข้ากับอะแดปเตอร์ wasm-bindgen-rayon เพื่อให้สร้าง Worker บนเว็บได้ กาว JavaScript ที่ใช้โดย wasm-bindgen-rayon ยังมีรูปแบบ new URL(...)
อยู่เบื้องหลังด้วย ดังนั้นเครื่องมือรวมไฟล์จึงจะค้นพบและรวม Workers ไว้ด้วย
ฟีเจอร์ในอนาคต
import.meta.resolve
การโทร import.meta.resolve(...)
โดยเฉพาะอาจเป็นการปรับปรุงในอนาคต ซึ่งจะช่วยให้การแก้ไขตัวระบุสัมพันธ์กับโมดูลปัจจุบันได้ง่ายขึ้นโดยไม่ต้องใช้พารามิเตอร์เพิ่มเติม
new URL('...', import.meta.url)
await import.meta.resolve('...')
นอกจากนี้ ยังผสานรวมกับแผนที่การนําเข้าและโปรแกรมแก้ไขที่กำหนดเองได้ดียิ่งขึ้น เนื่องจากจะผ่านระบบการแก้ไขโมดูลเดียวกันกับ import
การใช้รูปแบบนี้จะเป็นสัญญาณที่ชัดเจนยิ่งขึ้นสำหรับเครื่องมือรวมไฟล์ด้วย เนื่องจากเป็นไวยากรณ์แบบคงที่ที่ไม่ขึ้นอยู่กับรันไทม์ API เช่น URL
import.meta.resolve
มีการใช้งานเป็นการทดสอบใน Node.js แล้ว แต่ยังมีคำถามที่ยังไม่ได้รับคำตอบเกี่ยวกับวิธีการทำงานของ import.meta.resolve
ในเว็บ
นําเข้าการยืนยัน
การยืนยันการนําเข้าเป็นฟีเจอร์ใหม่ที่อนุญาตให้นําเข้าประเภทอื่นๆ นอกเหนือจากโมดูล ECMAScript ขณะนี้มีเพียง JSON เท่านั้น
foo.json:
{ "answer": 42 }
main.mjs:
import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42
นอกจากนี้ เครื่องมือจัดกลุ่มอาจใช้รูปแบบนี้แทนกรณีการใช้งานที่ปัจจุบันครอบคลุมโดยรูปแบบ new URL
แต่ระบบจะเพิ่มประเภทในการยืนยันการนําเข้าเป็นรายกรณี ขณะนี้มีเฉพาะ JSON เท่านั้น โดยโมดูล CSS จะพร้อมใช้งานในเร็วๆ นี้ แต่ชิ้นงานประเภทอื่นๆ จะยังคงต้องใช้โซลูชันทั่วไปมากกว่า
ดูข้อมูลเพิ่มเติมเกี่ยวกับฟีเจอร์นี้ได้ที่คำอธิบายฟีเจอร์ของ v8.dev
บทสรุป
ดังที่คุณเห็น มีหลายวิธีในการรวมทรัพยากรที่ไม่ใช่ JavaScript ไว้ในเว็บ แต่ก็มีข้อเสียหลายประการและไม่ทำงานในเครื่องมือต่างๆ โปรเจ็กต์ในอนาคตอาจทำให้เรานำเข้าชิ้นงานดังกล่าวด้วยไวยากรณ์เฉพาะได้ แต่เรายังไม่พร้อมที่จะดำเนินการในตอนนี้
ในระหว่างนี้ รูปแบบ new URL(..., import.meta.url)
เป็นโซลูชันที่น่าจะเป็นไปได้มากที่สุดซึ่งใช้งานได้ในเบราว์เซอร์ เครื่องมือจัดกลุ่มต่างๆ และชุดเครื่องมือ WebAssembly ในปัจจุบัน