وحدات جافا سكريبت: دليل المبتدئين

إذا كنت وافدًا جديدًا إلى JavaScript ، فإن المصطلحات مثل "مجمعات الوحدات مقابل برامج تحميل الوحدات" و "Webpack مقابل Browserify" و "AMD مقابل CommonJS" يمكن أن تصبح مربكة بسرعة.

قد يكون نظام وحدات جافا سكريبت مخيفًا ، لكن فهمه أمر حيوي لمطوري الويب.

في هذا المنشور ، سأقوم بفك هذه العبارات الطنانة لك بلغة إنجليزية بسيطة (وبعض نماذج الأكواد). أتمنى أن تجدها مفيدة!

ملاحظة: من أجل التبسيط ، سيتم تقسيم هذا إلى قسمين: الجزء 1 سوف يتعمق في شرح ماهية الوحدات ولماذا نستخدمها. سيتناول الجزء 2 (المنشور الأسبوع المقبل) ما يعنيه تجميع الوحدات والطرق المختلفة للقيام بذلك.

الجزء 1: هل يمكن لأي شخص أن يشرح ما هي الوحدات مرة أخرى؟

يقسم المؤلفون الجيدون كتبهم إلى فصول وأقسام ؛ يقسم المبرمجون الجيدون برامجهم إلى وحدات.

مثل فصل الكتاب ، فإن الوحدات النمطية هي مجرد مجموعات من الكلمات (أو رمز ، حسب الحالة).

ومع ذلك ، فإن الوحدات الجيدة تكون مكتفية ذاتيًا إلى حد كبير مع وظائف مميزة ، مما يسمح بتبديلها أو إزالتها أو إضافتها حسب الضرورة ، دون تعطيل النظام ككل.

لماذا نستخدم الوحدات؟

هناك الكثير من الفوائد لاستخدام الوحدات لصالح قاعدة كود مترامية الأطراف ومترابطة. وأهمها برأيي:

1) قابلية الصيانة: بحكم التعريف ، تكون الوحدة النمطية قائمة بذاتها. تهدف الوحدة المصممة جيدًا إلى تقليل التبعيات على أجزاء من قاعدة البيانات قدر الإمكان ، بحيث يمكن أن تنمو وتتحسن بشكل مستقل. يعد تحديث وحدة واحدة أسهل بكثير عندما يتم فصل الوحدة عن أجزاء أخرى من التعليمات البرمجية.

بالعودة إلى مثال كتابنا ، إذا كنت تريد تحديث فصل في كتابك ، فسيكون كابوسًا إذا تطلب تغيير بسيط في أحد الفصول تعديل كل فصل آخر أيضًا. بدلاً من ذلك ، قد ترغب في كتابة كل فصل بطريقة يمكن من خلالها إجراء التحسينات دون التأثير على الفصول الأخرى.

2) Namespacing: في JavaScript ، المتغيرات خارج نطاق وظيفة المستوى الأعلى عالمية (بمعنى ، يمكن للجميع الوصول إليها). لهذا السبب ، من الشائع وجود "تلوث مساحة الاسم" ، حيث تشترك التعليمات البرمجية غير ذات الصلة تمامًا في المتغيرات العالمية.

تعد مشاركة المتغيرات العالمية بين التعليمات البرمجية غير ذات الصلة أمرًا مهمًا في التطوير.

كما سنرى لاحقًا في هذا المنشور ، تسمح لنا الوحدات النمطية بتجنب تلوث مساحة الأسماء من خلال إنشاء مساحة خاصة لمتغيراتنا.

3) قابلية إعادة الاستخدام: لنكن صادقين هنا: لقد قمنا جميعًا بنسخ كود كتبناه سابقًا في مشاريع جديدة في وقت أو آخر. على سبيل المثال ، دعنا نتخيل أنك نسخت بعض طرق المرافق التي كتبتها من مشروع سابق إلى مشروعك الحالي.

كل هذا جيد وجيد ، ولكن إذا وجدت طريقة أفضل لكتابة جزء من هذا الرمز ، فسيتعين عليك الرجوع وتذكر تحديثه في أي مكان آخر كتبته فيه.

من الواضح أن هذا مضيعة كبيرة للوقت. ألن يكون الأمر أسهل بكثير إذا كان هناك - انتظر - وحدة يمكننا إعادة استخدامها مرارًا وتكرارًا؟

كيف يمكنك دمج الوحدات؟

هناك العديد من الطرق لدمج الوحدات في برامجك. دعنا نتصفح القليل منهم:

نمط الوحدة

