用PowerShell打造UniApp H5自动化打包部署脚本

📅 发布时间:2026/9/30 7:34:10
用PowerShell打造UniApp H5自动化打包部署脚本
前阵子给一个 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.jsonile管理靠 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 18PowerShell 里直接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.gzLinux 服务器基本都认识。如果服务器上实在只有 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 rootyour-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 全部流程执行结束。当前版本: $targetName4.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 rootserver:/path时PowerShell 可能把rootserver:/path里的冒号当成作用域符号来解析从而报错。所以我在脚本里故意留了一行错误示范和正确示范就是想提醒自己这种事到底是怎么发生的。更稳的做法是给整个目标参数加引号比如scp $fullTargetPath ${SshTarget}:/tmp/$targetName加上引号后冒号不会被解析成 PowerShell 的特殊语义scp 才能正常识别远端路径。ssh 后面的 remoteScript 是一个多行字符串。因为要执行的命令在远端我把它作为一个整体参数传给 ssh远端 shell 会逐行执行。这里要稍微小心的是命令里的2/dev/null || true它的意思是备份老版本时如果目录为空导致 cp 报错不要中断脚本继续往下跑。这属于远端 shell 的容错处理和 PowerShell 里的$ErrorActionPreference是一个思路。5. 实操全过程第一次跑通这个脚本第一次跑通自动化流程那种感觉和“手动点按钮成功”完全不一样。这里写一个完整的执行过程从运行前检查到最后的日志验证都给你过一遍。5.1 运行前的检查清单脚本写得再好环境不对等于零。我给自己总结了三件检查事项每次都按这个顺序过当前分支是否要发布的分支本地代码是否为最新。用git status和git log -1 --oneline确认。构建依赖是否变化。如果 package.json 有变动先npm install。远程服务器地址、目标目录是否符合本次发布预期。改动了哪个环境参数就要跟着改。检查完这三个直接执行.\deploy.ps1第一次跑我建议用-SkipDeploy先验证构建和压缩没有问题再打开真正的部署开关。别一上来就连服务器排查范围会变大。5.2 执行过程中的输出观察脚本执行过程中会有几处关键输出。第一步是npm run build:h5Vite 编译会有进度输出最后出现“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.ps1Bypass就是不校验执行策略直接跑。注意 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 的发布按钮了。