Πώς να εφαρμόσετε μεταβλητές περιβάλλοντος χρόνου εκτέλεσης με το create-react-app, το Docker και το Nginx

Υπάρχουν πολλοί τρόποι διαμόρφωσης της εφαρμογής React. Ας χρησιμοποιήσουμε μια προσέγγιση που σέβεται τη μεθοδολογία App δώδεκα παραγόντων. Αυτό σημαίνει ότι επιβάλλει τη διαμόρφωση κατά τη διάρκεια του χρόνου εκτέλεσης. Επομένως, δεν απαιτείται κατασκευή ανά περιβάλλον.

; Τι θέλουμε να πετύχουμε;

Θέλουμε να μπορούμε να εκτελέσουμε την εφαρμογή React ως κοντέινερ Docker που έχει κατασκευαστεί μία φορά. Τρέχει παντού με δυνατότητα ρύθμισης κατά τη διάρκεια του χρόνου εκτέλεσης. Η έξοδος πρέπει να είναι ένα ελαφρύ και ερμηνευτικό δοχείο που εξυπηρετεί την εφαρμογή React ως στατικό περιεχόμενο, το οποίο επιτυγχάνουμε χρησιμοποιώντας το Ngnix Alpine. Η εφαρμογή μας θα πρέπει να επιτρέπει τη διαμόρφωση σε αρχείο σύνθεσης docker όπως αυτό:

version: "3.2" services: my-react-app: image: my-react-app ports: - "3000:80" environment: - "API_URL=//production.example.com"

Θα πρέπει να είμαστε σε θέση να διαμορφώσουμε την εφαρμογή React χρησιμοποιώντας -eflag (μεταβλητές περιβάλλοντος) όταν χρησιμοποιούμε Docker runεντολή.

Με την πρώτη ματιά, αυτή η προσέγγιση μπορεί να φαίνεται να αποφέρει πολύ μικρό όφελος για την επιπλέον εργασία που απαιτείται για την αρχική εγκατάσταση. Αλλά μόλις ολοκληρωθεί η ρύθμιση, οι διαμορφώσεις και η ανάπτυξη συγκεκριμένων για το περιβάλλον θα είναι πολύ πιο εύκολο να χειριστούν. Έτσι, για όποιον στοχεύει δυναμικά περιβάλλοντα ή χρησιμοποιεί συστήματα ενορχήστρωσης, αυτή η προσέγγιση είναι σίγουρα κάτι που πρέπει να εξεταστεί.

; Το πρόβλημα

Πρώτα απ 'όλα, πρέπει να είναι σαφές ότι δεν υπάρχουν μεταβλητές περιβάλλοντος μέσα στο περιβάλλον του προγράμματος περιήγησης. Όποια λύση και αν χρησιμοποιούμε σήμερα δεν είναι παρά ψεύτικη αφαίρεση.

Αλλά, τότε μπορείτε να ρωτήσετε, τι γίνεται με τα .envαρχεία και τις REACT_APPπροκαταρκτικές μεταβλητές περιβάλλοντος που προέρχονται κατευθείαν από την τεκμηρίωση; Ακόμα και μέσα στον πηγαίο κώδικα, αυτά χρησιμοποιούνται όπως process.envακριβώς χρησιμοποιούμε μεταβλητές περιβάλλοντος στο Node.js.

Στην πραγματικότητα, το αντικείμενο processδεν υπάρχει μέσα στο περιβάλλον του προγράμματος περιήγησης, είναι συγκεκριμένο για κόμβο. Το CRA από προεπιλογή δεν κάνει απόδοση από διακομιστή. Δεν μπορεί να εισάγει μεταβλητές περιβάλλοντος κατά την προβολή περιεχομένου (όπως το Next.js). Κατά τη μεταφορά , η διαδικασία Webpack αντικαθιστά όλες τις εμφανίσεις process.envμε μια τιμή συμβολοσειράς που δόθηκε. Αυτό σημαίνει ότι μπορεί να διαμορφωθεί μόνο κατά το χρόνο κατασκευής .

