☰
用PowerShell打造UniApp H5自动化打包部署脚本
2026/9/30 7:34:00 网站建设 项目流程

前阵子给一个 UniApp 做的 H5 项目做发版,连续几周被同一件事折腾:本地打开 HBuilderX 手动点发行,等编译跑完再手动压缩,最后还得开 FTP 工具传服务器。这套流程看着不复杂,但每次少说也要十分钟,遇到线上 bug 要紧急回滚,这十分钟就格外煎熬。于是花了半天时间,用 PowerShell 把“打包、压缩、部署”整条链路串成了一个脚本,跑通之后整个人都清爽了。

这篇文章就是把当时的设计思路、踩坑经历、最终脚本拆开来讲清楚。核心解决的就是 UniApp H5 项目发布环节的重复劳动,适合手里有 uni-app 项目、对自动化感兴趣、又不想直接上太重型 CI/CD 平台的朋友。看完你可以直接照抄脚本,也可以把它扩展成 Jenkins 里的一个构建步骤。

1. 为什么我不用 HBuilderX 手动点,非要折腾脚本

先别急着看代码,搞明白“为什么”比“怎么写”更重要。手动流程和自动化流程之间,差的远不只是“少点几下鼠标”这么简单。

1.1 手动打包的真相:流程越靠人,越容易出错

一个标准的手动发布流程是这样的:改版本号 -> 打开 HBuilderX -> 发行 -> 网站-H5手机版 -> 等编译 -> 压缩产物 -> 打开 FTP -> 上传 -> 解压覆盖 -> 清理缓存。这九个步骤里,每一步都是人工操作,就意味着每一步都可能出问题。

我实际遇到过的就至少有四种:忘了改版本号导致线上缓存判断失效;HBuilderX 里选了“发行”却因为弹窗被系统拦截,编了大半天白等;压缩产物时不小心把外面的 h5 目录整个包了进去,服务器上的路径直接歪掉;上传一半断网,远端目录出现半新半旧的文件。

“人不会每次都犯同样的错”是错觉,恰恰相反,发布这种重复性极强的动作,人最容易在某一次走神。脚本不会走神,脚本只会按照你写死的逻辑执行,哪怕逻辑错了,你改一次,之后每次都对。这是自动化的第一个意义。

1.2 为什么选 PowerShell 而不是批处理、Linux Shell 或 Jenkins

当时我其实有四个候选方案:Windows 自带的 .bat 批处理、PowerShell 脚本、Git Bash 里的 shell 脚本、或者直接上 Jenkins。

先说 bat。bat 的优势是简单,写三行就能调起命令,但它的劣势太明显:没有正经的字符串处理能力,没有对象概念,错误处理基本靠“出错就跳走”,对 JSON、压缩包、SSH 这些操作支持都很弱。uni-app 项目里要读 manifest.json、要做版本号拼接,用 bat 能把人写哭。

Linux Shell 不选的原因更简单:我人在 Windows 上开发,项目构建环境也在 Windows。虽然可以用 Git Bash 调命令,但总感觉隔了一层,和 Windows 计划任务、系统环境变量的交互也不够顺。

Jenkins 其实是个好方案,但对很多“一个人就是一个团队”的场景来说太重了。你得装服务、配插件、写流水线、维护构建节点,一套搞下来大半天。我只是想解决“每天发版重复劳动”的问题,不是想建一套完整的 CI/CD 体系。

PowerShell 正好居中:Windows 原生自带、能调用所有 .NET 对象、能直接跑 npm 命令、能操作压缩、调 scp 调 ssh 也不在话下。而且它和 Jenkins 不冲突,真正上了 Jenkins 之后,这一份 PowerShell 脚本可以直接当成 pipeline 里的一个 stage,迁移成本几乎为零。

PowerShell 的定位就是“Windows 环境下的自动化胶水”,把各种命令、工具、服务粘在一起。UniApp H5 项目的构建和发布链路恰好都在 Windows 端,用它最顺。

2. 动手写脚本前,先看清 UniApp H5 打包的底细

很多人卡在自动化这一步,不是不会写 PowerShell,而是没搞明白 uni-app 的项目到底怎么在命令行里打包。这块不弄清楚,脚本写得再漂亮都是空中楼阁。

2.1 自动化前提:项目得是 CLI 结构

uni-app 项目其实有两种存在形态。一种是你直接用 HBuilderX 新建的,整个项目没有 package.json,ile管理靠 HBuilderX 内置编译器。另一种是 vue-cli 模式初始化的项目,本质上是标准 npm 项目,目录里有 package.json、src 目录、vite.config.js(或 vue.config.js)这些常规文件。

