Εκατομμύρια εφαρμογές front-end διαχειρίζονται εκδόσεις ειδικά για το περιβάλλον. Για κάθε περιβάλλον —είτε πρόκειται για ανάπτυξη, σκηνοθεσία ή παραγωγή— πρέπει να δημιουργηθεί μια ξεχωριστή έκδοση της εφαρμογής frontend και να ρυθμιστούν οι κατάλληλες μεταβλητές περιβάλλοντος. Ο αριθμός των εκδόσεων πολλαπλασιάζεται εάν εμπλέκονται πολλές εφαρμογές, αυξάνοντας την απογοήτευση. Αυτό ήταν ένα κοινό πρόβλημα εδώ και πολύ καιρό, αλλά υπάρχει καλύτερος τρόπος χειρισμού μεταβλητών περιβάλλοντος. Βρήκα έναν τρόπο να απλοποιήσω αυτήν τη διαδικασία και σε αυτό το άρθρο θα σας καθοδηγήσω βήμα προς βήμα για να δημιουργήσετε μια αποτελεσματική διαδικασία που θα μειώσει τους χρόνους κατασκευής και θα σας βοηθήσει να διασφαλίσετε τη συνέπεια μεταξύ των περιβαλλόντων στα έργα σας.
Πριν ξεκινήσουμε, νομίζω ότι πρέπει να κάνουμε μια ανακεφαλαίωση. Οι εφαρμογές Ιστού βασίζονται σχεδόν πάντα σε μεταβλητές γνωστές ως "μεταβλητές περιβάλλοντος ", οι οποίες συχνά περιλαμβάνουν εσωτερικά τελικά σημεία συστήματος, συστήματα ενοποίησης, κλειδιά συστήματος πληρωμών, αριθμούς έκδοσης κ.λπ. Φυσικά, οι τιμές αυτών των μεταβλητών διαφέρουν ανάλογα με το περιβάλλον στο οποίο αναπτύσσεται η εφαρμογή.
Για παράδειγμα, φανταστείτε μια εφαρμογή που αλληλεπιδρά με μια πύλη πληρωμής. Στο περιβάλλον ανάπτυξης, η διεύθυνση URL της πύλης πληρωμής μπορεί να οδηγεί σε ένα sandbox για δοκιμή (https://sandbox.paymentgateway.com), ενώ στο περιβάλλον παραγωγής, οδηγεί στη ζωντανή υπηρεσία (https://live.paymentgateway.com ). Ομοίως, διαφορετικά κλειδιά API ή οποιαδήποτε άλλη συγκεκριμένη ρύθμιση περιβάλλοντος χρησιμοποιούνται για κάθε περιβάλλον για να διασφαλιστεί η ασφάλεια των δεδομένων και να αποφευχθεί η ανάμειξη των περιβαλλόντων.
Κατά τη δημιουργία εφαρμογών υποστήριξης , αυτό δεν αποτελεί πρόβλημα. Η δήλωση αυτών των μεταβλητών στον κώδικα της εφαρμογής είναι αρκετή, καθώς οι τιμές αυτών των μεταβλητών αποθηκεύονται στο περιβάλλον διακομιστή όπου αναπτύσσεται το backend. Με αυτόν τον τρόπο, η εφαρμογή υποστήριξης αποκτά πρόσβαση σε αυτά κατά την εκκίνηση.
Ωστόσο, με τις εφαρμογές frontend τα πράγματα γίνονται κάπως πιο περίπλοκα. Δεδομένου ότι εκτελούνται στο πρόγραμμα περιήγησης του χρήστη, δεν έχουν πρόσβαση σε συγκεκριμένες τιμές μεταβλητών περιβάλλοντος. Για να αντιμετωπιστεί αυτό, οι τιμές αυτών των μεταβλητών τυπικά "ψήνονται" στην εφαρμογή frontend κατά το χρόνο δημιουργίας. Με αυτόν τον τρόπο, όταν η εφαρμογή εκτελείται στο πρόγραμμα περιήγησης του χρήστη, όλες οι απαραίτητες τιμές είναι ήδη ενσωματωμένες στην εφαρμογή frontend.
Αυτή η προσέγγιση, όπως πολλές άλλες, συνοδεύεται από μια προειδοποίηση: πρέπει να δημιουργήσετε μια ξεχωριστή έκδοση της ίδιας εφαρμογής frontend για κάθε περιβάλλον, έτσι ώστε κάθε έκδοση να περιέχει τις αντίστοιχες τιμές της.
Για παράδειγμα , ας υποθέσουμε ότι έχουμε τρία περιβάλλοντα:
Ανάπτυξη για εσωτερικές δοκιμές·
στάδιο για τη δοκιμή ολοκλήρωσης·
και παραγωγή για τους πελάτες.
Για να υποβάλετε την εργασία σας για δοκιμή, δημιουργείτε την εφαρμογή και την αναπτύσσετε στο περιβάλλον ανάπτυξης. Μετά την ολοκλήρωση των εσωτερικών δοκιμών, πρέπει να δημιουργήσετε ξανά την εφαρμογή για να την αναπτύξετε στη σκηνή και, στη συνέχεια, να την δημιουργήσετε ξανά για ανάπτυξη στην παραγωγή. Εάν το έργο περιέχει περισσότερες από μία εφαρμογές front-end, ο αριθμός τέτοιων εκδόσεων αυξάνεται σημαντικά. Επιπλέον, μεταξύ αυτών των εκδόσεων, η βάση κώδικα δεν αλλάζει — η δεύτερη και η τρίτη έκδοση βασίζονται στον ίδιο πηγαίο κώδικα.
Όλα αυτά καθιστούν τη διαδικασία απελευθέρωσης ογκώδη, αργή και δαπανηρή, καθώς και κίνδυνο διασφάλισης ποιότητας. Ίσως το build να ήταν καλά δοκιμασμένο στο περιβάλλον ανάπτυξης, αλλά το στάδιο κατασκευής είναι τεχνικά νέο, πράγμα που σημαίνει ότι πλέον υπάρχει νέα πιθανότητα λάθους.
Ένα παράδειγμα: Έχετε δύο εφαρμογές με χρόνους κατασκευής X και Y δευτερόλεπτα. Για αυτά τα τρία περιβάλλοντα, και οι δύο εφαρμογές θα χρειάζονταν 3X + 3Y σε χρόνο κατασκευής. Ωστόσο, εάν μπορούσατε να δημιουργήσετε κάθε εφαρμογή μόνο μία φορά και να χρησιμοποιήσετε την ίδια έκδοση σε όλα τα περιβάλλοντα, ο συνολικός χρόνος θα μειωνόταν σε μόλις X + Y δευτερόλεπτα, τριπλασιάζοντας τον χρόνο κατασκευής.
Αυτό μπορεί να κάνει μεγάλη διαφορά στους αγωγούς frontend, όπου οι πόροι είναι περιορισμένοι και οι χρόνοι κατασκευής μπορεί να κυμαίνονται από λίγα λεπτά έως πολύ περισσότερο από μία ώρα. Το ζήτημα είναι παρόν σχεδόν σε κάθε εφαρμογή διεπαφής παγκοσμίως και συχνά δεν υπάρχει τρόπος επίλυσής του. Ωστόσο, αυτό είναι ένα σοβαρό πρόβλημα, ειδικά από επιχειρηματική σκοπιά.
Δεν θα ήταν υπέροχο αν αντί να δημιουργήσετε τρεις ξεχωριστές εκδόσεις, μπορούσατε απλώς να δημιουργήσετε ένα και να το αναπτύξετε σε όλα τα περιβάλλοντα; Λοιπόν, βρήκα έναν τρόπο να κάνω ακριβώς αυτό.
Ρύθμιση μεταβλητών περιβάλλοντος
Αρχικά, πρέπει να δημιουργήσετε ένα αρχείο στο αποθετήριο του έργου σας στο frontend όπου θα παρατίθενται οι απαιτούμενες μεταβλητές περιβάλλοντος. Αυτά θα χρησιμοποιηθούν από τον προγραμματιστή τοπικά. Συνήθως, αυτό το αρχείο ονομάζεται .env.local
, το οποίο μπορούν να διαβάσουν τα περισσότερα σύγχρονα πλαίσια frontend. Ακολουθεί ένα παράδειγμα τέτοιου αρχείου:
CLIENT_ID='frontend-development' API_URL=/api/v1' PUBLIC_URL='/' COMMIT_SHA=''
Σημείωση: διαφορετικά πλαίσια απαιτούν διαφορετικές συμβάσεις ονομασίας για μεταβλητές περιβάλλοντος. Για παράδειγμα, στο React, πρέπει να προσαρτήσετε REACT_APP_
στα ονόματα των μεταβλητών. Αυτό το αρχείο δεν χρειάζεται απαραίτητα να περιλαμβάνει μεταβλητές που επηρεάζουν άμεσα την εφαρμογή. μπορεί επίσης να περιέχει χρήσιμες πληροφορίες εντοπισμού σφαλμάτων. Πρόσθεσα τη μεταβλητή COMMIT_SHA
, την οποία αργότερα θα τραβήξουμε από την εργασία κατασκευής για να παρακολουθήσουμε τη δέσμευση στην οποία βασίστηκε αυτή η κατασκευή.
Στη συνέχεια, δημιουργήστε ένα αρχείο που ονομάζεται environment.js
, όπου μπορείτε να ορίσετε ποιες μεταβλητές περιβάλλοντος χρειάζεστε. Το πλαίσιο frontend θα τα εγχύσει για εσάς. Για το React, για παράδειγμα, αποθηκεύονται στο αντικείμενο process.env
:
const ORIGIN_ENVIRONMENTS = window.ORIGIN_ENVIRONMENTS = { CLIENT_ID: process.env.CLIENT_ID, API_URL: process.env.API_URL, PUBLIC_URL: process.env.PUBLIC_URL, COMMIT_SHA: process.env.COMMIT_SHA }; export const ENVIRONMENT = { clientId: ORIGIN_ENVIRONMENTS.CLIENT_ID, apiUrl: ORIGIN_ENVIRONMENTS.API_URL, publicUrl: ORIGIN_ENVIRONMENTS.PUBLIC_URL ?? "/", commitSha: ORIGIN_ENVIRONMENTS.COMMIT_SHA, };
Εδώ, ανακτάτε όλες τις αρχικές τιμές για τις μεταβλητές στο αντικείμενο window.ORIGIN_ENVIRONMENTS
, το οποίο σας επιτρέπει να τις προβάλλετε στην κονσόλα του προγράμματος περιήγησης. Επιπλέον, πρέπει να τα αντιγράψετε στο αντικείμενο ENVIRONMENT
, όπου μπορείτε επίσης να ορίσετε ορισμένες προεπιλογές, για παράδειγμα: υποθέτουμε ότι publicUrl
είναι / από προεπιλογή. Χρησιμοποιήστε το αντικείμενο ENVIRONMENT
όπου χρειάζονται αυτές οι μεταβλητές στην εφαρμογή.
Σε αυτό το στάδιο, έχετε εκπληρώσει όλες τις ανάγκες για τοπική ανάπτυξη. Αλλά ο στόχος είναι να χειριστείτε διαφορετικά περιβάλλοντα.
Για να το κάνετε αυτό, δημιουργήστε ένα αρχείο .env
με το ακόλουθο περιεχόμενο:
CLIENT_ID='<client_id>' API_URL='<api_url>' PUBLIC_URL='<public_url>' COMMIT_SHA=$COMMIT_SHA
Σε αυτό το αρχείο, θα θελήσετε να καθορίσετε σύμβολα κράτησης θέσης για τις μεταβλητές που εξαρτώνται από το περιβάλλον. Μπορούν να είναι οτιδήποτε θέλετε, αρκεί να είναι μοναδικά και να μην επικαλύπτονται με κανέναν τρόπο με τον πηγαίο κώδικα σας. Για επιπλέον σιγουριά, μπορείτε ακόμη και να χρησιμοποιήσετε
Για αυτές τις μεταβλητές που δεν αλλάζουν σε περιβάλλοντα (π.χ. το commit hash), μπορείτε είτε να γράψετε τις πραγματικές τιμές απευθείας είτε να χρησιμοποιήσετε τιμές που θα είναι διαθέσιμες κατά την εργασία δημιουργίας (όπως $COMMIT_SHA ). Το πλαίσιο διεπαφής θα αντικαταστήσει αυτά τα σύμβολα κράτησης θέσης με πραγματικές τιμές κατά τη διαδικασία δημιουργίας:
Αρχείο
Τώρα έχετε την ευκαιρία να βάλετε πραγματικές αξίες αντί των placeholders. Για να το κάνετε αυτό, δημιουργήστε ένα αρχείο, inject.py
(Επέλεξα Python, αλλά μπορείτε να χρησιμοποιήσετε οποιοδήποτε εργαλείο για αυτόν τον σκοπό), το οποίο θα πρέπει πρώτα να περιέχει μια αντιστοίχιση των θέσεων κράτησης θέσης σε ονόματα μεταβλητών:
replacement_map = { "<client_id>": "CLIENT_ID", "<api_url>": "API_URL", "<public_url>": "PUBLIC_URL", "%3Cpublic_url%3E": "PUBLIC_URL" }
Σημειώστε ότι public_url
παρατίθεται δύο φορές και η δεύτερη καταχώρηση έχει διαφύγει αγκύλες. Το χρειάζεστε για όλες τις μεταβλητές που χρησιμοποιούνται σε αρχεία CSS και HTML.
base_path = 'usr/share/nginx/html' target_files = [ f'{base_path}/static/js/main.*.js', f'{base_path}/static/js/chunk.*.js', f'{base_path}/static/css/main.*.css', f'{base_path}/static/css/chunk.*.css', f'{base_path}/index.html' ]
injector.py
, όπου θα λάβουμε την αντιστοίχιση και τη λίστα των αρχείων τεχνουργημάτων κατασκευής (όπως αρχεία JS, HTML και CSS) και θα αντικαταστήσουμε τα σύμβολα κράτησης θέσης με τις τιμές των μεταβλητών από το τρέχον περιβάλλον μας:
import os import glob def inject_envs(filename, replacement_map): with open(filename) as r: lines = r.read() for key, value in replacement_map.items(): lines = lines.replace(key, os.environ.get(value) or '') with open(filename, "w") as w: w.write(lines) def inject(target_files, replacement_map, base_path): for target_file in target_files: for filename in glob.glob(target_file.glob): inject_envs(filename, replacement_map)
Και στη συνέχεια, στο αρχείο inject.py
, προσθέστε αυτήν τη γραμμή (μην ξεχάσετε να εισαγάγετε injector.py
):
injector.inject(target_files, replacement_map, base_path)
inject.py
εκτελείται μόνο κατά την ανάπτυξη. Μπορείτε να το προσθέσετε στο Dockerfile
στην εντολή CMD
αφού εγκαταστήσετε την Python και αντιγράψετε όλα τα τεχνουργήματα: RUN apk add python3 COPY nginx/default.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/ci /ci COPY --from=build /app/build /usr/share/nginx/html CMD ["/bin/sh", "-c", "python3 ./ci/inject.py && nginx -g 'daemon off;'"]That's it! This way, during each deployment, the pre-built files will be used, with variables specific to the deployment environment injected into them.
Αυτό είναι όλο! Με αυτόν τον τρόπο, κατά τη διάρκεια κάθε ανάπτυξης, θα χρησιμοποιούνται τα προκατασκευασμένα αρχεία, με μεταβλητές συγκεκριμένες για το περιβάλλον ανάπτυξης να εισάγονται σε αυτά.
Αρχείο:
Ένα πράγμα - εάν τα τεχνουργήματα κατασκευής σας περιλαμβάνουν κατακερματισμό περιεχομένου στα ονόματα αρχείων τους, αυτή η ένεση δεν θα επηρεάσει τα ονόματα αρχείων και αυτό θα μπορούσε να προκαλέσει προβλήματα με την προσωρινή αποθήκευση του προγράμματος περιήγησης. Για να το διορθώσετε αυτό, αφού τροποποιήσετε τα αρχεία με μεταβλητές που έχουν εισαχθεί, θα χρειαστεί να:
Για να το εφαρμόσετε, προσθέστε μια εισαγωγή βιβλιοθήκης κατακερματισμού ( import hashlib
) και τις ακόλουθες συναρτήσεις στο αρχείο inject.py
.
def sha256sum(filename): h = hashlib.sha256() b = bytearray(128 * 1024) mv = memoryview(b) with open(filename, 'rb', buffering=0) as f: while n := f.readinto(mv): h.update(mv[:n]) return h.hexdigest() def replace_filename_imports(filename, new_filename, base_path): allowed_extensions = ('.html', '.js', '.css') for path, dirc, files in os.walk(base_path): for name in files: current_filename = os.path.join(path, name) if current_filename.endswith(allowed_extensions): with open(current_filename) as f: s = f.read() s = s.replace(filename, new_filename) with open(current_filename, "w") as f: f.write(s) def rename_file(fullfilename): dirname = os.path.dirname(fullfilename) filename, ext = os.path.splitext(os.path.basename(fullfilename)) digest = sha256sum(fullfilename) new_filename = f'{filename}.{digest[:8]}' new_fullfilename = f'{dirname}/{new_filename}{ext}' os.rename(fullfilename, new_fullfilename) return filename, new_filename
Ωστόσο, δεν χρειάζεται να μετονομαστούν όλα τα αρχεία. Για παράδειγμα, το όνομα αρχείου index.html
πρέπει να παραμείνει αμετάβλητο και για να το πετύχετε αυτό, δημιουργήστε μια κλάση TargetFile
που θα αποθηκεύει μια σημαία που θα υποδεικνύει εάν είναι απαραίτητη η μετονομασία:
class TargetFile: def __init__(self, glob, should_be_renamed = True): self.glob = glob self.should_be_renamed = should_be_renamed
Τώρα πρέπει απλώς να αντικαταστήσετε τη συστοιχία διαδρομών αρχείων στο inject.py
με μια σειρά αντικειμένων κλάσης TargetFile
:
target_files = [ injector.TargetFile(f'{base_path}/static/js/main.*.js'), injector.TargetFile(f'{base_path}/static/js/chunk.*.js'), injector.TargetFile(f'{base_path}/static/css/main.*.css'), injector.TargetFile(f'{base_path}/static/css/chunk.*.css'), injector.TargetFile(f'{base_path}/index.html', False) ]
Και ενημερώστε τη συνάρτηση inject
στο injector.py
ώστε να συμπεριλάβει τη μετονομασία του αρχείου εάν έχει οριστεί η σημαία:
def inject(target_files, replacement_map, base_path): for target_file in target_files: for filename in glob.glob(target_file.glob): inject_envs(filename, replacement_map) if target_file.should_be_renamed: filename, new_filename = rename_file(filename) replace_filename_imports(filename, new_filename, base_path)
Ως αποτέλεσμα, τα αρχεία τεχνουργημάτων θα ακολουθούν αυτήν τη μορφή ονομασίας: <origin-file-name>
. <injection-hash>
. <extension>
.
Όνομα αρχείου πριν από την ένεση:
Όνομα αρχείου μετά την ένεση:
Οι ίδιες μεταβλητές περιβάλλοντος αποδίδουν το ίδιο όνομα αρχείου, επιτρέποντας στο πρόγραμμα περιήγησης του χρήστη να αποθηκεύσει σωστά το αρχείο προσωρινά. Υπάρχει πλέον εγγύηση ότι οι σωστές τιμές αυτών των μεταβλητών θα αποθηκευτούν στην κρυφή μνήμη του προγράμματος περιήγησης, ως αποτέλεσμα – καλύτερη απόδοση για τον πελάτη.
Η παραδοσιακή προσέγγιση της δημιουργίας ξεχωριστών κατασκευών για κάθε περιβάλλον έχει οδηγήσει σε μερικές κρίσιμες αναποτελεσματικότητα, που μπορεί να είναι ένα ζήτημα για ομάδες με περιορισμένους πόρους.
Τώρα έχετε ένα σχέδιο για μια διαδικασία έκδοσης που μπορεί να επιλύσει παρατεταμένους χρόνους ανάπτυξης, υπερβολικές εκδόσεις και αυξημένους κινδύνους όσον αφορά τη διασφάλιση ποιότητας για εφαρμογές frontend – όλα αυτά. Όλα αυτά εισάγοντας ένα νέο επίπεδο εγγυημένης συνέπειας σε όλα τα περιβάλλοντα.
Αντί να χρειάζεστε N builds, θα χρειαστείτε μόνο ένα. Για την επερχόμενη έκδοση, μπορείτε απλώς να αναπτύξετε την έκδοση που έχει ήδη δοκιμαστεί, η οποία βοηθά επίσης στην επίλυση πιθανών προβλημάτων σφαλμάτων, καθώς η ίδια έκδοση θα χρησιμοποιηθεί σε όλα τα περιβάλλοντα. Επιπλέον, η ταχύτητα εκτέλεσης αυτού του σεναρίου είναι ασύγκριτα ταχύτερη ακόμη και από την πιο βελτιστοποιημένη έκδοση. Για παράδειγμα, τα τοπικά σημεία αναφοράς σε ένα MacBook 14 PRO, M1, 32 GB είναι τα εξής:
Η προσέγγισή μου απλοποιεί τη διαδικασία έκδοσης, διατηρεί την απόδοση της εφαρμογής επιτρέποντας αποτελεσματικές στρατηγικές προσωρινής αποθήκευσης και διασφαλίζει ότι τα σφάλματα που σχετίζονται με την έκδοση δεν θα εισέλθουν στα περιβάλλοντα. Επιπλέον, όλος ο χρόνος και η προσπάθεια που ξοδεύονταν προηγουμένως σε κουραστικές εργασίες κατασκευής μπορούν τώρα να επικεντρωθούν στη δημιουργία μιας ακόμα καλύτερης εμπειρίας χρήστη. Τι να μην αγαπάς;
Διασφαλίζουμε ότι τα σφάλματα που σχετίζονται με την κατασκευή δεν γλιστρούν στην εφαρμογή για άλλα περιβάλλοντα. Μπορεί να υπάρχουν σφάλματα φαντασμάτων που εμφανίζονται λόγω ατελειών στα συστήματα κατασκευής. Οι πιθανότητες είναι ελάχιστες, αλλά υπάρχουν.