; Λύση

Η συγκεκριμένη στιγμή που είναι ακόμα δυνατή η έγχυση μεταβλητών περιβάλλοντος συμβαίνει όταν ξεκινάμε το κοντέινερ μας. Στη συνέχεια μπορούμε να διαβάσουμε μεταβλητές περιβάλλοντος από το εσωτερικό του κοντέινερ. Μπορούμε να τα γράψουμε σε ένα αρχείο που μπορεί να σερβιριστεί μέσω του Nginx (το οποίο εξυπηρετεί επίσης την εφαρμογή React). Εισάγονται στην εφαρμογή μας χρησιμοποιώντας ετικέτα στο τμήμα κεφαλίδας του index.html. Έτσι, εκείνη τη στιγμή, εκτελούμε ένα bash script που δημιουργεί ένα αρχείο JavaScript με μεταβλητές περιβάλλοντος που έχουν εκχωρηθεί ως ιδιότητες του καθολικού windowαντικειμένου. Ένεση για να είναι παγκοσμίως διαθέσιμη στην εφαρμογή μας με τον τρόπο του προγράμματος περιήγησης.

; Βήμα προς βήμα οδηγό

Ας ξεκινήσουμε με ένα απλό create-react-appέργο και να δημιουργήσουμε ένα .envαρχείο με την πρώτη μεταβλητή περιβάλλοντος που θέλουμε να εκθέσουμε.

# Generate React App create-react-app cra-runtime-environment-variables cd cra-runtime-environment-variables # Create default environment variables that we want to use touch .env echo "API_URL=https//default.dev.api.com" >> .env

Στη συνέχεια, ας γράψουμε ένα μικρό σενάριο bash το οποίο θα διαβάσει .envαρχείο και θα εξαγάγει μεταβλητές περιβάλλοντος που θα γραφτούν στο αρχείο. Εάν ορίσετε μια μεταβλητή περιβάλλοντος μέσα στο κοντέινερ, θα χρησιμοποιηθεί η τιμή της, διαφορετικά θα επιστρέψει στην προεπιλεγμένη τιμή από το αρχείο .env. Θα δημιουργήσει ένα αρχείο JavaScript το οποίο θέτει τιμές μεταβλητής περιβάλλοντος ως αντικείμενο που έχει εκχωρηθεί ως ιδιοκτησία windowαντικειμένου.

#!/bin/bash # Recreate config file rm -rf ./env-config.js touch ./env-config.js # Add assignment echo "window._env_ = {" >> ./env-config.js # Read each line in .env file # Each line represents key=value pairs while read -r line || [[ -n "$line" ]]; do # Split env variables by character `=` if printf '%s\n' "$line" | grep -q -e '='; then varname=$(printf '%s\n' "$line" | sed -e 's/=.*//') varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//') fi # Read value of current variable if exists as Environment variable value=$(printf '%s\n' "${!varname}") # Otherwise use value from .env file [[ -z $value ]] && value=${varvalue} # Append configuration property to JS file echo " $varname: \"$value\"," >> ./env-config.js done > ./env-config.js

Πρέπει να προσθέσουμε την ακόλουθη γραμμή στο στοιχείο index.htmlπου εισάγει στη συνέχεια το αρχείο που δημιουργήθηκε από το σενάριο bash.

Ας εμφανίσουμε τη μεταβλητή περιβάλλοντος στην εφαρμογή:

API_URL: {window._env_.API_URL}

; Ανάπτυξη

Κατά τη διάρκεια της ανάπτυξης, εάν δεν θέλουμε να χρησιμοποιήσουμε το Docker, μπορούμε να εκτελέσουμε το σενάριο bash μέσω του npm scriptδρομέα τροποποιώντας package.json:

 "scripts": { "dev": "chmod +x ./env.sh && ./env.sh && cp env-config.js ./public/ && react-scripts start", "test": "react-scripts test", "eject": "react-scripts eject", "build": "react-scripts build'" },