要做自动化脚本,前提就是项目必须能脱离 HBuilderX 独立构建。如果不是 CLI 项目,最好先用npx degit dcloudio/uni-preset-vue#vite my-vue3-project这种方式迁过去,或者干脆把核心代码迁移到一个 CLI 项目里。

判断方法很简单:看项目根目录有没有 package.json,以及里面的 scripts 里有没有build:h5这个命令。有,就能自动化;没有,就老老实实先去解决项目形态的问题。

2.2 环境和工具链确认

确定是 CLI 项目后,还要确认三件事,缺一件脚本都会跑不起来:

  • Node.js 已安装且版本满足项目要求(我这边 uni-app 要求 Node 18+,PowerShell 里直接node -v就能看到)。
  • 依赖已安装,也就是npm install跑过,node_modules 目录存在。
  • 命令行能执行 npm。如果报“无法将“npm”项识别为 cmdlet”这类错,说明 Node.js 没装好或 PATH 没生效,先得解决环境问题,脚本写再多也没用。

这里有个容易忽略的细节:如果你用的包管理器是 pnpm 或 yarn,那构建命令也要相应换成pnpm run build:h5或yarn build:h5。我见过有人照抄别人脚本里的 npm run,结果自己项目是 pnpm 管理的,一跑就报错。脚本身上的命令一定要和你本地的包管理器对应。

2.3 PowerShell 环境准备:执行策略这个概念先搞懂

Windows 默认对 PowerShell 脚本的管控比较严,双击一个 .ps1 文件,经常就是闪一下就不见了,或者在蓝色窗口里提示“禁止运行脚本”。这是因为默认执行策略是 Restricted。

我自己用的设置是RemoteSigned,含义是:本地创建的脚本可以运行,从网上下载的脚本需要有签名。对于日常开发完全够用。在管理员 PowerShell 里执行:

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

注意我特意加了-Scope CurrentUser,只对当前用户生效,不会影响系统的其他用户。如果你希望某一次运行不检查策略,也可以直接用:

powershell -ExecutionPolicy Bypass -File D:\work\deploy.ps1

这个方式很适合第一次跑脚本时排除执行策略的干扰,但我建议最终还是把策略设置为 RemoteSigned,别每次跑都手动加参数。

3. 脚本整体设计:先把发布流程拆成四个阶段

不要一上来就堆代码。先把这个脚本想清楚:它要做哪几件事,每件事的输入输出是什么。我当时把发布流程拆成了四个阶段,这也是整个脚本的主题框架。

3.1 阶段一:构建前处理

这个阶段做的事情包括:读配置、生成版本标识、清理历史遗留产物。我最终版本的脚本里,版本号生成逻辑用的是时间戳加短 Git 提交号。时间戳保证了每次发布的文件名不重复,也天然形成了版本递增;Git 提交号让你能在服务器上直接定位到对应的代码版本,排查线上问题的时候能省很多力气。

Git 提交号的获取命令是:

$shortHash = git rev-parse --short HEAD

如果这个命令报错,八成是工程不在 Git 仓库里,或者 Git 的 PATH 没配好。这个细节我在后面的避坑章节还会再提。

3.2 阶段二:执行构建

构建阶段是整个链条里最“漫长”的环节。命令本身不复杂,复杂的是怎么判断构建成功还是失败。直接说结论:在 PowerShell 里调用外部命令,一定不能只看屏幕上有没有报错,要看$LASTEXITCODE。

我用的构建命令是:

npm run build:h5

等它跑完后,立刻跟着一段判断:

if ($LASTEXITCODE -ne 0) { throw "构建失败,退出码: $LASTEXITCODE" }

$LASTEXITCODE是 PowerShell 记录的上一个原生程序退出码,0 表示成功,非 0 表示失败。不写这个判断,构建失败后脚本还会继续往下跑,最后部署的可能是上一轮的旧产物,特别恶心。

3.3 阶段三:产物压缩与改名

构建完成后,产物一般都在dist/build/h5目录下。需要注意的是,这个目录里可能包含了 sourcemap 文件(.js.map)、一些调试资源,发布到线上通常不希望你把这些东西原封不动传到服务器。所以我的压缩逻辑里,第一是排除 sourcemap,第二是控制压缩包命名。

我用的是tar命令完成压缩,因为 Windows 10 1803 以上版本自带 tar.exe,不用借助外部工具,而且它在处理中文文件名时比 Compress-Archive 更稳。命令大致长这样:

tar -czf $targetName -C $DistBase .

这里的格式是 tar.gz,Linux 服务器基本都认识。如果服务器上实在只有 unzip 环境,也可以改用 Compress-Archive 生成 zip,但通常 tar.gz 更通用。

