Single-page Application (SPA) คือชุดของไฟล์ JavaScript และ HTML แบบคงที่ รวมถึงรูปภาพและทรัพยากรอื่นๆ เนื่องจากไม่มีการเปลี่ยนแปลงแบบไดนามิก การเผยแพร่ทางออนไลน์จึงเป็นเรื่องง่ายมาก มีบริการราคาถูกและฟรีจำนวนมากสำหรับสิ่งนี้ เริ่มต้นด้วย GitHub Pages ธรรมดา (และสำหรับบางคนถึงกับ narod.ru) และลงท้ายด้วย CDN เช่น Amazon S3 อย่างไรก็ตาม ฉันต้องการอย่างอื่น
ฉันต้องการอิมเมจ Docker พร้อม SPA เพื่อให้สามารถเปิดใช้งานได้อย่างง่ายดายทั้งในการผลิตโดยเป็นส่วนหนึ่งของคลัสเตอร์ Kubernetes และบนเครื่องของนักพัฒนาแบ็คเอนด์ที่ไม่รู้ว่า SPA คืออะไร
ฉันได้กำหนดข้อกำหนดเกี่ยวกับรูปภาพต่อไปนี้สำหรับตัวเองแล้ว:
- ใช้งานง่าย (แต่ไม่ใช่การประกอบ)
- ขนาดต่ำสุดทั้งในแง่ของดิสก์และ RAM
- การกำหนดค่าผ่านตัวแปรสภาพแวดล้อมเพื่อให้ภาพสามารถใช้ในสภาพแวดล้อมที่แตกต่างกัน
- การกระจายไฟล์ที่มีประสิทธิภาพสูงสุด
วันนี้ฉันจะบอกคุณว่า:
- ลำไส้ nginx;
- สร้าง brotli จากแหล่งที่มา
- สอนไฟล์สแตติกเพื่อทำความเข้าใจตัวแปรสภาพแวดล้อม
- และแน่นอนว่าจะประกอบอิมเมจ Docker จากทั้งหมดนี้ได้อย่างไร
วัตถุประสงค์ของบทความนี้คือเพื่อแบ่งปันประสบการณ์ของฉันและกระตุ้นให้สมาชิกในชุมชนที่มีประสบการณ์วิพากษ์วิจารณ์อย่างสร้างสรรค์
การสร้างภาพเพื่อการประกอบ
หากต้องการทำให้อิมเมจ Docker สุดท้ายมีขนาดเล็ก คุณต้องปฏิบัติตามกฎสองข้อ: ขั้นต่ำของเลเยอร์และอิมเมจพื้นฐานที่เรียบง่าย อิมเมจพื้นฐานที่เล็กที่สุดคืออิมเมจ Alpine Linux ดังนั้นนั่นคือสิ่งที่ฉันจะเลือก บางคนอาจแย้งว่าเทือกเขาอัลไพน์ไม่เหมาะสำหรับการผลิต และอาจพูดถูก แต่โดยส่วนตัวแล้วฉันไม่เคยมีปัญหากับเขาและไม่มีข้อโต้แย้งกับเขา
หากต้องการให้มีเลเยอร์น้อยลง ฉันจะประกอบภาพเป็น 2 ขั้นตอน อย่างแรกคือแบบร่าง ยูทิลิตี้เสริมและไฟล์ชั่วคราวทั้งหมดจะยังคงอยู่ในนั้น และในขั้นตอนสุดท้าย ฉันจะเขียนเฉพาะเวอร์ชันสุดท้ายของแอปพลิเคชันเท่านั้น
เริ่มจากภาพเสริมกันก่อน
ในการคอมไพล์แอปพลิเคชัน SPA คุณมักจะต้องมี node.js ฉันจะถ่ายภาพอย่างเป็นทางการซึ่งมาพร้อมกับผู้จัดการแพ็คเกจ npm และเส้นด้ายด้วย ในนามของฉันเอง ฉันจะเพิ่ม node-gyp ซึ่งจำเป็นในการสร้างแพ็คเกจ npm และคอมเพรสเซอร์ Brotli จาก Google ซึ่งจะมีประโยชน์สำหรับเราในภายหลัง
Dockerfile พร้อมความคิดเห็น
# Базовый образ
FROM node:12-alpine
LABEL maintainer="Aleksey Maydokin <[email protected]>"
ENV BROTLI_VERSION 1.0.7
# Пакеты, которые нужны, чтобы собрать из исходников Brotli
RUN apk add --no-cache --virtual .build-deps
bash
gcc
libc-dev
make
linux-headers
cmake
curl
&& mkdir -p /usr/src
# Исходники Brotli скачиваем из официального репозитория
&& curl -LSs https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src
&& cd /usr/src/brotli-$BROTLI_VERSION
# Компилируем Brotli
&& ./configure-cmake --disable-debug && make -j$(getconf _NPROCESSORS_ONLN) && make install
# Добавляем node-gyp
&& yarn global add node-gyp
# Убираем за собой мусор
&& apk del .build-deps && yarn cache clean && rm -rf /usr/src
ตอนนี้ฉันกำลังต่อสู้เพื่อความเรียบง่าย ดังนั้นภาพจึงถูกรวบรวมโดยทีมใหญ่ทีมเดียว
สามารถดูภาพที่เสร็จแล้วได้ที่นี่:
Nginx
คุณสามารถใช้เว็บเซิร์ฟเวอร์ใดก็ได้เพื่อกระจายเนื้อหาแบบคงที่ ฉันคุ้นเคยกับการทำงานกับ nginx ดังนั้นฉันจะใช้มันตอนนี้
Nginx มีอิมเมจ Docker อย่างเป็นทางการ แต่มีโมดูลมากเกินไปสำหรับการกระจายแบบคงที่แบบธรรมดา สิ่งใดที่รวมอยู่ในการจัดส่งสามารถดูได้โดยทีมพิเศษหรือใน Dockerfile อย่างเป็นทางการ
$ นักเทียบท่าวิ่ง --rm nginx:1-อัลไพน์ nginx -V
nginx version: nginx/1.17.9
built by gcc 8.3.0 (Alpine 8.3.0)
built with OpenSSL 1.1.1d 10 Sep 2019
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --with-perl_modules_path=/usr/lib/perl5/vendor_perl --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-Os -fomit-frame-pointer' --with-ld-opt=-Wl,--as-needed
ฉันจะใช้ Dockerfile เป็นพื้นฐาน แต่ฉันจะเหลือไว้เฉพาะสิ่งที่จำเป็นสำหรับการกระจายเนื้อหาคงที่เท่านั้น เวอร์ชันของฉันไม่สามารถทำงานบน HTTPS ได้ ไม่รองรับการอนุญาต และอื่นๆ อีกมากมาย แต่เวอร์ชันของฉันจะสามารถแจกจ่ายไฟล์ที่บีบอัดด้วยอัลกอริธึม Brotli ซึ่งมีประสิทธิภาพมากกว่า gzip เล็กน้อย เราจะบีบอัดไฟล์หนึ่งครั้งโดยไม่จำเป็นต้องดำเนินการทันที
นี่คือ Dockerfile ที่ฉันใช้ ความคิดเห็นในภาษารัสเซียเป็นของฉันเป็นภาษาอังกฤษ - จากต้นฉบับ
ไฟล์นักเทียบท่า
# Базовый образ снова Alpine
FROM alpine:3.9
LABEL maintainer="Aleksey Maydokin <[email protected]>"
ENV NGINX_VERSION 1.16.0
ENV NGX_BROTLI_VERSION 0.1.2
ENV BROTLI_VERSION 1.0.7
RUN set -x
&& addgroup -S nginx
&& adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx
# Устанавливаем пакеты, которые нужны чтобы собрать nginx и модуль ngx_brotli к нему
&& apk add --no-cache --virtual .build-deps
gcc
libc-dev
make
linux-headers
curl
&& mkdir -p /usr/src
# Скачиваем исходники
&& curl -LSs https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz | tar xzf - -C /usr/src
&& curl -LSs https://github.com/eustas/ngx_brotli/archive/v$NGX_BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src
&& curl -LSs https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz | tar xzf - -C /usr/src
&& rm -rf /usr/src/ngx_brotli-$NGX_BROTLI_VERSION/deps/brotli/
&& ln -s /usr/src/brotli-$BROTLI_VERSION /usr/src/ngx_brotli-$NGX_BROTLI_VERSION/deps/brotli
&& cd /usr/src/nginx-$NGINX_VERSION
&& CNF="
--prefix=/etc/nginx
--sbin-path=/usr/sbin/nginx
--modules-path=/usr/lib/nginx/modules
--conf-path=/etc/nginx/nginx.conf
--error-log-path=/var/log/nginx/error.log
--http-log-path=/var/log/nginx/access.log
--pid-path=/var/run/nginx.pid
--lock-path=/var/run/nginx.lock
--http-client-body-temp-path=/var/cache/nginx/client_temp
--http-proxy-temp-path=/var/cache/nginx/proxy_temp
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp
--http-scgi-temp-path=/var/cache/nginx/scgi_temp
--user=nginx
--group=nginx
--without-http_ssi_module
--without-http_userid_module
--without-http_access_module
--without-http_auth_basic_module
--without-http_mirror_module
--without-http_autoindex_module
--without-http_geo_module
--without-http_split_clients_module
--without-http_referer_module
--without-http_rewrite_module
--without-http_proxy_module
--without-http_fastcgi_module
--without-http_uwsgi_module
--without-http_scgi_module
--without-http_grpc_module
--without-http_memcached_module
--without-http_limit_conn_module
--without-http_limit_req_module
--without-http_empty_gif_module
--without-http_browser_module
--without-http_upstream_hash_module
--without-http_upstream_ip_hash_module
--without-http_upstream_least_conn_module
--without-http_upstream_keepalive_module
--without-http_upstream_zone_module
--without-http_gzip_module
--with-http_gzip_static_module
--with-threads
--with-compat
--with-file-aio
--add-dynamic-module=/usr/src/ngx_brotli-$NGX_BROTLI_VERSION
"
# Собираем
&& ./configure $CNF
&& make -j$(getconf _NPROCESSORS_ONLN)
&& make install
&& rm -rf /usr/src/
# Удаляем динамический brotli модуль, оставляя только статический
&& rm /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so
&& sed -i '$ d' /etc/apk/repositories
# Bring in gettext so we can get `envsubst`, then throw
# the rest away. To do this, we need to install `gettext`
# then move `envsubst` out of the way so `gettext` can
# be deleted completely, then move `envsubst` back.
&& apk add --no-cache --virtual .gettext gettext
&& mv /usr/bin/envsubst /tmp/
&& runDeps="$(
scanelf --needed --nobanner /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst
| awk '{ gsub(/,/, "nso:", $2); print "so:" $2 }'
| sort -u
| xargs -r apk info --installed
| sort -u
)"
&& apk add --no-cache $runDeps
&& apk del .build-deps
&& apk del .gettext
&& mv /tmp/envsubst /usr/local/bin/
# Bring in tzdata so users could set the timezones through the environment
# variables
&& apk add --no-cache tzdata
# forward request and error logs to docker log collector
&& ln -sf /dev/stdout /var/log/nginx/access.log
&& ln -sf /dev/stderr /var/log/nginx/error.log
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
STOPSIGNAL SIGTERM
CMD ["nginx", "-g", "daemon off;"]
ฉันจะแก้ไข nginx.conf ทันทีเพื่อให้ gzip และ brotli เปิดใช้งานตามค่าเริ่มต้น ฉันจะรวมส่วนหัวของแคชด้วย เนื่องจากเราจะไม่มีวันเปลี่ยนแปลงแบบคงที่ และขั้นตอนสุดท้ายคือการเปลี่ยนเส้นทางคำขอ 404 ทั้งหมดไปยัง index.html ซึ่งจำเป็นสำหรับการนำทางใน SPA
nginx.conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
gzip_static on;
brotli_static on;
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
root html;
try_files $uri /index.html;
etag on;
expires max;
add_header Cache-Control public;
location = /index.html {
expires 0;
add_header Cache-Control "no-cache, public, must-revalidate, proxy-revalidate";
}
}
}
}
คุณสามารถดาวน์โหลดภาพที่เสร็จแล้วได้ที่นี่:
การสอนสถิติให้เข้าใจตัวแปรสภาพแวดล้อม
เหตุใดจึงจำเป็นต้องมีการตั้งค่าใน SPA? ตัวอย่างเช่น เพื่อระบุ RESTful API ที่จะใช้ โดยทั่วไป การตั้งค่าสำหรับสภาพแวดล้อมที่ต้องการจะถูกโอนไปยัง SPA ในขั้นตอนการสร้าง หากคุณต้องการเปลี่ยนแปลงบางอย่าง คุณจะต้องสร้างแอปพลิเคชันใหม่ ฉันไม่ต้องการมัน. ฉันต้องการให้สร้างแอปพลิเคชันเพียงครั้งเดียวในขั้นตอน CI และกำหนดค่าให้มากที่สุดเท่าที่จำเป็นในขั้นตอนซีดีโดยใช้ตัวแปรสภาพแวดล้อม
แน่นอนว่าไฟล์สแตติกเองก็ไม่เข้าใจตัวแปรสภาพแวดล้อมใดๆ ดังนั้นคุณจะต้องใช้เคล็ดลับ ในภาพสุดท้าย ฉันจะไม่เปิดตัว nginx แต่เป็นเชลล์สคริปต์พิเศษที่จะอ่านตัวแปรสภาพแวดล้อม เขียนลงในไฟล์สแตติก บีบอัด และจากนั้นจึงถ่ายโอนการควบคุมไปยัง nginx เท่านั้น
เพื่อจุดประสงค์นี้ Dockerfile จัดเตรียมพารามิเตอร์ ENTRYPOINT ให้สคริปต์ต่อไปนี้แก่เขา (โดยใช้ Angular เป็นตัวอย่าง):
นักเทียบท่า-entrypoint.sh
#!/bin/sh set -e FLAG_FILE="/configured" TARGET_DIR="/etc/nginx/html" replace_vars () { ENV_VARS='$(awk 'BEGIN{for(v in ENVIRON) print "
quot;v}')'
# В Angular ищем плейсхолдеры в main-файлах
for f in "$TARGET_DIR"/main*.js; do
# envsubst заменяет в файлах плейсхолдеры на значения из переменных окружения
echo "$(envsubst "$ENV_VARS" < "$f")" > "$f"
done
}
compress () {
for i in $(find "$TARGET_DIR" | grep -E ".css$|.html$|.js$|.svg$|.txt$|.ttf
quot;); do
# Используем максимальную степень сжатия
gzip -9kf "$i" && brotli -fZ "$i"
done
}
if [ "$1" = 'nginx' ]; then
# Флаг нужен, чтобы выполнить скрипт только при самом первом запуске
if [ ! -e "$FLAG_FILE" ]; then
echo "Running init script"
echo "Replacing env vars"
replace_vars
echo "Compressing files"
compress
touch $FLAG_FILE
echo "Done"
fi
fi
exec "$@"
เพื่อให้สคริปต์ทำงานได้ การตั้งค่าจะต้องเขียนลงในไฟล์ js ในรูปแบบนี้:${API_URL}
.เป็นที่น่าสังเกตว่า SPA สมัยใหม่ส่วนใหญ่จะเพิ่มแฮชให้กับไฟล์เมื่อสร้าง นี่เป็นสิ่งจำเป็นเพื่อให้เบราว์เซอร์สามารถแคชไฟล์ได้อย่างปลอดภัยเป็นเวลานาน หากไฟล์เปลี่ยนแปลง แฮชของไฟล์จะเปลี่ยน ซึ่งจะบังคับให้เบราว์เซอร์ดาวน์โหลดไฟล์อีกครั้ง
น่าเสียดายที่วิธีการของฉันการเปลี่ยนการกำหนดค่าผ่านตัวแปรสภาพแวดล้อมไม่ได้นำไปสู่การเปลี่ยนแปลงแฮชของไฟล์ซึ่งหมายความว่าแคชของเบราว์เซอร์จะต้องใช้งานไม่ได้ด้วยวิธีอื่น ฉันไม่มีปัญหานี้เนื่องจากมีการใช้งานการกำหนดค่าที่แตกต่างกันในสภาพแวดล้อมที่แตกต่างกัน
รวบรวมภาพสุดท้าย
ในที่สุด.
ไฟล์นักเทียบท่า
# Первый базовый образ для сборки FROM alexxxnf/spa-builder as builder # Чтобы эффктивнее использовать кэш Docker-а, сначала устанавливаем только зависимости COPY ./package.json ./package-lock.json /app/ RUN cd /app && npm ci --no-audit # Потом собираем само приложение COPY . /app RUN cd /app && npm run build -- --prod --configuration=docker # Второй базовый образ для раздачи FROM alexxxnf/nginx-spa # Забираем из первого образа сначала компрессор COPY --from=builder /usr/local/bin/brotli /usr/local/bin # Потом добавляем чудо-скрипт COPY ./docker/docker-entrypoint.sh /docker-entrypoint.sh # И в конце забираем само приложение COPY --from=builder /app/dist/app /etc/nginx/html/ ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["nginx", "-g", "daemon off;"]
ตอนนี้ภาพที่ได้สามารถประกอบและใช้งานได้ทุกที่
ที่มา: will.com