Και αν τρέξουμε yarn devθα πρέπει να δούμε έξοδο ως εξής:

There are two ways to reconfigure environment variables within dev. Either change the default value inside .env file or override defaults by running yarn devcommand with environment variables prepended:

API_URL=//my.new.dev.api.com yarn dev

And finally, edit .gitignore so that we exclude environment configurations out of the source code:

# Temporary env files /public/env-config.js env-config.js

As for the development environment, that’s it! We are half-way there. We haven’t made a huge difference at this point compared to what CRA offered by default for the development environment. The true potential of this approach shines in production.

? Production

Now we are going to create minimal Nginx configuration so that we can build an optimized image which serves the production-ready application.

# Create directory for Ngnix configuration mkdir -p conf/conf.d touch conf/conf.d/default.conf conf/conf.d/gzip.conf

The main configuration file should look somewhat like this:

server { listen 80; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; expires -1; # Set it to different value depending on your standard requirements } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }

It’s also useful to enable gzip compression so that our assets are more lightweight during network transition:

gzip on; gzip_http_version 1.0; gzip_comp_level 5; # 1-9 gzip_min_length 256; gzip_proxied any; gzip_vary on; # MIME-types gzip_types application/atom+xml application/javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component;

Now that our Nginx configuration is ready, we can finally create Dockerfile and docker-compose files:

touch Dockerfile docker-compose.yml

Initially, we use node:alpine image to create an optimized production build of our application. Then, we build a runtime image on top of nginx:alpine .

# => Build container FROM node:alpine as builder WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn COPY . . RUN yarn build # => Run container FROM nginx:1.15.2-alpine # Nginx config RUN rm -rf /etc/nginx/conf.d COPY conf /etc/nginx # Static build COPY --from=builder /app/build /usr/share/nginx/html/ # Default port exposure EXPOSE 80 # Copy .env file and shell script to container WORKDIR /usr/share/nginx/html COPY ./env.sh . COPY .env . # Add bash RUN apk add --no-cache bash # Make our shell script executable RUN chmod +x env.sh # Start Nginx server CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]

Now our container is ready. We can do all the standard stuff with it. We can build a container, run it with inline configurations and push it to a repository provided by services such as Dockerhub.

docker build . -t kunokdev/cra-runtime-environment-variables docker run -p 3000:80 -e API_URL=//staging.api.com -t kunokdev/cra-runtime-environment-variables docker push -t kunokdev/cra-runtime-environment-variables

The above docker run command should output application like so:

Lastly, let’s create our docker-compose file. You will usually have different docker-compose files depending on the environment and you will use -f flag to select which file to use.

version: "3.2" services: cra-runtime-environment-variables: image: kunokdev/cra-runtime-environment-variables ports: - "5000:80" environment: - "API_URL=production.example.com"

And if we do docker-compose up we should see output like so:

Great! We have now achieved our goal. We can reconfigure our application easily in both development and production environments in a very convenient way. We can now finally build only once and run everywhere!

If you got stuck or have additional ideas, access the source code on GitHub.

? Next steps

The current implementation of the shell script will print all variables included within the .env file. Most of the time we don’t want to expose all of them. You could implement filters for variables you don’t want to expose using prefixes or a similar technique.

? Alternative solutions

As noted above, the build time configuration will satisfy most use cases. You can rely on the default approach using .env file per environment and build a container for each environment and inject values via CRA Webpack provided environment variables.

You could also have a look at this CRA GitHub repository issue which covers this problem. By now, there should be more posts and issues which cover this topic. Each offers a similar solution as above. It’s up to you to decide how are you going to implement specific details. You might use Node.js to serve your application which means that you can also replace shells script with Node.js script. Note that Nginx is more convenient to serve static content.

Εάν έχετε οποιεσδήποτε ερωτήσεις ή θέλετε να δώσετε σχόλια. μη διστάσετε να ανοίξετε το ζήτημα στο GitHub. Προαιρετικά, ακολουθήστε με για περισσότερες δημοσιεύσεις που σχετίζονται με τεχνολογίες ιστού