أو كيفية التحكم في سلوك عمليات استيراد JavaScript
<script>
<base>
import.meta.resolve()
يسمح هذا الاقتراح بالتحكم في عناوين URL التي يتم جلبها بواسطة عبارات import
JavaScript وتعبيرات import()
. يسمح هذا لـ "محددات الاستيراد العارية"، مثل import moment from "moment"
، بالعمل.
آلية القيام بذلك هي عبر خريطة الاستيراد التي يمكن استخدامها للتحكم في دقة محددات الوحدة النمطية بشكل عام. كمثال تمهيدي، فكر في الكود
import moment from "moment" ;
import { partition } from "lodash" ;
اليوم، هذا أمر غير مقبول، حيث أن هذه المحددات العارية محجوزة بشكل صريح. من خلال تزويد المتصفح بخريطة الاستيراد التالية
< script type =" importmap " >
{
"imports" : {
"moment" : "/node_modules/moment/src/moment.js" ,
"lodash" : "/node_modules/lodash-es/lodash.js"
}
}
</ script >
ما ورد أعلاه سيعمل كما لو كنت قد كتبت
import moment from "/node_modules/moment/src/moment.js" ;
import { partition } from "/node_modules/lodash-es/lodash.js" ;
لمزيد من المعلومات حول قيمة "importmap"
الجديدة للسمة type=""
<script>
، راجع قسم التثبيت. في الوقت الحالي، سنركز على دلالات التعيين، مع تأجيل مناقشة التثبيت.
مطورو الويب الذين لديهم خبرة في أنظمة الوحدات النمطية لما قبل ES2015، مثل CommonJS (إما في Node أو مجمعة باستخدام webpack/browserify للمتصفح)، معتادون على القدرة على استيراد الوحدات النمطية باستخدام بناء جملة بسيط:
const $ = require ( "jquery" ) ;
const { pluck } = require ( "lodash" ) ;
سيتم ترجمتها إلى لغة نظام الوحدات المدمج في JavaScript
import $ from "jquery" ;
import { pluck } from "lodash" ;
في مثل هذه الأنظمة، يتم تعيين محددات الاستيراد العارية لـ "jquery"
أو "lodash"
إلى أسماء الملفات الكاملة أو عناوين URL. بمزيد من التفاصيل، تمثل هذه المحددات الحزم ، والتي يتم توزيعها عادةً على npm؛ من خلال تحديد اسم الحزمة فقط، فإنهم يطلبون ضمنيًا الوحدة الرئيسية لتلك الحزمة.
الميزة الرئيسية لهذا النظام هي أنه يسمح بالتنسيق السهل عبر النظام البيئي. يمكن لأي شخص كتابة وحدة وتضمين عبارة استيراد باستخدام الاسم المعروف للحزمة، والسماح لوقت تشغيل Node.js أو أدوات وقت البناء الخاصة به بالعناية بترجمتها إلى ملف فعلي على القرص (بما في ذلك معرفة اعتبارات الإصدار).
اليوم، العديد من مطوري الويب يستخدمون بناء جملة الوحدة الأصلية لجافا سكريبت، ولكنهم يجمعونها مع محددات الاستيراد العارية، مما يجعل التعليمات البرمجية الخاصة بهم غير قادرة على التشغيل على الويب دون تعديل مسبق لكل تطبيق. نود حل هذه المشكلة وتقديم هذه الفوائد إلى الويب.
ونوضح مميزات خريطة الاستيراد عبر سلسلة من الأمثلة.
وكما ذكر في المقدمة
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"lodash" : " /node_modules/lodash-es/lodash.js "
}
}
يوفر دعمًا محددًا للاستيراد في كود JavaScript:
import moment from "moment" ;
import ( "lodash" ) . then ( _ => ... ) ;
لاحظ أن الجانب الأيمن من التعيين (المعروف باسم "العنوان") يجب أن يبدأ بـ /
أو ../
أو ./
، أو أن يكون قابلاً للتحليل كعنوان URL مطلق، لتحديد عنوان URL. في حالة العناوين المشابهة لعنوان URL النسبي، يتم حلها بالنسبة إلى عنوان URL الأساسي لخريطة الاستيراد، أي عنوان URL الأساسي للصفحة لخرائط الاستيراد المضمنة، وعنوان URL لمورد خريطة الاستيراد لخرائط الاستيراد الخارجية.
على وجه الخصوص، لن تعمل عناوين URL النسبية "العارية" مثل node_modules/moment/src/moment.js
في هذه المواضع في الوقت الحالي. يتم ذلك كإجراء افتراضي متحفظ، حيث قد نرغب في المستقبل في السماح باستيراد خرائط متعددة، مما قد يغير معنى الجانب الأيمن بطرق تؤثر بشكل خاص على هذه الحالات المجردة.
من الشائع في نظام جافا سكريبت البيئي أن تحتوي الحزمة (بمعنى npm) على وحدات متعددة أو ملفات أخرى. في مثل هذه الحالات، نريد تعيين بادئة في مساحة محدد الوحدة النمطية، على بادئة أخرى في مساحة عنوان URL القابل للجلب.
تقوم خرائط الاستيراد بذلك عن طريق إعطاء معنى خاصًا لمفاتيح التحديد التي تنتهي بشرطة مائلة زائدة. وهكذا، خريطة مثل
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"moment/" : " /node_modules/moment/src/ " ,
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ "
}
}
سيسمح ليس فقط باستيراد الوحدات الرئيسية مثل
import moment from "moment" ;
import _ from "lodash" ;
ولكن أيضًا الوحدات غير الرئيسية، على سبيل المثال
import localeData from "moment/locale/zh-cn.js" ;
import fp from "lodash/fp.js" ;
ومن الشائع أيضًا في النظام البيئي Node.js استيراد الملفات دون تضمين الامتداد. لا نملك ترف تجربة امتدادات ملفات متعددة حتى نجد تطابقًا جيدًا. ومع ذلك، يمكننا محاكاة شيء مماثل باستخدام خريطة الاستيراد. على سبيل المثال،
{
"imports" : {
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ " ,
"lodash/fp" : " /node_modules/lodash-es/fp.js " ,
}
}
لن يسمح فقط import fp from "lodash/fp.js"
، بل سيسمح أيضًا import fp from "lodash/fp"
.
على الرغم من أن هذا المثال يوضح كيف يمكن السماح بعمليات استيراد بدون امتدادات باستخدام خرائط الاستيراد، إلا أنه ليس بالضرورة أمرًا مرغوبًا فيه . يؤدي القيام بذلك إلى تضخم خريطة الاستيراد، ويجعل واجهة الحزمة أقل بساطة - سواء بالنسبة للبشر أو للأدوات.
يمثل هذا الانتفاخ مشكلة خاصة إذا كنت بحاجة إلى السماح بعمليات استيراد بدون امتدادات داخل الحزمة. في هذه الحالة، ستحتاج إلى إدخال خريطة استيراد لكل ملف في الحزمة، وليس فقط نقاط الإدخال ذات المستوى الأعلى. على سبيل المثال، للسماح import "./fp"
من داخل الملف /node_modules/lodash-es/lodash.js
، ستحتاج إلى تعيين إدخال الاستيراد /node_modules/lodash-es/fp
إلى /node_modules/lodash-es/fp.js
. تخيل الآن تكرار هذا لكل ملف تمت الإشارة إليه بدون امتداد.
وعلى هذا النحو، نوصي بالحذر عند استخدام مثل هذه الأنماط في خرائط الاستيراد أو وحدات الكتابة. سيكون الأمر أسهل بالنسبة للنظام البيئي إذا لم نعتمد على خرائط الاستيراد لتصحيح عدم التطابقات المرتبطة بامتدادات الملفات.
كجزء من السماح بإعادة التعيين العام للمحددات، تسمح خرائط الاستيراد على وجه التحديد بإعادة تعيين المحددات المشابهة لعنوان URL، مثل "https://example.com/foo.mjs"
أو "./bar.mjs"
. الاستخدام العملي لذلك هو رسم خرائط للتجزئة، ولكننا نعرض هنا بعض العناصر الأساسية لتوصيل المفهوم:
{
"imports" : {
"https://www.unpkg.com/vue/dist/vue.runtime.esm.js" : " /node_modules/vue/dist/vue.runtime.esm.js "
}
}
تضمن عملية إعادة التعيين هذه أن أي عمليات استيراد لإصدار unpkg.com من Vue (على الأقل على عنوان URL هذا) ستلتقط النسخة من الخادم المحلي بدلاً من ذلك.
{
"imports" : {
"/app/helpers.mjs" : " /app/helpers/index.mjs "
}
}
تضمن عملية إعادة التعيين هذه أن أي عمليات استيراد تشبه عنوان URL والتي يتم حلها إلى /app/helpers.mjs
، بما في ذلك على سبيل المثال import "./helpers.mjs"
من ملفات داخل /app/
، أو import "../helpers.mjs"
من الملفات داخل /app/models
، سيتم تحويله بدلاً من ذلك إلى /app/helpers/index.mjs
. ربما لا تكون هذه فكرة جيدة؛ بدلاً من إنشاء طريقة غير مباشرة تؤدي إلى تشويش التعليمات البرمجية الخاصة بك، يجب عليك بدلاً من ذلك تحديث ملفات المصدر الخاصة بك لاستيراد الملفات الصحيحة. ولكنه مثال مفيد لإظهار إمكانيات استيراد الخرائط.
يمكن أيضًا إجراء إعادة التعيين هذه على أساس مطابقة البادئة، عن طريق إنهاء مفتاح المحدد بشرطة مائلة زائدة:
{
"imports" : {
"https://www.unpkg.com/vue/" : " /node_modules/vue/ "
}
}
يضمن هذا الإصدار أن بيانات الاستيراد للمحددات التي تبدأ بالسلسلة الفرعية "https://www.unpkg.com/vue/"
سيتم تعيينها إلى عنوان URL المقابل أسفل /node_modules/vue/
.
بشكل عام، النقطة المهمة هي أن إعادة التعيين تعمل بنفس الطريقة بالنسبة للواردات المشابهة لعنوان URL كما هي الحال بالنسبة للواردات المجردة. لقد غيرت الأمثلة السابقة دقة المحددات مثل "lodash"
، وبالتالي غيرت معنى import "lodash"
. نحن هنا نغير دقة المحددات مثل "/app/helpers.mjs"
، وبالتالي نغير معنى import "/app/helpers.mjs"
.
لاحظ أن متغير الشرطة المائلة اللاحقة لتعيين المحدد المشابه لعنوان URL يعمل فقط إذا كان المحدد المشابه لعنوان URL يحتوي على مخطط خاص: على سبيل المثال، تعيين "data:text/": "/foo"
لن يؤثر على معنى import "data:text/javascript,console.log('test')"
، ولكن بدلاً من ذلك لن يؤثر إلا على import "data:text/"
.
غالبًا ما يتم منح ملفات البرامج النصية تجزئة فريدة في اسم الملف الخاص بها لتحسين إمكانية التخزين المؤقت. راجع هذه المناقشة العامة حول هذه التقنية، أو هذه المناقشة التي تركز على جافا سكريبت وحزمة الويب.
مع الرسوم البيانية النموذجية، يمكن أن تكون هذه التقنية مشكلة:
خذ بعين الاعتبار رسمًا بيانيًا بسيطًا للوحدة، حيث يعتمد app.mjs
على dep.mjs
الذي يعتمد على sub-dep.mjs
. عادةً، إذا قمت بترقية sub-dep.mjs
أو تغييرها، فمن الممكن أن يظل app.mjs
و dep.mjs
مخبأين، مما يتطلب فقط نقل sub-dep.mjs
الجديد عبر الشبكة.
الآن فكر في نفس الرسم البياني للوحدة، باستخدام أسماء الملفات المجزأة للإنتاج. هناك لدينا عملية البناء الخاصة بنا والتي تولد app-8e0d62a03.mjs
و dep-16f9d819a.mjs
و sub-dep-7be2aa47f.mjs
من الملفات الثلاثة الأصلية.
إذا قمنا بترقية sub-dep.mjs
أو تغييره، فستعمل عملية الإنشاء الخاصة بنا على إعادة إنشاء اسم ملف جديد لإصدار الإنتاج، على سبيل المثال sub-dep-5f47101dc.mjs
. ولكن هذا يعني أننا بحاجة إلى تغيير بيان import
في إصدار الإنتاج من dep.mjs
. يؤدي ذلك إلى تغيير محتوياته، مما يعني أن الإصدار الإنتاجي من dep.mjs
نفسه يحتاج إلى اسم ملف جديد. ولكن هذا يعني أننا بحاجة إلى تحديث بيان import
في إصدار الإنتاج من app.mjs
...
أي أنه مع الرسوم البيانية للوحدات وبيانات import
التي تحتوي على ملفات نصية لأسماء الملفات المجزأة، تصبح التحديثات لأي جزء من الرسم البياني فيروسية لجميع تبعياتها، مما يؤدي إلى فقدان جميع فوائد إمكانية التخزين المؤقت.
توفر خرائط الاستيراد طريقة للخروج من هذه المعضلة، عن طريق فصل محددات الوحدة النمطية التي تظهر في بيانات import
عن عناوين URL الموجودة على الخادم. على سبيل المثال، يمكن أن يبدأ موقعنا بخريطة استيراد مثل
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-7be2aa47f.mjs "
}
}
ومع عبارات الاستيراد التي تكون بالصيغة import "./sub-dep.mjs"
بدلاً من import "./sub-dep-7be2aa47f.mjs"
. الآن، إذا قمنا بتغيير sub-dep.mjs
، فإننا ببساطة نقوم بتحديث خريطة الاستيراد الخاصة بنا:
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-5f47101dc.mjs "
}
}
واترك بيان import "./sub-dep.mjs"
بمفرده. وهذا يعني أن محتويات dep.mjs
لا تتغير، وبالتالي تظل مخزنة مؤقتًا؛ نفس الشيء بالنسبة لـ app.mjs
.
<script>
ملاحظة مهمة حول استخدام خرائط الاستيراد لتغيير معنى محددات الاستيراد هي أنها لا تغير معنى عناوين URL الأولية، مثل تلك التي تظهر في <script src="">
أو <link rel="modulepreload">
. وهذا هو، في ضوء المثال أعلاه، في حين
import "./app.mjs" ;
سيتم إعادة التعيين بشكل صحيح إلى نسخته المجزأة في المتصفحات التي تدعم استيراد الخريطة،
< script type =" module " src =" ./app.mjs " > </ script >
لن يحدث ذلك: في جميع فئات المتصفحات، سيحاول جلب app.mjs
مباشرة، مما يؤدي إلى 404. ما الذي قد ينجح، في المتصفحات التي تدعم استيراد الخريطة، سيكون
< script type =" module " > import "./app.mjs" ; </ script >
غالبًا ما تريد استخدام محدد الاستيراد نفسه للإشارة إلى إصدارات متعددة من مكتبة واحدة، اعتمادًا على من يقوم باستيرادها. يتضمن ذلك إصدارات كل تبعية قيد الاستخدام، ويتجنب جحيم التبعية (منشور مدونة أطول).
نحن ندعم حالة الاستخدام هذه في استيراد الخرائط من خلال السماح لك بتغيير معنى المحدد ضمن نطاق معين:
{
"imports" : {
"querystringify" : " /node_modules/querystringify/index.js "
},
"scopes" : {
"/node_modules/socksjs-client/" : {
"querystringify" : " /node_modules/socksjs-client/querystringify/index.js "
}
}
}
(هذا المثال هو واحد من عدة أمثلة واقعية لإصدارات متعددة لكل تطبيق مقدمة من @zkat. شكرًا، @zkat!)
باستخدام هذا التعيين، داخل أي وحدات تبدأ عناوين URL الخاصة بها بـ /node_modules/socksjs-client/
، سيشير محدد "querystringify"
إلى /node_modules/socksjs-client/querystringify/index.js
. وخلافًا لذلك، فإن تعيين المستوى الأعلى سيضمن أن "querystringify"
يشير إلى /node_modules/querystringify/index.js
.
لاحظ أن التواجد في النطاق لا يغير كيفية حل العنوان؛ لا يزال عنوان URL الأساسي لخريطة الاستيراد مستخدمًا، بدلاً من بادئة عنوان URL للنطاق على سبيل المثال.
"ترث" النطاقات من بعضها البعض بطريقة بسيطة عن عمد، حيث تندمج ولكنها تهيمن مع تقدمها. على سبيل المثال، خريطة الاستيراد التالية:
{
"imports" : {
"a" : " /a-1.mjs " ,
"b" : " /b-1.mjs " ,
"c" : " /c-1.mjs "
},
"scopes" : {
"/scope2/" : {
"a" : " /a-2.mjs "
},
"/scope2/scope3/" : {
"b" : " /b-3.mjs "
}
}
}
سيعطي القرارات التالية:
محدد | المُحيل | عنوان URL الناتج |
---|---|---|
أ | /scope1/foo.mjs | /a-1.mjs |
ب | /scope1/foo.mjs | /ب-1.mjs |
ج | /scope1/foo.mjs | /c-1.mjs |
أ | /scope2/foo.mjs | /a-2.mjs |
ب | /scope2/foo.mjs | /ب-1.mjs |
ج | /scope2/foo.mjs | /c-1.mjs |
أ | /scope2/scope3/foo.mjs | /a-2.mjs |
ب | /scope2/scope3/foo.mjs | /ب-3.mjs |
ج | /scope2/scope3/foo.mjs | /c-1.mjs |
يمكنك تثبيت خريطة استيراد لتطبيقك باستخدام عنصر <script>
، إما مضمنًا أو باستخدام سمة src=""
:
< script type =" importmap " >
{
"imports" : { ... } ,
"scopes" : { ... }
}
</ script >
< script type =" importmap " src =" import-map.importmap " > </ script >
عند استخدام السمة src=""
، يجب أن تحتوي استجابة HTTP الناتجة على نوع MIME application/importmap+json
. (لماذا لا تعيد استخدام application/json
؟ قد يؤدي القيام بذلك إلى تمكين تجاوزات CSP.) مثل البرامج النصية للوحدة، يتم تقديم الطلب مع تمكين CORS، ويتم تفسير الاستجابة دائمًا على أنها UTF-8.
ونظرًا لأنها تؤثر على كافة عمليات الاستيراد، يجب أن تكون أي خرائط استيراد موجودة ويتم جلبها بنجاح قبل إجراء أي تحليل للوحدة النمطية. وهذا يعني أن جلب الرسم البياني للوحدة محظور عند جلب خريطة الاستيراد.
وهذا يعني أنه يوصى بشدة باستخدام النموذج المضمّن لخرائط الاستيراد للحصول على أفضل أداء. وهذا مشابه لأفضل الممارسات المتمثلة في تضمين CSS المهم؛ كلا النوعين من الموارد يمنع تطبيقك من القيام بعمل مهم حتى تتم معالجته، لذا فإن تقديم رحلة ذهاب وإياب للشبكة الثانية (أو حتى ذهابًا وإيابًا لذاكرة التخزين المؤقت على القرص) يعد فكرة سيئة. إذا كنت ترغب في استخدام خرائط الاستيراد الخارجية، فيمكنك محاولة تخفيف هذه العقوبة ذهابًا وإيابًا باستخدام تقنيات مثل HTTP/2 Push أو تبادلات HTTP المجمعة.
كنتيجة أخرى لكيفية تأثير خرائط الاستيراد على جميع الواردات، فإن محاولة إضافة <script type="importmap">
جديد بعد بدء جلب أي رسم بياني للوحدة يعد خطأً. سيتم تجاهل خريطة الاستيراد، وسيطلق العنصر <script>
حدث error
.
في الوقت الحالي، يُسمح بـ <script type="importmap">
واحد فقط على الصفحة. ونحن نخطط لتوسيع هذا في المستقبل، بمجرد معرفة الدلالات الصحيحة للجمع بين خرائط الاستيراد المتعددة. وانظر الحديث في أرقام ١٤ و١٣٧ و١٦٧.
ماذا نفعل في العمال؟ ربما new Worker(someURL, { type: "module", importMap: ... })
؟ أم يجب ضبطه من داخل العامل؟ هل يجب على العاملين المتفانين استخدام خريطة وثيقة التحكم الخاصة بهم، إما بشكل افتراضي أو دائمًا؟ ناقش في رقم 2.
تعني القواعد المذكورة أعلاه أنه يمكنك إنشاء خرائط الاستيراد ديناميكيًا، طالما أنك تفعل ذلك قبل إجراء أي عمليات استيراد. على سبيل المثال:
< script >
const im = document . createElement ( 'script' ) ;
im . type = 'importmap' ;
im . textContent = JSON . stringify ( {
imports : {
'my-library' : Math . random ( ) > 0.5 ? '/my-awesome-library.mjs' : '/my-rad-library.mjs'
}
} ) ;
document . currentScript . after ( im ) ;
</ script >
< script type =" module " >
import 'my-library' ; // will fetch the randomly-chosen URL
</ script >
قد يستخدم مثال أكثر واقعية هذه الإمكانية لتجميع خريطة الاستيراد بناءً على اكتشاف الميزات:
< script >
const importMap = {
imports : {
moment : '/moment.mjs' ,
lodash : someFeatureDetection ( ) ?
'/lodash.mjs' :
'/lodash-legacy-browsers.mjs'
}
} ;
const im = document . createElement ( 'script' ) ;
im . type = 'importmap' ;
im . textContent = JSON . stringify ( importMap ) ;
document . currentScript . after ( im ) ;
</ script >
< script type =" module " >
import _ from "lodash" ; // will fetch the right URL for this browser
</ script >
لاحظ أنه (مثل عناصر <script>
الأخرى) لن ينجح تعديل محتويات <script type="importmap">
بعد إدراجه بالفعل في المستند. ولهذا السبب قمنا بكتابة المثال أعلاه من خلال تجميع محتويات خريطة الاستيراد قبل إنشاء وإدراج <script type="importmap">
.
يعد استيراد الخرائط شيئًا على مستوى التطبيق، يشبه إلى حد ما عمال الخدمة. (بشكل أكثر رسمية، ستكون عبارة عن خريطة لكل وحدة، وبالتالي لكل مجال.) ليس من المفترض أن يتم تأليفها، ولكن بدلاً من ذلك يتم إنتاجها بواسطة إنسان أو أداة ذات رؤية شاملة لتطبيق الويب الخاص بك. على سبيل المثال، لن يكون من المنطقي أن تقوم المكتبة بتضمين خريطة استيراد؛ يمكن للمكتبات ببساطة الإشارة إلى الوحدات حسب المحدد، والسماح للتطبيق بتحديد عناوين URL التي تعينها هذه المحددات.
هذا، بالإضافة إلى البساطة العامة، هو جزئيًا ما يحفز القيود المذكورة أعلاه على <script type="importmap">
.
نظرًا لأن خريطة استيراد التطبيق تغير خوارزمية الدقة لكل وحدة نمطية في خريطة الوحدة النمطية، فإنها لا تتأثر بما إذا كان النص المصدر للوحدة النمطية في الأصل من عنوان URL مشترك الأصل. إذا قمت بتحميل وحدة نمطية من CDN تستخدم محددات استيراد مجردة، فستحتاج إلى معرفة محددات الاستيراد العارية التي تضيفها الوحدة إلى تطبيقك مسبقًا، وتضمينها في خريطة استيراد تطبيقك. (أي أنك تحتاج إلى معرفة كل التبعيات المتعدية لتطبيقك.) من المهم أن يظل التحكم في عناوين URL المستخدمة لكل حزمة مع مؤلف التطبيق، حتى يتمكن من إدارة إصدار الوحدات ومشاركتها بشكل كلي.
تحتوي معظم المتصفحات على محلل HTML تخميني يحاول اكتشاف الموارد المعلنة في ترميز HTML بينما ينتظر محلل HTML جلب البرامج النصية المحظورة وتنفيذها. لم يتم تحديد هذا بعد، على الرغم من وجود جهود مستمرة للقيام بذلك في Whatwg/html#5959. يناقش هذا القسم بعض التفاعلات المحتملة التي يجب أن تكون على دراية بها.
أولاً، لاحظ أنه على الرغم من أنه على حد علمنا لا توجد متصفحات تفعل ذلك حاليًا، إلا أنه سيكون من الممكن للمحلل التخميني جلب https://example.com/foo.mjs
في المثال التالي، أثناء انتظار البرنامج النصي للحظر https://example.com/blocking-1.js
:
<!DOCTYPE html >
<!-- This file is https://example.com/ -->
< script src =" blocking-1.js " > </ script >
< script type =" module " >
import "./foo.mjs" ;
</ script >
وبالمثل، يمكن للمتصفح جلب https://example.com/foo.mjs
و https://example.com/bar.mjs
بشكل تخميني في المثال التالي، عن طريق تحليل خريطة الاستيراد كجزء من عملية التحليل التخميني:
<!DOCTYPE html >
<!-- This file is https://example.com/ -->
< script src =" blocking-2.js " > </ script >
< script type =" importmap " >
{
"imports" : {
"foo" : "./foo.mjs" ,
"https://other.example/bar.mjs" : "./bar.mjs"
}
}
</ script >
< script type =" module " >
import "foo" ;
import "https://other.example/bar.mjs" ;
</ script >
أحد التفاعلات التي يجب ملاحظتها هنا هو أن المتصفحات التي تقوم بتحليل وحدات JS المضمنة بشكل تخميني، ولكنها لا تدعم خرائط الاستيراد، من المحتمل أن تتكهن بشكل غير صحيح لهذا المثال: قد تقوم بجلب https://other.example/bar.mjs
بشكل تخميني، بدلاً من https://example.com/bar.mjs
تم تعيينه إليه.
وبشكل أكثر عمومية، يمكن أن تتعرض المضاربات القائمة على خرائط الاستيراد لنفس النوع من الأخطاء التي تتعرض لها المضاربات الأخرى. على سبيل المثال، إذا كانت محتويات blocking-1.js
const el = document . createElement ( "base" ) ;
el . href = "/subdirectory/" ;
document . currentScript . after ( el ) ;
ثم سيتم إهدار الجلب التخميني لـ https://example.com/foo.mjs
في مثال خريطة عدم الاستيراد، لأنه بحلول الوقت المناسب لإجراء التقييم الفعلي للوحدة، سنعيد حساب المحدد النسبي "./foo.mjs"
وأدرك أن المطلوب فعليًا هو https://example.com/subdirectory/foo.mjs
.
وبالمثل بالنسبة لحالة خريطة الاستيراد، إذا كانت محتويات blocking-2.js
كذلك
document . write ( `<script type="importmap">
{
"imports": {
"foo": "./other-foo.mjs",
"https://other.example/bar.mjs": "./other-bar.mjs"
}
}
</script>` ) ;
ثم سيتم إهدار عمليات الجلب التخمينية لـ https://example.com/foo.mjs
و https://example.com/bar.mjs
، حيث ستكون خريطة الاستيراد المكتوبة حديثًا سارية المفعول بدلاً من تلك التي تمت رؤيتها مضمنة في HTML.
<base>
عند وجود عنصر <base>
في المستند، يتم تحويل جميع عناوين URL والمحددات المشابهة لعنوان URL في خريطة الاستيراد إلى عناوين URL مطلقة باستخدام href
from <base>
.
< base href =" https://www.unpkg.com/vue/dist/ " >
< script type =" importmap " >
{
"imports" : {
"vue" : "./vue.runtime.esm.js" ,
}
}
</ script >
< script >
import ( "vue" ) ; // resolves to https://www.unpkg.com/vue/dist/vue.runtime.esm.js
</ script >
إذا كان المتصفح يدعم طريقة دعم (النوع) الخاصة بـ HTMLScriptElement، فيجب أن يُرجع HTMLScriptElement.supports('importmap')
صحيحًا.
if ( HTMLScriptElement . supports && HTMLScriptElement . supports ( 'importmap' ) ) {
console . log ( 'Your browser supports import maps.' ) ;
}
على عكس Node.js، لا نتمتع في المتصفح برفاهية وجود نظام ملفات سريع بشكل معقول يمكننا الزحف إليه بحثًا عن الوحدات النمطية. وبالتالي، لا يمكننا تنفيذ خوارزمية تحليل وحدة العقدة مباشرة؛ قد يتطلب الأمر إجراء عدة رحلات ذهابًا وإيابًا للخادم لكل بيان import
، مما يؤدي إلى إهدار النطاق الترددي والوقت مع استمرارنا في الحصول على 404s. نحتاج إلى التأكد من أن كل عبارة import
تؤدي إلى طلب HTTP واحد فقط؛ وهذا يتطلب قدرا من الحساب المسبق.
اقترح البعض تخصيص خوارزمية تحليل الوحدة النمطية للمتصفح باستخدام ربط JavaScript لتفسير كل محدد وحدة نمطية.
ولسوء الحظ، هذا أمر قاتل للأداء؛ يؤدي القفز إلى JavaScript والخروج منه لكل حافة من الرسم البياني للوحدة إلى إبطاء بدء تشغيل التطبيق بشكل كبير. (تحتوي تطبيقات الويب النموذجية على آلاف الوحدات، مع 3-4× هذا العدد من عبارات الاستيراد.) يمكنك تخيل العديد من عمليات التخفيف، مثل تقييد الاستدعاءات بمحددات الاستيراد العارية فقط أو مطالبة الخطاف بأخذ دفعات من المحددات و إرجاع دفعات من عناوين URL، ولكن في النهاية لا شيء يتفوق على الحساب المسبق.
هناك مشكلة أخرى في هذا وهي أنه من الصعب تخيل خوارزمية رسم خرائط مفيدة يمكن لمطور الويب كتابتها، حتى لو تم إعطاؤه هذا الرابط. لدى Node.js واحدة، ولكنها تعتمد على الزحف المتكرر إلى نظام الملفات والتحقق من وجود الملفات؛ كما ناقشنا أعلاه، هذا غير ممكن على الويب. الموقف الوحيد الذي قد تكون فيه الخوارزمية العامة مجدية هو إذا (أ) لم تكن بحاجة مطلقًا إلى تخصيص كل رسم بياني فرعي، أي وجود إصدار واحد فقط من كل وحدة في تطبيقك؛ (ب) تمكنت الأدوات من ترتيب الوحدات النمطية الخاصة بك مسبقًا بطريقة موحدة يمكن التنبؤ بها، بحيث تصبح الخوارزمية على سبيل المثال "return /js/${specifier}.js
". لكن إذا كنا في هذا العالم على أي حال، فإن الحل التصريحي سيكون أبسط.
أحد الحلول المستخدمة اليوم (على سبيل المثال في unpkg CDN عبر babel-plugin-unpkg) هو إعادة كتابة جميع محددات الاستيراد العارية إلى عناوين URL المطلقة المناسبة لها مسبقًا، باستخدام أدوات البناء. يمكن القيام بذلك أيضًا في وقت التثبيت، بحيث عندما تقوم بتثبيت حزمة باستخدام npm، فإنها تعيد كتابة محتويات الحزمة تلقائيًا لاستخدام عناوين URL المطلقة أو النسبية بدلاً من محددات الاستيراد المجردة.
المشكلة في هذا الأسلوب هي أنه لا يعمل مع import()
لأنه من المستحيل تحليل السلاسل التي تم تمريرها إلى تلك الوظيفة بشكل ثابت. يمكنك إدخال إصلاح، على سبيل المثال، يغير كل مثيل لـ import(x)
في import(specifierToURL(x, import.meta.url))
، حيث تكون specifierToURL
وظيفة أخرى تم إنشاؤها بواسطة أداة الإنشاء. ولكن في النهاية، يعد هذا تجريدًا متسربًا إلى حد ما، والدالة specifierToURL
تكرر إلى حد كبير عمل هذا الاقتراح على أي حال.
للوهلة الأولى، يبدو أن عمال الخدمة هم المكان المناسب للقيام بهذا النوع من ترجمة الموارد. لقد تحدثنا في الماضي عن إيجاد طريقة ما لتمرير المحدد مع حدث جلب عامل الخدمة، مما يسمح له بتقديم Response
المناسبة.
ومع ذلك، لا يتوفر عمال الخدمة عند التحميل الأول . وبالتالي، لا يمكن أن تكون جزءًا من البنية التحتية الحيوية المستخدمة لتحميل الوحدات. لا يمكن استخدامها إلا كتعزيز تقدمي فوق عمليات الجلب التي ستعمل بشكل عام.
إذا كان لديك تطبيقات بسيطة دون الحاجة إلى تحليل التبعيات المحددة النطاق، ولديك أداة تثبيت حزمة مريحة في إعادة كتابة المسارات على القرص داخل الحزمة (على عكس الإصدارات الحالية من npm)، فيمكنك الابتعاد عن طريق رسم خرائط أبسط بكثير. على سبيل المثال، إذا قامت أداة التثبيت الخاصة بك بإنشاء قائمة ثابتة للنموذج
node_modules_flattened/
lodash/
index.js
core.js
fp.js
moment/
index.js
html-to-dom/
index.js
ثم المعلومات الوحيدة التي تحتاجها هي
/node_modules_flattened/
)index.js
)يمكنك أن تتخيل تنسيق تكوين استيراد الوحدة النمطية الذي يحدد هذه الأشياء فقط، أو حتى مجموعة فرعية فقط (إذا وضعنا افتراضات للآخرين).
لا تعمل هذه الفكرة مع التطبيقات الأكثر تعقيدًا والتي تحتاج إلى حل محدد النطاق، لذلك نعتقد أن اقتراح خريطة الاستيراد الكاملة ضروري. ولكنه يظل جذابًا للتطبيقات البسيطة، ونتساءل عما إذا كانت هناك طريقة لجعل الاقتراح يشتمل أيضًا على وضع سهل لا يتطلب سرد جميع الوحدات، ولكنه يعتمد بدلاً من ذلك على الاتفاقيات والأدوات لضمان الحد الأدنى من رسم الخرائط. ناقش في رقم 7.
لقد ظهر عدة مرات الآن أن الأشخاص يرغبون في توفير البيانات الوصفية لكل وحدة؛ على سبيل المثال، بيانات تعريف التكامل، أو خيارات الجلب. على الرغم من أن البعض قد اقترح القيام بذلك باستخدام بيان الاستيراد، إلا أن الدراسة المتأنية للخيارات تؤدي إلى تفضيل ملف البيان خارج النطاق.
يمكن أن تكون خريطة الاستيراد هي ملف البيان هذا. ومع ذلك، قد لا يكون الخيار الأفضل، وذلك لعدة أسباب:
كما هو متصور حاليًا، لن تحتوي معظم الوحدات النمطية في التطبيق على إدخالات في خريطة الاستيراد. حالة الاستخدام الرئيسية مخصصة للوحدات التي تحتاج إلى الرجوع إليها بواسطة محددات مجردة، أو الوحدات التي تحتاج إلى القيام بشيء صعب مثل polyfilling أو Virtualization. إذا تصورنا وجود كل وحدة في الخريطة، فلن نقوم بتضمين ميزات ملائمة مثل الحزم عبر الشرطة المائلة اللاحقة.
جميع البيانات الوصفية المقترحة حتى الآن قابلة للتطبيق على أي نوع من الموارد، وليس فقط وحدات JavaScript. ربما يجب أن يعمل الحل على مستوى أكثر عمومية.
من الطبيعي أن تظهر عدة <script type="importmap">
على الصفحة، تمامًا كما يمكن أن تظهر عدة <script>
من الأنواع الأخرى. ونود تمكين هذا في المستقبل.
التحدي الأكبر هنا هو تحديد كيفية إنشاء خرائط الاستيراد المتعددة. بمعنى أنه في ضوء خريطتي استيراد يعيد كلاهما تعيين نفس عنوان URL، أو تعريفين للنطاق يغطيان نفس مساحة بادئة عنوان URL، ما هو التأثير الذي يجب أن يكون على الصفحة؟ المرشح الرئيسي الحالي هو الدقة المتتالية، التي تعيد صياغة خرائط الاستيراد من كونها محدد استيراد ← تعيينات URL، لتكون بدلاً من ذلك سلسلة متتالية من محدد الاستيراد ← تعيينات محدد الاستيراد، وفي النهاية تصل إلى القاع في "محدد استيراد قابل للجلب" (عنوان URL بشكل أساسي).
انظر هذه القضايا المفتوحة لمزيد من المناقشة.
ترغب بعض حالات الاستخدام في إيجاد طريقة لقراءة أو معالجة خريطة استيراد المجال من البرنامج النصي، بدلاً من إدراج عناصر <script type="importmap">
التعريفية. اعتبره "نموذج كائن استيراد خريطة"، مشابهًا لنموذج كائن CSS الذي يسمح للشخص بمعالجة قواعد CSS التعريفية عادةً للصفحة.
تكمن التحديات هنا في كيفية التوفيق بين خرائط الاستيراد التعريفية وأي تغييرات برمجية، بالإضافة إلى الوقت الذي يمكن أن تعمل فيه واجهة برمجة التطبيقات هذه في دورة حياة الصفحة. وبشكل عام، تكون التصميمات الأبسط أقل قوة وقد تلبي حالات استخدام أقل.
راجع هذه المشكلات المفتوحة لمزيد من المناقشة وحالات الاستخدام حيث يمكن أن تساعد واجهة برمجة التطبيقات الآلية.
import.meta.resolve()
تسمح وظيفة import.meta.resolve(specifier)
المقترحة للبرامج النصية للوحدة بحل محددات الاستيراد إلى عناوين URL في أي وقت. راجع Whatwg/html#5572 للمزيد. يرتبط هذا باستيراد الخرائط لأنه يسمح لك بحل الموارد "النسبية للحزمة"، على سبيل المثال
const url = import . meta . resolve ( "somepackage/resource.json" ) ;
سيمنحك الموقع المعين بشكل مناسب لـ resource.json
داخل somepackage/
مساحة الاسم التي تتحكم فيها خريطة استيراد الصفحة.
لقد عمل العديد من أعضاء المجتمع على عمليات التعبئة المتعددة والأدوات المتعلقة باستيراد الخرائط. فيما يلي ما نعرفه عن:
package.json
و node_modules/
.package.json
.<script type="systemjs-importmap">
.لا تتردد في إرسال طلب سحب مع المزيد! يمكنك أيضًا استخدام رقم 146 في متتبع المشكلات للمناقشة حول هذه المساحة.
نشأت هذه الوثيقة بعد سباق سريع استغرق يومًا كاملاً شارك فيه @domenic و@hiroshige-g و@justinfagnani و@MylesBorins و@nyaxt. منذ ذلك الحين، لعب @guybedford دورًا فعالًا في وضع النماذج الأولية ودفع المناقشات حول هذا الاقتراح إلى الأمام.
شكرًا أيضًا لجميع المساهمين في أداة تعقب المشكلات لمساعدتهم في تطوير الاقتراح!