آماده سازی برنامه ری اکتی برای انتشار (Deploy)

29 اردیبهشت 1398
درسنامه درس 27 از سری آموزش react (ری اکت)
React-deployment

بعد از اینکه تست برنامه تان را به تمام رساندیم، وقت آن است که برنامه را روی اینترنت منتشر کنیم تا کاربران بتوانند آن را مشاهده کنند. در این درس به آموزش نحوه انتشار یک برنامه روی اینترنت خواهیم پرداخت.

انتشار برنامه

منظور از انتشار یا Deploy یک برنامه، موارد زیر است:

  • میزبانی کردن
  • پیکربندی محیط انتشار
  • یکپارچگی پیوسته (به اختصار CI)
  • مدیریت هزینه های پهنای باند شبکه و ...
  • بررسی حجم فایل نهایی
  • و موارد دیگر ...

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

در این درس تمرکز را روی آماده سازی برنامه برای انتشار خواهیم گذاشت.

خارج کردن (eject) ابزار create-react-app از پروژه

اول از همه نیاز است که برخی از قابلیت های سفارشی سازی برنامه را خودمان به عهده بگیریم، برای اینکار دستور npm run eject را در روت پروژه اجرا می کنیم. با اجرای این دستور به برنامه می گوییم که از این لحظه به بعد خودمان مسئولیت مدیریت ساختاربندی پروژه را بر عهده می گیریم (یعنی بدون کمک گرفتن از create-react-app)

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

برای خارج کردن create-react-app از دستور eject مطابق زیر بهره می بریم:

npm run eject

بعد از خارج کردن ساختار create-react-app تعداد زیادی فایل جدید در دایرکتوری scripts و config در روت پروژه ایجاد می شود. دستور npm run eject تمام این فایل ها را به طور خودکار ایجاد کرده و آنها را در برنامه مان قرار می دهد.

یکی از بخش های مهم Create-react-app ، webpack است که در واقع یک ابزاری برای بسته بندی و ساخت ماژول است.

اصول webpack

webpack یک ابزار بسته بندی ماژول است که کاربران زیادی دارد و قابلیت های دیگری از قبیل داشتن هزاران پلاگین، سرعت بسیار بالا، پشتیبانی از قابلیت hot-code reloading و ... را دارد.

تا به اینجا اگرچه به طور مستقیم webpack را در برنامه فراخوانی نکردیم، اما با هر بار اجرای دستور npm start، خود webpack هم اجرا می شود. بدون webpack نمی توانستیم با دستور import ماژول ها را وارد برنامه بکنیم. webpack کدهای برنامه را نگاه کرده و اگر به کلمه کلیدی import برخورد کند می داند که ما نیاز به کدهایی که مسیر آن را مشخص کردیم، داریم تا بتوانیم برنامه را اجرا کنیم.

webpack قابلیتی به نام hot-reloading دارد که تقریباً به طور خودکار می تواند تعداد زیادی از فایل ها را بارگذاری و بسته بندی کند و همچنین این ابزار کدها را به بخش های مختلفی تقسیم کرده تا بتواند از قابلیت lazy-loading (بارگذاری تنبل) استفاده کند و همچنین حجم فایلی که کاربر باید آن را دانلود کند، کاهش می یابد.

استفاده از این ابزار اهمیت زیادی برای ما دارد، چون با بزرگتر و پیچیده تر شدن برنامه باید بدانیم که چطور باید از ابزارهای build برای پروژه مان استفاده کنیم.

فایل bundle.js چه کاری انجام می دهد؟

با نگاهی به فایل های تولید شده پس از اجرای دستور npm start می بینیم که چندین فایل برای ارسال به مرورگر آماده شده اند. اولی index.html و فایل بعدی bundle.js است. کاری که webpack در این قسمت انجام می دهد، این است که فایل bundle.js را وارد فایل index.js می کند، حتی اگر ما مستقیم برنامه را در فایل index.js بارگذاری نکرده باشیم، باز هم این قابلیت توسط وب پک انجام می شود.

فایل bundle.js، فایلی بزرگ است که محتوای آن شامل تمام کدهای جاوااسکریپتی که برنامه مان برای اجرا شدن به آن ها نیاز دارد. این فایل ها عبارتند از: وابستگی ها و حتی فایل اختصاصی خود پروژه. اگر به سورس های این فایل نگاه کنید، می بینید که webpack از روش های جالبی برای کنار هم قرار دادن کد فایل های مختلف استفاده کرده است.

webpack در پشت صحنه از Babel برای تبدیل کدهای ES6 به ES5 استفاده می کند.

