משתני סביבה: סקירה, שימוש, והקשר לטכנולוגיות צד לקוח.
שימוש במשתני סביבה (environment variables) זו דרך נהדרת להוצאת קונפיגורציה דינמית מהקוד, כך שבזמן ריצה/קומפילציה המידע יהיה נגיש, אך לא נצטרך לשמור את ערכו בקוד המקור.
נניח ובנינו שרת המייחצן API לקבלת נתונים ואנקדוטות על פרחים, כך שנותנים לשרת תמונה של פרח, ומקבלים את כל פרטיו. מדהים.
השרת עצמו מן הסתם לא מחזיק את המידע על כל הפרחים בעולם בתוכו, אלא רק מנתח את התמונה בעזרת כמה Buzzwords, וכשהניתוח מסתיים וברור מהו הפרח בתמונה או אז שולף השרת את נתוני הפרח מתוך בסיס הנתונים (DB, להלן: בסנת) ומעביר את הנתונים ללקוח. עד כאן הכל נפלא.
היות והשרת והבסנת מופרדים, בכדי לגשת לבסנת מהשרת צריך את כתובת הבסנת ופרטי אימות, מה עושים?
אם נכניס את הכתובת והאימות לקוד המקור, או אפילו לקובץ קונפיגורציה בקוד המקור, ניצור מספר בעיות חמורות ביותר:
משתנה סביבה, שניתן לערוך מחוץ לקוד שרץ, ולקרוא אותו בקוד בזמן ריצה. כך שבקוד קוראים את ערכו, אבל להזין אותו ניתן להזין ברמת המכונה או התהליך. (מדריך הזנת משתני מערכת)
אז אצלנו בשרת הפרחים, נקרא את ערכו של משתנה הסביבה
DB_URI
(או כל שם אחר שעולה בראש)
וזה מה שיתן לנו את כתובת ופרטי האימות של הבסנת,
ואו אז:
(נעבור על הבעיות)
נניח שאנחנו רוצים לבנות עבור אותו שרת פרחים מהולל ממשק משתמש מודרני ונוח, מה נקרא בעולמנו "לבנות לו אתר", הלכנו כתבנו HTML + CSS + JS, יופי של GUI, ועכשיו אנחנו רוצים לשים את הקבצים הללו ב-CDN, כך שכל מי שייגש ל-פרחים.קום יקבל את האתר המופלא שכתבנו.
נשמע טוב, עד שנזכרים שה-API שלנו נמצא על שרת אחר שכתובתו ממשק.פרחים.קום וממנו מקבלים את המידע.
מה נעשה? נכתוב בקובץ ה-JS בקשת AJAX לשרת הפרחים (לא לשכוח לפתוח בשרת CORS, כמובן), והכל יעבוד יחד.
את הקבצים של הפרונט-אנד הלקוח יקבל מה-CDN וכשהלקוח יזין תמונה תתבצע בקשת AJAX לשרת ה-API.
שאנחנו שוב פעם נופלים בבור של הזנת כתובת בקוד המקור. אמנם עכשיו הכתובת של ה-API אינה סוד שאנחנו לא רוצים לגלות, אבל כל השאר עדיין נכון גם כאן, קושי בהחלפת כתובות, אי יכולת לסביבות ריצה שונות ועוד.
כאן כבר אי אפשר, לכאורה, להשתמש בטריק של משתני סביבה כי ה -CDN נותן קבצים סטטיים מוכנים בלבד, וסביבת הריצה היא בלקוח, ואם צריך לרוץ משהו בלקוח זה חייב להופיע איפשהו באחד הקבצים הסטטיים.
ראשית חייבים לציין, שאם אין תהליך בילד אלא מה שאנחנו כותבים ב- HTML + CSS + JS זה מה שנותנים ללקוח, אני לא רואה דרך יפה לפתור את הבעיה.
שנית, לא משנה מה נעשה, בסוף הלקוח חשוף לכל מה ששולחים לו, ולכן לא משנה באיזה דרך נבחר, לעולם לא נשלח סודות ללקוח.
המפתחים של Vue.js פתרו את הבעיה בדרך האלגנטית והטובה ביותר, ניתן להשתמש במשתני סביבה בכל מקום בקוד, ובתהליך הבילד Vue.js יזין את הערך שיש במשנה הסביבה לתוך הקובץ הסטטי.
וכך הקובץ הסטטי יכיל את הכתובת האמיתית, אבל בקוד המקור זה לא יופיע, ובהתאמה כשנצטרך להגדיר את ה-CDN לבצע בילד, ב-CDN נזין את הכתובת המתאימה למשתנה הסביבה.
כך מצד אחד הקוד נקי, ומצד שני הלקוח מקבל את הכתובת המתאימה. וניתן מאותו הקוד להפנות לשרתים שונים בהתאם לסביבת הריצה.
נשים לב,
שמאחר ובפרקטיקה זו יכול להיווצר טעות שמתכנת ישתמש במשתנה סביבה שמכיל סוד,
ולא יבין שזה נגיש ללקוח,
מפתחי Vue.js הגבילו שרק משתנים שמתחילים
ב-VUE_APP_XXX
יכנסו לקובץ הסטטי,
ולכן בדוגמה זו נקרא למשתנה VUE_APP_API_URL
.
באנגולר אין תמיכה מובנית במשתני מערכת. מה כן יש? יש קובץ שנקרא
environment.ts
שבתוכו מחזיקים את משתני הסביבה,
וניתן להגדיר בבילד להחליף את הקובץ בקובץ אחר שבתוכו יש את אותם משתנים אבל עם ערכים שונים.
כך ניתן להגדיר משתנה production
ולשים בתוכו false
,
ובבילד של פרודקשן להחליף בקובץ שבו ערכו של production
הוא true
.
לקריאת הדוקומנטציה במלואה.
שזה פתרון נחמד אבל לנו הוא לא מספיק.
כי עדיין איפשהו יהיה צריך להיות קובץ
environment.xxx.ts
שיכיל כתובת מפורשת של ה-API
שנמצא בקוד המקור,
ושינוי שלו הוא שינוי בקוד וכל מה שראינו.
הפתרון שאני עשיתי (ייתכן ויש טובים יותר) היה ליצור סקריפט
js
שיוצר את הקובץ
environment.final.ts
ואת הערכים הוא מזין לפי משתני הסביבה.
וב-package.json
תחת prebuild
הגדרתי שהסקריפט ירוץ לפני כל קריאה לבילד.
סקריפט לדוגמה:
/** This script use to allow set API URL by system environment at build process time */
const fse = require('fs-extra');
const { API_URL } = process.env;
const defaultApiUrl = 'http://127.0.0.1:3000';
if (API_URL) {
console.log(`API URL set to be ${API_URL}`);
} else {
console.warn(`There is no 'API_URL' environment var, using ${defaultApiUrl} as default...`);
}
fse.outputFileSync('./src/environments/environment.final.ts', `
export const environment = {
production: true,
baseUrl: '${API_URL || defaultApiUrl}/API'
};
`);
וקובץ ה-package.json
:
{
"name": "my-app-name",
"version": "0.0.0",
"scripts": {
"prebuild": "node ./create-final-environment.js",
"build" : "ng build --prod"
},
"dependencies": {
},
"devDependencies": {
}
}
כך שכל פעם שמריצים
npm run build
יורץ קודם הסקריפט,
וב-angular.json
שיניתי שבפרודקשן הוא יחליף את הקובץ
environment.ts
ב-environment.final.ts
.
וב-gitignore
הוספתי את הקובץ environment.final.ts
כך שהוא לעולם לא יכנס לקוד המקור.
וכך ממשתנה סביבה בזמן בילד הלקוח יקבל את הכתובת המתאימה בהתאם לסביבת הריצה ללא שינוי בקוד.
את אותו התרגיל בשינויים מתבקשים ניתן לבצע לכל טכנולוגיית צד לקוח, ובתנאי שיש תהליך בילד כלשהוא.
ניתן גם להשתמש במנגנון משתני הסביבה של
webpack
(Vue.js מאחורי הקלעים משתמשים בזה)
גם ב-Angular
אבל זה יותר מורכב ודורש שינוי של הקונפיגורציה של
Angular
(או כל כלי דומה)
ושל webpack.
System environment variables in Angular
תודות לחוני גורליק על העזרה בעריכת המאמר.
Photo by Sarah Pflug from Burst