// config/environment.js
const environments = {
development: {
API_BASE_URL: 'http://localhost:3000/api',
LOG_LEVEL: 'debug',
DEBUG: true,
ENABLE_DEV_TOOLS: true
},
test: {
API_BASE_URL: 'https://test-api.example.com',
LOG_LEVEL: 'info',
DEBUG: false,
ENABLE_DEV_TOOLS: false
},
production: {
API_BASE_URL: 'https://api.example.com',
LOG_LEVEL: 'error',
DEBUG: false,
ENABLE_DEV_TOOLS: false
}
};
// 根據環(huán)境變量獲取配置
export const getConfig = () => {
const env = process.env.NODE_ENV || 'development';
return environments[env];
};
// 環(huán)境變量驗證
export const validateEnvironment = () => {
const requiredVars = [
'REACT_APP_API_BASE_URL',
'REACT_APP_VERSION'
];
const missingVars = requiredVars.filter(varName => !process.env[varName]);
if (missingVars.length > 0) {
throw new Error(`缺少必需的環(huán)境變量: ${missingVars.join(', ')}`);
}
};
# .env.development
REACT_APP_API_BASE_URL=http://localhost:3000/api
REACT_APP_VERSION=1.0.0-dev
REACT_APP_BUILD_DATE=2024-01-01
REACT_APP_ENABLE_MOCK=true
REACT_APP_LOG_LEVEL=debug
# .env.production
REACT_APP_API_BASE_URL=https://api.example.com
REACT_APP_VERSION=1.0.0
REACT_APP_BUILD_DATE=2024-01-01
REACT_APP_ENABLE_MOCK=false
REACT_APP_LOG_LEVEL=error
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
mode: isProduction ? 'production' : 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'build'),
filename: isProduction ? 'static/js/[name].[contenthash:8].js' : 'static/js/[name].js',
chunkFilename: isProduction ? 'static/js/[name].[contenthash:8].chunk.js' : 'static/js/[name].chunk.js',
publicPath: '/',
clean: true
},
optimization: {
minimize: isProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isProduction // 生產(chǎn)環(huán)境移除console
}
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
chunks: 'all'
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
},
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`
}
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [
['@babel/preset-env', { targets: 'defaults' }],
['@babel/preset-react', { runtime: 'automatic' }]
]
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true
}
}
}
]
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 10KB
}
},
generator: {
filename: 'static/media/[name].[hash:8][ext]'
}
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.REACT_APP_VERSION': JSON.stringify(process.env.REACT_APP_VERSION)
}),
...(isProduction ? [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false
})
] : [])
],
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@services': path.resolve(__dirname, 'src/services')
}
},
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
open: true,
compress: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
secure: false
}
}
},
devtool: isProduction ? 'source-map' : 'eval-source-map'
};
};
// .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
},
"useBuiltIns": "usage",
"corejs": 3
}
],
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-dynamic-import",
"babel-plugin-lodash"
],
"env": {
"production": {
"plugins": [
"transform-react-remove-prop-types"
]
}
}
}
# Dockerfile
FROM node:16-alpine as builder
# 設置工作目錄
WORKDIR /app
# 復制package文件
COPY package*.json ./
COPY yarn.lock ./
# 安裝依賴(lài)
RUN yarn install --frozen-lockfile --production=false
# 復制源代碼
COPY . .
# 構建應用
RUN yarn build
# 生產(chǎn)階段
FROM nginx:alpine
# 復制構建結果到nginx
COPY --from=builder /app/build /usr/share/nginx/html
# 復制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
# 啟動(dòng)nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
worker_processes auto;
events {
worker_connections 1024;
}
http {
include /etc/nginx/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;
error_log /var/log/nginx/error.log warn;
# 性能優(yōu)化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip壓縮
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 安全頭
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 靜態(tài)資源緩存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML文件不緩存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# SPA路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API代理
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 健康檢查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
# docker-compose.yml
version: '3.8'
services:
frontend:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
environment:
- NODE_ENV=production
depends_on:
- backend
networks:
- app-network
backend:
image: backend:latest
ports:
- "8080:8080"
environment:
- DB_HOST=database
- DB_PORT=5432
networks:
- app-network
database:
image: postgres:13
environment:
- POSTGRES_DB=invoicing
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=secret
volumes:
- db_data:/var/lib/postgresql/data
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
db_data:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run tests
run: yarn test --coverage --watchAll=false
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 16
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build application
run: yarn build
env:
CI: false
REACT_APP_VERSION: ${{ github.sha }}
REACT_APP_BUILD_DATE: ${{ github.event.head_commit.timestamp }}
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build
path: build/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build
path: build/
- name: Deploy to production
uses: appleboy/scp-action@v0.1.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
source: "build/*"
target: "/var/www/invoicing"
- name: Restart nginx
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
sudo systemctl restart nginx
#!/bin/bash
# deploy.sh
set -e
# 顏色輸出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 日志函數
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
}
warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}"
}
error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}"
exit 1
}
# 檢查環(huán)境
check_environment() {
log "檢查部署環(huán)境..."
# 檢查Node.js版本
if ! command -v node &> /dev/null; then
error "Node.js未安裝"
fi
local node_version=$(node -v | cut -d'v' -f2)
local required_version="16.0.0"
if [ "$(printf '%s\n' "$required_version" "$node_version" | sort -V | head -n1)" != "$required_version" ]; then
error "Node.js版本過(guò)低,需要 >= $required_version"
fi
log "環(huán)境檢查通過(guò)"
}
# 安裝依賴(lài)
install_dependencies() {
log "安裝依賴(lài)..."
if [ -f "yarn.lock" ]; then
yarn install --frozen-lockfile --production=false
else
npm install
fi
if [ $? -ne 0 ]; then
error "依賴(lài)安裝失敗"
fi
log "依賴(lài)安裝完成"
}
# 運行測試
run_tests() {
log "運行測試..."
if [ -f "package.json" ]; then
if grep -q "\"test\"" package.json; then
npm test -- --coverage --watchAll=false
else
warn "未找到測試腳本,跳過(guò)測試"
fi
else
warn "未找到package.json,跳過(guò)測試"
fi
log "測試完成"
}
# 構建應用
build_app() {
log "構建應用..."
# 設置構建環(huán)境變量
export NODE_ENV=production
export REACT_APP_VERSION=$(git rev-parse --short HEAD)
export REACT_APP_BUILD_DATE=$(date -Iseconds)
if [ -f "package.json" ]; then
if grep -q "\"build\"" package.json; then
npm run build
else
error "未找到構建腳本"
fi
else
error "未找到package.json"
fi
if [ $? -ne 0 ]; then
error "構建失敗"
fi
log "構建完成"
}
# 部署到服務(wù)器
deploy_to_server() {
local server_host=$1
local deploy_path=$2
log "部署到服務(wù)器: $server_host:$deploy_path"
# 使用rsync同步文件
rsync -avz --delete \
--exclude='.git' \
--exclude='node_modules' \
--exclude='*.log' \
./build/ $server_host:$deploy_path/
if [ $? -ne 0 ]; then
error "文件同步失敗"
fi
log "文件同步完成"
}
# 重啟服務(wù)
restart_service() {
local server_host=$1
local service_name=$2
log "重啟服務(wù): $service_name"
ssh $server_host "sudo systemctl restart $service_name"
if [ $? -ne 0 ]; then
error "服務(wù)重啟失敗"
fi
log "服務(wù)重啟完成"
}
# 健康檢查
health_check() {
local url=$1
local max_attempts=30
local attempt=1
log "進(jìn)行健康檢查: $url"
while [ $attempt -le $max_attempts ]; do
if curl -f -s $url > /dev/null; then
log "健康檢查通過(guò)"
return 0
fi
warn "健康檢查失敗 (嘗試 $attempt/$max_attempts)"
sleep 5
attempt=$((attempt + 1))
done
error "健康檢查超時(shí)"
}
# 主函數
main() {
local server_host=${1:-$DEPLOY_HOST}
local deploy_path=${2:-$DEPLOY_PATH}
local service_name=${3:-"nginx"}
local health_check_url=${4:-"http://localhost"}
if [ -z "$server_host" ]; then
error "請提供服務(wù)器地址"
fi
check_environment
install_dependencies
run_tests
build_app
deploy_to_server $server_host $deploy_path
restart_service $server_host $service_name
health_check $health_check_url
log "部署完成!"
}
# 執行主函數
main "$@"
// src/utils/monitoring.js
export class ApplicationMonitor {
static init() {
// 性能監控
this.setupPerformanceMonitoring();
// 錯誤監控
this.setupErrorMonitoring();
// 用戶(hù)行為監控
this.setupUserBehaviorMonitoring();
}
static setupPerformanceMonitoring() {
// 監控關(guān)鍵性能指標
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.logPerformanceMetric(entry);
}
});
observer.observe({ entryTypes: ['navigation', 'resource', 'paint'] });
}
// 監控長(cháng)任務(wù)
if ('PerformanceObserver' in window) {
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
this.logLongTask(entry);
}
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
}
}
static setupErrorMonitoring() {
window.addEventListener('error', (event) => {
this.logError({
type: 'UNHANDLED_ERROR',
message: event.error?.message,
stack: event.error?.stack,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
window.addEventListener('unhandledrejection', (event) => {
this.logError({
type: 'UNHANDLED_PROMISE_REJECTION',
reason: event.reason?.message || event.reason
});
});
}
static logPerformanceMetric(metric) {
const data = {
type: 'PERFORMANCE_METRIC',
name: metric.name,
value: metric.duration,
timestamp: Date.now()
};
this.sendToMonitoringService(data);
}
static logError(errorData) {
const data = {
...errorData,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
};
this.sendToMonitoringService(data);
}
static sendToMonitoringService(data) {
// 發(fā)送到監控服務(wù)
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
navigator.sendBeacon('/api/monitoring', blob);
} else {
fetch('/api/monitoring', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' }
}).catch(() => {
// 靜默失敗
});
}
}
}
這些部署配置為應用提供了完整的構建、部署和監控解決方案。