איך מייבאים סוגים שונים של נכסים מ-JavaScript ומקבצים אותם יחד.
נניח שאתם עובדים על אפליקציית אינטרנט. במקרה כזה, סביר להניח שתצטרכו להתמודד לא רק עם מודולים של JavaScript, אלא גם עם כל מיני משאבים אחרים – Web Workers (שגם הם JavaScript, אבל לא חלק מהתרשים הרגיל של המודולים), תמונות, גיליונות סגנונות, גופנים, מודולים של WebAssembly ועוד.
אפשר לכלול הפניות לחלק מהמשאבים האלה ישירות ב-HTML, אבל לרוב הם מקושרים באופן לוגי לרכיבים לשימוש חוזר. לדוגמה, גיליון סגנונות של תפריט נפתח בהתאמה אישית שמקושר לחלק של JavaScript שלו, קובצי אימג' של סמלים שמקושרים לרכיב של סרגל הכלים או מודול WebAssembly שמקושר לחלק של JavaScript שלו. במקרים כאלה, קל יותר להפנות למשאבים ישירות מהמודולים שלהם ב-JavaScript ולטעון אותם באופן דינמי כשהרכיב המתאים נטען (או אם הוא נטען).
עם זאת, ברוב הפרויקטים הגדולים יש מערכות build שמבצעות אופטימיזציות נוספות וארגון מחדש של התוכן – לדוגמה, קיבוץ וקידוד למינימום. הם לא יכולים להריץ את הקוד ולחזות מה תהיה תוצאת הביצוע, וגם לא לעבור על כל מחרוזת רגילה אפשרית ב-JavaScript ולנסות לנחש אם היא כתובת URL של משאב או לא. אז איך אפשר לגרום להם "לראות" את הנכסים הדינמיים שנטענים על ידי רכיבי JavaScript, ולכלול אותם ב-build?
ייבוא בהתאמה אישית ב-bundlers
גישה נפוצה אחת היא לעשות שימוש חוזר בתחביר של הייבוא הסטטי. בחלק מכלי ה-bundlers, המערכת עשויה לזהות את הפורמט באופן אוטומטי לפי סיומת הקובץ, בעוד שבאחרים, יישומי הפלאגין יכולים להשתמש בסכימת כתובת 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:
בדוגמה שלמעלה), הוא מוסיף את הנכס שאליו מתבצעת ההפניה לתרשים ה-build, מעתיק אותו ליעד הסופי, מבצע אופטימיזציות שמתאימות לסוג הנכס ומחזיר את כתובת ה-URL הסופית שתשמש במהלך זמן הריצה.
היתרונות של הגישה הזו: שימוש חוזר בסינטקס של ייבוא JavaScript מבטיח שכל כתובות ה-URL הן סטטיות ויחסיות לקובץ הנוכחי, וכך קל יותר למערכת ה-build לאתר יחסי תלות כאלה.
עם זאת, יש לכך חיסרון משמעותי אחד: קוד כזה לא יכול לפעול ישירות בדפדפן, כי הדפדפן לא יודע איך לטפל בסכמות או בתוספים בהתאמה אישית של הייבוא. זה יכול להיות בסדר אם אתם שולטים בכל הקוד ומסתמכים על חבילה לעיבוד קוד בכל שלבי הפיתוח, אבל יותר ויותר אנשים משתמשים במודולים של JavaScript ישירות בדפדפן, לפחות במהלך הפיתוח, כדי לצמצם את החיכוך. אם מישהו עובד על הדגמה קטנה, ייתכן שאפילו לא יהיה לו צורך ב-Bundler, גם לא בסביבת הייצור.
קו ביטול נעילה אוניברסלי לדפדפנים ול-bundler
אם אתם עובדים על רכיב לשימוש חוזר, אתם רוצים שהוא יפעל בכל סביבה, בין אם משתמשים בו ישירות בדפדפן או בונים אותו מראש כחלק מאפליקציה גדולה יותר. רוב ה-bundlers המודרניים מאפשרים זאת על ידי קבלת התבנית הבאה במודולים של 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));
איך זה עובד? ננסה להסביר. ה-constructor של new URL(...)
מקבל כתובת URL יחסית כארגומנט הראשון וממיר אותה לכתובת URL מוחלטת שצוינה כארגומנט השני. במקרה שלנו, הארגומנט השני הוא import.meta.url
, שמציג את כתובת ה-URL של מודול JavaScript הנוכחי, כך שהארגומנט הראשון יכול להיות כל נתיב ביחס אליו.
יש לכך השפעה דומה לזו של הייבוא הדינמי. אפשר להשתמש ב-import(...)
עם ביטויים שרירותיים כמו import(someUrl)
, אבל ה-bundlers נותנים טיפול מיוחד לדפוס עם כתובת URL סטטית import('./some-static-url.js')
, כדרך לעבד מראש תלות שידועה בזמן הידור, אבל לפצל אותה לקטע משלה שנטען באופן דינמי.
באופן דומה, אפשר להשתמש ב-new URL(...)
עם ביטויים שרירותיים כמו new URL(relativeUrl, customAbsoluteBase)
, אבל התבנית new URL('...', import.meta.url)
היא אות ברור לכלי ה-bundlers לבצע עיבוד מקדים ולכלול יחסי תלות לצד ה-JavaScript הראשי.
כתובות URL יחסיות לא ברורות
יכול להיות שתתהו למה חבילות לא יכולות לזהות דפוסים נפוצים אחרים – לדוגמה, fetch('./module.wasm')
בלי העטיפות של 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 הצפוי, וגם ייתן ל-bundlers דרך למצוא את הנתיבים היחסיים האלה גם במהלך ה-build.
תמיכה בכלים
חבילות
כבר יש תמיכה בסכימה new URL
בחבילות האוספים הבאות:
- Webpack v5
- אוסף נכסים (התכונה הזו מושגת באמצעות פלאגינים – @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, אפשר לבקש ממנו להפיק קוד דבק של 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 על ידי הוספת הדגל -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 שמסתמך על הצעה לשילוב של WebAssembly ESM. נכון למועד כתיבת המאמר, ההצעה הזו עדיין ניסיונית, והפלט יפעל רק כשמקבצים אותו עם Webpack.
במקום זאת, אפשר לבקש מ-wasm-pack ליצור מודול ES6 תואם-דפדפן באמצעות --target web
:
$ wasm-pack build --target web
הפלט ישתמש בתבנית new URL(..., import.meta.url)
המתוארת, וגם קובץ ה-Wasm יתגלה באופן אוטומטי על ידי חבילות.
אם רוצים להשתמש בשרשור WebAssembly עם Rust, העניין קצת יותר מורכב. מידע נוסף זמין בקטע המתאים במדריך.
בגרסה הקצרה לא ניתן להשתמש בממשקי API שרירותיים בשרשורים, אבל אם משתמשים ב-Rayon אפשר לשלב אותו עם המתאם wasm-bindgen-rayon כדי לאפשר את השימוש ב-Workers באינטרנט. הדבק של 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, אבל עדיין יש כמה שאלות לא פתורות לגבי האופן שבו הוא אמור לפעול באינטרנט.
ייבוא טענות נכוֹנוּת
טענות נכוֹנוּת (assertions) של ייבוא הן תכונה חדשה שמאפשרת לייבא סוגים שאינם מודולים של 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, אבל עדיין יהיה צורך בפתרון כללי יותר לסוגי נכסים אחרים.
במאמר הזה מוסבר בהרחבה על התכונה הזו.
סיכום
כפי שראיתם, יש דרכים שונות לכלול באינטרנט משאבים שאינם JavaScript, אבל לכל אחת מהן יש חסרונות שונים והן לא פועלות בכל כלי הפיתוח. הצעות עתידיות עשויות לאפשר לנו לייבא נכסים כאלה באמצעות תחביר מיוחד, אבל אנחנו עדיין לא שם.
עד אז, התבנית new URL(..., import.meta.url)
היא הפתרון המבטיח ביותר שכבר פועל בדפדפנים, ב-bundlers שונים ובערכות כלים של WebAssembly.