يتم استخدام نمط الوحدة النمطية لتقليد مفهوم الفئات (نظرًا لأن JavaScript لا يدعم الفئات أصلاً) حتى نتمكن من تخزين كل من الأساليب والمتغيرات العامة والخاصة داخل كائن واحد - على غرار كيفية استخدام الفئات في لغات البرمجة الأخرى مثل Java أو بايثون. يتيح لنا ذلك إنشاء واجهة برمجة تطبيقات عامة للطرق التي نريد كشفها للعالم ، مع الاستمرار في تغليف المتغيرات والطرق الخاصة في نطاق الإغلاق.

هناك عدة طرق لإنجاز نمط الوحدة. في هذا المثال الأول ، سأستخدم إغلاق مجهول. سيساعدنا ذلك في تحقيق هدفنا من خلال وضع جميع التعليمات البرمجية في وظيفة مجهولة. (تذكر: في JavaScript ، الوظائف هي الطريقة الوحيدة لإنشاء نطاق جديد.)

مثال 1: إغلاق مجهول

(function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); }()); // ‘You failed 2 times.’

مع هذا التركيب ، فإن وظيفتنا المجهولة لها بيئة التقييم الخاصة بها أو "الإغلاق" ، ثم نقوم بتقييمها على الفور. يتيح لنا هذا إخفاء المتغيرات من مساحة الاسم الأصل (العمومية).

ما هو لطيف في هذا النهج هو أنه يمكنك استخدام المتغيرات المحلية داخل هذه الوظيفة دون الكتابة فوق المتغيرات العامة الموجودة بطريق الخطأ ، مع الاستمرار في الوصول إلى المتغيرات العامة ، مثل:

var global = 'Hello, I am a global variable :)'; (function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); console.log(global); }()); // 'You failed 2 times.' // 'Hello, I am a global variable :)'

لاحظ أن الأقواس حول الوظيفة المجهولة مطلوبة ، لأن العبارات التي تبدأ بوظيفة الكلمة الأساسية تعتبر دائمًا إعلانات دالة (تذكر ، لا يمكنك الحصول على تعريفات دالة غير مسماة في JavaScript.) وبالتالي ، فإن الأقواس المحيطة تنشئ تعبير دالة في حين أن. إذا كنت تشعر بالفضول ، يمكنك قراءة المزيد هنا.

مثال 2: استيراد عالمي

نهج شائع آخر تستخدمه المكتبات مثل jQuery هو الاستيراد العالمي. إنه مشابه للإغلاق المجهول الذي رأيناه للتو ، إلا أننا نمرر الآن في الكرات الأرضية كمعلمات:

(function (globalVariable) { // Keep this variables private inside this closure scope var privateFunction = function() { console.log('Shhhh, this is private!'); } // Expose the below methods via the globalVariable interface while // hiding the implementation of the method within the // function() block globalVariable.each = function(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }; globalVariable.filter = function(collection, test) { var filtered = []; globalVariable.each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered; }; globalVariable.map = function(collection, iterator) { var mapped = []; globalUtils.each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped; }; globalVariable.reduce = function(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; globalVariable.each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator; }; }(globalVariable)); 

في هذا المثال ، globalVariable هو المتغير الوحيد العام. تكمن فائدة هذا الأسلوب على عمليات الإغلاق المجهولة في أنك تعلن عن المتغيرات العالمية مقدمًا ، مما يجعلها واضحة تمامًا للأشخاص الذين يقرؤون التعليمات البرمجية الخاصة بك.

مثال 3: واجهة الكائن

هناك طريقة أخرى تتمثل في إنشاء وحدات باستخدام واجهة كائن قائمة بذاتها ، مثل:

var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; // Expose these functions via an interface while hiding // the implementation of the module within the function() block return { average: function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }, failing: function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; } } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

كما ترون، فإن هذا النهج يتيح لنا أن تقرر ما المتغيرات / أساليب نريد الاحتفاظ بها (على سبيل المثال الخاص myGrades ) وما المتغيرات / الأساليب التي تريد كشف عن طريق وضعها في بيان عودة (على سبيل المثال المتوسط و الفشل ).

مثال 4: الكشف عن نمط الوحدة

هذا مشابه جدًا للنهج أعلاه ، إلا أنه يضمن الحفاظ على خصوصية جميع الطرق والمتغيرات حتى يتم الكشف عنها صراحة:

var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }; var failing = function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; }; // Explicitly reveal public pointers to the private functions // that we want to reveal publicly return { average: average, failing: failing } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

قد يبدو هذا كثيرًا ، لكنه مجرد غيض من فيض عندما يتعلق الأمر بأنماط الوحدة. فيما يلي بعض الموارد التي وجدتها مفيدة في استكشافاتي الخاصة:

  • تعلم أنماط تصميم JavaScript بواسطة Addy Osmani: كنز دفين من التفاصيل في قراءة موجزة بشكل مثير للإعجاب
  • جيد بشكل مناسب من إعداد Ben Cherry: نظرة عامة مفيدة مع أمثلة على الاستخدام المتقدم لنمط الوحدة
  • مدونة Carl Danley: نظرة عامة على نمط الوحدة وموارد لأنماط JavaScript أخرى.

