feat: SSG构建|GO二进制SSG产物打包

This commit is contained in:
MatrixSeven
2025-08-02 13:36:01 +08:00
parent 0000be2a68
commit c163756d2c
32 changed files with 1938 additions and 1072 deletions

530
build-fullstack.sh Executable file
View File

@@ -0,0 +1,530 @@
#!/bin/bash
# =============================================================================
# 全栈应用构建脚本
#
# 功能:
# 1. 构建 Next.js SSG 静态文件
# 2. 将静态文件复制到 Go 嵌入目录
# 3. 构建 Go 二进制文件,包含嵌入的前端文件
# 4. 生成单一可部署的二进制文件
#
# 使用方法:
# ./build-fullstack.sh [options]
#
# 选项:
# --clean 清理所有构建文件
# --frontend-only 只构建前端
# --backend-only 只构建后端
# --dev 开发模式构建
# --verbose 显示详细输出
# --help 显示帮助信息
# =============================================================================
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m'
# 配置变量
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$SCRIPT_DIR"
FRONTEND_DIR="$PROJECT_ROOT/chuan-next"
FRONTEND_OUT_DIR="$FRONTEND_DIR/out"
GO_WEB_DIR="$PROJECT_ROOT/internal/web"
FRONTEND_EMBED_DIR="$GO_WEB_DIR/frontend"
BINARY_NAME="file-transfer-server"
BINARY_PATH="$PROJECT_ROOT/$BINARY_NAME"
# 标志变量
CLEAN=false
FRONTEND_ONLY=false
BACKEND_ONLY=false
DEV_MODE=false
VERBOSE=false
# 打印函数
print_header() {
echo -e "${PURPLE}========================================${NC}"
echo -e "${PURPLE}🚀 $1${NC}"
echo -e "${PURPLE}========================================${NC}"
}
print_step() {
echo -e "${BLUE}📋 $1${NC}"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${CYAN} $1${NC}"
}
print_verbose() {
if [ "$VERBOSE" = true ]; then
echo -e "${CYAN}[VERBOSE]${NC} $1"
fi
}
# 显示帮助
show_help() {
cat << EOF
全栈应用构建脚本
此脚本将构建 Next.js 前端和 Go 后端,并将前端静态文件嵌入到 Go 二进制中。
使用方法:
$0 [选项]
选项:
--clean 清理所有构建文件和缓存
--frontend-only 只构建前端部分
--backend-only 只构建后端部分(需要前端已构建)
--dev 开发模式构建(包含调试信息)
--verbose 显示详细构建过程
--help 显示此帮助信息
示例:
$0 # 完整构建
$0 --clean # 清理后完整构建
$0 --frontend-only # 只构建前端
$0 --backend-only # 只构建后端
$0 --dev --verbose # 开发模式详细构建
输出:
构建成功后会生成 '$BINARY_NAME' 可执行文件,包含完整的前后端功能。
EOF
}
# 解析命令行参数
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--clean)
CLEAN=true
shift
;;
--frontend-only)
FRONTEND_ONLY=true
shift
;;
--backend-only)
BACKEND_ONLY=true
shift
;;
--dev)
DEV_MODE=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
--help)
show_help
exit 0
;;
*)
print_error "未知选项: $1"
show_help
exit 1
;;
esac
done
}
# 检查依赖
check_dependencies() {
print_step "检查构建依赖..."
local missing_deps=()
# 检查 Node.js
if ! command -v node &> /dev/null; then
missing_deps+=("Node.js")
fi
# 检查 yarn
if ! command -v yarn &> /dev/null; then
missing_deps+=("Yarn")
fi
# 检查 Go
if ! command -v go &> /dev/null; then
missing_deps+=("Go")
fi
if [ ${#missing_deps[@]} -gt 0 ]; then
print_error "缺少必要的依赖: ${missing_deps[*]}"
print_info "请安装缺少的依赖后重试"
exit 1
fi
print_verbose "Node.js 版本: $(node --version)"
print_verbose "Yarn 版本: $(yarn --version)"
print_verbose "Go 版本: $(go version)"
print_success "依赖检查完成"
}
# 清理函数
clean_all() {
if [ "$CLEAN" = true ]; then
print_step "清理构建文件..."
# 清理前端构建
if [ -d "$FRONTEND_DIR/.next" ]; then
rm -rf "$FRONTEND_DIR/.next"
print_verbose "已删除 $FRONTEND_DIR/.next"
fi
if [ -d "$FRONTEND_OUT_DIR" ]; then
rm -rf "$FRONTEND_OUT_DIR"
print_verbose "已删除 $FRONTEND_OUT_DIR"
fi
# 清理嵌入的前端文件
if [ -d "$FRONTEND_EMBED_DIR" ]; then
find "$FRONTEND_EMBED_DIR" -name "*.html" -o -name "*.js" -o -name "*.css" -o -name "*.json" -o -name "*.png" -o -name "*.jpg" -o -name "*.svg" -o -name "*.ico" | xargs rm -f 2>/dev/null || true
print_verbose "已清理嵌入的前端文件"
fi
# 清理 Go 构建
if [ -f "$BINARY_PATH" ]; then
rm -f "$BINARY_PATH"
print_verbose "已删除 $BINARY_PATH"
fi
# 清理 Go 模块缓存(可选)
if [ "$VERBOSE" = true ]; then
go clean -modcache
fi
print_success "清理完成"
fi
}
# 构建前端
build_frontend() {
if [ "$BACKEND_ONLY" = true ]; then
print_info "跳过前端构建 (--backend-only)"
return
fi
print_step "构建 Next.js 前端..."
# 检查前端目录
if [ ! -d "$FRONTEND_DIR" ]; then
print_error "前端目录不存在: $FRONTEND_DIR"
exit 1
fi
cd "$FRONTEND_DIR"
# 安装依赖
print_verbose "安装前端依赖..."
if [ "$VERBOSE" = true ]; then
yarn install
else
yarn install --silent
fi
# 执行 SSG 构建
print_verbose "执行 SSG 构建..."
# 临时移除 API 目录
if [ -d "src/app/api" ]; then
mv src/app/api /tmp/next-api-backup-$(date +%s) 2>/dev/null || true
fi
# 构建
if [ "$VERBOSE" = true ]; then
NEXT_EXPORT=true yarn build
else
NEXT_EXPORT=true yarn build > build.log 2>&1
if [ $? -ne 0 ]; then
print_error "前端构建失败,查看 $FRONTEND_DIR/build.log"
cat build.log
exit 1
fi
rm -f build.log
fi
# 恢复 API 目录
api_backup=$(ls /tmp/next-api-backup-* 2>/dev/null | head -1)
if [ -n "$api_backup" ]; then
mv "$api_backup" src/app/api 2>/dev/null || true
fi
cd "$PROJECT_ROOT"
# 验证构建结果
if [ ! -d "$FRONTEND_OUT_DIR" ] || [ ! -f "$FRONTEND_OUT_DIR/index.html" ]; then
print_error "前端构建失败:输出文件不存在"
exit 1
fi
print_success "前端构建完成"
}
# 复制前端文件到嵌入目录
copy_frontend_files() {
if [ "$BACKEND_ONLY" = true ]; then
print_info "跳过前端文件复制 (--backend-only)"
return
fi
print_step "复制前端文件到嵌入目录..."
# 确保嵌入目录存在
mkdir -p "$FRONTEND_EMBED_DIR"
# 清理现有文件(除了 .gitkeep
find "$FRONTEND_EMBED_DIR" -type f ! -name ".gitkeep" -delete 2>/dev/null || true
# 复制所有文件
if [ -d "$FRONTEND_OUT_DIR" ]; then
cp -r "$FRONTEND_OUT_DIR"/* "$FRONTEND_EMBED_DIR/" 2>/dev/null || true
# 统计复制的文件
file_count=$(find "$FRONTEND_EMBED_DIR" -type f ! -name ".gitkeep" | wc -l)
total_size=$(du -sh "$FRONTEND_EMBED_DIR" 2>/dev/null | cut -f1 || echo "未知")
print_verbose "复制了 $file_count 个文件,总大小: $total_size"
print_success "前端文件复制完成"
else
print_error "前端输出目录不存在: $FRONTEND_OUT_DIR"
exit 1
fi
}
# 构建后端
build_backend() {
if [ "$FRONTEND_ONLY" = true ]; then
print_info "跳过后端构建 (--frontend-only)"
return
fi
print_step "构建 Go 后端..."
cd "$PROJECT_ROOT"
# 构建参数
local build_args=()
if [ "$DEV_MODE" = true ]; then
build_args+=("-gcflags" "all=-N -l") # 禁用优化,启用调试
print_verbose "开发模式构建(包含调试信息)"
else
build_args+=("-ldflags" "-s -w") # 移除调试信息和符号表
print_verbose "生产模式构建(移除调试信息)"
fi
build_args+=("-o" "$BINARY_NAME" "./cmd")
# 执行构建
print_verbose "执行 Go 构建: go build ${build_args[*]}"
if [ "$VERBOSE" = true ]; then
go build "${build_args[@]}"
else
go build "${build_args[@]}" 2>&1
if [ $? -ne 0 ]; then
print_error "Go 构建失败"
exit 1
fi
fi
# 验证构建结果
if [ ! -f "$BINARY_PATH" ]; then
print_error "Go 构建失败:二进制文件不存在"
exit 1
fi
# 显示二进制文件信息
if command -v file &> /dev/null; then
file_info=$(file "$BINARY_PATH")
print_verbose "二进制文件信息: $file_info"
fi
binary_size=$(du -sh "$BINARY_PATH" | cut -f1)
print_verbose "二进制文件大小: $binary_size"
print_success "后端构建完成"
}
# 验证最终结果
verify_build() {
print_step "验证构建结果..."
if [ "$FRONTEND_ONLY" = true ]; then
if [ -d "$FRONTEND_OUT_DIR" ] && [ -f "$FRONTEND_OUT_DIR/index.html" ]; then
print_success "前端构建验证通过"
else
print_error "前端构建验证失败"
exit 1
fi
return
fi
if [ "$BACKEND_ONLY" = true ]; then
if [ -f "$BINARY_PATH" ]; then
print_success "后端构建验证通过"
else
print_error "后端构建验证失败"
exit 1
fi
return
fi
# 完整构建验证
local errors=()
if [ ! -f "$BINARY_PATH" ]; then
errors+=("二进制文件不存在")
fi
if [ ! -d "$FRONTEND_EMBED_DIR" ]; then
errors+=("前端嵌入目录不存在")
fi
embedded_files=$(find "$FRONTEND_EMBED_DIR" -type f ! -name ".gitkeep" | wc -l)
if [ "$embedded_files" -eq 0 ]; then
errors+=("没有嵌入的前端文件")
fi
if [ ${#errors[@]} -gt 0 ]; then
print_error "构建验证失败:"
for error in "${errors[@]}"; do
echo " - $error"
done
exit 1
fi
print_success "构建验证通过"
}
# 显示构建摘要
show_summary() {
print_header "构建完成"
echo -e "${GREEN}🎉 全栈应用构建成功!${NC}"
echo ""
if [ "$FRONTEND_ONLY" = true ]; then
print_info "📁 前端文件输出目录: $FRONTEND_OUT_DIR"
if [ -d "$FRONTEND_OUT_DIR" ]; then
file_count=$(find "$FRONTEND_OUT_DIR" -type f | wc -l)
dir_size=$(du -sh "$FRONTEND_OUT_DIR" | cut -f1)
echo " - 文件数量: $file_count"
echo " - 总大小: $dir_size"
fi
return
fi
if [ "$BACKEND_ONLY" = true ]; then
print_info "📦 后端二进制文件: $BINARY_PATH"
if [ -f "$BINARY_PATH" ]; then
binary_size=$(du -sh "$BINARY_PATH" | cut -f1)
echo " - 文件大小: $binary_size"
fi
return
fi
# 完整构建摘要
print_info "📦 单一二进制文件: $BINARY_PATH"
if [ -f "$BINARY_PATH" ]; then
binary_size=$(du -sh "$BINARY_PATH" | cut -f1)
echo " - 文件大小: $binary_size"
fi
if [ -d "$FRONTEND_EMBED_DIR" ]; then
embedded_files=$(find "$FRONTEND_EMBED_DIR" -type f ! -name ".gitkeep" | wc -l)
echo " - 嵌入的前端文件: $embedded_files"
fi
echo ""
print_info "🚀 部署说明:"
echo " 1. 只需部署单个二进制文件: $BINARY_NAME"
echo " 2. 运行命令: ./$BINARY_NAME"
echo " 3. 访问地址: http://localhost:8080"
echo ""
print_info "💡 特性:"
echo " ✅ 前端界面完全嵌入"
echo " ✅ 无需额外的静态文件服务器"
echo " ✅ 支持 SPA 路由"
echo " ✅ 自动处理 API 代理"
echo ""
if [ "$DEV_MODE" = true ]; then
print_warning "⚠️ 这是开发模式构建,包含调试信息,不适合生产部署"
fi
}
# 错误处理
error_cleanup() {
print_error "构建过程中发生错误"
# 尝试恢复 API 目录
api_backup=$(ls /tmp/next-api-backup-* 2>/dev/null | head -1)
if [ -n "$api_backup" ] && [ -d "$FRONTEND_DIR" ]; then
mv "$api_backup" "$FRONTEND_DIR/src/app/api" 2>/dev/null || true
print_verbose "已恢复 API 目录"
fi
exit 1
}
# 主函数
main() {
print_header "全栈应用构建脚本"
# 设置错误处理
trap error_cleanup ERR INT TERM
# 解析参数
parse_args "$@"
# 显示构建配置
if [ "$VERBOSE" = true ]; then
print_info "构建配置:"
echo " - 清理模式: $CLEAN"
echo " - 仅前端: $FRONTEND_ONLY"
echo " - 仅后端: $BACKEND_ONLY"
echo " - 开发模式: $DEV_MODE"
echo " - 详细输出: $VERBOSE"
echo ""
fi
# 执行构建步骤
check_dependencies
clean_all
build_frontend
copy_frontend_files
build_backend
verify_build
show_summary
}
# 如果脚本被直接执行
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi