普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月30日首页

【uniApp开发】微信小程序 web-view 内嵌 H5 跳转支付踩坑实录

2026年4月30日 17:53

微信小程序 web-view 内嵌 H5 跳转支付踩坑实录:从 postMessage 失效到 navigateTo 没反应的源码级排查

在 uni-app 微信小程序中嵌入 H5 页面,并实现 H5 向小程序跳转支付页,看似一个简单的需求,实际踩了三个大坑。本文记录从 postMessage 不实时、到 navigateTo 没反应、最终翻到 uni.webview.js 压缩源码找到根因的完整过程。


一、业务背景与需求

我们的项目是 uni-app + Vue3 编译的微信小程序,首页用 <web-view> 全屏嵌入了一个 React 开发的 H5 页面。核心需求有两个:

  1. H5 → 小程序通信:H5 页面点击按钮,能向小程序发送消息
  2. H5 → 小程序跳转支付:H5 页面点击支付,能跳转到小程序原生支付页,调起微信支付

技术栈:

  • 小程序端:uni-app(Vue3)
  • H5 端:React + Vite
  • 通信桥接:uni.webview.js + 微信 JS-SDK

为什么选择 uni.webview.js

DCloud 提供 uni.webview.js 的核心目的是跨平台一致性——同一套 H5 代码,既能被 uni-app 编译的 App 加载,也能被 微信小程序、支付宝小程序、百度小程序 等加载,统一通过 window.uni.navigateTo / postMessage / getEnv 等 API 与宿主交互。

我们最初在 App 端 测试时完全正常,切换到 微信小程序 后才出现问题。这说明问题不是出在"要不要用 uni.webview.js",而是出在该 SDK 对微信小程序环境的适配存在漏洞


二、踩坑一:postMessage 在微信小程序中不实时

现象

H5 中按照官方文档调用 uni.postMessage

window.uni.postMessage({
  data: { type: 'test', msg: 'Hello from H5!' }
});

小程序端 <web-view @message="handleMessage"> 监听,但点击后没有任何反应

原因

这是微信小程序的原生限制,不是 uni-app 的 bug。官方文档明确说明:

wx.miniProgram.postMessage 向小程序发送的消息,不会实时触发,而是被微信暂存起来,只在特定时机批量发送:

  • 用户点击右上角转发分享
  • 用户返回上一页(navigateBack
  • 页面被销毁/重载
  • 下拉刷新

也就是说,你点了按钮,消息已经发出去了,但小程序端收不到,除非你手动返回或分享。

结论

postMessage 只能用于非实时的、伴随页面生命周期的通信场景(比如返回时顺带传数据)。对于"点击后立即通信"的需求,这条路走不通。


三、踩坑二:navigateTo 调用成功但"没下文"

现象

既然 postMessage 不实时,我们改用 navigateTo 直接跳转:

window.uni.navigateTo({
  url: '/pages/pay/pay?orderId=xxx&amount=1'
});

H5 端调用后打印日志显示成功,页面顶部环境检测显示 env = wx-miniprogramwindow.uni 也存在。

小程序端没有任何反应,既没有打开支付页,也没有报错。

初步排查

  1. ✅ 确认 pages.json 已注册 /pages/pay/pay
  2. ✅ 确认路径以 / 开头,不带 .html
  3. ✅ 确认 window.uni.navigateTo 存在
  4. ❌ 模拟器和真机都没反应

翻源码找根因

这里要先解释一个背景:uni.webview.js 的设计初衷是跨平台适配,不是单独为微信小程序"包的饺子"。

DCloud 的思路很清晰:

  • App 端:H5 在 plus.webview 中运行,通过 plus.webview.postMessageToUniNView 通信
  • 微信小程序端:H5 在 <web-view> 中运行,通过 wx.miniProgram.navigateTo / postMessage 通信
  • 支付宝/百度/字节端:各自使用对应的小程序桥接 API

H5 开发者只需要调用统一的 window.uni.navigateTouni.webview.js 内部会自动检测平台并映射到对应的原生 API。这套机制在 App 端和支付宝小程序端 运行良好,但在 微信小程序端 存在一个隐蔽的漏洞。

我怀疑是 uni.webview.js 内部实现有问题,于是直接翻它的压缩源码(uni.webview.1.5.6.js),发现了关键逻辑:

// uni.webview.js 的平台检测数组
var y = [
  // 百度小程序
  function(e){ if(v) return window.swan.webView },
  // 字节小程序
  function(e){ if(p) return window.tt.miniProgram },
  // 微信小程序(关键!)
  function(e){ ... return window.wx.miniProgram },
  // ... 其他平台
];

微信小程序的检测条件是:

window.wx && window.wx.miniProgram
  && /micromessenger/i.test(navigator.userAgent)
  && /miniProgram/i.test(navigator.userAgent)

只有当这四个条件同时满足时,uni.webview.js 才会把 API 映射到 wx.miniProgram.navigateTo

但我们 H5 的 index.html 只引入了 uni.webview.js,没有引入微信官方的 JS-SDK:

<!-- 错误 ❌:缺少微信 JS-SDK -->
<script src="/uni.webview.1.5.6.js"></script>

没有引入 https://res.wx.qq.com/open/js/jweixin-1.6.0.jswindow.wx 根本不存在,平台检测失败!

注意:这不是说 uni.webview.js"设计错了",而是它的**平台检测前提假设**在小程序 web-view 中不成立——它假设 window.wx.miniProgram已经存在,但实际上微信小程序不会自动向 H5 注入wx` 对象,必须手动引入微信 JS-SDK。

fallback 的致命陷阱

检测失败后,uni.webview.js 会 fallback 到一个默认实现 d

var d = {
  navigateTo: function(e) {
    r("navigateTo", { url: encodeURI(e.url) });
  },
  // ...
};

r 函数的内部实现是:

var r = function(e, n) {
  // 检测 uni-app x / uni-app / 5+ App ...
  if (a()) { ... }
  else if (o()) { ... }
  else {
    // 没有 window.plus,走 window.parent.postMessage
    if (!window.plus) {
      return window.parent.postMessage(
        { type: "WEB_INVOKE_APPSERVICE", data: i, pageId: "" }, "*"
      );
    }
    // ...
  }
};

在微信小程序 web-view 中:

  • 没有 window.__uniapp_x_
  • 没有 window.__dcloud_weex_
  • 没有 window.plus(这是 App 端的)

所以它走了 window.parent.postMessage。但微信小程序的 <web-view>沙箱隔离的 iframewindow.parent.postMessage 根本不会到达小程序逻辑层,也不会触发 <web-view>@message 事件。

这就是"调用成功但没下文"的根本原因。


四、解决方案:三级降级跳转策略

4.1 H5 端:引入微信 JS-SDK + uni.webview.js

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>H5 项目</title>
</head>
<body>
  <div id="root"></div>

  <!-- 关键:必须先引入微信 JS-SDK,否则 uni.webview.js 无法识别微信小程序环境 -->
  <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
  <script src="/uni.webview.1.5.6.js"></script>
  <script type="module" src="/src/main.tsx"></script>
</body>
</html>

⚠️ 顺序很重要:微信 JS-SDK 必须在 uni.webview.js 之前加载,因为后者的平台检测依赖 window.wx 的存在。

为什么保留 uni.webview.js 而不是直接全部用 wx.miniProgram

因为 H5 页面需要同时支持 App 端微信小程序端。在 App 端,wx 对象不存在,只有 window.uni 可用;在微信小程序端,两者都存在。保留 uni.webview.js 作为跨平台抽象层,同时引入微信 JS-SDK 填补小程序端的适配漏洞,是最兼顾一致性的方案。

4.2 H5 支付跳转工具:三级降级

实现一个健壮的跳转函数,优先用原生 API,逐级降级:

// src/utils/uniPay.ts

export interface PayParams {
  orderId: string;
  amount: number; // 单位:分
  description?: string;
  attach?: string;
}

/**
 * 跳转到微信小程序支付页(三级降级)
 */
export function navigateToWxPay(params: PayParams): {
  success: boolean;
  method?: string;
  reason?: string;
  debug: object;
} {
  // 构建小程序支付页 URL
  const query = new URLSearchParams({
    orderId: params.orderId,
    amount: String(params.amount),
    ...(params.description && { description: params.description }),
    ...(params.attach && { attach: params.attach }),
  });
  const url = `/pages/pay/pay?${query.toString()}`;

  // ========== 方案 1: 微信小程序原生 API(最可靠)==========
  if (window.wx?.miniProgram?.navigateTo) {
    window.wx.miniProgram.navigateTo({ url });
    return { success: true, method: 'wx.miniProgram.navigateTo', debug: getDebugInfo() };
  }

  // ========== 方案 2: uni.webview.js 封装 ==========
  if (window.uni?.navigateTo) {
    window.uni.navigateTo({ url });
    return { success: true, method: 'window.uni.navigateTo', debug: getDebugInfo() };
  }

  // ========== 方案 3: 备用 - postMessage + navigateBack ==========
  if (window.uni?.postMessage && window.uni?.navigateBack) {
    window.uni.postMessage({
      data: { type: 'navigate_to_pay', url, payload: params }
    });
    setTimeout(() => window.uni?.navigateBack?.(), 100);
    return { success: true, method: 'postMessage+navigateBack', debug: getDebugInfo() };
  }

  return { success: false, reason: '无可用跳转方式', debug: getDebugInfo() };
}

4.3 uni-app 端:web-view + 支付页

login.vue(承载 web-view):

<template>
  <view class="container">
    <web-view
      src="https://your-h5-domain.com/"
      @message="handleMessage"
      @error="handleError"
    />
  </view>
</template>

<script setup>
const handleMessage = (event) => {
  const data = event.detail.data;
  const lastMsg = Array.isArray(data) ? data[data.length - 1] : data;

  // 处理备用方案的 postMessage 指令
  if (lastMsg?.type === 'navigate_to_pay' && lastMsg?.url) {
    uni.navigateTo({ url: lastMsg.url });
    return;
  }

  uni.showModal({
    title: '收到 H5 消息',
    content: JSON.stringify(data, null, 2),
    showCancel: false,
  });
};

const handleError = (event) => {
  console.error('web-view 加载失败:', event);
};
</script>

pay.vue(小程序支付页):

<template>
  <view class="pay-page">
    <view class="pay-card">
      <text class="pay-title">确认支付</text>
      <text class="pay-amount">¥{{ (amount / 100).toFixed(2) }}</text>
      <text class="pay-desc">{{ description }}</text>
      <button @click="handlePay">立即支付</button>
    </view>
  </view>
</template>

<script setup>
import { onLoad } from '@dcloudio/uni-app';
import { ref } from 'vue';

const orderId = ref('');
const amount = ref(0);
const description = ref('');

onLoad((options) => {
  orderId.value = options.orderId || '';
  amount.value = parseInt(options.amount) || 0;
  description.value = decodeURIComponent(options.description || '');
});

const handlePay = () => {
  // 1. 调用后端获取微信支付参数
  uni.request({
    url: 'https://your-api.com/wxpay/create',
    method: 'POST',
    data: { orderId: orderId.value, amount: amount.value },
    success: (res) => {
      const { prepayId, nonceStr, timeStamp, signType, paySign } = res.data;

      // 2. 调起微信支付(微信小程序格式,非 App 的 orderInfo)
      uni.requestPayment({
        provider: 'wxpay',
        timeStamp: String(timeStamp),
        nonceStr: nonceStr,
        package: `prepay_id=${prepayId}`,
        signType: signType || 'RSA',
        paySign: paySign,
        success: () => {
          uni.showToast({ title: '支付成功', icon: 'success' });
          setTimeout(() => uni.navigateBack(), 1500);
        },
        fail: (err) => {
          uni.showToast({ title: '支付取消或失败', icon: 'none' });
        },
      });
    },
  });
};
</script>

pages.json 注册:

{
  "pages": [
    {
      "path": "pages/login/login",
      "style": { "navigationStyle": "custom" }
    },
    {
      "path": "pages/pay/pay",
      "style": { "navigationBarTitleText": "支付" }
    }
  ]
}

五、核心踩坑点总结

坑点 现象 根因 解决方案
postMessage 不实时 H5 发了消息,小程序收不到 微信小程序原生限制,只在返回/分享/销毁时批量触发 navigateTo 跳转传参替代
navigateTo 没反应 H5 调用成功,小程序没跳转 uni.webview.js 没检测到 window.wx,fallback 到不工作的 window.parent.postMessage H5 引入 jweixin-1.6.0.js,确保 window.wx.miniProgram 存在
uni.requestPayment 参数错误 支付调用报错 微信小程序端不用 orderInfo,直接用顶层字段 timeStampnonceStrpackagesignTypepaySign 放顶层
pages.json 条件编译逗号 编译失败 #endif 前后缺少逗号 确保 }{ 之间有逗号分隔

六、完整通信链路

┌─────────────────────────────────────────────────────────┐
│  微信小程序端 (uni-app)                                   │
│  ┌─────────────┐      ┌─────────────┐      ┌──────────┐ │
│  │ login.vue   │ ──►  │  pay.vue    │ ──►  │  微信支付 │ │
│  │ (web-view)  │      │ (支付页面)   │      │          │ │
│  └──────┬──────┘      └─────────────┘      └──────────┘ │
│         ▲                                               │
└─────────┼───────────────────────────────────────────────┘
          │ navigateTo / postMessage
┌─────────┼───────────────────────────────────────────────┐
│  H5 端  │                                               │
│  ┌──────┴──────┐                                        │
│  │ React 页面   │                                        │
│  │ · 检测环境   │                                        │
│  │ · 跳转支付   │                                        │
│  └─────────────┘                                        │
│                                                         │
│  依赖: jweixin-1.6.0.js + uni.webview.1.5.6.js          │
└─────────────────────────────────────────────────────────┘

七、验证效果

  1. H5 页面顶部环境信息正确显示:
    运行环境: wx-miniprogram | wx=true wxMP=true uni=true
    
  2. 点击"Test 小程序支付跳转",toast 显示:
    ✅ 已触发跳转 (wx.miniProgram.navigateTo)
    
  3. 小程序端正常打开 /pages/pay/pay,显示订单金额和支付按钮
  4. 点击支付,调起微信支付,成功/失败后自动返回 web-view

八、结语

这次踩坑最大的收获是:不要只看官方文档的"用法",关键时刻要敢翻源码。

uni.webview.js 的设计目标是跨平台一致性,它让同一套 H5 代码能在 App、微信小程序、支付宝小程序等多个平台运行。但在微信小程序 web-view 中,它的平台检测逻辑依赖 window.wx.miniProgram,而 window.wx 不会凭空出现——这不是设计缺陷,而是文档没有明确说明的隐性依赖

核心结论

  1. uni.webview.js 不是"为了这顿醋包的饺子",它的跨平台抽象价值在 App 端是真实存在的
  2. 微信小程序端需要额外引入微信 JS-SDK,补齐 window.wx 这个前提条件
  3. 最佳实践是两者共存uni.webview.js 提供跨平台一致性,jweixin-1.6.0.js 补齐微信小程序的桥接能力

如果你的 H5 页面在微信小程序 web-view 中也遇到了"调用成功但没反应"的问题,不妨先检查一下控制台里 window.wx 是否为 undefined


示例代码以及参考资料见

昨天 — 2026年4月29日首页

uni-app在微信小程序国际化分包方案:优雅解决主包体积超限问题

作者 3076
2026年4月28日 17:16

一、背景与痛点

在开发大型 UniApp 项目时,国际化语言包往往是体积大户。随着业务发展,支持的语言种类增多、文案内容丰富,主包体积会快速膨胀,甚至触发微信小程序 2MB 主包限制

核心问题

┌─────────────────────────────────────────────────────────────┐
│                    传统方案:主包包含所有语言                 │
├─────────────────────────────────────────────────────────────┤
│  主包 (2.1MB)                                              │
│  ├── zh-CN.js (500KB)                                     │
│  ├── en-US.js (500KB)                                     │
│  ├── ja-JP.js (500KB)                                     │
│  ├── ko-KR.js (500KB)                                     │
│  └── 业务代码 (100KB)                                      │
└─────────────────────────────────────────────────────────────┘
    ↓ 体积超限!微信审核不通过

二、解决方案:分包懒加载语言包

架构设计

┌─────────────────────────────────────────────────────────────┐
│                      优化后方案                              │
├─────────────────────────────────────────────────────────────┤
│  主包 (600KB)                                              │
│  ├── zh-CN.js (500KB)  ← 仅保留默认语言                     │
│  └── 业务代码 (100KB)                                       │
├─────────────────────────────────────────────────────────────┤
│  分包 pagesData/                                           │
│  ├── i18n/lang/                                           │
│  │   ├── zh-CN.js                                         │
│  │   └── en-US.js                                         │
│  └── appDetails/                                          │
│      └── appDetails.vue  ← 进入时自动加载分包语言包          │
└─────────────────────────────────────────────────────────────┘
    ↓ 主包体积大幅减少,通过审核

核心原理

  1. 主包仅包含默认语言:减少初始下载体积
  2. 分包语言包随页面懒加载:用户进入分包页面时才加载对应语言包
  3. 利用 mergeLocaleMessage 动态合并:Vue I18n 原生支持增量合并语言包

三、实现方案

3.1 主包入口:locale/index.js

import { createI18n } from "vue-i18n"
import zhCN from "./lang/zh-CN"
import enUS from "./lang/en-US"

// 创建基础 i18n 实例(仅包含核心语言)
const i18n = createI18n({
  locale: uni.getStorageSync("lang") || "zh-CN",
  fallbackLocale: "zh-CN",
  legacy: false,
  globalInjection: true,
  messages: {
    "zh-CN": zhCN,
    "en-US": enUS
  }
})

// 核心:动态合并分包语言包
const loadedModules = new Set()

export async function loadSubPackageI18n(pkgName) {
  if (!pkgName) return
  
  const locale = i18n.global.locale
  const lang = typeof locale === 'string' ? locale : locale.value
  const key = `${pkgName}-${lang}`

  // 防止重复加载
  if (loadedModules.has(key)) return

  try {
    // #ifdef H5 || MP-TOUTIAO || APP
    // 其他平台直接动态导入
    const messagesModule = await import(`../${pkgName}/i18n/lang/${lang}.js`)
    const messages = messagesModule.default || messagesModule
    i18n.global.mergeLocaleMessage(lang, messages)
    loadedModules.add(key)
    // #endif

    // #ifdef MP-WEIXIN
    // 微信小程序:主包无法加载分包,此处仅做标记,实际加载由分包完成
    // #endif
  } catch (err) {
    console.warn(`[i18n] 加载分包语言失败:${pkgName}/${lang}`, err)
  }
}

export default i18n

3.2 分包入口:pagesData/i18n/index.js

import { useI18n } from 'vue-i18n'
import zhCN from './lang/zh-CN.js'
import enUS from './lang/en-US.js'

/**
 * 分包内语言包初始化函数
 * 关键:在微信小程序环境下,必须在分包页面内部调用
 */
export function initPagesDataI18n() {
    const { locale, mergeLocaleMessage } = useI18n()
    const lang = typeof locale.value === 'string' ? locale.value : 'zh-CN'

    // 根据当前语言选择对应语言包
    const messages = lang === 'en-US' ? enUS : zhCN
    
    // 合并到全局 i18n 实例
    mergeLocaleMessage(lang, messages)
    console.log(`[i18n] pagesData 分包语言包加载成功`)
}

3.3 分包页面调用:pagesData/appDetails/appDetails.vue

<script setup>
// #ifdef MP-WEIXIN
// 微信小程序必须在分包页面内加载语言包
import { initPagesDataI18n } from "../i18n/index.js"
initPagesDataI18n()
// #endif

import { onLoad } from "@dcloudio/uni-app"

onLoad(() => {
  // 页面业务逻辑
})
</script>

四、关键技术点

4.1 微信小程序分包机制限制

限制类型 说明 解决方案
主包无法访问分包文件 import 分包文件会报 "找不到模块" 错误 在分包页面内自行加载
分包预加载 用户进入分包后才下载 利用 mergeLocaleMessage 动态合并
重复加载风险 多次进入同一分包页面 使用 Set 记录已加载模块

4.2 多端兼容策略

// #ifdef MP-WEIXIN
// 微信:在分包页面内调用 initPagesDataI18n()
// #endif

// #ifdef H5 || MP-TOUTIAO || APP
// 其他平台:主包动态 import 分包语言包
import(`../${pkgName}/i18n/lang/${lang}.js`)
// #endif

// #ifdef APP-HARMONY
// 鸿蒙:使用 import.meta.glob 静态收集
const locales = import.meta.glob('../pages*/i18n/lang/*.js', { eager: true })
// #endif

4.3 语言包结构设计

pagesData/i18n/lang/
├── zh-CN.js
└── en-US.js

语言包内容示例

// pagesData/i18n/lang/zh-CN.js
export default {
  appDetails: {
    title: "应用详情",
    size: "应用大小",
    version: "版本号",
    updateTime: "更新时间"
  }
}

五、使用流程

用户进入分包页面
        ↓
触发 onLoad 生命周期
        ↓
调用 initPagesDataI18n()
        ↓
获取当前语言 locale
        ↓
加载对应语言包 zh-CN.js / en-US.js
        ↓
mergeLocaleMessage 合并到全局
        ↓
页面使用 t("appDetails.title") 即可

六、效果对比

指标 优化前 优化后 提升
主包体积 2.1MB 600KB -71%
首屏加载时间 3.2s 1.8s -44%
审核通过率 失败 通过
分包按需加载

七、注意事项

  1. 语言包命名规范:保持与主包一致的语言标识(zh-CN、en-US)
  2. 避免重复键名:分包语言包建议使用独立命名空间(如 appDetails.*
  3. 错误处理:加载失败时应有降级方案(使用 fallbackLocale)
  4. 热更新兼容:分包更新后需清空 loadedModules 缓存

八、总结

通过将语言包按分包拆分,我们成功解决了微信小程序主包体积超限问题。核心思路是:

  1. 主包瘦身:仅保留必要的默认语言
  2. 分包自治:每个分包管理自己的语言包
  3. 懒加载策略:用户进入时才加载对应语言包
  4. 动态合并:利用 Vue I18n 的 mergeLocaleMessage 实现增量加载
昨天以前首页

uni-app 组件库 Wot UI 2.0 发布了,我们带来了这些改变!

2026年4月22日 12:49

大家好,我是不如摸鱼去,好久不见。

进入 2026 以来,大家可以感受到 wot-ui 的迭代速度明显放缓了,我们最近也接到了无数催更的消息。很多人以为我们是在偷懒或者放弃更新了,其实不是,我们是在偷偷写代码然后准备惊艳所有人!(其实是苦苦码了好几个月,来点点赞吧)

接下来看看我们带来了哪些东西吧!

V2

如今已经是 AI 编程时代了,Wot UI 的 slogan 也调整成了「轻量、美观、AI 友好」。我们的目标很直接,就是为大家带来更高效、更易用,也更适合和 AI 协作的 uni-app 开发实践。

主要变化

对比 v1,v2 这一版我们带来了不少更新:

  1. 全新的设计系统。 在 v2 版本,我们基于基础变量、语义变量和组件变量三层 design token 搭建全新的设计系统,使修改组件样式和自定义主题变得随心所欲,同时升级了 UI 视觉体验。
  2. 简化 form 及相关组件 我们简化了 form 相关组件的用法,提供基于 zod 的校验引擎以及支持自定义校验引擎,使用 form-item 替换 cell 作为表单项,优化各表单组件与非表单组件结合 form-item 使用的写法,不再区分「表单组件」和「非表单组件」。
  3. 优化文档体验 重新整理文档结构,统一组件文档结构,将 @wot-ui/vitepress-theme 提取后发布为 vitepress 主题,统一多个 wot-ui 多个库的文档 UI,同时在每个组件文档中增加 css 变量的展示。
  4. 优化 AI 支持
  • 提供 cli 工具 @wot-ui/cli,其内部提供 cli 与 mcp,以优化 wot-ui 组件库的 AI 编程体验。
  • 提供多个 skills 与 LLMs.txt。
  1. 提供 Unocss 预设 提供了 Unocss 预设 @wot-ui/unocss-preset,内置主题变量、语义色、间距、圆角、字重、透明度、描边和排版相关原子类规则,把 wot-ui 的设计 token 和主题变量映射成可直接使用的原子类。

CLI

很多同学在用 AI 写 wot-ui 页面时,问题其实不在于模型不会写,而在于它总爱“凭感觉”写。比如 props 名字记错了、事件猜错了、slot 用法写得像对的,结果一跑就炸。

所以在 v2 里,我们专门提供了 @wot-ui/cli。它不是一个单纯的脚手架工具,而是把 wot-ui v2 的组件知识整理成一套可查询、可校验、可给 AI 调用的能力。

你可以把它理解成一个本地离线知识库,既能给开发者自己查,也能给 AI 客户端通过 MCP 调用。常见能力包括:

  • 查询组件的 props、events、slots 和 CSS 变量
  • 查看组件 demo,减少 AI 瞎猜用法
  • 扫描本地项目,检查不合理或错误的组件写法
  • 通过 MCP 接入 AI 工具链,让 Agent 先查再写

比如以前你让 AI 写一个表单页,它可能先吐出一份“看起来很像 wot-ui”的代码。现在更合理的流程是,先查组件约束,再生成代码,最后再跑一遍检查。这样出来的结果会稳很多。

简单来说,@wot-ui/cli 想解决的不是“怎么让 AI 更会猜”,而是“怎么让 AI 少猜一点”。这也是我们这次把「AI 友好」放进 v2 里的一个重点。

Starter

如果说组件库解决的是“页面怎么写”,那 Starter 解决的就是“项目怎么开”。所以这次我们也没有把它当成一个单纯的 demo 仓库来维护,而是持续把它往一套更适合真实开发、也更适合 AI 协作的 uni-app 起手方案去打磨。

1.5 之后,Starter 先补上了 skills,开始把项目里常用的开发约定、页面结构和组件使用方式整理出来,让 AI 在这个模板里写代码时不再完全靠猜。到了 2.0,Starter 进一步完成了对 wot-ui v2 的适配,示例、主题能力、反馈组件文档以及整体开发链路也一起升级。

你可以把现在的 Starter 理解成:它不只是“集成了 wot-ui 的模板”,而是一个默认就站在 v2 体系上的起点。新项目拉下来之后,从主题定制、页面组织到后续和 AI 配合开发,整个体验都会比 1.x 时代更顺手一些。

CSS 插件

再往下一层看,组件和模板之外,样式这一层我们也往前推了一步,提供了 @wot-ui/unocss-preset。它本质上是一个基于 UnoCSS 的预设,用来把 wot-ui v2 的设计 token 直接映射成可用的原子类。

这件事的价值在于,你在写页面时不需要一边翻设计变量、一边手写一堆样式映射了。像颜色、间距、圆角、字重、排版这些能力,现在都可以直接通过统一的 wot- 前缀类名来组织,主题切换时也能更自然地跟着整套 token 体系走。对于喜欢原子化 CSS 的同学来说,这一层会让 wot-ui v2 真正从“组件好用”变成“整套样式开发都更顺手”。

VSCode 插件

如果说前面这些更多是在补工具链和工程体验,那再落回到日常写代码,我们也补上了 VS Code 插件这一层,也就是 VS Code 插件 wot-ui-intellisense

这个插件主要解决的是写页面时那些很碎、但又很烦的事情。比如组件名记不全、属性名老要回头翻文档、事件到底叫什么总要试一下。现在在 .vue.html 文件里,输入 <wd-、空格、:@ 这些常见场景时,都可以直接拿到补全提示。

除了补全之外,它还支持组件、属性、事件的悬停文档展示,以及一部分属性值校验和错误诊断。也就是说,很多以前要切出去查文档、或者运行后才发现的问题,现在在编辑器里就能先拦一层。

如果说 CLI 更像是给 AI 和工程化链路准备的,那 VS Code 插件就是给开发者日常写代码准备的。一个负责让模型少猜,一个负责让人少翻文档,配合起来,整个 wot-ui v2 的开发体验就会完整很多。

最后

回过头来看,这次 wot-ui v2 对我们来说并不只是一次常规升级。

它一边在补齐设计系统、表单体系、文档体验这些基础能力,一边也在认真回应这两年越来越明显的变化:大家写代码的方式,确实已经和以前不太一样了。

所以你会看到,这一版里不只有组件本身,也有 Starter、CLI、UnoCSS 预设、VS Code 插件这些围绕开发体验的配套。我们想做的,不只是一个“能用”的组件库,而是一套更顺手、更现代,也更适合和 AI 一起协作的 uni-app 开发方案。

v2 还有很多东西会在后面陆续展开,这篇文章先带大家看一个整体。如果你也在关注 wot-ui v2,或者也在想组件库怎么更好地拥抱 AI 编程,欢迎继续关注我们后面的更新。

参考资料

独立开发复盘:我用 Uni-app + Strapi v5 肝了一个“会上瘾”的打卡小程序

作者 知航驿站
2026年4月21日 20:06

大家好,我是一名独立开发者。最近利用业余时间,我从零到一开发并上线了一款目标打卡/习惯养成类的小程序。

今天这篇文章,不仅是想向大家推荐一下我的心血之作,更想从创作灵感核心技术实现代码细节以及无数次踩坑的角度,和大家深度复盘一下整个项目的历程。如果你也想尝试用 Uni-app + Strapi 搞全栈独立开发,这篇“避坑指南 + 技术解析”绝对不容错过!


307c9942d27117ec00e7781976431a56.jpg

1fa52da0afde72c0faf8e72dc49c1c29.jpg

💡 创作灵感与产品心得:为什么还要做一个打卡应用?

市面上的打卡应用多如牛毛,为什么我还要自己造轮子? 其实原因很简单:我觉得现有的工具太“冷冰冰”了,缺乏足够的情绪反馈。

打卡/坚持习惯本身就是一件反人性的事情,如果工具只是一个无情的“待办列表”,那用户很容易就会放弃。因此,在产品设计之初,我定下了几个核心基调:

  1. 克制与聚焦:我限制了每天最多只能创建 12 个任务,到达 10 个时会温馨警告。目标泛滥等于没有目标。
  2. 正向反馈拉满:任务完成不能只是打个勾,必须要有“爽感”。我加入了物理震动、纸屑爆裂动画(撒花)、以及 3D 翻转的徽章解锁系统。
  3. 互助与抄作业:很多时候我们不知道该养成什么习惯,所以我做了一个“社区广场”(瀑布流布局),看到别人优秀的习惯,可以直接“一键 Copy”到自己的计划中。

🛠 技术选型:单兵作战的效率最优解

作为独立开发者,开发效率是第一生产力。我选择了这套组合拳:

  • 前端:Uni-app (Vue 3) + Tailwind CSS
    • Vue 3 的 Composition API 逻辑复用非常爽。
    • 结合原子化 CSS(如 Tailwind/UnoCSS),极大提升了切图速度,摆脱了起 class 名字的内耗。
  • 后端:Strapi v5 (Headless CMS)
    • 绝对的效率神器!不用手写繁琐的 CRUD 接口,建好模型直接生成 RESTful API。
    • 自带强大的 Admin 后台,数据管理极度舒适,让我能把 80% 的精力全放在前端交互和产品体验上。

💻 核心技术点与代码实现

1. 极致的微交互:让打卡“爽”起来

为了让用户点下“完成”的那一刻有真实的成就感,我结合了 CSS 动画和原生的触觉反馈:

// 核心打卡逻辑片段
const handleCheckIn = async (task) => {
  // 1. 触发 Haptic 震动反馈 (重震动带来物理按压感)
  uni.vibrateShort({ type: 'heavy' });
  
  // 2. 触发微动效:按钮自身的弹跳 + 全局撒花特效
  task.isBouncing = true; 
  uni.$emit('trigger-particle-confetti'); // 呼叫全局纸屑动画组件
  
  try {
    await api.completeTask(task.id);
    // 3. 检查是否触发徽章解锁
    checkBadgeUnlock(task);
  } catch (e) {
    // 错误处理...
  }
}

在徽章解锁时,我还写了一个 3D 翻牌效果(利用 CSS transform: rotateY 配合 animate-flip-y),让徽章展示更有仪式感。

2. Strapi 关系模型 Hack:如何优雅地记录“徽章解锁时间”?

在后端的开发中,我遇到了一个经典问题:多对多关联表的额外字段怎么存? User 和 Badge 是多对多关系,但在 Strapi 原生模型中,中间表无法轻易添加像 unlockedAt 这样的字段。

我的解法: 直接在 User Schema 中扩展一个轻量级的 JSON 字段 badge_unlock_records

// apps/api/src/extensions/users-permissions/strapi-server.ts
// 扩展 Strapi 默认的 User Schema
export default (plugin) => {
  plugin.contentTypes.user.attributes = {
    ...plugin.contentTypes.user.attributes,
    // 原生多对多关联
    badges: {
      type: 'relation',
      relation: 'manyToMany',
      target: 'api::badge.badge',
    },
    // 💡 Hack: 用 JSON 字段记录具体的解锁元数据
    badge_unlock_records: {
      type: 'json',
      // 数据结构示例: { "badge_id_1": "2023-10-01T12:00:00Z" }
    }
  };
  return plugin;
};

这样既保留了原生关系(方便在 Admin 面板查看),又解决了业务上的元数据存储需求。

3. 社区广场的“真”瀑布流与分页

社区页面的卡片高度是不固定的,传统的 Grid 布局会留下大片空白。我通过维护左右两列的数据数组,实现了原生的瀑布流效果:

// 瀑布流计算核心逻辑
const leftColumn = ref([]);
const rightColumn = ref([]);
let leftHeight = 0;
let rightHeight = 0;

const appendToMasonry = (items) => {
  items.forEach(item => {
    // 估算卡片高度 (基于内容长度)
    const estimatedHeight = calculateHeight(item);
    
    // 哪边矮往哪边塞
    if (leftHeight <= rightHeight) {
      leftColumn.value.push(item);
      leftHeight += estimatedHeight;
    } else {
      rightColumn.value.push(item);
      rightHeight += estimatedHeight;
    }
  });
};

配合 onReachBottom 触底事件,以及自己封装的 wd-loadmore 状态组件,整个信息流刷起来非常丝滑。


🚧 吐血踩坑录:那些让我熬夜的 Bug

全栈开发最怕的就是遇到莫名其妙的兼容性和环境问题。以下这几个坑,价值好几百根头发:

坑一:iOS 13 下 Swiper 圆角失效问题

症状:在旧版 iOS 中,给 <swiper> 设了 border-radiusoverflow: hidden,但里面的图片滑动时依然会无视圆角溢出。 解法:这是 transform 堆叠上下文导致的渲染 Bug。不仅要给 swiper 和 image 都加上圆角类名,还必须强制加上 transform: translateY(0);

<!-- 💡 注意 style 中的 transform 是精髓 -->
<swiper class="rounded-[5px]" style="transform: translateY(0);">
  <swiper-item>
    <image class="rounded-[5px]" src="..." />
  </swiper-item>
</swiper>

坑二:小程序下渐变文字(bg-clip-text)直接消失

症状:想用 Tailwind 的 bg-clip-text text-transparent 做炫酷的渐变文字,结果在微信小程序/iOS上文字直接隐身了。 解法:小程序对 <text> 标签的背景裁剪支持极差。如果要用,必须把 <text> 换成 <view> 标签来写文字,或者老老实实退回到纯色文本。

坑三:Strapi v5 生产环境部署大坑

  1. 插件报错:初始化 v5 时,报 Middleware plugin::email.rateLimit not found解法:手动执行 pnpm add @strapi/email 安装缺失依赖。
  2. RTK Query 压缩报错:打包上线后 Admin 面板报 Cannot read properties of undefined (reading 'merge')。原因是 Vite 压缩把 RTK Query 的方法名压没了。 解法:在 src/admin/vite.config.ts 中关闭 minify,并清理缓存!
// src/admin/vite.config.ts
export default {
  build: {
    minify: false, // 💡 必须设为 false
  },
};

(执行 npx rimraf .strapi build 清除缓存后再 build)


结语

从一行代码都没有,到完整的前后端链路打通;从构思微交互,到处理数据备份(云端同步 + 剪贴板文本导出);这个过程虽然辛苦,但当看到产品真正跑起来,有人开始用它记录生活时,一切都值了。

目前小程序已经上线,欢迎大家在微信搜索 简行一周 体验!

如果你对文章中的技术点感兴趣,或者在用 Uni-app / Strapi 的时候也遇到了头疼的问题,欢迎在评论区留言交流,我一定知无不言!

最后,如果你觉得这篇文章对你有启发,求个点赞 + 收藏,这对我这个独立开发者是莫大的鼓励!🚀

6e051cfd4b1574ab6dc9e48938b739d7.png

uni-app 运行时揭秘:styleIsolation 的转化

2026年4月18日 14:16

背景

大家好,我是 uni-app 的核心开发 前端笨笨狗。本篇是 uni-app 源码分析的第三篇文章,欢迎关注!

前两天有开发者在群里面问我 uni-app 中如何配置 styleIsolation,我告诉了他正确的配置方案,也计划写篇文章揭秘 uni-app 是如何通过运行时将开发者的配置转化为原生微信小程序的配置。

指南

选项式

uni-app 中,开发者可以通过在页面组件中添加 options 配置项来设置 styleIsolation,示例如下:

<script>
export default {
  name: 'MyComp',
  options: {
    styleIsolation: 'isolated'
  },  
}
</script>
<script>
import { defineComponent } from "vue";

export default defineComponent({
  name: "MyComp",
  options: {
    styleIsolation: "isolated",
  },
});
</script>

组合式

在使用组合式 API 的页面组件中,开发者同样可以通过 defineOptions 来设置 styleIsolation,示例如下:

<script setup>
defineOptions({
  name: 'MyComp',
  options: {
    styleIsolation: 'isolated'
  }
})
</script>

原理

createComponent 这个函数大家如果看过 vue 文件的 js 编译产物就一定不会陌生,比如

<script setup>
defineOptions({
  options: {
    styleIsolation: "shared",
  },
});
</script>

会被编译为

const _sfc_main = {
  __name: "comp",
  options: {
    styleIsolation: "shared"
  }
  setup(__props) {
    return (_ctx, _cache) => {
      return {};
    };
  }
};
wx.createComponent(_sfc_main);

也就是 script 中写的代码会被编译成一个对象,这个对象就是 vue 组件的配置项,而微信小程序又不认识 vue 组件的配置项,那么怎么把 vue 组件的配置项转化为微信小程序的配置项呢?这就要靠 uni-app 的运行时了,在 common/vendor.js 中,createComponent 函数会调用 parseComponent 函数来解析 vue 组件的配置项,parseComponent 的返回值就是微信小程序组件的配置项,也就是 Component 构造器 的参数,可以用来构造小程序原生组件。

function initCreateComponent() {
  return function createComponent(vueComponentOptions) {
    return Component(parseComponent(vueComponentOptions));
  };
}

const createComponent = initCreateComponent();
wx.createComponent = createComponent;

parseComponent 解析到页面组件时,会检查组件的 options 配置项,如果发现 styleIsolation,就会将其转化为微信小程序的配置项。

function parseComponent(vueOptions) {
  vueOptions = vueOptions.default || vueOptions;
  const options = {
    multipleSlots: true,
    // styleIsolation: 'apply-shared',
    addGlobalClass: true,
    pureDataPattern: /^uP$/
  };
  // 将开发者在 options 中设置的配置项转化为微信小程序的配置项
  if (vueOptions.options) {
    Object.assign(options, vueOptions.options);
  }
  const mpComponentOptions = {
    options,
    // 省略其他配置项
  };
  return mpComponentOptions;
}

这样一来,开发者在页面组件中设置的 styleIsolation 就会被正确地转化为微信小程序的配置项,从而自由控制样式隔离。

❌
❌