阅读视图

发现新文章,点击刷新页面。

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

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

今天这篇文章,不仅是想向大家推荐一下我的心血之作,更想从创作灵感核心技术实现代码细节以及无数次踩坑的角度,和大家深度复盘一下整个项目的历程。如果你也想尝试用 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 的转化

背景

大家好,我是 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 就会被正确地转化为微信小程序的配置项,从而自由控制样式隔离。

小程序包体积分析利器 -- vite-plugin-component-insight

背景

大家好,我是 uni-app 的核心开发 笨笨狗吞噬者,欢迎关注我的微信公众号 前端笨笨狗,或者加我的微信 wxid_olsjlzuh4ivf22 沟通交流!

微信小程序支持分包异步化 跨分包自定义组件引用,但是,很多业务项目往往都比较复杂,组件使用情况也不容易看清,开发中很容易遇到这些问题:

  • 无法快速获悉某个组件到底被哪些页面使用
  • 不清楚一个组件在项目里出现了多少次
  • 做分包优化时,不知道组件放在主包还是分包更合适

于是,我写了一个插件 vite-plugin-component-insight 来简化这一过程。

特性

  • 开箱即用,配置简单
  • 统计组件的使用次数和调用情况
  • 结合主包和分包关系输出组件划分建议
  • 支持生成 markdown 报告,方便查看更加详细的信息
  • 支持 hx 项目和 cli 项目
  • 支持 uni-app 和 uni-app-x (vue3)

使用指南

安装

npm install @uni_toolkit/vite-plugin-component-insight -D
# 或
pnpm add @uni_toolkit/vite-plugin-component-insight -D
# 或
yarn add @uni_toolkit/vite-plugin-component-insight -D

配置插件

vite.config.js 中使用:

import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
import componentInsight from '@uni_toolkit/vite-plugin-component-insight';

export default defineConfig({
  plugins: [
    uni(),
    componentInsight(), // 在 uni 之后调用
  ],
});

Tips

插件默认不会生成文件,而是在控制台直接输出分析结果。

sub.jpg

如果需要生成 markdown 报告,可以这样配置:

componentInsight({
  reportMarkdownPath: 'logs/component-insight-report.md',
})

如果只想生成 markdown,不输出控制台日志,可以这样配置:

componentInsight({
  logToConsole: false,
  reportMarkdownPath: 'logs/component-insight-report.md',
})

完整配置项

interface VitePluginComponentInsightOptions {
  reportMarkdownPath?: string;
  logToConsole?: boolean;
  exclude?: ReadonlyArray<string | RegExp> | string | RegExp | null;
  include?: ReadonlyArray<string | RegExp> | string | RegExp | null;
}
选项 说明
reportMarkdownPath 自定义 Markdown 报告输出路径,不传则不生成 Markdown
logToConsole 是否输出控制台日志,默认开启
exclude 指定过滤的文件,默认过滤 node_modules 和 uni_modules
include 指定包含的文件,默认为空

我从瑞幸咖啡小程序里,拆出了一套 22 个组件的开源 UI 库

我从瑞幸咖啡小程序里,拆出了一套 22 个组件的开源 UI 库

把它的设计语言完整提炼出来,做成了一个可以直接 npm install 的微信小程序组件库。

效果截图

Snipaste_2026-04-15_16-25-37.jpg

Snipaste_2026-04-15_16-27-47.jpg

Snipaste_2026-04-15_16-27-15.jpg

Snipaste_2026-04-15_16-26-37.jpg

Snipaste_2026-04-15_16-26-20.jpg

Snipaste_2026-04-15_16-26-00.jpg

克制的双色系统(蓝+橙),无阴影的卡片层次,菜单页那个从 + 按钮展开到数量步进器的丝滑交互,会员卡页面方案选择器的信息架构……这些细节放在一起,构成了一套非常完整且高辨识度的设计语言。

项目叫 LKCN UI,22 个组件,纯原生微信小程序自定义组件,零依赖,原生 / Taro / uni-app 项目都能直接用。

GitHub: https://github.com/user/lkcn-ui

色彩系统

瑞幸全局只用 两个强调色

色值 用途 使用场景
#1A6EFF Brand Blue 交互元素 TabBar 激活态、按钮、加购圆钮、链接
#FF6B35 Accent Orange 促销与价格 价格数字、CTA 按钮、Badge、优惠券

辅助色包括会员金 #C8A26E、即享绿 #2B7D5B、咖啡棕 #3D2D1F

一个重要发现:瑞幸的卡片没有阴影。整个 App 的层次感完全靠圆角 + 间距 + 背景色差来实现,这使得渲染性能非常好,也让整体视觉特别干净。

字体体系

价格是瑞幸 UI 最有辨识度的元素。它把价格拆成了三段不同大小的文字:

¥(小号加粗) 9(大号加粗) .9(小号加粗)  ¥32(小号灰色删除线)

这种「符号小、整数大、小数小」的层次处理让价格数字极具视觉冲击力,同时原价的删除线灰色处理制造了强烈的价差感知。我在 lkcn-price 组件里完整还原了这个效果。

间距与圆角

间距体系是标准的 8px 递增:4 / 8 / 12 / 16 / 24 / 32(rpx 翻倍)。

圆角有 5 级:4px(标签)→ 8px(按钮、输入框)→ 12px(卡片)→ 20px(弹窗)→ 999px(胶囊)。

所有 Token 都通过 CSS 变量注入,覆盖变量即可全局换肤:

page {
  --lkcn-blue: #1A6EFF;
  --lkcn-orange: #FF6B35;
  --lkcn-radius-md: 24rpx;
  /* ... 60+ 个变量 */
}

22 个组件一览

全部组件从瑞幸小程序的真实页面中提取,不是凭空设计的:

基础组件: Button(6 种类型 × 3 尺寸)、Tag(4 类型 × 4 颜色)、Price(整数/小数自动拆分)、Badge、Avatar

布局容器: Card、Grid(3/4/5 列自适应)、Swiper(胶囊形指示点)、CouponScroll、PromoCard

导航: TabBar(safe-area 适配)、Tabs(滑动下划线)、SegmentControl、SearchBar、CategorySidebar、LocationBar

业务组件: ProductCard(菜单列表项)、Stepper(折叠→展开态)、LevelCard(会员等级)、MembershipPlan(订阅方案选择)、NoticeBar、FloatingButton

1. Stepper:瑞幸的加购交互

瑞幸菜单页的加购交互是我见过最优雅的——数量为 0 时只显示一个蓝色 + 圆钮,点击后展开为 [-] [数字] [+] 三段式控件。

<!-- 使用方式 -->
<lkcn-stepper value="{{count}}" bind:change="onChange" />

组件内部的关键判断:

<!-- value <= min 时只显示 + 按钮 -->
<view wx:if="{{value <= min}}" class="lkcn-stepper__add lkcn-stepper__add--solo">
  <text class="lkcn-stepper__icon">+</text>
</view>
<!-- 否则展开完整控件 -->
<view wx:else class="lkcn-stepper__controls">
  <!-- [-] [count] [+] -->
</view>

加购按钮的 scale(0.88) + cubic-bezier(0.34, 1.56, 0.64, 1) 弹性回弹动画让点击手感特别好。

2. Price:三段式价格渲染

<lkcn-price value="9.9" original="32" prefix="预估到手" />

组件自动将 9.9 拆分为整数 9 和小数 .9,分别用不同字号渲染,currency symbol ¥ 用小号加粗。这种处理在电商类小程序里非常实用,直接拿去用就行。

3. CategorySidebar:菜单页左侧导航

这个组件还原了菜单页左侧的完整细节——激活态的白色背景、左侧橙色指示条、分类标签(新品产地季苦瓜轻体),以及新品小红点。

<lkcn-category-sidebar
  categories="{{categories}}"
  active="{{catActive}}"
  height="100vh"
  bind:change="onCatChange"
/>

数据结构支持纯文字和对象两种格式:

categories: [
  '人气Top',                           // 纯文字
  { text: '周边NEW', tag: '周边NEW', tagColor: 'blue' },  // 带标签
  { text: '果C美式', tag: '苦瓜轻体', tagColor: 'green', dot: true },
]

4. MembershipPlan:会员方案选择器

会员卡页面底部那个方案选择 + 订阅 CTA + 协议勾选的完整流程,一个组件搞定:

<lkcn-membership-plan
  plans="{{plans}}"
  active="{{planActive}}"
  agreement="开通会员代表接受"
  agreement-links="{{[{text:'《服务协议》'}, {text:'《续费说明》'}]}}"
  bind:subscribe="onSubscribe"
/>

为什么选原生而不是 Taro / uni-app

这是我在开发前做的一个关键决策。核心理由就一个——受众最大化

原生微信小程序自定义组件能被所有技术栈引入:

原生组件 → 原生项目 ✅、uni-app 项目 ✅、Taro 项目 ✅
uni-app 组件 → 只有 uni-app 能用 ❌
Taro 组件 → 只有 Taro 能用 ❌

uni-app 引入原生组件只需要放到 wxcomponents/ 目录,在 pages.json 注册即可。Taro 也类似。写一份代码三个生态都能吃到,这是 Vant Weapp 走过的路。

快速上手

npm install lkcn-ui

在微信开发者工具中构建 npm,然后注册组件:

{
  "usingComponents": {
    "lkcn-button": "lkcn-ui/button/index",
    "lkcn-price": "lkcn-ui/price/index",
    "lkcn-product-card": "lkcn-ui/product-card/index"
  }
}

直接使用:

<lkcn-button type="primary" round>立即下单</lkcn-button>

<lkcn-product-card
  image="/images/coconut-latte.png"
  title="生椰拿铁(首创)"
  tags="{{['全球销量第一', 'IIAC金奖']}}"
  price="9.9"
  original-price="32"
  bind:add="onAddToCart"
/>

也可以不用 npm,直接把 packages/ 下需要的组件目录复制到你的项目里。

换肤

所有视觉变量都通过 CSS 变量控制,覆盖即可适配你自己的品牌:

page {
  --lkcn-blue: #7C3AED;    /* 换成你的品牌紫 */
  --lkcn-orange: #F59E0B;  /* 换成你的品牌黄 */
  --lkcn-radius-md: 32rpx; /* 更大的圆角 */
}

不需要改任何组件源码,Design Token 体系的优势就在这里。

项目数据

  • 22 个组件,全部完成
  • 143 个源文件
  • 0 外部依赖
  • 每个组件 4 件套(wxml / wxss / js / json)
  • 60+ Design Token CSS 变量
  • 11 个可交互 demo 页面
  • 包体积 < 90KB(未压缩)

后续计划

  • 组件 TypeScript .d.ts 类型声明
  • VitePress 文档站
  • 暗色模式适配
  • GitHub Actions CI 自动发布

如果你也觉得有用,欢迎 Star:

GitHub: https://github.com/user/lkcn-ui

❌