Sådan implementeres runtime-miljøvariabler med create-react-app, Docker og Nginx

Der er mange måder at konfigurere din React-applikation på. Lad os bruge en tilgang, der respekterer Twelve-Factor App-metoden. Dette betyder, at den håndhæver omkonfiguration under runtime. Derfor er det ikke nødvendigt med nogen opbygning pr. Miljø.

? Hvad vil vi opnå?

Vi vil være i stand til at køre vores React-applikation som en Docker-container, der er bygget en gang. Det kører overalt ved at være konfigurerbart under kørselstid. Outputtet skal være en let og performant beholder, der tjener vores React-applikation som statisk indhold, hvilket vi opnår ved hjælp af Ngnix Alpine. Vores applikation skal tillade konfiguration inden for docker-compose-fil som denne:

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

Vi skal være i stand til at konfigurere vores React-applikation ved hjælp af -eflag (miljøvariabler), når vi bruger Docker runkommandoen.

Ved første øjekast kan denne tilgang synes at give for lille fordel for det ekstra arbejde, det kræver for den første opsætning. Men når installationen er færdig, vil miljøspecifikke konfigurationer og implementering være meget nemmere at håndtere. Så for enhver, der er målrettet mod dynamiske miljøer eller bruger orkestreringssystemer, er denne tilgang bestemt noget at overveje.

? Problemet

Først og fremmest skal det være klart, at der ikke er noget som miljøvariabler inde i browsermiljøet. Uanset hvilken løsning vi bruger i dag er intet andet end en falsk abstraktion.

Men så kan du måske spørge, hvad med .envfiler og REACT_APPpræfikserede miljøvariabler, der kommer direkte fra dokumentation? Selv inde i kildekoden bruges disse som process.envligesom vi bruger miljøvariabler inde i Node.js.

I virkeligheden processeksisterer objektet ikke i browsermiljøet, det er Node-specifikt. CRA foretager som standard ikke rendering på serversiden. Det kan ikke injicere miljøvariabler under visning af indhold (som Next.js gør). Under transpilering erstatter Webpack-processen alle forekomster af process.envmed en strengværdi, der blev givet. Dette betyder, at det kun kan konfigureres i løbet af byggetiden .

? Opløsning

Det specifikke øjeblik, hvor det stadig er muligt at indsprøjte miljøvariabler, sker, når vi starter vores container. Derefter kan vi læse miljøvariabler inde fra containeren. Vi kan skrive dem i en fil, der kan serveres via Nginx (som også tjener vores React-app). De importeres til vores applikation ved hjælp af tag inden i hovedafsnittet af index.html. Så i det øjeblik kører vi et bash-script, der opretter JavaScript-fil med miljøvariabler, der er tildelt som egenskaber for det globale windowobjekt. Injiceret til at være globalt tilgængelig i vores applikation via browsermåden.

? Trin for trin guide

Lad os starte med et simpelt create-react-appprojekt og oprette .envfil med vores første miljøvariabel, som vi vil eksponere.

# 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

Lad os så skrive et lille bash-script, der læser .envfilen og udtrækker miljøvariabler, der skrives ind i filen. Hvis du indstiller en miljøvariabel inde i containeren, bruges dens værdi, ellers falder den tilbage til standardværdien fra .env-filen. Det opretter en JavaScript-fil, der sætter miljøvariabelværdier som et objekt, der tildeles som en egenskab for windowobjektet.

#!/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

Vi skal tilføje følgende linje til element inde, index.htmlsom derefter importerer filen oprettet af vores bash-script.

Lad os vise vores miljøvariabler i applikationen:

API_URL: {window._env_.API_URL}

? Udvikling

Hvis vi ikke vil bruge Docker under udviklingen, kan vi køre bash-script via npm scriptrunner ved at ændre 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'" },

Og hvis vi kører, skal yarn devvi se output som dette:

Der er to måder at omkonfigurere miljøvariabler inden for dev. Du kan enten ændre standardværdien i .envfilen eller tilsidesætte standardindstillinger ved at køre yarn devkommando med miljøvariabler forudbestemt:

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

Og endelig rediger, .gitignoreså vi udelukker miljøkonfigurationer fra kildekoden:

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

Med hensyn til udviklingsmiljøet er det det! Vi er halvvejs der. Vi har ikke gjort en enorm forskel på dette tidspunkt sammenlignet med hvad CRA tilbød som standard til udviklingsmiljøet. Det virkelige potentiale ved denne tilgang skinner i produktionen.

? Produktion

Nu skal vi oprette minimal Nginx-konfiguration, så vi kan opbygge et optimeret billede, der tjener den produktionsklare applikation.

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

Hovedkonfigurationsfilen skal se sådan ud:

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; } }

Det er også nyttigt at aktivere gzip-komprimering, så vores aktiver er lettere under netværksovergangen:

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;

Nu hvor vores Nginx-konfiguration er klar, kan vi endelig oprette Dockerfile- og docker-compose-filer:

touch Dockerfile docker-compose.yml

Oprindeligt bruger vi node:alpineimage til at skabe en optimeret produktionsopbygning af vores applikation. Derefter bygger vi et runtime-billede oven på 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;\""]

Nu er vores container klar. Vi kan gøre alle standard ting med det. Vi kan bygge en container, køre den med integrerede konfigurationer og skubbe den til et lager, der leveres af tjenester som 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

Ovenstående docker runkommando skal output applikation som sådan:

Endelig, lad os oprette vores docker-compose-fil. Du vil normalt have forskellige docker-compose-filer afhængigt af miljøet, og du vil bruge -fflag til at vælge hvilken fil du vil bruge.

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.

Hvis du har spørgsmål eller ønsker at give feedback; er du velkommen til at åbne problemet på GitHub. Følg mig eventuelt for yderligere indlæg relateret til webteknologier.