Browse Source
- 支持自动检测包管理器(npm/yarn) - 包含Node.js版本检查 - 自动安装依赖和TypeScript - 构建生产版本并创建发布目录 - 提供详细的部署说明 - 支持命令行参数控制构建过程feature/x1-web-request
8 changed files with 589 additions and 10 deletions
@ -0,0 +1,19 @@ |
|||
node_modules |
|||
npm-debug.log |
|||
.git |
|||
.gitignore |
|||
README.md |
|||
.env |
|||
.env.local |
|||
.env.development |
|||
.env.test |
|||
.nyc_output |
|||
coverage |
|||
.vscode |
|||
.idea |
|||
*.log |
|||
dist |
|||
.DS_Store |
|||
Thumbs.db |
|||
build.bat |
|||
deploy.sh |
|||
@ -0,0 +1,32 @@ |
|||
# 使用官方Node.js 18 Alpine镜像作为构建环境 |
|||
FROM node:18-alpine AS builder |
|||
|
|||
# 设置工作目录 |
|||
WORKDIR /app |
|||
|
|||
# 复制package.json和package-lock.json |
|||
COPY package*.json ./ |
|||
|
|||
# 安装依赖 |
|||
RUN npm ci --only=production |
|||
|
|||
# 复制源代码 |
|||
COPY . . |
|||
|
|||
# 构建生产版本 |
|||
RUN npm run build |
|||
|
|||
# 使用nginx Alpine镜像作为运行环境 |
|||
FROM nginx:alpine |
|||
|
|||
# 复制构建产物到nginx目录 |
|||
COPY --from=builder /app/dist /usr/share/nginx/html |
|||
|
|||
# 复制nginx配置文件 |
|||
COPY nginx.conf /etc/nginx/nginx.conf |
|||
|
|||
# 暴露端口 |
|||
EXPOSE 80 |
|||
|
|||
# 启动nginx |
|||
CMD ["nginx", "-g", "daemon off;"] |
|||
@ -0,0 +1,248 @@ |
|||
@echo off |
|||
setlocal enabledelayedexpansion |
|||
chcp 65001 >nul |
|||
|
|||
REM 设置错误时退出 |
|||
set "EXIT_CODE=0" |
|||
|
|||
REM 解析命令行参数 |
|||
call :parse_arguments %* |
|||
|
|||
REM 主函数 |
|||
call :main |
|||
goto :end |
|||
|
|||
REM 主函数 |
|||
:main |
|||
call :log_info "Starting X1.WebUI build process..." |
|||
|
|||
call :check_node_version |
|||
if !EXIT_CODE! neq 0 goto :end |
|||
|
|||
call :detect_package_manager |
|||
if !EXIT_CODE! neq 0 goto :end |
|||
|
|||
call :install_dependencies |
|||
if !EXIT_CODE! neq 0 goto :end |
|||
|
|||
call :build_project |
|||
if !EXIT_CODE! neq 0 goto :end |
|||
|
|||
call :create_publish_dir |
|||
call :show_publish_info |
|||
call :show_deployment_instructions |
|||
|
|||
call :log_success "Build completed!" |
|||
call :log_info "Please upload the publish directory contents to server /home/hyh/x1_web/" |
|||
goto :eof |
|||
|
|||
REM 检查Node.js版本 |
|||
:check_node_version |
|||
call :log_info "Checking Node.js version..." |
|||
node --version >nul 2>&1 |
|||
if errorlevel 1 ( |
|||
call :log_error "Node.js not installed, please install Node.js 18+" |
|||
set "EXIT_CODE=1" |
|||
goto :end |
|||
) |
|||
|
|||
for /f "tokens=*" %%i in ('node --version') do set "NODE_VERSION=%%i" |
|||
call :log_success "Node.js version check passed: !NODE_VERSION!" |
|||
goto :eof |
|||
|
|||
REM 解析命令行参数 |
|||
:parse_arguments |
|||
set "PACKAGE_MANAGER=" |
|||
set "SHOW_HELP=false" |
|||
|
|||
:parse_loop |
|||
if "%~1"=="" goto :parse_done |
|||
if /i "%~1"=="--npm" ( |
|||
set "PACKAGE_MANAGER=npm" |
|||
call :log_info "Package manager set to npm via command line" |
|||
) else if /i "%~1"=="--yarn" ( |
|||
set "PACKAGE_MANAGER=yarn" |
|||
call :log_info "Package manager set to yarn via command line" |
|||
) else if /i "%~1"=="--help" ( |
|||
set "SHOW_HELP=true" |
|||
) else ( |
|||
call :log_error "Unknown parameter: %~1" |
|||
call :show_help |
|||
set "EXIT_CODE=1" |
|||
goto :end |
|||
) |
|||
shift |
|||
goto :parse_loop |
|||
|
|||
:parse_done |
|||
if "!SHOW_HELP!"=="true" ( |
|||
call :show_help |
|||
set "EXIT_CODE=0" |
|||
goto :end |
|||
) |
|||
goto :eof |
|||
|
|||
REM 显示帮助信息 |
|||
:show_help |
|||
echo. |
|||
echo Usage: build.bat [options] |
|||
echo. |
|||
echo Options: |
|||
echo --npm Force using npm package manager |
|||
echo --yarn Force using yarn package manager |
|||
echo --help Show this help message |
|||
echo. |
|||
echo Examples: |
|||
echo build.bat - Auto detect package manager |
|||
echo build.bat --npm - Force using npm |
|||
echo build.bat --yarn - Force using yarn |
|||
echo build.bat --help - Show help |
|||
echo. |
|||
goto :eof |
|||
|
|||
REM 检测包管理器 |
|||
:detect_package_manager |
|||
call :log_info "Detecting package manager..." |
|||
if not "!PACKAGE_MANAGER!"=="" ( |
|||
call :log_info "Package manager already set to: !PACKAGE_MANAGER!" |
|||
goto :eof |
|||
) |
|||
|
|||
if exist "yarn.lock" ( |
|||
set "PACKAGE_MANAGER=yarn" |
|||
call :log_info "Found yarn.lock, using yarn" |
|||
) else if exist "package-lock.json" ( |
|||
set "PACKAGE_MANAGER=npm" |
|||
call :log_info "Found package-lock.json, using npm" |
|||
) else ( |
|||
set "PACKAGE_MANAGER=npm" |
|||
call :log_info "No lock file found, defaulting to npm" |
|||
) |
|||
goto :eof |
|||
|
|||
REM 安装依赖 |
|||
:install_dependencies |
|||
call :log_info "Installing project dependencies..." |
|||
if not exist "node_modules" ( |
|||
if "!PACKAGE_MANAGER!"=="yarn" ( |
|||
call yarn install --frozen-lockfile |
|||
) else ( |
|||
call npm ci |
|||
) |
|||
) else ( |
|||
if "!PACKAGE_MANAGER!"=="yarn" ( |
|||
call yarn install --frozen-lockfile --production |
|||
) else ( |
|||
call npm ci --only=production |
|||
) |
|||
) |
|||
if errorlevel 1 ( |
|||
call :log_error "Dependency installation failed" |
|||
set "EXIT_CODE=1" |
|||
goto :end |
|||
) |
|||
call :log_success "Dependencies installed successfully" |
|||
goto :eof |
|||
|
|||
REM 构建项目 |
|||
:build_project |
|||
call :log_info "Building production version..." |
|||
|
|||
REM 直接尝试构建,如果失败则安装TypeScript |
|||
call :log_info "Attempting to build project..." |
|||
if "!PACKAGE_MANAGER!"=="yarn" ( |
|||
call yarn build |
|||
) else ( |
|||
call npm run build |
|||
) |
|||
if errorlevel 1 ( |
|||
call :log_warning "Build failed, checking if TypeScript is needed..." |
|||
call :log_info "Installing TypeScript as dev dependency..." |
|||
if "!PACKAGE_MANAGER!"=="yarn" ( |
|||
call yarn add -D typescript |
|||
) else ( |
|||
call npm install --save-dev typescript |
|||
) |
|||
|
|||
call :log_info "Retrying build after TypeScript installation..." |
|||
if "!PACKAGE_MANAGER!"=="yarn" ( |
|||
call yarn build |
|||
) else ( |
|||
call npm run build |
|||
) |
|||
if errorlevel 1 ( |
|||
call :log_error "Project build failed even after TypeScript installation" |
|||
set "EXIT_CODE=1" |
|||
goto :end |
|||
) |
|||
) |
|||
call :log_success "Project build completed" |
|||
goto :eof |
|||
|
|||
REM 创建发布目录 |
|||
:create_publish_dir |
|||
call :log_info "Creating publish directory..." |
|||
if exist "publish" rmdir /s /q "publish" |
|||
mkdir "publish" |
|||
|
|||
REM 复制构建产物到发布目录根目录 |
|||
xcopy "dist\*" "publish\" /E /Y /Q |
|||
|
|||
REM 复制Dockerfile和nginx配置到发布目录根目录 |
|||
copy "Dockerfile" "publish\" >nul |
|||
copy "nginx.conf" "publish\" >nul |
|||
|
|||
REM 复制部署脚本到发布目录根目录 |
|||
copy "deploy.sh" "publish\" >nul |
|||
|
|||
REM 注意:.dockerignore 文件不需要复制到发布目录,只在本地构建时使用 |
|||
|
|||
call :log_success "Publish directory created: publish/" |
|||
goto :eof |
|||
|
|||
REM 显示文件大小 |
|||
:show_publish_info |
|||
call :log_info "Publish directory info:" |
|||
for /f "tokens=1" %%i in ('dir publish /s ^| find "个文件"') do echo File count: %%i |
|||
echo. |
|||
call :log_info "Publish directory contents:" |
|||
dir publish /B |
|||
echo. |
|||
call :log_info "Publish directory structure:" |
|||
tree publish /F |
|||
goto :eof |
|||
|
|||
REM 显示部署说明 |
|||
:show_deployment_instructions |
|||
call :log_info "Deployment instructions:" |
|||
echo. |
|||
echo 1. Upload all contents of publish directory to server /home/hyh/x1_web/ |
|||
echo 2. Ensure the following files are in /home/hyh/x1_web/ root directory: |
|||
echo - Dockerfile |
|||
echo - nginx.conf |
|||
echo - deploy.sh |
|||
echo - index.html |
|||
echo - assets/ directory |
|||
echo 3. On server run: chmod +x deploy.sh ^&^& ./deploy.sh |
|||
echo. |
|||
goto :eof |
|||
|
|||
REM 日志函数(简化版本,不使用颜色) |
|||
:log_info |
|||
echo [INFO] %~1 |
|||
goto :eof |
|||
|
|||
:log_success |
|||
echo [SUCCESS] %~1 |
|||
goto :eof |
|||
|
|||
:log_warning |
|||
echo [WARNING] %~1 |
|||
goto :eof |
|||
|
|||
:log_error |
|||
echo [ERROR] %~1 |
|||
goto :eof |
|||
|
|||
:end |
|||
exit /b %EXIT_CODE% |
|||
@ -0,0 +1,207 @@ |
|||
#!/bin/bash |
|||
|
|||
# 设置错误时退出 |
|||
set -e |
|||
|
|||
# 颜色定义 |
|||
RED='\033[0;31m' |
|||
GREEN='\033[0;32m' |
|||
YELLOW='\033[1;33m' |
|||
BLUE='\033[0;34m' |
|||
NC='\033[0m' # No Color |
|||
|
|||
# 日志函数 |
|||
log_info() { |
|||
echo -e "${BLUE}[INFO]${NC} $1" |
|||
} |
|||
|
|||
log_success() { |
|||
echo -e "${GREEN}[SUCCESS]${NC} $1" |
|||
} |
|||
|
|||
log_warning() { |
|||
echo -e "${YELLOW}[WARNING]${NC} $1" |
|||
} |
|||
|
|||
log_error() { |
|||
echo -e "${RED}[ERROR]${NC} $1" |
|||
} |
|||
|
|||
# 默认配置 |
|||
IMAGE_NAME="x1-webui" |
|||
CONTAINER_NAME="x1-webui-container" |
|||
PORT="9905" |
|||
REMOVE_MODE=false |
|||
|
|||
# 解析命令行参数 |
|||
parse_arguments() { |
|||
while [[ $# -gt 0 ]]; do |
|||
case $1 in |
|||
-p|--port) |
|||
PORT="$2" |
|||
shift 2 |
|||
;; |
|||
-i|--image) |
|||
IMAGE_NAME="$2" |
|||
shift 2 |
|||
;; |
|||
-c|--container) |
|||
CONTAINER_NAME="$2" |
|||
shift 2 |
|||
;; |
|||
-r|--remove) |
|||
REMOVE_MODE=true |
|||
shift |
|||
;; |
|||
-h|--help) |
|||
echo "用法: $0 [选项]" |
|||
echo "选项:" |
|||
echo " -p, --port PORT 指定端口 (默认: 9905)" |
|||
echo " -i, --image NAME 指定镜像名称 (默认: x1-webui)" |
|||
echo " -c, --container NAME 指定容器名称 (默认: x1-webui-container)" |
|||
echo " -r, --remove 删除旧容器和镜像" |
|||
echo " -h, --help 显示帮助信息" |
|||
exit 0 |
|||
;; |
|||
*) |
|||
log_error "未知选项: $1" |
|||
exit 1 |
|||
;; |
|||
esac |
|||
done |
|||
} |
|||
|
|||
# 检查Docker |
|||
check_docker() { |
|||
log_info "检查Docker安装状态..." |
|||
if ! command -v docker &> /dev/null; then |
|||
log_error "Docker未安装,请先安装Docker" |
|||
exit 1 |
|||
fi |
|||
|
|||
if ! docker info &> /dev/null; then |
|||
log_error "Docker服务未运行,请启动Docker服务" |
|||
exit 1 |
|||
fi |
|||
|
|||
log_success "Docker检查通过" |
|||
} |
|||
|
|||
# 检查必要文件 |
|||
check_files() { |
|||
log_info "检查必要文件..." |
|||
|
|||
local required_files=("Dockerfile" "nginx.conf" "index.html") |
|||
for file in "${required_files[@]}"; do |
|||
if [ ! -f "$file" ]; then |
|||
log_error "缺少必要文件: $file" |
|||
exit 1 |
|||
fi |
|||
done |
|||
|
|||
log_success "文件检查通过" |
|||
} |
|||
|
|||
# 停止并删除旧容器 |
|||
stop_old_container() { |
|||
log_info "停止旧容器..." |
|||
if docker ps -a --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then |
|||
docker stop "$CONTAINER_NAME" || true |
|||
docker rm "$CONTAINER_NAME" || true |
|||
log_success "旧容器已停止并删除" |
|||
else |
|||
log_info "没有找到旧容器" |
|||
fi |
|||
} |
|||
|
|||
# 删除旧镜像 |
|||
remove_old_image() { |
|||
if [ "$REMOVE_MODE" = true ]; then |
|||
log_info "删除旧镜像..." |
|||
if docker images --format "table {{.Repository}}" | grep -q "^${IMAGE_NAME}$"; then |
|||
docker rmi "$IMAGE_NAME" || true |
|||
log_success "旧镜像已删除" |
|||
else |
|||
log_info "没有找到旧镜像" |
|||
fi |
|||
fi |
|||
} |
|||
|
|||
# 构建Docker镜像 |
|||
build_image() { |
|||
log_info "构建Docker镜像: $IMAGE_NAME" |
|||
docker build -t "$IMAGE_NAME" . |
|||
log_success "镜像构建完成" |
|||
} |
|||
|
|||
# 运行容器 |
|||
run_container() { |
|||
log_info "启动容器: $CONTAINER_NAME" |
|||
docker run -d \ |
|||
--name "$CONTAINER_NAME" \ |
|||
-p "$PORT:80" \ |
|||
--restart unless-stopped \ |
|||
"$IMAGE_NAME" |
|||
|
|||
log_success "容器启动完成" |
|||
} |
|||
|
|||
# 健康检查 |
|||
health_check() { |
|||
log_info "等待应用启动..." |
|||
local max_attempts=30 |
|||
local attempt=1 |
|||
|
|||
while [ $attempt -le $max_attempts ]; do |
|||
if curl -f -s "http://localhost:$PORT/health" > /dev/null 2>&1; then |
|||
log_success "应用启动成功!" |
|||
return 0 |
|||
fi |
|||
|
|||
log_info "等待应用启动... ($attempt/$max_attempts)" |
|||
sleep 2 |
|||
((attempt++)) |
|||
done |
|||
|
|||
log_warning "应用启动超时,但容器可能仍在运行" |
|||
return 1 |
|||
} |
|||
|
|||
# 显示部署信息 |
|||
show_deployment_info() { |
|||
log_success "部署完成!" |
|||
echo "" |
|||
echo "部署信息:" |
|||
echo " 镜像名称: $IMAGE_NAME" |
|||
echo " 容器名称: $CONTAINER_NAME" |
|||
echo " 访问端口: $PORT" |
|||
echo " 访问地址: http://localhost:$PORT" |
|||
echo "" |
|||
|
|||
log_info "容器状态:" |
|||
docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" |
|||
|
|||
echo "" |
|||
log_info "容器日志:" |
|||
docker logs --tail 10 "$CONTAINER_NAME" |
|||
} |
|||
|
|||
# 主函数 |
|||
main() { |
|||
log_info "开始部署X1.WebUI..." |
|||
|
|||
parse_arguments "$@" |
|||
check_docker |
|||
check_files |
|||
stop_old_container |
|||
remove_old_image |
|||
build_image |
|||
run_container |
|||
health_check |
|||
show_deployment_info |
|||
|
|||
log_success "部署完成!" |
|||
} |
|||
|
|||
# 执行主函数 |
|||
main "$@" |
|||
@ -0,0 +1,74 @@ |
|||
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; |
|||
|
|||
# 基本设置 |
|||
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_proxied any; |
|||
gzip_comp_level 6; |
|||
gzip_types |
|||
text/plain |
|||
text/css |
|||
text/xml |
|||
text/javascript |
|||
application/json |
|||
application/javascript |
|||
application/xml+rss |
|||
application/atom+xml |
|||
image/svg+xml; |
|||
|
|||
server { |
|||
listen 80; |
|||
server_name localhost; |
|||
root /usr/share/nginx/html; |
|||
index index.html; |
|||
|
|||
# 处理React Router的客户端路由 |
|||
location / { |
|||
try_files $uri $uri/ /index.html; |
|||
} |
|||
|
|||
# 静态资源缓存 |
|||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { |
|||
expires 1y; |
|||
add_header Cache-Control "public, immutable"; |
|||
} |
|||
|
|||
# API代理到后端9905端口 |
|||
location /api/ { |
|||
proxy_pass http://host.docker.internal:9905; |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue