اساس سیستم ماژول JavaScript را یاد گرفته، و کتابخانه شخصی خود را بسازید

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 06 شهریور 1397
دسته بندی ها : جاوا اسکریپت

اخیرا، همگی درباره ماژول‌های JavaScript می‌شنویم. تقریبا همه ما در تعجبیم که با آن‌ها چه کار می‌توانیم انجام دهیم و آن‌ها چه نقشی می‌توانند در زندگی ما داشته باشند؟

سیستم ماژول JavaScript چیست؟

همینطور که توسعه JavaScript وسیع‌تر می‌شود، مدیریت namespaceها و Dependencyها سخت‌تر می‌شود. راه حل‌های مختلفی برای رسیدگی به این مشکل در قالب سیستم‌های ماژول توسعه داده شده‌اند.

چرا درک سیستم ماژول JavaScript مهم است؟

کار روزمره من، طراحی و معماری پروژه‌ها است، و من سریعا پی بردم که تعداد زیادی عملکرد مشترک در میان پروژه‌های مختلف مورد نیاز هستند. ما همیشه در حال کپی و پیست کردن این عملکردها به پروژه‌های جدید هستیم.

اما مشکل در اینجاست که هر زمان یه تکه از کد تغییر می‌کند، باید به صورت دستی آن تغییرات را بین تمام آبجکت‌ها اعمال کنیم. برای جلوگیری از این عملیات‌های خسته کننده، تصمیم گرفتم که این عملکردهای رایج را استخراج کرده، و یک پکیج npm از آن‌ها بسازیم. به این صورت، باقی اعضای تیم می‌توانستند از آن‌ها به عنوان Dependency استفاده کنند و هر زمان که یک نسخه جدید از پروژه منتشر می‌شد، آن‌ها را بروزرسانی کنند.

این روش برخی مزایا را به همراه داشت:

  • اگر تغییری در کتابخانه هسته اعمال می‌شد، آن تغییر فقط لازم بود که در یک جا اعمال شود، و نیاز به بازنویسی کل کد برنامه نبود.
  • تمام برنامه‌ها به صورت همگام باقی ماندند. هر زمان که تغییری اعمال می‌شد، تمام برنامه‌ها فقط نیاز داشتند که دستور «npm update» را اجرا کنند.

پس همانطور که می‌دانید، قدم بعدی انتشار کتابخانه بود.

این سخت‌ترین بخش کار بود؛ زیرا افکار زیادی در سر من می‌چرخیدند، مثلا:

  1. چگونه کدهای غیر ضروری پروژه را حذف کنم؟
  2. چه سیستم‌های ماژول JavaScript را باید هدف قرار دهم؟ (commonjs، amd، harmony)
  3. آیا بهتر است که سورس کد را transpile کنم؟
  4. آیا بهتر بود که سورس کد را bundle کنم؟
  5. چه فایل‌هایی را باید منتشر کنم؟

تک تک ما در هنگام ساخت یک کتابخانه، اینگونه سوال‌ها را در ذهن خود داریم. حال سعی خواهم کرد که تمام این سوال‌ها را پاسخ دهم.

انواع مختلف سیستم‌های ماژول JavaScript

1. CommonJS

  • توسط node پیاده‌سازی شده است.
  • وقتی که ماژول‌ها را نصب کرده‌اید، برای سمت سرور استفاده می‌شود.
  • هیچ بارگذاری‌ای برای ماژول async نیاز نیست.
  • توسط «require» وارد (import) می‌شود.
  • توسط «module.exports» خروجی گرفته می‌شود.
  • وقتی که آن را وارد می‌کنید، یک آبجکت را پس می‌گیرید.
  • از آنجایی که پس از وارد کردن آن یک آبجکت را پس می‌گیرید، نیازی به حذف کدهای غیر ضروری (Tree shaking) نیست.
  • هیچ تجزیه و تحلیل استاتیکی وجود ندارد.
  • همیشه یک کپی از آبجکت را دریافت می‌کنید؛ پس هیچ تغییر زنده‌ای به خود ماژول اعمال نمی‌شود.
  • مدیریت چرخه Dependency ضعیف.
  • سینتکس ساده.
// فایل log.js
function log(){

  console.log('Example of CJS module system');

}

// لاگ را در معرض ماژول‌های دیگر قرار دهید

module.exports = { log }

// فایل index.js

var logModule = require('./log');

logModule.log();

2. AMD (Async Module Definition = تعریف ماژول Async)

  • توسط RequireJS پیاده‌سازی شده است.
  • وقتی که یک بارگذاری دینامیک برای ماژول‌ها می‌خواهید، برای سمت کاربر (مرورگر) استفاده می‌شود.
  • توسط «require» وارد (import) می‌شود.
  • سینتکس پیچیده.
// فایل log.js
define(['logModule'], function(){

    // لاگ را در معرض ماژول‌های دیگر قرار دهید

    return {

        log: function(){

          console.log('Example of AMD module system');    

      }

    };

 });

// فایل index.js

require(['log'], function (logModule) {

  logModule.log();

});

3. UMD (Universal Module Definition = تعریف ماژول همگانی)

  • ترکیبی از CommonJS و AMD (به عبارتی سینتکس CommonJS و بارگذاری Async از AMD)
  • می‌تواند برای هر دو محیط AMD و CommonJS استفاده شود.
  • اساسا راهی برای استفاده هر دو مورد می‌سازد، در حالیکه از تعریف متغیر global نیز پشتیبانی می‌کند. در نتیجه، ماژول‌های UMD قادر به کار کردن بر روی هر دو سمت سرور و کاربر هستند.
// فایل log.js
(function (global, factory) {

  if (typeof define === "function" && define.amd) {

    define(["exports"], factory);

  } else if (typeof exports !== "undefined") {

    factory(exports);

  } else {

    var mod = {

      exports: {}

    };

    factory(mod.exports);

    global.log = mod.exports;

  }

})(this, function (exports) {

  "use strict";

  function log() {

    console.log("Example of UMD module system");

  }

  // لاگ را در معرض ماژول‌های دیگر قرار دهید

  exports.log = log;

});

4. ECMAScript Harmony (ES6)

  • هم برای سمت سرور و هم برای سمت کاربر استفاده می‌شود.
  • پشتیبانی از بارگذاری استاتیک ماژول‌ها.
  • وقتی که import می‌کنید، مقادیر واقعی را پس می‌گیرید.
  • می‌توانید توسط «import» وارد کرده، و توسط «export» خروجی بگیرید.
  • تجزیه و تحلیل استاتیک -  می‌توانید در زمان کمپایل کردن، وارد کردن‌ها و خروجی گرفتن‌ها را (به صورت استاتیک) تعیین کنید. فقط باید به سورس کد خود نگاه کنید، نیازی به اجرای آن نیست.
  • با توجه به پشتیبانی از تجزیه و تحلیل استاتیک توسط ES6، قابلیت حذف کدهای اضافه را دارید.
  • همیشه مقادیر واقعی دریافت می‌کنید، پس تغییرات زنده‌ای به خود ماژول اعمال می‌شود.
  • مدیریت چرخه Dependency بهتر نسبت به CommonJS.
// فایل log.js
const log = () => {

  console.log('Example of ES module system');

}

export default log

// فایل index.js

import log from "./log"

log();

پس حال همه ما تفاوت‌های میان انواع مختلف سیستم‌های ماژول JavaScript، و نحوه تکامل آن‌ها را می‌دانیم.

گرچه ماژول ES Harmony توسط تمام ابزار و مرورگرهای مدرن پشتیبانی می‌شود، هیچ وقت در هنگام انتشار کتابخانه‌های خود نمی‌دانیم که دریافت کنندگان آن‌ها چگونه از آن‌ها استفاده خواهند کرد.

بیایید بیشتر به این مسئله وارد شویم و یک کتابخانه ساده بسازیم، تا به تمام سوالات در زمینه انتشار کتابخانه به روشی درست پاسخ دهیم.

من یک کتابخانه رابط کاربری ساده ساخته‌ام، و حال تمام تجربه خود در زمینه transpile کردن، bundle کردن و انتشار آن را با شما به اشتراک خواهم گذاشت. (می‌توانید سورس کد این کتابخانه را بر روی گیت‌هاب بیابید)

در اینجا یک کتابخانه ساده داریم که ۳ کامپوننت دارد: Button، Card و NavBar. حال بیایید آن را قدم به قدم transpile کرده، و انتشار دهیم.

بهترین روش‌ها برای انتشار

1. حذف کدهای اضافه

  • حذف کدهای اضافه (Tree shaking) یک اصطلاح رایج است که در JavaScript با منظور حذف کدهای بی کاربرد استفاده می‌شود. این کار، به ساختار استاتیک سینتکس ماژول ES2015، یا به عبارتی import و export تکیه می‌کند. این نام و مفهوم، توسط اتصال دهنده ماژول ES2015، یعنی Rollup محبوب شده است.
  • هم Webpack و هم Rollup از Tree Shaking پشتیبانی می‌کنند، که یعنی ما موارد خاصی را به یاد نگه می‌داریم، تا کد ما قابلیت حذف بخش‌های اضافه را داشته باشد.
