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

ดูวิธีนำเข้าและรวมกลุ่มเนื้อหาประเภทต่างๆ จาก JavaScript

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

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

กราฟแสดงภาพชิ้นงานประเภทต่างๆ ที่นำเข้าไปยัง JS

อย่างไรก็ตาม โครงการขนาดใหญ่ส่วนใหญ่จะมีระบบที่ทำการเพิ่มประสิทธิภาพเพิ่มเติมและการจัดระเบียบเนื้อหาใหม่ เช่น การรวมและการลดขนาด และไม่สามารถเรียกใช้โค้ดและคาดการณ์ผลลัพธ์ของการดำเนินการ และไม่สามารถข้ามผ่านสตริงทุกตัวที่เป็นไปได้ใน JavaScript และคาดเดาว่าเป็น URL ทรัพยากรหรือไม่ แล้วจะทำอย่างไรให้ผู้ใช้ "เห็น" เนื้อหาแบบไดนามิกที่โหลดโดยคอมโพเนนต์ JavaScript และรวมเนื้อหาเหล่านั้นไว้ในบิลด์

การนําเข้าที่กําหนดเองในไฟล์แพ็กเกจ

วิธีหนึ่งที่ใช้บ่อยคือการใช้ไวยากรณ์การนำเข้าแบบคงที่มาใช้ซ้ำ ในโปรแกรม Bundle บางตัว ระบบอาจตรวจหารูปแบบโดยอัตโนมัติจากนามสกุลไฟล์ ขณะที่บางตัวอนุญาตให้ปลั๊กอินใช้ 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);

เมื่อปลั๊กอิน Bundler พบการนำเข้าที่มีส่วนขยายที่รู้จักหรือสคีมที่กำหนดเองที่ชัดเจนดังกล่าว (asset-url: และ js-url: ในตัวอย่างด้านบน) ปลั๊กอินจะเพิ่มเนื้อหาที่อ้างอิงลงในกราฟบิลด์ คัดลอกไปยังปลายทางสุดท้าย ทำการเพิ่มประสิทธิภาพที่เกี่ยวข้องกับประเภทเนื้อหา และส่ง URL สุดท้ายที่จะใช้ในระหว่างรันไทม์

ประโยชน์ของวิธีนี้: การใช้ไวยากรณ์การนำเข้า JavaScript ซ้ำจะช่วยรับประกันว่า URL ทั้งหมดจะเป็นแบบคงที่และสัมพันธ์กับไฟล์ปัจจุบัน ซึ่งทำให้การค้นหาทรัพยากร Dependency ดังกล่าวง่ายขึ้นสำหรับระบบการสร้าง

อย่างไรก็ตาม มีข้อเสียสำคัญอย่างหนึ่งคือ โค้ดดังกล่าวไม่สามารถทำงานในเบราว์เซอร์ได้โดยตรง เนื่องจากเบราว์เซอร์ไม่รู้วิธีจัดการรูปแบบหรือส่วนขยายการนำเข้าที่กำหนดเองเหล่านั้น อย่างไรก็ตาม การควบคุมโค้ดทั้งหมดและอาศัย Bundler ในการพัฒนาซอฟต์แวร์อาจไม่มีปัญหา แต่การใช้โมดูล JavaScript โดยตรงในเบราว์เซอร์โดยตรงนั้นกลายเป็นเรื่องปกติมากขึ้นเพื่อลดความยุ่งยาก คนที่ทำงานในการสาธิตขนาดเล็กอาจไม่จำเป็นต้องใช้ Bundler เลย แม้แต่ในเวอร์ชันที่ใช้งานจริง

รูปแบบสากลสำหรับเบราว์เซอร์และ Bundler

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

ซึ่งมีข้อดีและข้อเสียใกล้เคียงกับการนําเข้าแบบไดนามิก แม้ว่าจะสามารถใช้ import(...) กับนิพจน์ที่กำหนดเอง เช่น import(someUrl) แต่ Bundler จะดำเนินการพิเศษกับรูปแบบที่มี URL คงที่ import('./some-static-url.js') เป็นวิธีประมวลผลทรัพยากร Dependency ล่วงหน้าที่รู้จัก ณ เวลาที่คอมไพล์ แต่ก็แยกออกเป็นส่วนๆ ของตัวเองซึ่งโหลดแบบไดนามิก

ในทำนองเดียวกัน คุณสามารถใช้ new URL(...) กับนิพจน์ที่กำหนดเอง เช่น new URL(relativeUrl, customAbsoluteBase) แต่รูปแบบ new URL('...', import.meta.url) เป็นสัญญาณที่ชัดเจนสำหรับ Bundler ในการประมวลผลล่วงหน้าและรวม Dependency ไว้ข้าง JavaScript หลัก

URL สัมพัทธ์ที่ไม่ชัดเจน

คุณอาจสงสัยว่าทําไมจึงตรวจหารูปแบบทั่วไปอื่นๆ ไม่ได้ เช่น 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 มีวิธีค้นหาเส้นทางแบบสัมพัทธ์เหล่านั้นในช่วงเวลาบิลด์ด้วย

