การรวมทรัพยากรที่ไม่ใช่ JavaScript

ดูวิธีนําเข้าและรวมชิ้นงานประเภทต่างๆ จาก JavaScript

สมมติว่าคุณกําลังทําเว็บแอป ในกรณีนี้ คุณอาจต้องจัดการกับโมดูล JavaScript ไม่เพียงเท่านั้น แต่ยังต้องจัดการกับทรัพยากรอื่นๆ อีกมากมาย เช่น Web Worker (ซึ่งเป็น JavaScript เช่นกัน แต่ไม่ได้เป็นส่วนหนึ่งของกราฟโมดูลปกติ), รูปภาพ, สไตล์ชีต, แบบอักษร, โมดูล WebAssembly และอื่นๆ

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

กราฟแสดงภาพเนื้อหาประเภทต่างๆ ที่นําเข้าไปยัง JS

อย่างไรก็ตาม โปรเจ็กต์ขนาดใหญ่ส่วนใหญ่มีระบบบิลด์ที่เพิ่มประสิทธิภาพและจัดระเบียบเนื้อหาเพิ่มเติม เช่น การรวมและการทำให้เล็กลง ผู้ใช้จะไม่สามารถเรียกใช้โค้ดและคาดการณ์ผลลัพธ์ของการดำเนินการ รวมทั้งไม่สามารถข้ามผ่านสตริงที่เป็นไปได้ทั้งหมดใน 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 อยู่แล้ว

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 ในปัจจุบัน