sync: add d8d-weapp-publish skill and uni-app-compatibility rule #68
207
.claude/skills/d8d-dev/rules/uni-app-compatibility.md
Normal file
207
.claude/skills/d8d-dev/rules/uni-app-compatibility.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# uni-app 兼容性检查铁律
|
||||
|
||||
## 规则 1: 自定义导航栏胶囊检测
|
||||
|
||||
**问题模式**: 扫描所有 `Navigation*.vue` / `NavBar*.vue` / `CustomHeader*.vue` 组件
|
||||
|
||||
**检查项**:
|
||||
1. ✅ **生命周期正确性**: 必须使用 `onReady` (uni-app),禁止使用 `onMounted`
|
||||
2. ✅ **跨平台检测**: 必须包含 `systemInfo.platform === 'h5'` 环境判断
|
||||
3. ✅ **胶囊 API 调用**: 必须调用 `uni.getMenuButtonBoundingClientRect()`
|
||||
4. ✅ **动态 padding**: 必须根据胶囊 `left` 位置计算 `padding-right`
|
||||
5. ✅ **动态高度**: 必须根据胶囊 `bottom` 位置计算导航栏高度
|
||||
|
||||
**自动修复模板(检测到缺失时应用)**:
|
||||
```typescript
|
||||
import { ref } from "vue"
|
||||
import { onReady } from "@dcloudio/uni-app"
|
||||
|
||||
const navBarHeight = ref(64)
|
||||
const statusBarHeight = ref(0)
|
||||
const rightPadding = ref(24)
|
||||
|
||||
onReady(() => {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
statusBarHeight.value = systemInfo.statusBarHeight || 0
|
||||
|
||||
// H5 环境下跳过胶囊适配
|
||||
if (systemInfo.platform === "h5") return
|
||||
|
||||
// 微信小程序环境执行胶囊适配
|
||||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||
|
||||
// 计算导航栏总高度 = 胶囊底部 + 胶囊与状态栏间距
|
||||
navBarHeight.value = menuButton.bottom + (menuButton.top - statusBarHeight.value)
|
||||
|
||||
// ⚠️ 重要:胶囊只在右侧,只需要给右侧加 padding,左侧固定 24px
|
||||
// 左侧不要动态计算,否则整个内容会被推到右边!
|
||||
rightPadding.value = systemInfo.windowWidth - menuButton.left + 12
|
||||
})
|
||||
```
|
||||
|
||||
**模板内联样式绑定(含 Flex 防挤压)**:
|
||||
```vue
|
||||
<view
|
||||
class="nav-bar"
|
||||
:style="{
|
||||
height: `${navBarHeight}px`,
|
||||
paddingTop: `${statusBarHeight}px`,
|
||||
paddingRight: `${rightPadding}px`,
|
||||
paddingLeft: '24px',
|
||||
}"
|
||||
>
|
||||
<view class="nav-left">
|
||||
<text class="logo-text" style="white-space: nowrap">{{ logoText }}</text>
|
||||
</view>
|
||||
<view class="nav-right" style="flex-shrink: 0">
|
||||
<!-- 图标 -->
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
**配套 CSS 规范(必须同时应用)**:
|
||||
```css
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.nav-left {
|
||||
flex: 1; /* ⚠️ 关键:左侧占据剩余空间,防止被右侧挤压 */
|
||||
}
|
||||
.nav-right {
|
||||
flex-shrink: 0; /* ⚠️ 关键:右侧图标不被挤压 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.logo-text {
|
||||
white-space: nowrap; /* ⚠️ 关键:标题不换行竖排 */
|
||||
}
|
||||
```
|
||||
|
||||
**违规后果**:
|
||||
- 微信小程序环境下右侧图标被胶囊按钮遮挡
|
||||
- 用户无法点击搜索、通知等核心功能按钮
|
||||
- P0 级 UI 缺陷
|
||||
|
||||
---
|
||||
|
||||
## 规则 2: uni-app 生命周期误用检测
|
||||
|
||||
**检查范围**: 所有 Vue 组件文件
|
||||
|
||||
**禁止模式**:
|
||||
- `onMounted` 中调用 `uni.*` API 进行布局计算
|
||||
- `onMounted` 中调用 `uni.createSelectorQuery()`
|
||||
- `onMounted` 中获取元素位置尺寸
|
||||
|
||||
**推荐模式**: 使用 `onReady` 替代(uni-app 页面生命周期)
|
||||
|
||||
**自动修复**:
|
||||
1. 将 `onMounted` → `onReady`
|
||||
2. 添加导入: `import { onReady } from '@dcloudio/uni-app'`
|
||||
3. 移除错误的 `onMounted` 导入
|
||||
|
||||
**检测正则**:
|
||||
```
|
||||
/onMounted\s*\(\s*\(\s*\)\s*=>\s*{[\s\S]*?uni\.(getSystemInfoSync|getMenuButtonBoundingClientRect|createSelectorQuery)/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 规则 3: pages.json 自定义导航栏自动检查
|
||||
|
||||
**触发条件**: 检测到 `"navigationStyle": "custom"` 配置
|
||||
|
||||
**自动执行**:
|
||||
1. 扫描所有使用自定义导航栏的页面
|
||||
2. 检查每个页面引用的导航栏组件是否通过胶囊适配检测
|
||||
3. 为未通过检测的页面创建修复任务
|
||||
|
||||
**报告格式**:
|
||||
```
|
||||
⚠️ 自定义导航栏胶囊适配检测报告
|
||||
--------------------------------
|
||||
页面 pages/home/index: ✅ 已适配
|
||||
页面 pages/square/index: ✅ 已适配
|
||||
页面 pages/profile/index: ✅ 已适配
|
||||
页面 pages/event-detail: ❌ 未适配
|
||||
- 缺失: uni.getMenuButtonBoundingClientRect() 调用
|
||||
- 建议: 应用规则 1 自动修复模板
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 规则 4: H5/MP 条件编译检查
|
||||
|
||||
**场景**: 小程序专属 API 不应该在 H5 环境下直接调用
|
||||
|
||||
**检测项**:
|
||||
- ❌ 直接调用 `uni.getMenuButtonBoundingClientRect()` 而没有平台检测
|
||||
- ❌ 直接调用 `wx.*` API 而没有条件编译包裹
|
||||
- ✅ 推荐: 先检测 platform,再执行平台专属代码
|
||||
|
||||
**安全模式**:
|
||||
```typescript
|
||||
// ✅ 正确 - 先检测平台
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
if (systemInfo.platform !== "h5") {
|
||||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**违规自动修复**: 为缺失平台检测的 API 调用添加 `if` 包裹
|
||||
|
||||
---
|
||||
|
||||
## 规则 5: 导航栏 Flex 布局防挤压(新增)
|
||||
|
||||
**问题描述**:左右 padding 很大时,如果没有 flex 布局控制,标题会被挤压得竖排显示,或者整体内容右移。
|
||||
|
||||
**检查项**:
|
||||
1. ✅ `.nav-left` 必须有 `flex: 1`
|
||||
2. ✅ `.nav-right` 必须有 `flex-shrink: 0`
|
||||
3. ✅ 标题文字必须有 `white-space: nowrap`
|
||||
|
||||
**修复优先级**:P0(用户直接感知的视觉缺陷)
|
||||
|
||||
---
|
||||
|
||||
## 规则 6: wxss 标签名选择器禁用(新增)
|
||||
|
||||
**问题描述**:微信小程序组件 wxss 不允许使用标签名选择器(如 `svg`, `view`, `text`),编译后样式不生效,控制台警告。
|
||||
|
||||
**禁止的选择器模式**:
|
||||
```css
|
||||
/* ❌ 禁止:标签名选择器 */
|
||||
.icon-item > view svg { ... }
|
||||
.nav-left text { ... }
|
||||
view { ... }
|
||||
```
|
||||
|
||||
**✅ 正确写法**:
|
||||
```css
|
||||
/* 只使用类名选择器 */
|
||||
.icon-item > .icon-wrapper { ... }
|
||||
.nav-left .title { ... }
|
||||
```
|
||||
|
||||
**检测命令**:
|
||||
```bash
|
||||
grep -rn ">\s*svg\|>\s*view\|>\s*text" .claude/skills/d8d-dev/ app/mini-program/src/ --include="*.vue" --include="*.md"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 规则 7: uni-app vs Vue 生命周期正确选择(新增)
|
||||
|
||||
| 使用场景 | 正确生命周期 | 错误选择 | 原因 |
|
||||
|---------|-------------|---------|------|
|
||||
| 获取胶囊位置、元素尺寸 | `onReady` | `onMounted` | `onMounted` 时组件可能还没渲染到位 |
|
||||
| 获取 API 数据(普通场景) | `onMounted` 或 `onReady` | - | 都可以,通常 onMounted 足够 |
|
||||
| navigateBack 返回后需要刷新数据 | `onShow` | `onMounted` | `onMounted` 只在首次创建时触发 |
|
||||
|
||||
**记忆口诀**:布局计算用 `onReady`,数据请求随便,返回刷新用 `onShow`
|
||||
89
.claude/skills/d8d-weapp-publish/SKILL.md
Normal file
89
.claude/skills/d8d-weapp-publish/SKILL.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
name: d8d-weapp-publish
|
||||
description: 微信小程序自动化发布工具。支持企业小程序和人才小程序的开发版和体验版发布。当用户需要:发布小程序到微信平台、上传体验版、生成开发版预览二维码、配置小程序发布环境时触发。使用 scripts/publish-weapp.js 脚本执行发布流程。
|
||||
---
|
||||
|
||||
# 微信小程序发布
|
||||
|
||||
支持两种框架:**Taro**(企业小程序、人才小程序)和 **uni-app**(酒吧组局小程序)。
|
||||
|
||||
- **企业小程序**(用人方)- Taro
|
||||
- **人才小程序**(求职者)- Taro
|
||||
- **酒吧组局小程序** - uni-app
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 发布命令
|
||||
|
||||
```bash
|
||||
# 企业小程序 - 体验版上传
|
||||
pnpm run publish:enterprise
|
||||
|
||||
# 企业小程序 - 开发版生成二维码
|
||||
pnpm run publish:enterprise:dev
|
||||
|
||||
# 人才小程序 - 体验版上传
|
||||
pnpm run publish:talent
|
||||
|
||||
# 人才小程序 - 开发版生成二维码
|
||||
pnpm run publish:talent:dev
|
||||
|
||||
# 酒吧组局小程序 (uni-app) - 体验版上传
|
||||
pnpm run publish:hazy
|
||||
|
||||
# 酒吧组局小程序 (uni-app) - 开发版生成二维码
|
||||
pnpm run publish:hazy:dev
|
||||
```
|
||||
|
||||
### 自定义版本和描述
|
||||
|
||||
```bash
|
||||
VERSION=1.2.0 DESC="新增功能" pnpm run publish:enterprise
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 发布流程
|
||||
|
||||
### 自动构建说明
|
||||
|
||||
**重要**: 开发版预览会**自动重新构建** `development` 环境,确保使用最新代码。
|
||||
|
||||
**为什么开发版需要重新构建?**
|
||||
|
||||
微信小程序开发版预览有 **2MB 大小限制**,而使用 `pnpm run dev:weapp`(watch 模式)生成的开发版输出体积较大(通常超过 2MB),会导致上传失败:
|
||||
|
||||
```
|
||||
errcode: 80051, errmsg: source size 2368KB exceed max limit 2MB
|
||||
```
|
||||
|
||||
因此开发版预览必须使用 `NODE_ENV=development` 进行完整构建,这样可以:
|
||||
- 控制输出大小在限制内(约 1.3MB)
|
||||
- 确保使用最新代码
|
||||
- 避免因 watch 模式输出过大导致上传失败
|
||||
|
||||
**构建环境说明**:
|
||||
- **开发版预览**: 使用 `NODE_ENV=development` 构建,输出到 `dist/weapp/development/`
|
||||
- **体验版**: 使用 `NODE_ENV=production` 构建,输出到 `dist/weapp/production/`
|
||||
|
||||
如需跳过构建(使用已有构建输出),可添加 `--no-build` 参数:
|
||||
|
||||
```bash
|
||||
node .claude/skills/d8d-weapp-publish/scripts/publish-weapp.js enterprise dev --no-build
|
||||
```
|
||||
|
||||
### 发布步骤
|
||||
|
||||
1. 构建项目(Taro 编译)
|
||||
2. 上传到微信服务器
|
||||
3. 生成体验版或预览二维码
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
- **配置说明**: [config.md](references/config.md) - 小程序信息、package.json 配置、环境变量
|
||||
- **常见问题**: [faq.md](references/faq.md) - 故障排查、注意事项
|
||||
- **使用示例**: [usage.md](references/usage.md) - 完整使用示例、快捷命令
|
||||
9
.claude/skills/d8d-weapp-publish/package.json
Normal file
9
.claude/skills/d8d-weapp-publish/package.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "weapp-publish-skill",
|
||||
"version": "1.0.0",
|
||||
"description": "WeChat mini-program publishing skill",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"miniprogram-ci": "^2.1.26"
|
||||
}
|
||||
}
|
||||
185
.claude/skills/d8d-weapp-publish/references/config.md
Normal file
185
.claude/skills/d8d-weapp-publish/references/config.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# 配置参考
|
||||
|
||||
## 支持的小程序
|
||||
|
||||
| 小程序 | 目录 | AppID | 说明 |
|
||||
|--------|------|-------|------|
|
||||
| **企业小程序** | `mini/` | `wx1e791ed2e0229eb8` | 用人方小程序(企业管理端) |
|
||||
| **人才小程序** | `mini-talent/` | `wx3c47dbce1ea7d43c` | 人才小程序(求职者端) |
|
||||
|
||||
---
|
||||
|
||||
## 技能架构
|
||||
|
||||
```
|
||||
.claude/skills/weapp-publish/
|
||||
├── package.json # 技能依赖
|
||||
├── node_modules/ # 技能依赖安装目录
|
||||
├── SKILL.md # 技能说明
|
||||
├── scripts/
|
||||
│ └── publish-weapp.js # 发布脚本
|
||||
└── references/ # 参考文档
|
||||
├── config.md
|
||||
├── faq.md
|
||||
└── usage.md
|
||||
```
|
||||
|
||||
**技能是自包含的**,有自己的依赖管理。
|
||||
|
||||
---
|
||||
|
||||
## 安装技能依赖
|
||||
|
||||
首次使用或更新依赖时,在技能目录安装:
|
||||
|
||||
```bash
|
||||
cd .claude/skills/weapp-publish
|
||||
pnpm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## package.json 配置
|
||||
|
||||
项目根目录 `package.json` 中的发布命令:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"publish:enterprise": "node .claude/skills/weapp-publish/scripts/publish-weapp.js enterprise experience",
|
||||
"publish:enterprise:dev": "node .claude/skills/weapp-publish/scripts/publish-weapp.js enterprise dev",
|
||||
"publish:talent": "node .claude/skills/weapp-publish/scripts/publish-weapp.js talent experience",
|
||||
"publish:talent:dev": "node .claude/skills/weapp-publish/scripts/publish-weapp.js talent dev"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 命令行参数
|
||||
|
||||
### 发布类型 (action)
|
||||
|
||||
| 参数 | 说明 | 构建行为 | 输出路径 |
|
||||
|------|------|----------|----------|
|
||||
| `experience` | 体验版(默认) | 默认执行构建 | `dist/weapp/production/` |
|
||||
| `dev` | 开发版 | 默认跳过构建 | `dist/weapp/development/` |
|
||||
|
||||
### 构建控制参数
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--build` | 强制执行构建 |
|
||||
| `--no-build` | 跳过构建 |
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 开发版使用 watch 模式输出(快速)
|
||||
node scripts/publish-weapp.js enterprise dev
|
||||
|
||||
# 开发版强制重新构建
|
||||
node scripts/publish-weapp.js enterprise dev --build
|
||||
|
||||
# 体验版跳过构建(不推荐)
|
||||
node scripts/publish-weapp.js enterprise experience --no-build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
发布时可通过环境变量自定义:
|
||||
|
||||
```bash
|
||||
# 发布版本号
|
||||
VERSION=1.2.0
|
||||
|
||||
# 发布描述
|
||||
DESC=新增订单管理功能
|
||||
|
||||
# CI 机器人编号 (1-30)
|
||||
ROBOT=1
|
||||
|
||||
# 公网访问域名(可选,用于预览二维码 URL)
|
||||
WEB_HOST=example.com
|
||||
```
|
||||
|
||||
使用方式:
|
||||
```bash
|
||||
VERSION=1.2.0 DESC="新增功能" pnpm run publish:enterprise
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 输出路径说明
|
||||
|
||||
发布脚本根据类型自动选择输出路径:
|
||||
|
||||
| 类型 | 环境变量 | 输出目录 | 用途 |
|
||||
|------|----------|----------|------|
|
||||
| **体验版** | `NODE_ENV=production` | `dist/weapp/production/` | 提交审核/体验版 |
|
||||
| **开发版** | `NODE_ENV=development` | `dist/weapp/development/` | 开发调试 |
|
||||
|
||||
开发版使用 `.env.development` 中的 API 配置,体验版使用 `.env.production`。
|
||||
|
||||
---
|
||||
|
||||
## 发布前准备
|
||||
|
||||
### 1. 安装技能依赖
|
||||
|
||||
```bash
|
||||
cd .claude/skills/weapp-publish
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 2. 获取上传密钥
|
||||
|
||||
使用小程序管理员身份访问"微信公众平台 > 开发 > 开发设置",下载代码上传密钥。
|
||||
|
||||
**企业小程序密钥文件**: `mini/certs/private.upload.key`
|
||||
**人才小程序密钥文件**: `mini-talent/certs/private.upload.key`
|
||||
|
||||
> 密钥文件已存在于各自小程序目录的 `certs/` 目录下。
|
||||
|
||||
### 3. 配置 IP 白名单
|
||||
|
||||
在微信公众平台配置服务器的 IP 白名单。
|
||||
|
||||
### 4. 确保 ES5 转译配置
|
||||
|
||||
小程序的 `config/dev.ts` 中应包含以下配置,确保所有依赖被转译为 ES5:
|
||||
|
||||
```ts
|
||||
mini: {
|
||||
compile: {
|
||||
include: [
|
||||
// 确保产物为 es5,转译所有 node_modules(除了 Babel 核心)
|
||||
filename => /node_modules\/(?!(@babel|core-js|style-loader|css-loader|react|react-dom))/.test(filename)
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**原因**:微信小程序的预览编译器不完全支持 ES6+ 语法(如可选链 `?.`),需要确保所有代码被转译为 ES5。
|
||||
|
||||
### 5. 开发版预览 - 关闭 Prebundle(重要)
|
||||
|
||||
微信小程序开发版预览有 **2MB 主包大小限制**,Taro 在开发模式默认开启 prebundle(依赖预编译),会导致主包超过限制。
|
||||
|
||||
在 `config/dev.ts` 中添加以下配置关闭 prebundle:
|
||||
|
||||
```ts
|
||||
compiler: {
|
||||
// 关闭开发模式的依赖预编译,减少主包体积以支持微信小程序开发版预览(2MB限制)
|
||||
prebundle: {
|
||||
enable: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 此配置仅影响开发版预览,不影响体验版和生产版
|
||||
- 关闭 prebundle 后首次编译会稍慢,但主包体积可从 ~3MB 降至 ~650KB
|
||||
- 人才小程序 (`mini-talent`) 也需要同样配置
|
||||
100
.claude/skills/d8d-weapp-publish/references/faq.md
Normal file
100
.claude/skills/d8d-weapp-publish/references/faq.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 常见问题
|
||||
|
||||
### Q: 发布失败提示 IP 不在白名单
|
||||
|
||||
**A**: 请在微信公众平台"开发 > 开发设置"中添加服务器 IP 到白名单。
|
||||
|
||||
### Q: 构建成功但发布失败
|
||||
|
||||
**A**: 检查密钥文件路径是否正确,密钥是否匹配对应的 AppID。
|
||||
|
||||
### Q: 构建时提示找不到模块(如 `tailwindcss` 或 `@/` 路径别名)
|
||||
|
||||
**A**: **此问题已在脚本中修复**(2025-03-18)。如果仍然遇到,请使用 `--no-build` 参数作为备用方案。
|
||||
|
||||
**备用方案**:
|
||||
```bash
|
||||
# 1. 进入小程序目录手动构建
|
||||
cd mini
|
||||
NODE_ENV=production pnpm taro build --type weapp
|
||||
|
||||
# 2. 返回根目录,使用 --no-build 发布
|
||||
cd ..
|
||||
pnpm run publish:enterprise --no-build
|
||||
```
|
||||
|
||||
### Q: 如何设置体验版
|
||||
|
||||
**A**: 发布成功后,登录小程序后台"版本管理",将指定版本设置为体验版。
|
||||
|
||||
### Q: 开发版预览二维码生成失败,提示语法错误
|
||||
|
||||
**A**: 这通常是因为代码中包含了微信小程序不支持的 ES6+ 语法(如可选链 `?.`)。
|
||||
|
||||
**解决方案**:
|
||||
1. 确保小程序的 `config/dev.ts` 中配置了 ES5 转译:
|
||||
```ts
|
||||
mini: {
|
||||
compile: {
|
||||
include: [
|
||||
filename => /node_modules\/(?!(@babel|core-js|style-loader|css-loader|react|react-dom))/.test(filename)
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
2. 使用 `--build` 参数强制重新构建
|
||||
|
||||
### Q: 开发版为什么默认跳过构建?
|
||||
|
||||
**A**: 开发版默认使用 `dev:weapp` (watch 模式) 的构建输出,因为:
|
||||
1. 开发时通常会运行 watch 模式,代码实时编译
|
||||
2. 跳过构建可以快速生成预览二维码,提高开发效率
|
||||
3. 如需重新构建,添加 `--build` 参数
|
||||
|
||||
### Q: 体验版和开发版有什么区别?
|
||||
|
||||
**A**:
|
||||
| 类型 | 用途 | 构建行为 | 输出路径 |
|
||||
|------|------|----------|----------|
|
||||
| **体验版** (experience) | 提交给测试人员体验 | 默认执行构建 | `dist/weapp/production/` |
|
||||
| **开发版** (dev) | 开发者自测 | 默认跳过构建 | `dist/weapp/development/` |
|
||||
|
||||
### Q: 开发版预览上传失败,提示超过 2MB 限制
|
||||
|
||||
**A**: 微信小程序开发版预览有严格的 **2MB 主包大小限制**,这是微信服务器端的硬性限制,无法通过配置参数绕过。
|
||||
|
||||
**根本原因**:
|
||||
1. `maxUploadSourceSize` **不是** miniprogram-ci 的有效参数(只在 `project.config.json` 中对本地开发者工具有效)
|
||||
2. Taro 在开发模式默认开启 **prebundle(依赖预编译)**,预编译的依赖包会占用约 2MB 空间
|
||||
|
||||
**解决方案**:关闭开发模式的 prebundle 功能
|
||||
|
||||
在 `mini/config/dev.ts` 中添加:
|
||||
```typescript
|
||||
compiler: {
|
||||
// 关闭开发模式的依赖预编译,减少主包体积以支持微信小程序开发版预览(2MB限制)
|
||||
prebundle: {
|
||||
enable: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**优化效果**:
|
||||
| 优化前 | 优化后 | 减少 |
|
||||
|--------|--------|------|
|
||||
| ~3MB | ~650KB | ~78% |
|
||||
|
||||
**注意**:
|
||||
- 关闭 prebundle 后首次编译会稍慢(需要完整编译依赖)
|
||||
- 不影响体验版和生产版(这些版本本身就需要完整的依赖编译)
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **密钥安全**: 上传密钥拥有预览、上传权限,请勿提交到代码仓库
|
||||
2. **构建依赖**: 发布前确保项目依赖已安装 (`pnpm install`)
|
||||
3. **版本管理**: 建议使用语义化版本号 (Semantic Versioning)
|
||||
4. **IP 白名单**: 确保服务器 IP 已在微信公众平台配置白名单
|
||||
5. **机器人编号**: 多个 CI 并发时使用不同的 robot 编号 (1-30)
|
||||
6. **开发模式**: 开发版预览前请确保 `dev:weapp` 正在运行
|
||||
69
.claude/skills/d8d-weapp-publish/references/usage.md
Normal file
69
.claude/skills/d8d-weapp-publish/references/usage.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 使用示例
|
||||
|
||||
## 发布企业小程序
|
||||
|
||||
```bash
|
||||
# 使用默认版本号和描述(体验版)
|
||||
pnpm run publish:enterprise
|
||||
|
||||
# 自定义版本号和描述
|
||||
VERSION=1.2.0 DESC="新增订单管理功能" pnpm run publish:enterprise
|
||||
|
||||
# 生成预览二维码(默认跳过构建,使用 watch 模式输出)
|
||||
pnpm run publish:enterprise:dev
|
||||
|
||||
# 强制重新构建后生成预览二维码
|
||||
pnpm run publish:enterprise:dev --build
|
||||
```
|
||||
|
||||
## 发布人才小程序
|
||||
|
||||
```bash
|
||||
# 使用默认版本号和描述(体验版)
|
||||
pnpm run publish:talent
|
||||
|
||||
# 自定义版本号和描述
|
||||
VERSION=2.0.0 DESC="全新改版,优化求职流程" pnpm run publish:talent
|
||||
|
||||
# 生成预览二维码(默认跳过构建,使用 watch 模式输出)
|
||||
pnpm run publish:talent:dev
|
||||
|
||||
# 强制重新构建后生成预览二维码
|
||||
pnpm run publish:talent:dev --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 构建行为说明
|
||||
|
||||
### 开发版 (dev)
|
||||
- **默认跳过构建**:直接使用 `dev:weapp` (watch 模式) 的输出
|
||||
- 适用场景:本地开发时快速生成预览二维码
|
||||
- 强制构建:添加 `--build` 参数
|
||||
|
||||
### 体验版 (experience)
|
||||
- **默认执行构建**:确保发布的是最新代码
|
||||
- 跳过构建:添加 `--no-build` 参数(适用于已手动构建的情况)
|
||||
|
||||
### 输出路径
|
||||
- 开发版:`dist/weapp/development/`
|
||||
- 体验版:`dist/weapp/production/`
|
||||
|
||||
---
|
||||
|
||||
## 快捷命令参考
|
||||
|
||||
| 用户指令 | Claude 行为 |
|
||||
|----------|-------------|
|
||||
| "发布企业小程序" | 运行 `pnpm run publish:enterprise` |
|
||||
| "发布人才小程序" | 运行 `pnpm run publish:talent` |
|
||||
| "生成企业小程序预览二维码" | 运行 `pnpm run publish:enterprise:dev` |
|
||||
| "发布两个小程序" | 依次运行企业小程序和人才小程序的发布命令 |
|
||||
|
||||
---
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [miniprogram-ci 官方文档](https://www.npmjs.com/package/miniprogram-ci)
|
||||
- [微信公众平台](https://mp.weixin.qq.com/)
|
||||
- [miniprogram-ci Skill](.claude/skills/miniprogram-ci/SKILL.md)
|
||||
330
.claude/skills/d8d-weapp-publish/scripts/publish-weapp.js
Normal file
330
.claude/skills/d8d-weapp-publish/scripts/publish-weapp.js
Normal file
@@ -0,0 +1,330 @@
|
||||
const ci = require('miniprogram-ci')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
const fs = require('fs')
|
||||
|
||||
// 公网访问域名(从环境变量读取,默认本地)
|
||||
const WEB_HOST = process.env.WEB_HOST || 'localhost:8080'
|
||||
const PUBLIC_HOST = WEB_HOST.startsWith('http') ? WEB_HOST : `https://${WEB_HOST}`
|
||||
|
||||
// 项目根目录(从 scripts/ 目录向上 4 级)
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '../../../../')
|
||||
|
||||
// 小程序配置基础信息
|
||||
const MINI_CONFIGS = {
|
||||
enterprise: {
|
||||
name: '企业小程序',
|
||||
framework: 'taro',
|
||||
appid: 'wx1e791ed2e0229eb8',
|
||||
dir: 'mini',
|
||||
privateKeyPath: path.resolve(PROJECT_ROOT, 'mini/certs/private.upload.key'),
|
||||
distPath: 'dist/weapp',
|
||||
},
|
||||
talent: {
|
||||
name: '人才小程序',
|
||||
framework: 'taro',
|
||||
appid: 'wx3c47dbce1ea7d43c',
|
||||
dir: 'mini-talent',
|
||||
privateKeyPath: path.resolve(PROJECT_ROOT, 'mini-talent/certs/private.upload.key'),
|
||||
distPath: 'dist/weapp',
|
||||
},
|
||||
hazy: {
|
||||
name: '酒吧组局',
|
||||
framework: 'uniapp',
|
||||
appid: process.env.WECHAT_APPID || '',
|
||||
dir: 'app/mini-program',
|
||||
privateKeyPath: path.resolve(PROJECT_ROOT, 'app/mini-program/certs/private.upload.key'),
|
||||
distPath: 'dist/build/mp-weixin',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取构建配置(根据 action 类型动态选择)
|
||||
*
|
||||
* 说明:
|
||||
* - dev (开发版预览): 使用 development 模式,自动构建
|
||||
* - experience (体验版): 使用 production 模式,自动构建(体验版=生产环境)
|
||||
* - production (正式版): 使用 production 模式,自动构建
|
||||
*
|
||||
* 目录结构:dist/weapp/{mode}/
|
||||
*/
|
||||
function getBuildConfig(config, action) {
|
||||
// 只有开发版使用 development 模式
|
||||
// 体验版和正式版都使用 production 模式
|
||||
const mode = action === 'dev' ? 'development' : 'production'
|
||||
|
||||
// 根据模式设置对应的 NODE_ENV
|
||||
// 这样构建输出目录和上传目录就能匹配了
|
||||
const nodeEnv = mode === 'development' ? 'development' : 'production'
|
||||
// 小程序目录的绝对路径,用于 execSync 的 cwd 参数
|
||||
const miniDir = path.resolve(PROJECT_ROOT, config.dir)
|
||||
|
||||
// 根据框架选择不同的构建命令和输出路径
|
||||
let buildCmd, projectPath
|
||||
|
||||
if (config.framework === 'uniapp') {
|
||||
// uni-app: 固定输出路径,不区分环境目录
|
||||
buildCmd = `NODE_ENV=${nodeEnv} pnpm build:mp-weixin`
|
||||
projectPath = path.resolve(PROJECT_ROOT, `${config.dir}/${config.distPath}`)
|
||||
} else {
|
||||
// Taro: 按环境分目录输出
|
||||
buildCmd = `NODE_ENV=${nodeEnv} pnpm taro build --type weapp`
|
||||
projectPath = path.resolve(PROJECT_ROOT, `${config.dir}/${config.distPath}/${mode}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
mode,
|
||||
nodeEnv,
|
||||
miniDir,
|
||||
projectPath,
|
||||
buildCmd,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建小程序
|
||||
*/
|
||||
function buildMiniProject(config) {
|
||||
console.log(`\n🔨 正在构建 ${config.name}...`)
|
||||
console.log(` 工作目录: ${config.miniDir}`)
|
||||
console.log(` 环境变量: NODE_ENV=${config.nodeEnv}`)
|
||||
try {
|
||||
execSync(config.buildCmd, { stdio: 'inherit', cwd: config.miniDir, shell: true })
|
||||
console.log(`✅ ${config.name} 构建完成`)
|
||||
} catch (error) {
|
||||
console.error(`❌ ${config.name} 构建失败`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布小程序体验版到微信服务器
|
||||
*/
|
||||
async function uploadExperienceProject(config, options) {
|
||||
const { version, desc, robot = 1 } = options
|
||||
|
||||
console.log(`\n📤 正在发布体验版 ${config.name}...`)
|
||||
console.log(` 版本: ${version}`)
|
||||
console.log(` 描述: ${desc}`)
|
||||
|
||||
// 更新 project.config.json 中的版本号(确保微信后台正确显示)
|
||||
const projectConfigPath = path.join(config.projectPath, 'project.config.json')
|
||||
if (fs.existsSync(projectConfigPath)) {
|
||||
const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, 'utf-8'))
|
||||
projectConfig.version = version
|
||||
fs.writeFileSync(projectConfigPath, JSON.stringify(projectConfig, null, 2))
|
||||
console.log(` 已更新 project.config.json 版本号为: ${version}`)
|
||||
}
|
||||
|
||||
const project = new ci.Project({
|
||||
appid: config.appid,
|
||||
type: 'miniProgram',
|
||||
projectPath: config.projectPath,
|
||||
privateKeyPath: config.privateKeyPath,
|
||||
ignores: ['node_modules/**/*'],
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await ci.upload({
|
||||
project,
|
||||
version,
|
||||
desc,
|
||||
setting: {
|
||||
useProjectConfig: true,
|
||||
es7: true,
|
||||
minify: true,
|
||||
minifyJS: true,
|
||||
minifyWXML: true,
|
||||
minifyWXSS: true,
|
||||
autoPrefixWXSS: true,
|
||||
},
|
||||
onProgressUpdate: (progress) => {
|
||||
process.stdout.write(`\r 上传进度: ${progress}%`)
|
||||
},
|
||||
robot,
|
||||
})
|
||||
|
||||
console.log(`\n✅ ${config.name}体验版上传成功!`)
|
||||
console.log(` 时间: ${result.time}`)
|
||||
console.log(` 版本: ${result.version}`)
|
||||
console.log(` 请前往小程序后台查看: https://mp.weixin.qq.com/`)
|
||||
} catch (error) {
|
||||
console.error(`\n❌ ${config.name}体验版上传失败`)
|
||||
console.error(error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览小程序(生成二维码)
|
||||
*/
|
||||
async function previewDevProject(config, options) {
|
||||
const { desc, robot = 1 } = options
|
||||
|
||||
console.log(`\n👀 正在生成 ${config.name} 预览二维码...`)
|
||||
|
||||
const project = new ci.Project({
|
||||
appid: config.appid,
|
||||
type: 'miniProgram',
|
||||
projectPath: config.projectPath,
|
||||
privateKeyPath: config.privateKeyPath,
|
||||
ignores: ['node_modules/**/*'],
|
||||
})
|
||||
|
||||
// 二维码保存目录
|
||||
const qrcodeDir = path.resolve(PROJECT_ROOT, 'web/public/qrcode')
|
||||
const qrcodeFileName = `qrcode-${config.appid}.jpg`
|
||||
const qrcodePath = path.join(qrcodeDir, qrcodeFileName)
|
||||
|
||||
// 确保 qrcode 目录存在
|
||||
if (!fs.existsSync(qrcodeDir)) {
|
||||
fs.mkdirSync(qrcodeDir, { recursive: true })
|
||||
}
|
||||
|
||||
try {
|
||||
await ci.preview({
|
||||
project,
|
||||
desc,
|
||||
setting: {
|
||||
useProjectConfig: false, // 不使用项目配置,完全使用下方自定义设置
|
||||
es6: false,
|
||||
es7: false, // 增强编译
|
||||
minify: true, // 压缩所有代码
|
||||
minifyWXSS: true, // 压缩样式
|
||||
minifyWXML: true, // 压缩模板
|
||||
minifyJS: true, // 压缩JS
|
||||
codeProtect: false,
|
||||
autoPrefixWXSS: false,
|
||||
},
|
||||
qrcodeFormat: 'image',
|
||||
qrcodeOutputDest: qrcodePath,
|
||||
onProgressUpdate: (progress) => {
|
||||
process.stdout.write(`\r 生成进度: ${progress}%`)
|
||||
},
|
||||
robot,
|
||||
uploadType: 'preview',
|
||||
})
|
||||
|
||||
// 公网访问 URL
|
||||
const publicUrl = `${PUBLIC_HOST}/qrcode/${qrcodeFileName}`
|
||||
|
||||
console.log(`\n✅ 开发版预览二维码已生成!`)
|
||||
console.log(`📱 本地路径: ${qrcodePath}`)
|
||||
console.log(`🌐 公网访问: ${publicUrl}`)
|
||||
console.log(`\n>>>QRCode_IMAGE_START<<<`)
|
||||
console.log(qrcodePath)
|
||||
console.log(`>>>QRCode_URL_START<<<`)
|
||||
console.log(publicUrl)
|
||||
console.log(`>>>QRCode_URL_END<<<`)
|
||||
} catch (error) {
|
||||
console.error(`\n❌ 开发版预览二维码生成失败`)
|
||||
console.error(error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递增版本号 (例如: 0.0.39 -> 0.0.40)
|
||||
*/
|
||||
function bumpVersion(version) {
|
||||
const parts = version.split('.')
|
||||
const patch = parseInt(parts[2] || '0')
|
||||
parts[2] = (patch + 1).toString()
|
||||
return parts.join('.')
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 package.json 读取当前版本号
|
||||
*/
|
||||
function getCurrentVersion(config) {
|
||||
const packageJsonPath = path.join(PROJECT_ROOT, config.dir, 'package.json')
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
||||
return packageJson.version || '1.0.0'
|
||||
}
|
||||
return '1.0.0'
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 package.json 中的版本号
|
||||
*/
|
||||
function updatePackageVersion(config, version) {
|
||||
const packageJsonPath = path.join(PROJECT_ROOT, config.dir, 'package.json')
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
|
||||
packageJson.version = version
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
||||
console.log(` 已更新 package.json 版本号为: ${version}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
// --no-build 参数可以跳过构建(默认会自动构建)
|
||||
const noBuildIndex = args.indexOf('--no-build')
|
||||
const noBuild = noBuildIndex !== -1
|
||||
if (noBuild) {
|
||||
args.splice(noBuildIndex, 1)
|
||||
}
|
||||
|
||||
const [miniType, action = 'experience'] = args
|
||||
|
||||
// 只有指定 --no-build 时才跳过构建
|
||||
const shouldSkipBuild = noBuild
|
||||
|
||||
// 获取构建配置(需要提前获取以便读取版本号)
|
||||
const config = getBuildConfig(MINI_CONFIGS[miniType], action)
|
||||
|
||||
// 解析参数 - 体验版如果没有指定 VERSION,则自动递增版本号
|
||||
let version = process.env.VERSION
|
||||
if (action === 'experience' && !version) {
|
||||
const currentVersion = getCurrentVersion(config)
|
||||
version = bumpVersion(currentVersion)
|
||||
console.log(`\n🔢 自动递增版本号: ${currentVersion} -> ${version}`)
|
||||
updatePackageVersion(config, version)
|
||||
}
|
||||
|
||||
const options = {
|
||||
version: version || '1.0.0',
|
||||
desc: process.env.DESC || '自动化发布',
|
||||
robot: parseInt(process.env.ROBOT || '1'),
|
||||
}
|
||||
|
||||
if (!miniType || !MINI_CONFIGS[miniType]) {
|
||||
console.error('❌ 请指定小程序类型: enterprise 或 talent')
|
||||
console.error('示例: node scripts/publish-weapp.js enterprise')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建项目(跳过或显式指定构建时)
|
||||
if (!shouldSkipBuild && !noBuild) {
|
||||
buildMiniProject(config)
|
||||
} else {
|
||||
console.log(`\n⏭️ 跳过构建,使用现有构建输出`)
|
||||
console.log(` 使用路径: ${config.projectPath}`)
|
||||
}
|
||||
|
||||
// 执行操作
|
||||
if (action === 'experience') {
|
||||
await uploadExperienceProject(config, options)
|
||||
} else if (action === 'dev') {
|
||||
await previewDevProject(config, {
|
||||
desc: options.desc,
|
||||
robot: options.robot,
|
||||
})
|
||||
} else {
|
||||
console.error(`❌ 操作无效: ${action}`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (_error) {
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user