اگر به کامنت های ابتدای فایل app.js نگاه کنید، می بینید که عدد 254 نوشته شده است.

/* 254 */
/*!********************!*\
  !*** ./src/app.js ***!
  \********************/

خود این ماژول داخل متدی شبیه زیر کپسوله شده است:

function(module, exports, __webpack_require__) {
  // The chaotic `app.js` code here
}

تمام ماژول های برنامه داخل متدی که امضایی شبیه به متد فوق دارد، کپسوله می شود. webpack به هر کدام از ماژول ها، یک ID نسبت می دهد. (در این مثال، به ماژول app.js شناسه ۲۴۵ نسبت داده شده است)

اما در اینجا منظور ما از ماژول، صرفاً ماژول های ES6 نیست. مثلاً همان طور که در زیر می بینید، ما تابع ()makeRoutes را به فایل app.js، وارد یا import کرده ایم.

import makeRoutes from './routes'

و در زیر می بینید که webpack دستور بالا را به کد زیر تبدیل می کند:

var _logo = __webpack_require__(/*! ./src/routes.js */ 255);

کمی عجیب به نظر می رسد، webpack یک کامنت هم به منظور دیباگ کردن به کد اضافه می کند. کامنت بالا را حذف کنید.

var _logo = __webpack_require__(255);

دستور import به کدهای ES5 تبدیل می شود. حال در این فایل src/routes.js را جستجو کنید.

/* 255 */
/*!**********************!*\
  !*** ./src/routes.js ***!
  \**********************/

همان طور که می بینید، ID این ماژول ها برابر 255 است، یعنی دقیقاً همان عددی که به __webpack_require__ پاس داده می شود.

webpack به همه چیز، به دید یک ماژول نگاه می کند، مثلاً عکس های استفاده شده در برنامه.

برای مثال تصویر logo.svg را در نظر بگیرید. webpack یک شناسه به این فایل اختصاص می دهد و اگر مرورگر را باز کرده و به مسیر زیر بروید، می بینید که تصویر، به شما نشان داده می شود.

(احتمالاً مسیر فایل تصویر در سیستم شما متفاوت است)، اما در این مثال مسیر فایل مشابه زیر می باشد:

static/media/logo.5d5d9eef.svg

حال اگر آدرس زیر را در مرورگر باز کنید، تصویر فوق نمایش داده می شود:

http://localhost:3000/static/media/logo.5d5d9eef.svg

همان طور که دیدید webpack یک ماژول برای فایل logo.svg ایجاد کرد که به مسیر svg در سرور توسعه webpack اشاره دارد.

webpack به خاطر استفاده از این الگوی ماژولار، می تواند به طور هوشمندانه دستوری مانند زیر را گرفته:

import makeRoutes from './routes'

و به کدهای ES6 کامپایل کند.

var _makeRoutes = __webpack_require__(255);

اما در مورد فایل های css چطور؟ بله، webpack همه فایل ها را به دید ماژول می بیند. رشته متنی src/app.css را جستجو کنید.

فایل index.html به هیچ فایل css ایی ارجاع داده نشده است. به این دلیل که webpack فایل های css را وارد فایل bunde.js کرده است. هنگام بارگذاری برنامه، webpack محتوای فایل app.js را از داخل bundle.js برداشته و وارد تگ های style صفحه می کند.

پس تا به اینجا فهمیدید که webpack هر چیزی که قابلیت ماژول شدن دارد را گرفته و وارد فایل bundle.js می کند. اما شاید بپرسید، دلیل اینکار چیست؟

دلیل اول این است که webpack تمام ماژول های ES6 را به سینتکس ماژول های ES5 تبدیل می کند. همان طور که در بالا دیدید تمام ماژول های جاوا اسکریپت را در داخل یک سری توابع خاص قرار می دهد. سپس برای ارجاع دادن راحت به ماژول ها، یک شناسه ID به هر کدام از آنها نسبت می دهد.

webpack هم مانند دیگر ابزارهای پکیج بندی، تمام ماژول های جاوا اسکریپت را وارد یک فایل می کند. البته می شود ماژول های جاوا اسکریپت را در فایل های جداگانه ای هم نگهداری کرد، اما برای اینکار باید پیکربندی create-react-app را تغییر دهید. همان طور که دیدیم با فایل های تصویر، CSS و پکیج های npm (مانند React، ReactDOM) با همان الگوی ماژولار رفتار می کند. این الگو قدرت زیادی را به ما می هد که در ادامه به بعضی از این قدرت ها نگاهی می اندازیم.

وب پک (Webpack) پیچیده است؟

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

در ادامه به بخش های مختلف پیکربندی webpack که با آن کار خواهیم کرد، نگاهی می اندازیم. اگر بعضی از قسمت های آن سخت به نظر می رسد، کافی است که مراحل را همراه با ما انجام دهید تا اصول کار با این ابزار را بهتر درک کنید.

با دانشی که تا به اینجا راجع به Webpack پیدا کردیم، به برنامه باز می گردیم. حال webpack را کمی تغییر داده تا بتواند پیکربندی محیط های دیگر را هم پشتیبانی کند.

پیکربندی محیط (Environment)

هنگامی که یک برنامه جدید را برای انتشار آماده می کنید، لازم است راجع به یک سری موارد فکر کنیم.

برای مثال، فرض کنید که ما یک درخواست را به یک سرور API ارسال می کنیم. هنگام توسعه برنامه به احتمال زیاد این سرور API روی همان ماشین محلی قرار دارد (که از طریق localhost می توان به آن دسترسی داشت)

اما هنگام انتشار برنامه می خواهیم درخواست ها را به یک سرور دیگر که خارج از برنامه مان قرار دارد ارسال کنیم، که در اکثر مواقع این سرور لزوماً در محلی که درخواست از آنجا ارسال می شود، نیست. در اینجا، localhost دیگر کار نخواهد کرد.

یک راه دیگر برای مدیریت پیکربندی های برنامه استفاده از فایل های env. است. این فایل های env. برای هر محیطی، از متغیرهای مختلفی استفاده می کند.

معمولاً یک فایل env. در ریشه سایت قرار دارد که به عنوان تنظیمات سراسری عمل می کند و می تواند تمام فایل های پیکربندی که روی محیط های دیگری قرار گرفته است را override کند.

حال پکیج dotenv را نصب می کنیم تا به کمک آن بتوانیم تنظیمات پیکربندی برنامه مان را انجام دهیم.

npm install --save-dev dotenv

کتابخانه dotenv به ما کمک می کند تا بتوانیم متغیرهای محیطی را وارد فایل env. برنامه مان بکنیم.

معمولاً بهتر است که فایل env. را وارد فایل gitignore. کنید تا تغییرات آن مورد بررسی قرار نگیرد. همچنین بهتر است که یک نسخه کپی شده از فایل env. را وارد ریپازیتوری یا مخزن گیت کنید. برای مثال می توانیم یک کپی از فایل env. گرفته و نام آن را env.example. بگذاریم و متغیرهای مورد نیاز را هم وارد این فایل کنیم.

این فایل توسط توسعه دهندگان دیگر یا حتی خود ما، می تواند به عنوان یک قالب برای فایل env. استفاده شود.

متغیرهایی که در فایل های env. نوشته می شوند، استایلی مشابه متغیرهای یونیکس دارند.

حال می خواهیم یک متغیر سراسری به نام APP_NAME و با مقدار 30Days ایجاد کنیم.

touch .env
echo "APP_NAME=30days" > .env

اگر به دایرکتوری config نگاه کنید می بینید که برای ما ابزارهای زیادی جهت build پروژه نوشته شده است، اما نمی خواهیم در این درس به تمام آنها بپردازیم. در اینجا نگاهی به config/webpack.config.js می اندازیم تا ببینیم که این ابزار چه کاری را برای مان انجام می دهد.

در این فایل تمام پیکربندی های webpack که برای ساخت برنامه مان استفاده می شود، قرار دارد، مواردی مانند بارگذارکننده ها(loader)، پلاگین ها، ورودی های مختلف و ... .

به عنوان مثال، به خطی که پروپرتی plugins تعریف شده نگاهی بیندازید. همان طور که می بینید، متد ()DefinePlugin را در آن تعریف کرده ایم:

module.exports = {
  // ...
  plugins: [
    // ...
    // Makes some environment variables available to 
    // the JS code, for example:
    // if (process.env.NODE_ENV === 'development') {
    //  ... 
    // }. See `env.js` 
    new webpack.DefinePlugin(env),
    // ...
  ]
}

پلاگین webpack.DefinePlugin یک آبجکت شامل کلیدها (keys) و مقادیر (values) را گرفته و کدهای برنامه را جستجو می کند و هر جا که از این key استفاده کرده ایم را یافته و آن را با مقدار value جایگزین می کند.

برای مثال یک آبجکت به نام env مانند زیر داریم:

{
  '__NODE_ENV__': 'development'
}

حال اگر از متغیر __NODE_ENV__ را در سورس برنامه مان استفاده کنیم، این متغیر با مقدار 'development' جایگزین می شود.