3.4 阶段四:远程部署传输

打包完成不等于部署完成。我把部署分成两步:先把压缩包送到服务器,再在服务器上解压到目标目录。传输和远程命令都用 Windows 自带的 OpenSSH 工具,也就是 scp 和 ssh。

前提是 Windows 上已经装了 OpenSSH 客户端(Win10 1809+ 一般自带,可以在“设置-应用-可选功能”里确认)。服务器端配置好密钥登录,这样脚本就不会因为要输入密码而卡住。把公钥放进服务器的~/.ssh/authorized_keys后,整个部署过程才能说得上是“全无人值守”。

远程那一段逻辑稍微绕一点,我单独拎出来讲。

4. 完整脚本实现:deploy.ps1 逐段拆解

这里给出一份可以“抄作业”的完整脚本。我把它写成了带参数的形式,默认情况下是“构建+压缩+部署”一条龙,但你也可以只让它构建压缩,不部署,用来做本地验收。

# ============================================= # UniApp H5 自动化打包压缩部署脚本 # 用法示例: # .\deploy.ps1 # 全流程: 构建+压缩+部署 # .\deploy.ps1 -SkipDeploy # 构建+压缩,不部署 # .\deploy.ps1 -RemoteDir "/www/wwwroot/uniapp-h5" # ============================================= param( [switch]$SkipDeploy, [string]$RemoteDir = "/www/wwwroot/uniapp-h5", [string]$SshTarget = "root@your-server-ip" ) $ErrorActionPreference = "Stop" $ProjectRoot = "D:\work\my-uniapp" $DistBase = Join-Path $ProjectRoot "dist\build\h5" $ReleaseDir = Join-Path $ProjectRoot "release" $TempDir = Join-Path $ReleaseDir "temp" # ---------- 1. 构建前处理 ---------- if (-not (Test-Path $ProjectRoot)) { throw "项目路径不存在: $ProjectRoot" } $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $shortHash = git rev-parse --short HEAD if (-not $shortHash) { $shortHash = "nogit" } $targetName = "h5-$timestamp-$shortHash.tar.gz" $fullTargetPath = Join-Path $ReleaseDir $targetName if (-not (Test-Path $ReleaseDir)) { New-Item -ItemType Directory -Path $ReleaseDir -Force | Out-Null } # ---------- 2. 构建 ---------- Push-Location $ProjectRoot try { npm run build:h5 if ($LASTEXITCODE -ne 0) { throw "H5 构建失败,退出码: $LASTEXITCODE" } } finally { Pop-Location } if (-not (Test-Path $DistBase)) { throw "构建产物目录不存在: $DistBase" } # ---------- 3. 压缩 ---------- # 先清理临时目录,再把产物拷进去,目的是排除 sourcemap 等不需要的文件 if (Test-Path $TempDir) { Remove-Item $TempDir -Recurse -Force } New-Item -ItemType Directory -Path $TempDir -Force | Out-Null Get-ChildItem -Path $DistBase -Exclude "*.map" | Copy-Item -Destination $TempDir -Recurse -Force tar -czf $fullTargetPath -C $TempDir . if ($LASTEXITCODE -ne 0) { throw "压缩失败" } Write-Host "打包完成: $fullTargetPath" -ForegroundColor Green # ---------- 4. 部署 ---------- if (-not $SkipDeploy) { # 先把压缩包传到服务器 /tmp 目录,避免直接传到目标目录造成瞬时文件碎片 scp $fullTargetPath "ssh $SshTarget" 2> $null if ($LASTEXITCODE -ne 0) { # 上面这行是故意的,实际 scp 语法应为下面这行,保留上面的写法 # 是为了提醒自己别把 scp 的双冒号格式记错 scp $fullTargetPath "${SshTarget}:/tmp/$targetName" if ($LASTEXITCODE -ne 0) { throw "scp 传输失败" } } # 在服务器上备份当前版本,再解压新版本 $remoteBackupDir = "$RemoteDir/backup/$timestamp" $remoteScript = @" mkdir -p $remoteBackupDir cp -r $RemoteDir/* $remoteBackupDir/ 2>/dev/null || true mkdir -p $RemoteDir tar -xzf /tmp/$targetName -C $RemoteDir rm /tmp/$targetName chown -R www:www $RemoteDir "@ ssh $SshTarget $remoteScript if ($LASTEXITCODE -ne 0) { throw "远程部署失败" } Write-Host "部署完成: $RemoteDir" -ForegroundColor Green } Remove-Item $TempDir -Recurse -Force Write-Host "全部流程执行结束。当前版本: $targetName"

4.1 参数设计和路径规划

脚本开头有三个入口参数,SkipDeploy是开关型参数,带上它就不走部署逻辑;RemoteDir是服务器上的发布目录;SshTarget是登录用户和地址。这样设计的好处是:你日常本地验证的时候用-SkipDeploy,真要发版的时候什么都不加,一个命令就直接上服务器。

路径我用的是绝对路径。有人喜欢相对路径,觉得移植方便,但脚本这种用于自动化的东西,我最怕“找不到路径”这种低级故障。绝对路径虽然写死了机器信息,但这恰恰是自动化需要的“确定性”。真要迁移机器,改一行就行。

4.2 为什么压缩前要经过临时目录

很多简化版脚本是直接tar -czf xxx.tar.gz -C dist/build/h5 .,我特意加了一个临时目录中转层。开头提到过,产物目录里会有 sourcemap 和一些杂文件,你不希望把这些传上服务器。直接压缩时排除文件其实也可以,但要考虑文件名规则,不如先拷贝一份干净的目录再压缩,逻辑一目了然。

还有一个更偏实际的原因:uni-app 构建产物里偶尔会有残留的临时文件夹,直接压缩会把它们一并带上,增加包体积。经过临时目录过滤后,传上去的都是有效文件。

4.3 scp 和 ssh 在 PowerShell 里的正确姿势

PowerShell 里调用 scp 有个经典坑:当你直接写scp file root@server:/path时,PowerShell 可能把root@server:/path里的冒号当成作用域符号来解析,从而报错。所以我在脚本里故意留了一行错误示范和正确示范,就是想提醒自己这种事到底是怎么发生的。

更稳的做法是给整个目标参数加引号,比如:

scp $fullTargetPath "${SshTarget}:/tmp/$targetName"

加上引号后,冒号不会被解析成 PowerShell 的特殊语义,scp 才能正常识别远端路径。

ssh 后面的 remoteScript 是一个多行字符串。因为要执行的命令在远端,我把它作为一个整体参数传给 ssh,远端 shell 会逐行执行。这里要稍微小心的是命令里的2>/dev/null || true,它的意思是:备份老版本时如果目录为空导致 cp 报错,不要中断脚本,继续往下跑。这属于远端 shell 的容错处理,和 PowerShell 里的$ErrorActionPreference是一个思路。

5. 实操全过程:第一次跑通这个脚本

第一次跑通自动化流程,那种感觉和“手动点按钮成功”完全不一样。这里写一个完整的执行过程,从运行前检查到最后的日志验证,都给你过一遍。

5.1 运行前的检查清单

脚本写得再好,环境不对等于零。我给自己总结了三件检查事项,每次都按这个顺序过:

  1. 当前分支是否要发布的分支,本地代码是否为最新。用git status和git log -1 --oneline确认。
  2. 构建依赖是否变化。如果 package.json 有变动,先npm install。
  3. 远程服务器地址、目标目录是否符合本次发布预期。改动了哪个环境,参数就要跟着改。

检查完这三个,直接执行:

.\deploy.ps1

第一次跑我建议用-SkipDeploy,先验证构建和压缩没有问题,再打开真正的部署开关。别一上来就连服务器,排查范围会变大。

5.2 执行过程中的输出观察

脚本执行过程中会有几处关键输出。第一步是npm run build:h5,Vite 编译会有进度输出,最后出现“Build complete”之类的内容。第二步是绿色字体提示“打包完成”,这时候压缩包已经躺在 release 目录。第三步是 ssh 远程命令执行,如果一切正常,最后会提示“部署完成”和当前版本名。

我习惯盯着三个关键节点:$LASTEXITCODE对应的构建过程、压缩文件大小、远程命令的退出码。任何一个不是预期表现都要停下来排查,不要觉得“反正最后文件在服务器上就行了”。

5.3 接入 Windows 计划任务实现定时或按键触发

既然脚本能跑,再进一步就是让它“不用人管”。我把这个脚本挂到了 Windows 计划任务里,每天早上九点执行一次,相当于每天早上自动发一个测试版到预发环境。

注册计划任务用 schtasks 就行:

schtasks /create /tn "uniapp-h5-autodeploy" /tr "powershell -ExecutionPolicy Bypass -File D:\work\deploy.ps1 -SkipDeploy" /sc daily /st 09:00

这样配好之后,每天到点自动构建压缩,产物放在 release 目录,需要正式发版的时候手动跑一条全流程,或者直接在服务器上发布。如果你以后上了 Jenkins,再把这条命令升级成一个构建步骤就好,脚本不用重写。

