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

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

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

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

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

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

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