class SomeComponent extends React.Component {
  render() {
    return (
      <div>Hello from {__NODE_ENV__}</div>
    )
  }
}

در نتیجه پس از اجرای متد ()render متن "Hello from development" نمایش داده می شود.

برای افزودن یک متغیر به برنامه از همین آبجکت env استفاده کرده و متغیر مورد نظر خودمان را در این آبجکت تعریف می کنیم. حال اگر به بالای فایل نگاه کنید می بینید که این آبجکت از فایل config/env.js، استخراج یا export می شود.

با نگاهی به فایل config/env.js می بینید که تمام متغیرهای محیطی و NODE_ENV و همچنین هر متغیری که پیشوند REACT_APP_ را داشته باشد، را به محیط (Environment) اضافه کنیم.

همچنین می توانیم از تمام بخش های پیچیده برنامه عبور کنیم و تنها کاری که باید انجام دهیم، تغییر دادن آرگومان دوم تابع reduce است.

به عبارتی، ما آبجکت را بروزرسانی می کنیم:

// ...
module.exports = Object
  .keys(process.env)
  .filter(key => REACT_APP.test(key))
  .reduce((env, key) => {
    env['process.env.' + key] = JSON.stringify(process.env[key]);
    return env;
  }, {
    'process.env.NODE_ENV': NODE_ENV
  });

این آبجکت به عنوان آبجکت اولیه به تابع reduce پاس داده می شود. تابع reduce تمام متغیرهایی که پیشوند REACT_APP_ دارد را داخل این آبجکت قرار می دهد و ما می توانیم توسط process.env.NODE_ENV  به مقدار آن دسترسی داشته باشیم.

در حقیقت کاری که انجام می دهیم عبارت است از:

  • بارگذاری فایل پیش فرض env.
  • این دو متغیر را به همراه تمام متغیرهای پیش فرض در یک جا قرار می دهیم
  • بارگذاری تمام فایل های env. محیطی
  • یک آبجکت جدید با تمام متغیرهای محیطی ایجاد می کنیم و مقدار هر کدام از متغیرها را تعیین می کنیم.
  • آبجکت اولیه را برای سازنده (Constructor) محیط های موجود بروزرسانی می کنیم.

همچنین برای بارگذاری فایل env. نیاز به import کردن پکیج dotenv داریم.

همچنین کتابخانه path را به برنامه import کرده و چند متغیر را برای مسیرها تنظیم می کنیم.

فایل config/env.js را مطابق زیر ویرایش کنید.

var REACT_APP = /^REACT_APP_/i;
var NODE_ENV = process.env.NODE_ENV || 'development';

const path = require('path'),
      resolve = path.resolve,
      join = path.join;

const currentDir = resolve(__dirname);
const rootDir = join(currentDir, '..');

const dotenv = require('dotenv');

برای بارگذاری محیط سراسری از تابع ()config که توسط کتابخانه dotenv ارائه شده، استفاده می کنیم و مسیر فایل env. روت پروژه را به آن پاس می دهیم. همچنین از همین تابع برای پیدا کردن یک فایل با نام NODE_END.config.env در دایرکتوری config استفاده می کنیم. همچنین نمی خواهیم هیچکدام از این متدها، اگر به خطایی برخورد کردند، آن خطا را برگردانند. به همین دلیل گزینه silent:true را به آن اضافه کرده تا اگر فایلی را پیدا نکرد، هیچ خطا یا استثنایی را پرتاب نکند.

// 1. Step one (loading the default .env file)
const globalDotEnv = dotenv.config({
  path: join(rootDir, '.env'),
  silent: true
});
// 2. Load the environment config
const envDotEnv = dotenv.config({
  path: join(currentDir, NODE_ENV + `.config.env`),
  silent: true
});

حال می خواهیم تمام این متغیرها را به همراه NODE_ENV در این آبجکت قرار داده شود. متد ()object.assign یک آبجکت جدید ایجاد کرده و هرکدام از این آبجکت ها را از راست به چپ ادغام می کند.

const allVars = Object.assign({}, {
  'NODE_ENV': NODE_ENV
}, globalDotEnv, envDotEnv);

با تنظیماتی که تا به اینجا انجام دادیم، متغیر allvars باید مانند زیر باشد:

{
  'NODE_ENV': 'development',
  'APP_NAME': '30days'
}

در نهایت یک آبجکت ایجاد می کنیم تا بتوانیم این متغیرها را داخل process.env قرار دهیم و مطمئن شویم که آنها رشته های معتبری دارند (با استفاده از ()JSON.stringify)

