AI书签管理工具开发全记录(二十):打包(完结篇)
文章目录
- AI书签管理工具开发全记录(二十):打包
- 1.前言 📝
- 2.修改前端配置 ✏️
- 3.打包 🔄
- 3.1 Makefile
- 3.2 build.py
- 3.3 完整编译流程
- 4.编译多平台包 🏗️
- 4.1 gox简介
- 4.2 主要特点
- 4.3 基本用法
- 4.4 与标准Go工具的区别
- 4.5 安装
- 4.6 适用场景
- 4.7 build.py改造
AI书签管理工具开发全记录(二十):打包
1.前言 📝
在上一篇文章中,我们完成了嵌入资源部分相关代码实现,至此,本项目预期功能已基本全部实现,剩下的部分就是修复使用过程中相关bug以及项目打包。本文将聚焦于打包部分实现。
2.修改前端配置 ✏️
前端部分需要动态获取实际运行中端口,动态配置基础url,否则会造成接口数据异常。
// web/src/utils/request.jsfunction getApiBaseUrl() {// 1. 优先使用显式配置的环境变量if (import.meta.env.VUE_APP_API_URL) {return import.meta.env.VUE_APP_API_URL}// 2. 开发环境特殊处理if (process.env.NODE_ENV === 'development') {return 'http://localhost:8080/api'}// 3. 生产环境使用当前域名const { protocol, hostname, port } = window.locationlet basePort = port ? `:${port}` : ''// 处理非标准端口if ((protocol === 'http:' && port === '80') || (protocol === 'https:' && port === '443')) {basePort = ''}return `${protocol}//${hostname}${basePort}/api`
}// 创建 axios 实例
const service = axios.create({baseURL: getApiBaseUrl(),timeout: 10000
})
3.打包 🔄
由于需要将将前端资源嵌入exe中,所有每次打包前需要先运行npm run build
,将打包的dist目录中内容放置到resources\static
目录中。
流程可以表示为
打包完的可执行文件一般较大,不利用分发,我们可以利用upx进行压缩,因此完整流程应该为
在此,我们提供两个脚本,来加速构建过程(任选其一即可)
- Makefile 需要安装了make
- build.py 需要安装了python
3.1 Makefile
# 应用名称
APP_NAME=aibookmark# 检测操作系统
ifeq ($(OS),Windows_NT)# WindowsRM_CMD = if existRM_END = del /F /QMKDIR_CMD = if not existMKDIR_END = mkdirCP_CMD = xcopy /E /I /Y /HPATH_SEP = \\EXE_EXT = .exeNPM_CMD = npm.cmd# 设置代码页为UTF-8CHCP_CMD = chcp 65001 >nul
else# Linux/macOSRM_CMD = rm -rfMKDIR_CMD = mkdir -pCP_CMD = cp -rPATH_SEP = /EXE_EXT =NPM_CMD = npm
endif# 默认目标
.PHONY: all
all: build# 构建前端
.PHONY: build-frontend
build-frontend:
ifeq ($(OS),Windows_NT)@$(CHCP_CMD)
endif@echo "构建前端..."cd web && $(NPM_CMD) install && $(NPM_CMD) run build# 复制静态资源
.PHONY: copy-static
copy-static:
ifeq ($(OS),Windows_NT)@$(CHCP_CMD)
endif@echo "复制静态资源..."
ifeq ($(OS),Windows_NT)@if not exist resources\static $(MKDIR_END) resources\static@$(CP_CMD) "web\dist\*" "resources\static\"
else@$(MKDIR_CMD) resources/static@$(CP_CMD) web/dist/* resources/static/
endif# 检查UPX是否可用
.PHONY: check-upx
check-upx:
ifeq ($(OS),Windows_NT)@$(CHCP_CMD)
endif@echo "检查UPX..."@upx --version >nul 2>&1 || (echo "UPX未安装,跳过压缩步骤" && exit 0)# 使用UPX压缩
.PHONY: compress-upx
compress-upx: check-upx
ifeq ($(OS),Windows_NT)@$(CHCP_CMD)
endif@echo "开始UPX压缩..."@upx --best --verbose $(APP_NAME)$(EXE_EXT)@echo "清理UPX临时文件..."
ifeq ($(OS),Windows_NT)@if exist $(APP_NAME).000 $(RM_END) $(APP_NAME).000@if exist $(APP_NAME).upx $(RM_END) $(APP_NAME).upx
else@$(RM_CMD) $(APP_NAME).000 2>/dev/null || true@$(RM_CMD) $(APP_NAME).upx 2>/dev/null || true
endif# 构建Go应用
.PHONY: build-go
build-go:
ifeq ($(OS),Windows_NT)@$(CHCP_CMD)
endif@echo "构建Go应用..."go build -o $(APP_NAME)$(EXE_EXT)# 构建所有内容
.PHONY: build
build: build-frontend copy-static build-go compress-upx# 清理构建文件
.PHONY: clean
clean:
ifeq ($(OS),Windows_NT)@$(CHCP_CMD)
endif@echo "清理构建文件..."
ifeq ($(OS),Windows_NT)@if exist web\dist $(RM_END) web\dist@if exist resources\static $(RM_END) resources\static\*@if exist $(APP_NAME)$(EXE_EXT) $(RM_END) $(APP_NAME)$(EXE_EXT)@if exist $(APP_NAME).000 $(RM_END) $(APP_NAME).000@if exist $(APP_NAME).upx $(RM_END) $(APP_NAME).upx
else@$(RM_CMD) web/dist@$(RM_CMD) resources/static/*@$(RM_CMD) $(APP_NAME)$(EXE_EXT)@$(RM_CMD) $(APP_NAME).000 2>/dev/null || true@$(RM_CMD) $(APP_NAME).upx 2>/dev/null || true
endif# 帮助信息
.PHONY: help
help:
ifeq ($(OS),Windows_NT)@$(CHCP_CMD)
endif@echo "可用的命令:"@echo " make build - 构建整个应用"@echo " make build-frontend - 仅构建前端"@echo " make copy-static - 仅复制静态资源"@echo " make build-go - 仅构建Go应用"@echo " make compress-upx - 仅压缩可执行文件"@echo " make clean - 清理所有构建文件"@echo ""@echo "当前操作系统: $(OS)"@echo "可执行文件扩展名: $(EXE_EXT)"
如果想要构建,运行
make build
3.2 build.py
from pathlib import Path
import subprocess
import logging as log
import os
import shutil
log.basicConfig(level=log.INFO, format="%(asctime)s - %(levelname)s - %(message)s")def run_npm_build():target_dir = Path(__file__).parent.joinpath("web")target_dir_path = target_dir.resolve()log.info("target_dir_path: " + str(target_dir_path))# 在Windows系统上使用npm.cmdnpm_cmd = "npm.cmd" if os.name == 'nt' else "npm"try:result = subprocess.run([npm_cmd, "run", "build"], cwd=target_dir_path, capture_output=True, text=True,shell=True,encoding='utf-8', # 显式指定UTF-8编码errors='replace' # 替换无法解码的字符)if result.returncode == 0:log.info("npm run build 命令执行成功")if result.stdout:log.info(result.stdout)else:log.error("npm run build 命令执行失败")if result.stderr:log.error(result.stderr)raise Exception("npm run build 命令执行失败")except Exception as e:log.error(f"执行npm命令时出错: {str(e)}")raisedef remove_old_build():target_dir = Path(__file__).parent.joinpath("resources").joinpath("static")# 检查并创建目标目录try:target_dir.mkdir(parents=True, exist_ok=True)except Exception as e:log.error(f"创建目录失败: {str(e)}")raise# 删除目录内容(包括子目录)for item in target_dir.glob('*'):try:if item.is_file():item.unlink()elif item.is_dir():shutil.rmtree(item)except Exception as e:log.error(f"删除 {item} 失败: {str(e)}")raisedef copy_new_build():source_dir = Path(__file__).parent.joinpath("web").joinpath("dist")target_dir = Path(__file__).parent.joinpath("resources").joinpath("static")# 确保源目录存在if not source_dir.exists():log.error("源目录不存在: " + str(source_dir))raise FileNotFoundError("源目录不存在")# 复制整个目录结构try:shutil.copytree(source_dir,target_dir,dirs_exist_ok=True # 允许目标目录已存在)log.info(f"成功复制文件从 {source_dir} 到 {target_dir}")except Exception as e:log.error(f"复制文件失败: {str(e)}")raisedef main():run_npm_build()remove_old_build()copy_new_build()if __name__ == "__main__":main()
打包运行
python build.py
3.3 完整编译流程
梳理完整编译流程
4.编译多平台包 🏗️
推荐使用gox
4.1 gox简介
gox 是一个用于 Go 语言交叉编译的工具,它简化了为多个平台和架构构建 Go 程序的过程。
4.2 主要特点
- 交叉编译:可以轻松地为不同操作系统和CPU架构构建二进制文件
- 并行构建:能够同时构建多个平台的版本
- 简化流程:比直接使用
go build
的交叉编译更简单易用
4.3 基本用法
gox -osarch="linux/amd64 windows/amd64 darwin/amd64"
4.4 与标准Go工具的区别
- 标准
go build
需要设置GOOS
和GOARCH
环境变量 - gox 自动处理这些设置,并可以一次性构建多个平台版本
- gox 会自动处理一些依赖问题,如CGO_ENABLED等
4.5 安装
go get github.com/mitchellh/gox
4.6 适用场景
- 需要为多个平台分发Go应用程序时
- 自动化构建和发布流程中
- 开发需要支持多平台的工具时
gox 特别适合需要为Linux、Windows和macOS等多个平台提供二进制文件的开发者。
4.7 build.py改造
def get_version():"""获取版本号优先使用 Git 标签作为版本号,如果没有标签则使用 Git 提交哈希"""try:# 尝试获取最新的 Git 标签tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"],stderr=subprocess.DEVNULL,universal_newlines=True).strip()return tagexcept subprocess.CalledProcessError:try:# 如果没有标签,使用 Git 提交哈希commit = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"],stderr=subprocess.DEVNULL,universal_newlines=True).strip()return f"0.0.0-{commit}"except subprocess.CalledProcessError:# 如果 Git 命令失败,返回默认版本号return "0.0.0-dev"def get_build_info():"""获取构建信息"""try:# 获取 Git 提交哈希commit = subprocess.check_output(["git", "rev-parse", "HEAD"],stderr=subprocess.DEVNULL,universal_newlines=True).strip()# 获取 Git 分支名branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"],stderr=subprocess.DEVNULL,universal_newlines=True).strip()return {"commit": commit,"branch": branch,"build_time": time.strftime("%Y-%m-%d %H:%M:%S")}except subprocess.CalledProcessError:return {"commit": "unknown","branch": "unknown","build_time": time.strftime("%Y-%m-%d %H:%M:%S")}def build_with_gox():"""使用gox进行交叉编译"""if not check_gox():log.error("gox未安装,请先安装gox: go install github.com/mitchellh/gox@v1.0.1")raise Exception("gox未安装")try:# 设置输出目录output_dir = "build"# 清理输出目录if os.path.exists(output_dir):shutil.rmtree(output_dir)os.makedirs(output_dir)# 获取版本信息和构建信息version = get_version()build_info = get_build_info()# 设置编译参数ldflags = (f"-X 'main.Version={version}' "f"-X 'main.BuildTime={build_info['build_time']}' "f"-X 'main.GitCommit={build_info['commit']}' "f"-X 'main.GitBranch={build_info['branch']}'")# 执行gox命令,在项目根目录下执行return_code = run_command(["gox","-os", "windows linux darwin","-arch", "amd64","-ldflags", ldflags,"-output", "%s/aibookmark_{{.OS}}_{{.Arch}}" % output_dir])if return_code == 0:log.info(f"gox交叉编译成功,版本: {version}")# 为Windows版本添加.exe扩展名for file in os.listdir(output_dir):if file.startswith("aibookmark_windows") and not file.endswith(".exe"):old_path = os.path.join(output_dir, file)new_path = old_path + ".exe"os.rename(old_path, new_path)log.info(f"重命名Windows可执行文件: {file} -> {file}.exe")# 只对当前平台的可执行文件进行UPX压缩current_os = "windows" if os.name == 'nt' else "linux" if os.name == 'posix' else "darwin"for file in os.listdir(output_dir):if file.startswith(f"aibookmark_{current_os}"):file_path = os.path.join(output_dir, file)if os.path.isfile(file_path):log.info(f"开始压缩当前平台文件: {file}")compress_with_upx(file_path)else:log.info(f"跳过非可执行文件: {file}")else:log.info(f"跳过其他平台文件: {file}")else:log.error("gox交叉编译失败")raise Exception("gox交叉编译失败")except Exception as e:log.error(f"使用gox编译时出错: {str(e)}")raise
往期系列
- Ai书签管理工具开发全记录(一):项目总览与技术蓝图
- Ai书签管理工具开发全记录(二):项目基础框架搭建
- AI书签管理工具开发全记录(三):配置及数据系统设计
- AI书签管理工具开发全记录(四):日志系统设计与实现
- AI书签管理工具开发全记录(五):后端服务搭建与API实现
- AI书签管理工具开发全记录(六):前端管理基础框框搭建 Vue3+Element Plus
- AI书签管理工具开发全记录(七):页面编写与接口对接
- AI书签管理工具开发全记录(八):Ai创建书签功能实现
- AI书签管理工具开发全记录(九):用户端页面集成与展示
- AI书签管理工具开发全记录(十):命令行中结合ai高效添加书签
- AI书签管理工具开发全记录(十一):MCP集成
- AI书签管理工具开发全记录(十二):MCP集成查询
- AI书签管理工具开发全记录(十三):TUI基本框架搭建
- AI书签管理工具开发全记录(十四):TUI基本界面完善
- AI书签管理工具开发全记录(十五):TUI基本逻辑实现与数据展示
- AI书签管理工具开发全记录(十六):Sun-Panel接口分析
- AI书签管理工具开发全记录(十七):Sun-Panel书签同步实现
- AI书签管理工具开发全记录(十八):书签导入导出
- 动态API地址配置:利用window.location.origin解决前后端分离部署难题