ดูวิธีนําเข้าและรวมชิ้นงานประเภทต่างๆ จาก JavaScript
สมมติว่าคุณกําลังทําเว็บแอป ในกรณีนี้ คุณอาจต้องจัดการกับโมดูล JavaScript ไม่เพียงเท่านั้น แต่ยังต้องจัดการกับทรัพยากรอื่นๆ อีกมากมาย เช่น Web Worker (ซึ่งเป็น JavaScript เช่นกัน แต่ไม่ได้เป็นส่วนหนึ่งของกราฟโมดูลปกติ), รูปภาพ, สไตล์ชีต, แบบอักษร, โมดูล WebAssembly และอื่นๆ
คุณสามารถใส่การอ้างอิงทรัพยากรบางส่วนเหล่านั้นใน HTML ได้โดยตรง แต่โดยปกติแล้ว ทรัพยากรเหล่านี้จะเชื่อมโยงกับคอมโพเนนต์ที่นํากลับมาใช้ซ้ำได้ เช่น สไตล์ชีตสำหรับเมนูแบบเลื่อนลงที่กำหนดเองซึ่งเชื่อมโยงกับส่วน JavaScript, รูปภาพไอคอนที่เชื่อมโยงกับคอมโพเนนต์แถบเครื่องมือ หรือโมดูล WebAssembly ที่เชื่อมโยงกับกาว JavaScript ในกรณีเหล่านี้ การอ้างอิงทรัพยากรจากโมดูล JavaScript โดยตรงและโหลดแบบไดนามิกเมื่อ (หรือหาก) โหลดคอมโพเนนต์ที่เกี่ยวข้องจะสะดวกกว่า
อย่างไรก็ตาม โปรเจ็กต์ขนาดใหญ่ส่วนใหญ่มีระบบบิลด์ที่เพิ่มประสิทธิภาพและจัดระเบียบเนื้อหาเพิ่มเติม เช่น การรวมและการทำให้เล็กลง ผู้ใช้จะไม่สามารถเรียกใช้โค้ดและคาดการณ์ผลลัพธ์ของการดำเนินการ รวมทั้งไม่สามารถข้ามผ่านสตริงที่เป็นไปได้ทั้งหมดใน JavaScript และคาดเดาได้ว่า URL นั้นเป็นทรัพยากรหรือไม่ คุณจึงต้องทําให้เครื่องมือ "เห็น" ชิ้นงานแบบไดนามิกที่โหลดโดยคอมโพเนนต์ JavaScript และรวมไว้ในบิลด์
การนำเข้าที่กำหนดเองใน Bundler
แนวทางหนึ่งที่พบบ่อยคือการนําไวยากรณ์การนําเข้าแบบคงที่มาใช้ซ้ำ ใน Bundler บางราย อาจมีการตรวจหารูปแบบจากนามสกุลไฟล์โดยอัตโนมัติ ขณะที่บางรายการอนุญาตให้ปลั๊กอินใช้ Scheme ของ 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 ปัจจุบัน ดังนั้นอาร์กิวเมนต์แรกอาจเป็นเส้นทางใดก็ได้ที่สัมพันธ์กับ URL นั้น
แต่ก็มีข้อเสียที่คล้ายกับการนำเข้าแบบไดนามิก แม้ว่าคุณจะใช้ import(...)
กับนิพจน์ที่กำหนดเองได้ เช่น import(someUrl)
แต่เครื่องมือสร้างแพ็กเกจจะดำเนินการกับรูปแบบที่มี URL แบบคงที่ import('./some-static-url.js')
เป็นกรณีพิเศษเพื่อประมวลผลข้อมูลที่ต้องพึ่งพาซึ่งทราบ ณ เวลาที่คอมไพล์ แต่แยกออกเป็นกลุ่มของตัวเองซึ่งจะโหลดแบบไดนามิก
ในทํานองเดียวกัน คุณสามารถใช้ new URL(...)
กับนิพจน์ที่กำหนดเองได้ เช่น new URL(relativeUrl, customAbsoluteBase)
แต่รูปแบบ new URL('...', import.meta.url)
เป็นสัญญาณที่ชัดเจนสำหรับเครื่องมือสร้างแพ็กเกจให้ประมวลผลล่วงหน้าและรวมไฟล์ที่ต้องพึ่งพาไว้กับ JavaScript หลัก
URL แบบสัมพัทธ์ที่คลุมเครือ
คุณอาจสงสัยว่าทำไม Bundler ถึงตรวจไม่พบรูปแบบทั่วไปอื่นๆ เช่น fetch('./module.wasm')
ที่ไม่มี Wrapper new URL
สาเหตุก็คือ คำขอแบบไดนามิกต่างจากคำสั่งการนำเข้าตรงที่ตัวเอกสารจะได้รับการแปลค่าเดี่ยวๆ แทนไฟล์ 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 ที่คาดไว้ได้สำเร็จ รวมถึงช่วยให้เครื่องมือรวมไฟล์มีวิธีค้นหาเส้นทางแบบสัมพัทธ์เหล่านั้นในระหว่างเวลาสร้างด้วย
การสนับสนุนการใช้เครื่องมือ
เครื่องมือรวม
Bundler ต่อไปนี้รองรับรูปแบบ new URL
อยู่แล้ว
- Webpack v5
- ภาพรวม (สร้างผ่านปลั๊กอิน - @web/rollup-plugin-import-meta-assets สำหรับเนื้อหาทั่วไป และ @surma/rollup-plugin-off-main-thread สำหรับผู้ปฏิบัติงานโดยเฉพาะ)
- พัสดุ 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 ที่สร้างขึ้นจะรวมอยู่ในรูปแบบเดียวกัน และ Bundler และเบราว์เซอร์ต่างๆ สามารถค้นพบได้
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 เพื่อให้สร้างผู้ปฏิบัติงานบนเว็บได้ กาว 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 แล้ว แต่ก็ยังมีคำถามที่ยังไม่ได้แก้ไขอยู่จำนวนหนึ่งเกี่ยวกับวิธีการทำงานบนเว็บ
นำเข้าการยืนยัน
การยืนยันการนำเข้าเป็นฟีเจอร์ใหม่ที่ช่วยให้สามารถนำเข้าประเภทอื่นที่ไม่ใช่โมดูล 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 ในปัจจุบัน