ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

PowerShell+UniApp H5自动化打包部署实战:构建、压缩、上传与回滚

2026/10/6 11:25:00 拓冰建站 浏览量
PowerShell+UniApp H5自动化打包部署实战:构建、压缩、上传与回滚 做过 uni-app 项目的人都懂一个 H5 版本从开发完成到真正上线最烦人的往往不是写代码而是打包、压缩、上传、解压这一串机械动作。早期我每次发版都是手动打开 HBuilderX点发行、选 H5等编译结束再去 dist 目录里找文件压缩成 zip开 SFTP 工具传服务器再登录后台解压替换。这套流程看着简单但一旦代码改得勤、发版发得密每天重复三五次之后你一定会想搞个自动化。这篇文章分享的是我用 PowerShell 脚本把 UniApp H5 项目的打包、压缩、部署整条链路串起来的完整方案含实际可用的脚本源码和踩坑记录适合被手动发版折磨的前端、需要为小团队搭建发布工具的工程师也适合想在 Windows 下快速实现构建发布自动化的朋友。1. 为什么要用 PowerShell 扛下整条发版链路1.1 手动发版的真实成本先老老实实盘点一下手动发版的动作序列改完代码提交到仓库打开 HBuilderX等它把项目加载起来点“发行”选“网站-H5手机版”进入编译等待编译完成后去dist/build目录确认产物右键压缩成 zip打开 WinSCP 或 FileZilla 把压缩包传到服务器登录服务器解压、备份旧版本、替换新文件最后刷新页面验证。这串动作单看每一步都不难但累计起来的成本很可怕。一次发版平均十分钟起步遇到编译报错、压缩包传错目录、服务器上解压权限不对这些问题二十分钟也打不住。最要命的是整个过程没有日志哪一步出了问题只能靠记忆复盘上次部署了哪个版本、服务器上现在是什么状态全凭脑子记。我统计过自己一个月的发版频率迭代高峰期一天至少三次。也就是说光手动发版这件事一个月就要吃掉我大半天时间。而且这类重复操作非常磨人它不产生任何新价值纯粹是体力活。当时我就下决心必须把这套流程脚本化。1.2 PowerShell 相比批处理和 CI/CD 的取舍可能有人会说想自动化为什么不直接上 Jenkins 或者 GitHub Actions这得结合团队规模来看。单人项目或者两三个人的小组专门部署一套 Jenkins 要维护服务、配置流水线、管理插件成本比收益还高GitHub Actions 虽然方便但私有仓库要收费而且国内访问和触发也有各种幺蛾子。至于 GitLab Runner前提是你得有一台能跑 Runner 的机器。那为什么不用 Windows 批处理.bat批处理写简单命令确实快但一旦牵扯到字符串处理、目录遍历、错误控制、日志格式化写起来就非常痛苦一句set /p和if errorlevel 1能让你怀疑人生。PowerShell 是 Windows 自带的现代脚本语言有对象管道、有异常处理、有$LASTEXITCODE来判断外部命令成败最关键的是它天生就是给“系统管理自动化”这种场景设计的。另一个容易忽略的点是PowerShell 脚本并不会和 CI/CD 体系冲突。就算将来团队规模大了、上了 GitLab Runner也可以用 Windows Runner 直接调用这套脚本作为流水线的一步。写脚本的关键是把它当作一个可以被调度的工具而不是只能手敲的玩具。2. 打包前先看清项目出身HBuilderX 工程和 CLI 工程是两条路2.1 先判断项目属于哪种类型uni-app 项目有两种常见创建方式一种是在 HBuilderX 可视化界面里新建的工程一种是基于 CLI 模板vue-cli 或 vite创建的工程。很多人把自动化卡在第一步就是因为没搞清这个区别。判断方法很简单打开项目根目录看是否存在package.json。HBuilderX 可视化创建的工程默认是不带package.json的它的编译能力完全靠 HBuilderX 内置编译器提供所以你在命令行里执行npm run build:h5会直接提示找不到脚本。而 CLI 创建的工程一定有package.json而且scripts里通常写着build:h5: uni build。更完整的判断方式看下面这张表特征HBuilderX 可视化创建CLI / Vite 创建package.json通常不存在存在且有 uni 相关依赖manifest.json项目根目录src 目录下构建命令依赖 HBuilderX 菜单npm run build:h5构建产物HBuilderX 自动输出dist/build/h5或dist/build/web自动化友好度低高还有一种情况比较特殊项目虽然是 CLI 创建的但被导入过 HBuilderX 或者加了 HBuilderX 的插件此时根目录可能会有manifest.json但pages.json仍在src下。遇到这种混合形态以package.json和scripts为准。2.2 HBuilderX 工程怎么接入自动化链路如果你的项目确实是 HBuilderX 可视化创建的又想把发版自动化有两条路。第一条路是临时方案用 HBuilderX 安装目录下的cli.exe在命令行里打开项目并触发发行操作。这个工具确实存在安装 HBuilderX 后能在安装目录下找到用法一般类似cli open 项目路径。但不同 HBuilderX 版本对命令行发行支持的稳定性差异很大而且它本质还是调起图形界面程序一旦界面弹出来脚本就很难做到完全无人值守。我的建议是除非只是应急用一两次否则别指望它。第二条路是彻底改造把工程迁移成 CLI 工程。操作思路是先用 CLI 模板新建一个空项目比如npx degit dcloudio/uni-preset-vue#vite my-uniapp然后把原来pages、static、components、store、utils这些目录整体复制进新项目注意manifest.json、pages.json要从根目录挪到src下接着安装依赖跑通npm run dev:h5开发调试最后确认npm run build:h5能产出正常页面。迁移过程中最麻烦的是项目里用到的原生插件和第三方库。HBuilderX 插件市场里的部分插件不一定能在 CLI 工程里正常安装需要到 npm 上找等价包。如果项目重度依赖 HBuilderX 生态迁移前务必在分支上做一次完整验证不要贸然替换线上构建方式。2.3 构建输出目录的确认与清理策略CLI 工程执行npm run build:h5后产物默认在dist/build/h5但个别版本或自定义配置下会输出到dist/build/web还有团队自己改过 Vite 配置的会输出到其他自定义目录。脚本里千万不要把这个路径写死否则哪天升级脚手架脚本会突然找不到产物直接罢工。我在脚本里用的是探测方式$candidates (dist/build/h5, dist/build/web, dist/build/h5-tmp) $buildDir $null foreach ($cand in $candidates) { $fullPath Join-Path $ProjectPath $cand if (Test-Path $fullPath) { $buildDir $fullPath break } } if (-not $buildDir) { throw 未找到构建产物目录请先手动执行 npm run build:h5 确认输出位置 }这样即使以后 uni-app 升级改了默认目录也只需要在$candidates数组里加一个值而不需要改主逻辑。关于清理我只会删除dist/build这一层不会动dist/dev和整个dist。因为开发时可能还依赖dist/dev下的调试产物误删会影响正在进行的本地联调。构建前做一次清理可以避免上次编译残留的旧文件被打进新包尤其是那些单纯删除的页面不清理的话旧html可能会被原样保留。3. PowerShell 脚本核心实现构建、压缩与版本归档三板斧3.1 脚本参数设计既然要做自动化脚本就不能是写死的至少要把最常用的变量作为参数暴露出来。我设计的参数如下参数说明示例-ProjectPathuni-app 项目根目录D:\code\my-uniapp-ArchiveDir本地压缩包存放目录D:\backup\uniapp-h5-Keep本地保留最新几个版本默认 1010-Server目标服务器userhostdeployer192.168.1.10-RemotePath远端应用根目录/var/www/app-SkipDeploy只构建压缩不上传-对应的param块param( [string]$ProjectPath D:\code\my-uniapp, [string]$ArchiveDir D:\backup\uniapp-h5, [int]$Keep 10, [string]$Server deployer192.168.1.10, [string]$RemotePath /var/www/app, [switch]$SkipDeploy ) $ErrorActionPreference Stop $timestamp Get-Date -Format yyyyMMdd_HHmmss所有参数都带默认值是因为这个脚本的典型使用场景是“每天早上手动跑一遍”有默认值可以少打很多字但团队协作时建议把服务器、项目路径这类敏感信息放到外部配置或环境变量里至少别直接写死在脚本文件里提交到仓库。3.2 npm 调用的正确姿势调用npm run build:h5是脚本最核心的动作写法上要特别注意。我的推荐写法是Push-Location $ProjectPath try { Write-Host [BUILD] 开始执行 npm run build:h5 npm run build:h5 if ($LASTEXITCODE -ne 0) { throw 构建失败退出码: $LASTEXITCODE } } finally { Pop-Location }这里有几个关键点。第一用Push-Location和Pop-Location切换工作目录这样脚本执行完不会改变终端当前路径避免干扰后续手动命令。第二调用外部命令用调用操作符这是 PowerShell 执行原生命令的标准姿势。第三判断成败必须用$LASTEXITCODE这是 npm 进程返回给操作系统的退出码日志打印错、依赖缺失、编译报错退出码通通不是 0。我早期也试过写成npm run build:h5 21 | Tee-Object -FilePath build.log想同时把输出打到控制台和日志文件。但这个写法在 Windows PowerShell 5.1 里有个很恶心的坑21会把 npm 的 stderr 流重定向成 PowerShell 的 ErrorRecord一旦脚本开头设置了$ErrorActionPreference Stopnpm 哪怕只是打印了几行无害的警告脚本也可能被直接中断。所以我现在的做法是构建输出原样留在控制台日志留痕交给Start-Transcript或者外层统一重定向到文件。3.3 压缩选型Windows 的 tar 比 Compress-Archive 更合适不少人的第一反应是用 PowerShell 自带的Compress-Archive但实际跑过几次就会后悔。这个命令压缩小文件夹还行项目一旦大一点速度慢得让人着急而且它默认只能生成 zip 格式在 Linux 服务器上解压还得额外装unzip压缩率也不如 gzip。Windows 10 以上系统自带tar.exe底层是 libarchive完全可以在 PowerShell 里直接调。我的压缩命令是这样的$archiveName uniapp-h5-$timestamp.tar.gz $archivePath Join-Path $ArchiveDir $archiveName # 删除 sourcemap避免把源码映射文件一起发上服务器 Get-ChildItem $buildDir -Recurse -Filter *.map | Remove-Item -Force tar -czf $archivePath -C $buildDir . if ($LASTEXITCODE -ne 0) { throw 压缩失败退出码: $LASTEXITCODE }-C $buildDir .的组合是关键意思是先进入产物目录再把当前目录下所有内容打进压缩包。这样解压出来的直接就是index.html、static这些根内容而不是多套一层h5目录。很多人打出来的包解压后还要手动把文件挪一层就是因为少了-C参数。删除.map文件这个习惯是我后来才养成的。H5 项目一旦开启 sourcemap构建产物里会带一批.map文件它们不会影响运行但会让整个压缩包变大、把源码暴露在服务器上纯属有害无益。与其在 Vite 配置里折腾开关不如在打包前直接物理删除。3.4 版本归档与保留策略压缩包不能生成一个扔一个必须有规律地归档否则一个月后D:\backup\uniapp-h5里全是一堆早期版本想找个历史包都不知道哪个是哪个。我建议的目录结构是D:\backup\uniapp-h5\ ├── uniapp-h5-20250315_102345.tar.gz ├── uniapp-h5-20250314_093012.tar.gz └── uniapp-h5-20250313_154201.tar.gz时间戳直接体现在文件名里按修改时间排序就能看到完整的发版历史。保留策略用脚本自动执行只保留最近 N 个$oldArchives Get-ChildItem $ArchiveDir -Filter uniapp-h5-*.tar.gz | Sort-Object LastWriteTime -Descending | Select-Object -Skip $Keep foreach ($file in $oldArchives) { Remove-Item $file.FullName -Force Write-Host [CLEAN] 删除旧归档: $($file.Name) }这里用Sort-Object LastWriteTime -Descending拿到最新到最旧的排列Select-Object -Skip $Keep跳过最近 N 个剩下的就是要清理的历史包。为什么本地要保留多个版本而不只留最新因为有时候你发现线上出了问题想对比一下“上一个版本”和“当前版本”的差异本地没有历史压缩包就只能重新构建费时费力。4. 部署环节SCP 上传、远端解压与一键回滚4.1 密钥认证与主机指纹部署环节要解决的第一件事是免密登录。脚本里绝对不能出现服务器密码一方面不安全另一方面密码认证没法无人值守——第一次连上去让你输入密码脚本就卡死了。正确做法是生成 SSH 密钥把公钥放到服务器上。生成密钥ssh-keygen -t ed25519 -C deploy-key默认会生成到C:\Users\你的用户名\.ssh\id_ed25519和id_ed25519.pub。然后把公钥追加到服务器的authorized_keys文件里Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub | ssh deployer192.168.1.10 mkdir -p ~/.ssh; cat ~/.ssh/authorized_keys这里需要注意第一次连接任意一台服务器时SSH 都会弹出确认主机指纹的提示手动输入yes并回车确认。这个动作要在自动化脚本跑之前手动完成一次让指纹写进 known_hosts 文件。之后脚本里再连接就不会卡交互。脚本里我会加上-o BatchModeyes这个参数表示“连接过程中禁止任何交互式提示”万一密钥失效或主机指纹变化脚本会直接失败而不是挂在那里等输入CI 场景尤其需要这个特性。4.2 上传与远端解压上传用系统自带的scp命令if (-not $SkipDeploy) { scp -o BatchModeyes $archivePath ${Server}:~/deploy/$archiveName if ($LASTEXITCODE -ne 0) { throw 上传失败退出码: $LASTEXITCODE } }上传目标不建议直接扔/tmp。/tmp是系统公共临时目录任何系统用户都能读放部署包不太稳妥。我在服务器上习惯建一个~/deploy目录专门放待部署的压缩包。真正麻烦的是远端解压和目录切换。与其每次从本地拼一大堆远程命令不如直接把一套部署脚本放进服务器本地脚本只负责传参调用。我在服务器/var/www/app下维护一个deploy.sh#!/bin/bash set -e APP_BASE/var/www/app VERSION$1 ARCHIVE~/deploy/$VERSION.tar.gz if [ ! -f $ARCHIVE ]; then echo 压缩包不存在: $ARCHIVE exit 1 fi mkdir -p $APP_BASE/releases/$VERSION tar -xzf $ARCHIVE -C $APP_BASE/releases/$VERSION ln -sfn $APP_BASE/releases/$VERSION $APP_BASE/current cd $APP_BASE/releases ls -1 | sort -r | tail -n 4 | while read dir; do rm -rf $dir done echo 部署完成: $VERSION本地调用 ssh -o BatchModeyes $Server bash $RemotePath/deploy.sh $timestamp这个方案比在 PowerShell 里拼一长串远程命令靠谱得多。远程脚本自身处理解压、软链、清理本地只需要传版本号。ln -sfn这行是整个部署的关键它把current这个软链接原子性地切换到新版本目录。Web 服务器配置里只要让站点根目录指向current那一次发版本质上就是替换一次软链不会有“正在上传一半、页面访问 404”的尴尬窗口。4.3 远端目录结构与权限部署完成后的服务器目录大致长这样/var/www/app/ ├── current - releases/20250315_102345 └── releases/ ├── 20250315_102345 │ ├── index.html │ ├── static │ └── ... ├── 20250314_093012 └── 20250313_154201Nginx 配置里的root /var/www/app/current;即可Nginx 默认会解引用软链所以切换版本对 Web 服务是即时生效的。这里有一个常见问题部署用户 deployer 创建的文件默认属主是 deployer如果 Nginx 的 worker 进程跑在www-data用户下可能会因为目录权限不足返回 403。我一般会先设置目录属组和读权限要么把站点目录的属组改成www-data并加grx要么干脆让 Nginx 用 deployer 用户跑具体按团队权限管理习惯来。至少要在上线前手动检查一次sudo -u www-data ls current/能列出内容才算过关。4.4 一键回滚这个设计最值钱的地方在于回滚只需要一条命令。假设新版本20250315_102345上线后首页白屏回滚到上一个版本$rollbackVersion 20250314_093012 ssh -o BatchModeyes $Server ln -sfn $RemotePath/releases/$rollbackVersion $RemotePath/current软链切换是原子的、即时的回滚过程中不像整包替换那样要重新上传文件。我用这个方案应对过一次线上事故发布后用户反馈登录异常我直接在终端里切了一下软链十几秒内服务恢复然后才有时间慢慢看新版本的日志。这种安全感是手动发版给不了的。5. 实际跑起来才会踩到的坑编码、执行策略与路径细节5.1 PowerShell 5.1 中文乱码的根源与根治我自己部署这套脚本时第一个坑就是中文乱码。现象分两种一种是 npm 构建时终端提示乱码全是一堆菱形问号另一种是脚本里的中文注释和Write-Host在 PowerShell 5.1 里直接显示成乱码。根源很简单Windows PowerShell 5.1 控制台默认代码页是 GBK而 Node/npm 输出的是 UTF-8脚本文件本身如果存成了无 BOM 的 UTF-8PowerShell 5.1 会按 ANSI 去读中文自然全挂。根治办法分三步。第一步把系统脚本执行时的控制台编码设为 UTF-8[Console]::OutputEncoding [System.Text.Encoding]::UTF8 $OutputEncoding [Console]::OutputEncoding第二步把脚本文件保存成带 BOM 的 UTF-8。在 VS Code 里可以通过右下角“选择编码”改成“UTF-8 with BOM”或者直接用 PowerShell 重存$content Get-Content -Raw deploy.ps1 Set-Content -Path deploy.ps1 -Value $content -Encoding UTF8第三步如果条件允许干脆装一个 PowerShell 7pwsh。它在设计上默认全链路 UTF-8乱码问题基本消失而且连接符、$PSNativeCommandUseErrorActionPreference这些现代语法都能用。现在 Windows 上winget install Microsoft.PowerShell一条命令就能装好别再死守 5.1 了。5.2 执行策略限制与团队分发新机器上第一次运行脚本十有八九会碰到这个报错无法加载文件 deploy.ps1因为在此系统上禁止运行脚本。原因是 Windows PowerShell 默认执行策略是Restricted不允许运行任何脚本。在你自己的开发机上执行一次Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserRemoteSigned的意思是本地创建的脚本可以运行从网络下载的脚本需要可信签名。这个策略对绝大多数开发机足够。也可以不改策略在命令行启动时临时绕过powershell -NoProfile -ExecutionPolicy Bypass -File deploy.ps1如果脚本要发给团队其他人用最省心的包装方式是在同目录下放一个deploy.batecho off powershell -NoProfile -ExecutionPolicy Bypass -File %~dp0deploy.ps1 %*这样同事双击 bat 就能跑不用每个人理解什么是执行策略。5.3 路径带空格与外部命令传参陷阱开发机上项目路径带空格是常态比如D:\My Projects\uni-app-demo。PowerShell 自身处理带空格的路径没问题变量作为一个整体传给外部命令也没问题——它不像老式 CMD 那样遇到空格就截断。真正的坑出现在拼字符串的场景。我踩过的一个实际例子scp命令里把路径拼进一个大字符串时忘记用变量而是手写路径结果传到远端服务器后目标目录多被拆了一段。另一个常见问题是在tar的-C参数后传一个带空格的路径。虽然变量传参没问题但如果你写成tar -czf $archivePath -C $buildDir .而$buildDir恰好是数组类型PowerShell 会把数组展开成多个参数瞬间把所有路径打散。我的建议是脚本内部统一使用Join-Path拼路径杜绝手写字符串拼接对外部命令传参时全部使用变量项目路径尽量约定不带空格。如果团队里实在避免不了那就严格要求所有路径变量在赋值时用[string]类型约束防止被意外设置成数组。5.4 定时任务和开机自启场景脚本跑通之后完全可以挂进 Windows 任务计划程序实现每天固定时间自动构建。用schtasks注册schtasks /Create /TN UniAppH5DailyBuild /TR powershell.exe -NoProfile -ExecutionPolicy Bypass -File D:\scripts\deploy.ps1 -SkipDeploy /SC DAILY /ST 02:00这里有一个易踩的坑任务计划程序执行时的工作目录默认是C:\Windows\System32脚本里所有相对路径都会失效。所以脚本内部必须全部用绝对路径或者执行第一条命令时就Push-Location到明确路径。还有一点计划任务如果配置成“不管用户是否登录都要运行”那 SSH 密钥要放在该任务账户的.ssh目录下不是你的个人账户下这个细节经常被忽略导致任务一直报错。你也可以把脚本做成开机自启放一个deploy.ps1 -SkipDeploy的快捷方式到shell:startup目录但我的看法是发布动作最好显式触发不要开机自动发版。真要定时构建最多是-SkipDeploy生成压缩包部署这步留给人工确认后执行。5.5 敏感信息与安全边界最后提醒一下安全层面的习惯。脚本里的服务器地址、用户名这些信息属于敏感配置最好不要直接提交进 Git 仓库。我通常的做法是把这类变量放进用户级环境变量或者一个单独的被.gitignore排除的config.ps1文件里主脚本通过Import-PowerShellDataFile或简单的dot sourcing加载。SSH 私钥权限也要保持默认别图省事把私钥拷给项目组每个人而是每人各自生成自己的密钥离职时从服务器上删掉对应公钥即可。写到这里这套 PowerShell 自动化链路基本完整了。它不炫技也没有用什么高级框架核心就是先把手工流程拆成可以重复执行的原子步骤再用脚本把它们串起来。实际用下来我最深的感受是自动化的收益不只是省那几分钟而是每次发版的路径变得可预测、可留痕、可回滚。如果你也打算给自己写一个建议从最小闭环开始——先把打包和压缩跑通再逐步加上部署、清理、回滚而不是一开始就追求全自动。