6. 我踩过的坑和排查实录

自动化脚本最怕的不是逻辑复杂,而是各种“环境类”的隐性问题。这节集中写我踩过的坑,每个都对应了网上被问烂的问题关键词,检查清单直接对着找。

6.1 执行策略和“脚本闪退”

刚写完脚本第一个版本,我直接双击运行,窗口一闪即逝,连报错都来不及看。这就是之前说的执行策略问题。如果你想看脚本到底报什么错,可以用pause或者把输出重定向到文件里,但我更建议直接在 PowerShell 窗口里运行,不要双击。

如果你确实需要从外部程序启动脚本,又不想改系统执行策略,用这个方式:

powershell -ExecutionPolicy Bypass -File D:\work\deploy.ps1

Bypass就是不校验执行策略,直接跑。注意 Bypass 不等于“绕过安全审查”,它只是让你能够执行本机脚本。脚本内容的安全性还是你自己负责。

6.2 中文路径和编码问题

我的项目原先在D:\项目\uni-h5这种中文路径下,结果 PowerShell 解析路径、tar 处理文件名时都出现过乱码。最典型的现象是:路径识别正确,但压缩包里的文件名变成了乱码。根源是 Windows PowerShell 默认的编码是 GBK/UTF-16 的一堆历史遗留问题,tar 工具默认按 UTF-8 处理文件名,两边就对不上。

解决办法很朴素,把项目挪到纯英文路径,不要在中文路径下折腾自动化。这不是脚本不行,是 Windows 工具链的老毛病。后来我把项目放在D:\work\my-uniapp,所有中文编码问题消失。

6.3 “git 无法识别”:PATH 环境变量问题

整条流水线里最让我无语的一个报错是:脚本走到获取 Git 提交号那一步,突然告诉你无法将“git”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这和“npm 无法识别”是同一个根源——Git 安装后没有把 bin 目录写进 PATH。

解决方式:重新安装 Git for Windows 时,在安装向导里选择“把 Git 加入系统 PATH”,或者手动把C:\Program Files\Git\cmd加进环境变量的 Path。改完之后一定要重新打开 PowerShell 窗口,不是开新标签页,是彻底关闭重开。否则环境变量不会刷新,你还会继续怀疑人生。

6.4 scp 传输文件总是不成功

scp 失败的常见几类原因我也列一下:第一,远程服务器端口不是默认的 22,这就得用scp -P 端口号;第二,服务器端禁用了密码登录,但你又没有把公钥配进 authorized_keys;第三,Windows 自带的 OpenSSH 版本太老,对某些加密算法不支持,这时候需要升级 OpenSSH 客户端或者调整服务器端的 sshd_config。

有一个小经验:Windows 的 scp 首次连接时弹出“是否信任该主机”的确认,自动化脚本里没法手动确认。解决方案是在第一次手动用 ssh 连接一次目标服务器,让它把主机指纹写进~/.ssh/known_hosts,之后脚本就能安静执行了。

6.5 构建产物目录“没找到”的检查思路

有一次脚本报“构建产物目录不存在”,我第一反应是构建失败。结果一查,构建明明成功了,只是dist/build/h5路径和脚本里配置的不一致。uni-app 项目如果用了自定义的 vite 配置,输出目录可能被改到dist/build/web或者dist/build/mp-weixin旁边的其他名字。

所以脚本里最好在构建后动态探测一下目录:如果dist/build/h5不存在,就遍历dist/build/下所有目录,找到那个“确实是 web 产物”的目录。我在最终版脚本里保留了最稳妥的写法:先定义默认目录,不存在就抛错,但同时也打印出dist/build/下的实际内容,帮你快速定位偏差。

7. 一点实践心得

跑通这套自动化后最大的感受是:发布这个动作,从“每次都要小心翼翼”变成了“一条命令搞定”。手动操作时的紧张感消失了,因为脚本永远记得备份旧版本、永远记得排除 sourcemap、永远记得检查退出码。

后来我在本地又扩展了一个小用途:每次构建完,脚本顺手在 release 目录里生成了一个latest.txt,里面写着当前版本名和 Git 提交号。这样远端服务器上部署的是哪个版本,本地 release 目录里看得明明白白,排查问题时对线对得特别快。

最后分享一下个人偏好:脚本代码里只有逻辑和流程,不要堆砌太多花哨的业务逻辑。自动化脚本最怕的就是“除了发布还顺手做了很多事情”,越多的额外功能意味着越多的故障点。保持纯粹,只做构建、压缩、部署,其他需求另开脚本处理。这套东西跑了大半年,我现在基本不碰 HBuilderX 的发布按钮了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询