การสนับสนุนการใช้เครื่องมือ

Bundler

Bundler ต่อไปนี้รองรับรูปแบบ new URL อยู่แล้ว

WebAssembly

เมื่อทำงานกับ WebAssembly โดยทั่วไปแล้วคุณจะไม่โหลดโมดูล Wasm ด้วยมือ แต่ให้นำเข้ากาว JavaScript ที่ปล่อยโดย Toolchain เชนเครื่องมือต่อไปนี้จะแสดงรูปแบบ new URL(...) ที่อธิบายไว้ขั้นสูงให้คุณได้

C/C++ ผ่าน 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) ในส่วนเบื้องหลัง เพื่อให้ Bundler ค้นหาไฟล์ Wasm ที่เกี่ยวข้องได้โดยอัตโนมัติ

คุณยังใช้ตัวเลือกนี้กับชุดข้อความ WebAssembly ได้โดยการเพิ่มแฟล็ก -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 และเบราว์เซอร์ต่างๆ ก็จะค้นพบได้

สนิมผ่าน Wasm-pack / Wasm-bindgen

wasm-pack ซึ่งเป็นห่วงเครื่องมือ Rust หลักสำหรับ WebAssembly ยังมีโหมดเอาต์พุตอีกหลายโหมดด้วย

โดยค่าเริ่มต้น โปรแกรมจะปล่อยโมดูล JavaScript ที่ใช้ข้อเสนอการผสานรวม WebAssembly ESM ในขณะที่กำลังเขียน ข้อเสนอนี้ยังอยู่ในช่วงทดลอง และผลลัพธ์จะใช้ได้เมื่อรวมชุดกับ Webpack เท่านั้น

แต่คุณสามารถขอให้ Wasm-pack ปล่อยโมดูล ES6 ที่เข้ากันได้กับเบราว์เซอร์ผ่าน --target web แทน ดังนี้

$ wasm-pack build --target web

เอาต์พุตจะใช้รูปแบบ new URL(..., import.meta.url) ที่อธิบายไว้ และ Bundler จะค้นหาไฟล์ Wasm โดยอัตโนมัติด้วย

หากคุณต้องการใช้ชุดข้อความ WebAssembly กับ Rust เรื่องราวจะซับซ้อนกว่าเล็กน้อย โปรดดูข้อมูลเพิ่มเติมในส่วนที่เกี่ยวข้องของคู่มือ

เวอร์ชันสั้นๆ คือคุณไม่สามารถใช้ API เทรดที่กำหนดเอง แต่หากคุณใช้ Rayon คุณสามารถรวม API ดังกล่าวกับอะแดปเตอร์ wasm-bindgen-rayon เพื่อสร้าง Workers บนเว็บได้ กาว JavaScript ที่ Wasm-bindgen-rayon ยังมีรูปแบบ new URL(...) ไว้ภายในระบบด้วย เพื่อให้ Bundleer ค้นพบผู้ปฏิบัติงานและค้นพบ Worker ได้เช่นกัน

ฟีเจอร์ในอนาคต

import.meta.resolve

การโทร import.meta.resolve(...) โดยเฉพาะอาจเป็นการปรับปรุงในอนาคต วิธีนี้จะช่วยให้แก้ไขตัวระบุที่ค่อนข้างเป็นโมดูลปัจจุบันในลักษณะที่ตรงไปตรงมามากขึ้น โดยไม่มีพารามิเตอร์เพิ่มเติม ดังนี้

new URL('...', import.meta.url)
await import.meta.resolve('...')

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

นอกจากนี้ Bundler อาจนำไปใช้และแทนที่ Use Case ที่รูปแบบ new URL ครอบคลุมอยู่ในปัจจุบัน แต่จะเพิ่มการยืนยันการนำเข้าประเภทเป็นรายกรณี ในตอนนี้ เวอร์ชันเหล่านี้ครอบคลุมเฉพาะ JSON เท่านั้น โดยจะเพิ่มโมดูล CSS ในเร็วๆ นี้ แต่เนื้อหาประเภทอื่นๆ ยังคงต้องใช้โซลูชันแบบทั่วไปมากกว่า

ดูคำอธิบายฟีเจอร์ v8.dev เพื่อดูข้อมูลเพิ่มเติมเกี่ยวกับฟีเจอร์นี้

บทสรุป

จะเห็นได้ว่าในการรวมทรัพยากรที่ไม่ใช่ JavaScript ลงในเว็บนั้นทำได้หลายวิธี แต่ก็มีข้อเสียหลายอย่างและใช้ใน Toolchain ต่างๆ ไม่ได้ ข้อเสนอในอนาคตอาจให้เรานำเข้าเนื้อหาประเภทดังกล่าวพร้อมด้วยไวยากรณ์เฉพาะทาง แต่เรายังไม่มีโอกาสเช่นนั้น

ในระหว่างนี้ รูปแบบ new URL(..., import.meta.url) เป็นโซลูชันที่น่าสนใจที่สุดซึ่งใช้งานได้อยู่แล้วในเบราว์เซอร์, Bundler ต่างๆ และ Toolchain ของ WebAssembly