CommonJS و AMD

تشترك الأساليب السابقة في شيء واحد: استخدام متغير عالمي واحد لالتفاف رمزه في دالة ، وبالتالي إنشاء مساحة اسم خاصة لنفسه باستخدام نطاق إغلاق.

في حين أن كل نهج فعال بطريقته الخاصة ، إلا أن له سلبياته.

أولاً ، بصفتك مطورًا ، تحتاج إلى معرفة ترتيب التبعية الصحيح لتحميل ملفاتك. على سبيل المثال ، لنفترض أنك تستخدم Backbone في مشروعك ، لذلك قمت بتضمين علامة البرنامج النصي لشفرة مصدر Backbone في ملفك.

ومع ذلك ، نظرًا لأن Backbone له تبعية شديدة على Underscore.js ، فلا يمكن وضع علامة البرنامج النصي لملف Backbone قبل ملف Underscore.js.

بصفتك مطورًا ، يمكن أن تكون إدارة التبعيات وتصحيح هذه الأشياء في بعض الأحيان مشكلة.

الجانب السلبي الآخر هو أنه لا يزال من الممكن أن تؤدي إلى تصادمات مساحة الاسم. على سبيل المثال ، ماذا لو كانت اثنتان من الوحدات الخاصة بك لها نفس الاسم؟ أو ماذا لو كان لديك نسختان من الوحدة وتحتاج كلاهما؟

لذا ربما تتساءل: هل يمكننا تصميم طريقة لطلب واجهة وحدة نمطية دون المرور بالنطاق العالمي؟

لحسن الحظ، فإن الجواب هو نعم.

هناك نهجان شائعان ومُنفذان جيدًا: CommonJS و AMD.

CommonJS

CommonJS هي مجموعة عمل تطوعية تصمم وتنفذ واجهات برمجة تطبيقات JavaScript لإعلان الوحدات النمطية.

وحدة CommonJS هي في الأساس جزء قابل لإعادة الاستخدام من JavaScript والذي يصدر كائنات معينة ، مما يجعلها متاحة للوحدات النمطية الأخرى التي تتطلبها في برامجها. إذا قمت بالبرمجة في Node.js ، فستكون على دراية بهذا التنسيق.

باستخدام CommonJS ، يخزن كل ملف JavaScript وحدات في سياق الوحدة الفريد الخاص به (تمامًا مثل تغليفه في الإغلاق). في هذا المجال، ونحن نستخدم module.exports الاعتراض على فضح وحدات، و يتطلب استيرادها.

عندما تقوم بتعريف وحدة CommonJS ، فقد تبدو كما يلي:

function myModule() { this.hello = function() { return 'hello!'; } this.goodbye = function() { return 'goodbye!'; } } module.exports = myModule;

We use the special object module and place a reference of our function into module.exports. This lets the CommonJS module system know what we want to expose so that other files can consume it.

Then when someone wants to use myModule, they can require it in their file, like so:

var myModule = require('myModule'); var myModuleInstance = new myModule(); myModuleInstance.hello(); // 'hello!' myModuleInstance.goodbye(); // 'goodbye!'

There are two obvious benefits to this approach over the module patterns we discussed before:

1. Avoiding global namespace pollution

2. Making our dependencies explicit

Moreover, the syntax is very compact, which I personally love.

Another thing to note is that CommonJS takes a server-first approach and synchronously loads modules. This matters because if we have three other modules we need to require, it’ll load them one by one.

Now, that works great on the server but, unfortunately, makes it harder to use when writing JavaScript for the browser. Suffice it to say that reading a module from the web takes a lot longer than reading from disk. For as long as the script to load a module is running, it blocks the browser from running anything else until it finishes loading. It behaves this way because the JavaScript thread stops until the code has been loaded. (I’ll cover how we can work around this issue in Part 2 when we discuss module bundling. For now, that’s all we need to know).

AMD

CommonJS is all well and good, but what if we want to load modules asynchronously? The answer is called Asynchronous Module Definition, or AMD for short.

Loading modules using AMD looks something like this:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) { console.log(myModule.hello()); });

What’s happening here is that the define function takes as its first argument an array of each of the module’s dependencies. These dependencies are loaded in the background (in a non-blocking manner), and once loaded define calls the callback function it was given.

Next, the callback function takes, as arguments, the dependencies that were loaded — in our case, myModule and myOtherModule — allowing the function to use these dependencies. Finally, the dependencies themselves must also be defined using the define keyword.

For example, myModule might look like this:

define([], function() { return { hello: function() { console.log('hello'); }, goodbye: function() { console.log('goodbye'); } }; });

So again, unlike CommonJS, AMD takes a browser-first approach alongside asynchronous behavior to get the job done. (Note, there are a lot of people who strongly believe that dynamically loading files piecemeal as you start to run code isn’t favorable, which we’ll explore more when in the next section on module-building).

Aside from asynchronicity, another benefit of AMD is that your modules can be objects, functions, constructors, strings, JSON and many other types, while CommonJS only supports objects as modules.

That being said, AMD isn’t compatible with io, filesystem, and other server-oriented features available via CommonJS, and the function wrapping syntax is a bit more verbose compared to a simple require statement.

UMD

For projects that require you to support both AMD and CommonJS features, there’s yet another format: Universal Module Definition (UMD).

UMD essentially creates a way to use either of the two, while also supporting the global variable definition. As a result, UMD modules are capable of working on both client and server.

Here’s a quick taste of how UMD goes about its business:

(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['myModule', 'myOtherModule'], factory); } else if (typeof exports === 'object') { // CommonJS module.exports = factory(require('myModule'), require('myOtherModule')); } else { // Browser globals (Note: root is window) root.returnExports = factory(root.myModule, root.myOtherModule); } }(this, function (myModule, myOtherModule) { // Methods function notHelloOrGoodbye(){}; // A private method function hello(){}; // A public method because it's returned (see below) function goodbye(){}; // A public method because it's returned (see below) // Exposed public methods return { hello: hello, goodbye: goodbye } }));

For more examples of UMD formats, check out this enlightening repo on GitHub.

Native JS

Phew! Are you still around? I haven’t lost you in the woods here? Good! Because we have *one more* type of module to define before we’re done.

As you probably noticed, none of the modules above were native to JavaScript. Instead, we’ve created ways to emulate a modules system by using either the module pattern, CommonJS or AMD.

Fortunately, the smart folks at TC39 (the standards body that defines the syntax and semantics of ECMAScript) have introduced built-in modules with ECMAScript 6 (ES6).

ES6 offers up a variety of possibilities for importing and exporting modules which others have done a great job explaining — here are a few of those resources:

  • jsmodules.io
  • exploringjs.com

What’s great about ES6 modules relative to CommonJS or AMD is how it manages to offer the best of both worlds: compact and declarative syntax and asynchronous loading, plus added benefits like better support for cyclic dependencies.

Probably my favorite feature of ES6 modules is that imports are live read-only views of the exports. (Compare this to CommonJS, where imports are copies of exports and consequently not alive).

Here’s an example of how that works:

// lib/counter.js var counter = 1; function increment() { counter++; } function decrement() { counter--; } module.exports = { counter: counter, increment: increment, decrement: decrement }; // src/main.js var counter = require('../../lib/counter'); counter.increment(); console.log(counter.counter); // 1

In this example, we basically make two copies of the module: one when we export it, and one when we require it.

Moreover, the copy in main.js is now disconnected from the original module. That’s why even when we increment our counter it still returns 1 — because the counter variable that we imported is a disconnected copy of the counter variable from the module.

So, incrementing the counter will increment it in the module, but won’t increment your copied version. The only way to modify the copied version of the counter variable is to do so manually:

counter.counter++; console.log(counter.counter); // 2

On the other hand, ES6 creates a live read-only view of the modules we import:

// lib/counter.js export let counter = 1; export function increment() { counter++; } export function decrement() { counter--; } // src/main.js import * as counter from '../../counter'; console.log(counter.counter); // 1 counter.increment(); console.log(counter.counter); // 2

Cool stuff, huh? What I find really compelling about live read-only views is how they allow you to split your modules into smaller pieces without losing functionality.

ثم يمكنك الالتفاف ودمجهم مرة أخرى ، لا مشكلة. إنها فقط "تعمل".

استشراف المستقبل: تجميع الوحدات

نجاح باهر! أين يذهب الوقت؟ كانت هذه رحلة برية ، لكنني آمل بصدق أن تمنحك فهمًا أفضل للوحدات في JavaScript.

في القسم التالي ، سأتناول تجميع الوحدات ، والتي تغطي الموضوعات الأساسية بما في ذلك:

  • لماذا نقوم بتجميع الوحدات
  • مناهج مختلفة للتجميع
  • واجهة برمجة تطبيقات محمل وحدة ECMAScript's
  • …و اكثر. :)

ملاحظة: لإبقاء الأمور بسيطة ، تخطيت بعض التفاصيل الدقيقة (فكر في: التبعيات الدورية) في هذا المنشور. إذا تركت أي شيء مهم و / أو رائع ، فيرجى إخبارنا بذلك في التعليقات!