const initialVariableObject =
  Object.keys(allVars)
  .reduce((memo, key) => {
    memo['process.env.' + key.toUpperCase()] = 
      JSON.stringify(allVars[key]);
    return memo;
  }, {});

با تنظیماتی که تا به اینجا انجام دادیم (در فایل env. روت پروژه)، آبجکت initialVariableObject مقدار زیر را خواهد داشت:

{
  'process.env.NODE_ENV': '"development"',
  'process.env.APP_NAME': '"30days"'
}

حال می توانیم از این آبجکت initialVariableObject به عنوان آرگومان دوم module.export استفاده کنیم. حال آن را برروزرسانی کرده تا بتواند از این آبجکت استفاده کند:

module.exports = Object
  .keys(process.env)
  .filter(key => REACT_APP.test(key))
  .reduce((env, key) => {
    env['process.env.' + key] = JSON.stringify(process.env[key]);
    return env;
  }, initialVariableObject);

حال در هر جای برنامه که خواستیم می توانیم از متغیرهایی که در فایل env. تعریف کردیم، استفاده کنیم.

چون ما در برنامه مان یک درخواست به سرور خارج از برنامه داریم، باید از یک پیکربندی جدید برای بروزرسانی این هاست استفاده کنیم.

فرض کنید که می خواهیم مقدار متغیر TIME_SERVER را برابر  http://localhost:3001 قرار می دهیم، چون اگر TIME_SERVER را در پیکربندی محیطی تنظیم نکنیم، بصورت پیش فرض از localhost استفاده می کند.

اینکار را می توانیم با اضافه کردن متغیر TIME_SERVER به فایل سراسری env. انجام دهیم.

فایل env. را مطابق زیر بروزرسانی کنید:

APP_NAME=30days
TIME_SERVER='http://localhost:3001'

همجنین می توان متغیر TIME_SERVER زیر را در فایل config/development.config.env قرار داد که در این صورت متغیر TIME_SERVER سراسری را override می کند.

TIME_SERVER='https://fullstacktime.herokuapp.com'

هنگام اجرا دستور npm start متغیر process.env.TIME_SERVER با مقداری که دارد جایگزین می شود.

حال می خواهیم ماژول src/redux/modules/currentTime.js را برای استفاده از سرور جدید بروزرسانی کنیم:

// ...
export const reducer = (state = initialState, action) => {
  // ...
}

const host = process.env.TIME_SERVER;
export const actions = {
  updateTime: ({timezone = 'pst', str='now'}) => ({
    type: types.FETCH_NEW_TIME,
    meta: {
      type: 'api',
      url: host + '/' + timezone + '/' + str + '.json',
      method: 'GET'
    }
  })
}

برای انتشار برنامه از Heroku استفاده می کنیم و برای اینکار باید یک کپی از فایل development.config.env با نام production.config.env گرفته و در دایرکتوری config/. قرار می دهیم.

cp config/development.config.env config/production.config.env

middleware های سفارشی برای محیط های پیکربندی

ما از یک middleware ریداکس (Redux) برای ورود در برنامه استفاده می کنیم. این یکی از قابلیت های خیلی خوب هنگام توسعه است، اما در هنگام انتشار برنامه نمی خواهیم که این قابلیت فعال باشد.

حال باید پیکربندی middlewareمان را بروزرسانی کنیم تا تنها از middleware لاگین در توسعه برنامه استفاده کنند و نه در تمام مراحل (از جمله، مرحله انتشار برنامه).

در فایل src/redux/configuraionStore.js ،middleware های موردنیازمان را بوسیله یک آرایه ساده بارگذاری می کنیم.

let middleware = [
  loggingMiddleware,
  apiMiddleware
];
const store = createStore(reducer, applyMiddleware(...middleware));

همچنین می توانیم آرایه middleware را بسته به محیطی که برنامه را در آن اجرا می کنیم، بروزرسانی کنیم.

اکنون می خواهیم هنگامی که در مرحله توسعه برنامه هستیم، middleware لاگین را اضافه کنیم.

let middleware = [apiMiddleware];
if ("development" === process.env.NODE_ENV) {
  middleware.unshift(loggingMiddleware);
}

const store = createStore(reducer, applyMiddleware(...middleware));

با اجرای کد بالا، هنگامی که در مرحله توسعه برنامه مان هستیم، متغیر loggingMiddleware به پیکربندی ما اضافه شده و در محیط های دیگر این middleware غیرفعال می شود.

در درس امروز که نسبتاً طولانی هم بود، برنامه مان را برای انتشار آماده کردیم. در درس آینده برنامه را روی یک سرور بارگذاری یا آپلود می کنیم.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش react (ری اکت) توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.