// فایل shakebake.js
const shake = () => console.log('shake');

const bake = () => console.log('bake');

//وقتی که آن را به عنوان ماژول خروجی می‌گیریم، کدهای اضافه می‌توانند حذف شوند

export { shake, bake };

// فایل index.js

import { shake } from './shakebake.js'

// only shake is included in the output

// فایل shakebake.js

const shake = () => console.log('shake');

const bake = () => console.log('bake');

//از آنجایی که یک آبجکت را خروجی گرفته‌ایم، کدهای اضافه نمی‌توانند حذف شوند

export default { shake, bake };

// فایل index.js

import { shake } from './shakebake.js'

// both shake and bake are included in the output

2. تمام انواع مختلف ماژول را انتشار دهید

  • بهتر است که تمام انواع ماژول مانند UMD و ES را منتشر کنیم. زیرا هیچ وقت نمی‌دانیم که دریافت کنندگان ما در کدام نسخه مرورگر / Webpack از این کتابخانه / پکیج استفاده می‌کنند.
  • با این که تمام bundlerها مانند Webpack و Rollup ماژول ES را درک می‌کنند، اما اگر دریافت کننده ما از Webpack 1.x استفاده کند، نمی‌تواند ماژول ES را درک کند.
// package.json

{
"name": "js-module-system",
"version": "0.0.1",
...

"main": "dist/index.js",
"module": "dist/index.es.js",

...
}

فیلد main در فایل package.json معمولا برای اشاره به نسخه UMD موجود در کتابخانه / پکیج استفاده می‌شود.

  • شاید از خود بپرسید: «چگونه می‌توانم نسخه ES کتابخانه / پکیج خود را انتشار دهم؟»
    فیلد module در فایل package.json برای اشاره به نسخه ES موجود در کتابخانه / پکیج استفاده می‌شود. قبلا فیلدهای زیادی مانند js:next و js:main استفاده می‌شدند، اما حال «module» استاندارد سازی شده است و توسط bundlerها به عنوان یک نمایانگر نسخه ES موجود در کتابخانه / پکیج استفاده می‌شود.

نکته کمتر شناخته شده: Webpack از resolve.mainfields برای تعیین این که کدام فیلدها در فایل package.json بررسی می‌شوند استفاده می‌کند.

نکته کارایی: همیشه سعی کنید نسخه ES کتابخانه / پکیج خود را نیز منتشر کنید؛ زیرا امروزه تمام مرورگرهای مدرن از ماژول‌های ES پشتیبانی می‌کنند. پس می‌توانید کد کمتری را transpile کنید، و در نهایت کد کمتری را مجبورید به کاربران خود تحویل دهید. این کار، کارایی برنامه شما را افزایش خواهد داد.

پس قدم بعدی چیست؟ transpile کردن یا bundle کردن؟ از چه ابزاری باید استفاده کنیم؟

در اینجا، به بخش پیچیده کار می‌رسیم.

Webpack، Rollup و Babel علیه یکدیگر

این‌ها ابزاری هستند که ما در زندگی روزمره خود برای ساخت برنامه‌ها / کتابخانه‌ها / پکیج‌ها استفاده می‌کنیم. به هیچ وجه نمی‌توان توسعه وب مدرن را بدون آن‌ها تصور کرد. از این رو، نمی‌توان آن‌ها را با هم مقایسه کرد؛ پس این سوال اشتباهی است.

هر ابزار مزایای خود را دارد و بر حسب نیاز شما، اهداف مختلفی را میسر می‌سازد. حال بیایید به هر کدام از این ابزار نگاهی داشته باشیم:

Webpack

Webpack یک اتصال دهنده ماژول عالی است که توسط افراد زیادی پذیرفته شده است و معمولا برای ساخت SPAها استفاده می‌شود. این ابزار تمام ویژگی‌ها مانند تقسیم‌بندی کد، بارگذاری async برای bundleها، حذف کدهای اضافه و... را فراهم می‌کند. این ابزار از سیستم ماژول CommonJS استفاده می‌کند.

RollupJS

Rollup هم یک اتصال دهنده ماژول مانند Webpack است. گرچه، برتری اصلی Rollup در این است که از قالب‌بندی استاندارد جدید برای ماژول‌های کد موجود در ES6 پشتیبانی می‌کند، پس می‌توانید از آن برای bundle کردن انواع مختلف ماژول‌های ES خود در کتابخانه‌ها / پکیج‌ها استفاده کنید. این ابزار از بارگذاری async پشتیبانی نمی‌کند.

Babel

Babel یک transpiler برای JavaScript است که بیشتر برای قابلیتش در تبدیل کد ES6 به کدی که در مرورگر اجرا می‌شود، معروف است. دقت کنید که این ابزار فقط کد را transpile می‌کند و آن را bundle نمی‌کند.

پیشنهاد من: از Rollup برای کتابخانه‌ها و از Webpack برای برنامه‌ها استفاده کنید.

منبع را transpile کرده (با استفاده از babel) یا اتصال دهید (bundle کنید)

وقتی که این کتابخانه را می‌ساختم، اکثر وقت خود را برای درک این که کدام پاسخ برای سوال مناسب است صرف کردم. من شروع به گشتن ماژول‌های node خود کردم تا تمام کتابخانه‌های خوب را نگاه کرده، و سیستم ساخت آن‌ها را بررسی کنم.

پس از نگاه به خروجی ساخت برای کتابخانه‌ها / پکیج‌های مختلف، تصویر واضحی از استراتژی‌های مختلفی که نویسندگان این کتابخانه‌ها ممکن بوده است قبل از انتشار آن کتابخانه در ذهن داشته باشند، به دست آوردم.

همانطور که در تصویر بالا می‌توانید ببینید، این کتابخانه‌ها را بر حسب مشخصات آن‌ها به دو دسته تقسیم کردم:

  1. کتابخانه‌های رابط کاربری (styled-components، material-ui)
  2. پکیج‌های هسته‌ای (react، react-dom)

اگر دقت کرده باشید، ممکن است تفاوت میان این دو دسته را درک کرده باشید.

کتابخانه‌های رابط کاربری، یک پوشه به نام dist دارند که نسخه bundle شده و منحصر به فرد ES6 و سیستم ماژول UMD/CJS را در خود دارند. یک پوشه به نام lib هم وجود دارد که نسخه transpile شده کتابخانه را در خود دارد.

پکیج‌های هسته‌ای فقط یک پوشه دارند که نسخه‌های bundle شده و منحصر به فرد سیستم ماژول CJS و UMD را به عنوان یک هدف در خود دارد.

اما چرا یک تفاوت میان نسخه ساخت کتابخانه‌های رابط کاربری و پکیج‌های هسته‌ای وجود دارد؟

کتابخانه‌های رابط کاربری

فرض کنید که فقط نسخه bundle شده کتابخانه خود را انتشار دهیم و آن را بر روی CDN میزبانی کنیم. دریافت کننده ما از آن مستقیما در تگ <script/> استفاده خواهد کرد. حال اگر دریافت کننده ما فقط بخواهد از کامپوننت <Button/> استفاده کند، باید کل کتابخانه را بارگذاری کند. همچنین، در یک مرورگر هیچ bundlerای وجود ندارد که به حذف کدهای اضافه رسیدگی کند، و در نهایت کل کتابخانه را به کاربر خود ارسال خواهیم کرد. ما نمی‌خواهیم که این اتفاق بیفتد.

<script type="module">
import {Button} from "https://unpkg.com/uilibrary/index.js";
</script>

حال اگر به سادگی src را به داخل lib ترنسپایل کنیم و lib را بر روی یک CDN میزبانی کنیم، دریافت کننده ما می‌تواند هر چیزی که می‌خواهد را دریافت کند.

<script type="module">
import {Button} from "https://unpkg.com/uilibrary/lib/button.js";
</script>

پکیج‌های هسته‌ای

پکیج‌های هسته‌ای هیچ وقت توسط تگ <script/> استفاده نمی‌شوند، زیرا باید بخشی از برنامه اصلی باشند. پس می‌توانیم به صورت امن نسخه bundle شده (UMD، ES) را برای این نوع پکیج‌ها انتشار دهیم و سیستم ساخت را به دریافت کنندگان واگذار کنیم.

برای مثال، آن‌ها می‌توانند از انواع UMD بدون هیچ‌گونه حذف کد اضافه استفاده کنند، یا اگر bundler قادر به تشخیص و دریافت منفعت‌های حذف کدهای اضافه است، از انواع ES استفاده کنند.

// CJS require
const Button = require("uilibrary/button");

// ES import
import {Button} from "uilibrary";

اما پاسخ سوال ما چه خواهد بود؟ باید سورس کد را transpile کنیم یا bundle کنیم؟

برای کتابخانه رابط کاربری، باید سورس کد را با استفاده از Babel، و ماژول es به عنوان هدف transpile کنیم و آن را در lib‌ قرار دهیم. حتی می‌توانیم lib را بر روی CDN میزبانی کنیم.

ما باید سورس کد را با استفاده از Rollup برای سیستم ماژول cjs/umd و ماژول es به عنوان هدف، bundle کرده و کاهش دهیم. فایل package.json را تغییر دهید تا سیستم هدف مناسب را داشته باشد.

// package.json

{
"name": "js-module-system",
"version": "0.0.1",
...

"main": "dist/index.js",      // for umd/cjs builds
"module": "dist/index.es.js", // for es build

...
}

برای پکیج‌های هسته‌ای، ما نیازی به نسخه lib نداریم.

فقط باید سورس کد را با استفاده از Rollup برای سیستم ماژول cjs/umd و ماژول es به عنوان هدف، bundle کرده و کاهش دهیم. فایل package.json را تغییر دهید تا سیستم هدف مناسب را داشته باشد.

نکته: ما می‌توانیم برای دریافت کنندگانی که مایل به دانلود کل کتابخانه / پکیج با استفاده از تگ <script/> هستند، پوشه dist را بر روی CDN نیز میزبانی کنیم.

چگونه باید این را بسازیم؟

ما باید اسکریپت‌های مختلفی برای هر سیستم هدف در فایل package.json‌ داشته باشیم. می‌توانیم فایل پیکربندی Rollup را در مخزن گیت‌هاب پیدا کنید.

// package.json

{
...
"scripts": {
"clean": "rimraf dist",
"build": "run-s clean && run-p build:es build:cjs build:lib:es",
"build:es": "NODE_ENV=es rollup -c",
"build:cjs": "NODE_ENV=cjs rollup -c",
"build:lib:es": "BABEL_ENV=es babel src -d lib"
}
...
}

چه چیزی را باید انتشار دهیم؟

  • لایسنس
  • فایل Readme
  • Changelog (لاگ تغییرات)
  • Metadata (main, module, bin) - package.json
  • کنترل از طریق ویژگی فایل‌های package.json

فیلد «files» در فایل package.json، آرایه‌ای از الگوها است که ورودی‌های موجود هنگام نصب پکیج شما به عنوان یک Dependency را توضیح می‌دهد. اگر یک پوشه را در یک آرایه نامگذاری کنید، فایل‌های داخل آن پوشه را نیز شامل خواهد شد.

در این مورد، ما پوشه‌های lib و dist را در فیلد «files» شامل خواهیم کرد.

// package.json

{
...
"files": ["dist", "lib"]
...
}

در آخر، کتابخانه ما آماده انتشار است. فقط دستور npm run build را در ترمینال خود اجرا کنید، و سپس می‌توانید خروجی زیر را ببینید. به دقت به پوشه‌های dist و lib نگاه کنید.

جمع بندی

امیدوارم توانسته باشم درک بهتری از سیستم ماژول‌های JavaScript و نحوه ساخت و انتشار یک کتابخانه را به شما داده باشم.

فقط مطمئن شوید که به این موارد رسیدگی می‌کنید:

  1. پروژه خود را Tree Shakeable کنید. (قابلیت حذف کدهای اضافه را به آن بدهید.)
  2. حداقل سیستم‌های ماژول ES Harmony و CJS را هدف قرار دهید.
  3. از Babel و bundlerها برای کتابخانه‌ها استفاده کنید.
  4. از bundlerها برای پکیج‌های هسته‌ای استفاده کنید.
  5. فیلد module در فایل package.json را طوری تنظیم کنید که نسخه ES ماژول شما را هدف قرار دهد.
  6. فولدرهایی که transpile شده‌اند را نیز به همراه نسخه‌های bundle شده ماژول خود انتشار دهید.

منبع

مقالات پیشنهادی

به طرح های شخصی و تجاری خود سر و سامان بدهید

از زمان پديد آمدن صنعت طراحي هميشه بين طرح هاي خلاقانه شخصي و طرح هاي تجاري بحث و اختلاف بوده است. اصولا خلاقيت و تجارت بدون يکديگر معنا ندارند. شما ب...

15 کتابخانه جالب javascript و css

ماموریت ما در راکت این است که شما را بر اساس تکنولوژی روز طراحی وب به روز نگه داریم . به همین خاطر هم هست که ما تقریبا ماهانه یا چند هفته در میان پستی...

15 کتابخانه جالب javascript و css دی ۹۵

ماموریت ما در راکت این است که شما را بر اساس تکنولوژی روز طراحی وب به روز نگه داریم . به همین خاطر هم هست که ما تقریبا ماهانه یا چند هفته در میان پستی...

Gridder یک سیستم Grid بر اساس Flexbox

Gridder یک سیستم ساده Grid است که بر اساس Flexbox طراحی شده و به شما کمک میکنه تا layout بندی های خودتون رو بر اساس Flexbox طراحی کنید . این ابزار دار...