接手一个多端项目,产品负责人把需求丢过来的时候,往往只有一句话:“小程序、H5、App 都得能跑,下周出第一版”。这种场景下,从零手写一套组件库基本不现实,业务方也不愿意为一个卡片圆角开三次评审会。uView就是在这种“多端要快、样式要统一、交互要够用”的夹缝里长出来的东西——它是 uni-app 生态里一套基于 Vue 的 UI 组件库,把表单、列表、弹层、导航、图标、校验这些高频零件打包好,开发者直接拼装业务页面,不用再重复造轮子。这篇文章面向的是已经会用 Vue、能看懂main.js和pages.json,但第一次把 uView 塞进 uni-app 项目、或者已经用上但总在样式和跨端上踩坑的人。下面的内容不讲官网上能抄到的入门三步,重点讲选型逻辑、接入细节、组件实操、样式冲突、体积控制,以及那些只有真正在真机和模拟器之间来回切换过的人才会遇到的问题。文章里的项目名、团队场景都做过脱敏处理,只保留可复现的技术路径。
1. 选型:uView 在 uni-app 里为什么值得占用一个依赖位
1.1 uni-app 生态里绕不开的几个 UI 选项
做过 uni-app 的人都清楚,这个框架本身只解决“一套代码编译到多端”的问题,UI 层是空白的。所以每个项目启动时都要做一次组件库决策,常见的方向大概有三类。第一类是官方维护的 uni-ui,优点是跟框架版本咬得紧、文档和框架一致、出问题容易找到原因,缺点是组件数量和设计精细度偏保守,复杂表单、上传、分页列表这类东西要自己补。第二类是第三方商业组件库,样式完整、模板丰富,但授权和定制成本要先算清楚。第三类就是 uView 这类社区驱动、长时间迭代的开源库,组件覆盖面广,从基础的按钮、输入框一直铺到日历、签名板、步骤条、瀑布流,基本上一个中型业务 App 需要的零件它都有。
选型这事我自己的判断标准很朴素:看这个库里“最难自己写的那几个组件”做得怎么样。按钮和卡片谁都能写,真正拉开差距的是表单校验、滚动列表、弹层层级、日期选择这些又臭又长又容易出兼容问题的东西。uView 在这几块上是有积累的,它的u-form校验体系、u-list的滚动处理、u-popup的层级管理,都属于“你自己写要花两三天、还不一定写得比它稳”的部分。
1.2 uView 真正省事的地方在哪里
很多人对组件库的印象是“省 CSS”,这个理解其实偏了。uView 省的更多是交互状态和边界处理。举个具体的例子,一个带清除按钮、带字数统计、带最大长度限制、聚焦时边框变色、禁用时置灰的输入框,你手写大概要写七八个状态分支,还要处理 iOS 上键盘弹起把页面顶飞的问题。uView 的u-input把这些状态都封装成了 props,你只需要传clearable、maxlength、showWordLimit、disabled就行。
再比如u-popup,它内部处理了遮罩点击关闭、动画时长、z-index递增、锁定背景滚动、安全区适配这些细节。锁定背景滚动这件事在小程序端尤其麻烦,因为小程序的页面滚动和 H5 的 body 滚动是两套机制,自己写很容易出现“弹窗开着但背后的页面还能滑”的问题。这类坑属于典型的“不踩不知道,踩了就很难受”,用成熟组件库本质上是把别人的踩坑成本买过来。
1.3 版本选择:Vue2 的 1.x、2.x 与 Vue3 的社区分支
这是第一个必须讲清楚的问题,因为版本选错后面全是白干。uView 1.x 是早期版本,API 相对简单,组件命名以u-开头,主要面向 Vue2 的 uni-app 项目,现在基本处于修复性维护状态,新项目不太建议从它开始。uView 2.x 在 1.x 基础上重写了不少组件,增加了u-list虚拟列表、更完善的表单校验、更细的主题配置,是目前 Vue2 项目里的主流选择。而如果项目用的是 uni-app 的 Vue3 版本,官方 2.x 不能直接跑,社区里有一套 API 基本对齐的 Vue3 移植版本,组件前缀和用法几乎一致,迁移成本主要落在main.js的注册方式和部分生命周期写法上。
我在一个双版本并行的项目里做过对比,同一套业务页面在 Vue2 + 2.x 和 Vue3 + 移植版下的差异,主要集中在三处:全局注册从Vue.use()变成app.use();this.$refs的取值时机在setup里要改成ref引用;部分组件的v-model语法糖细节有差别。业务逻辑和模板结构基本不动,这也是这套库能长期活下来的原因——约定足够稳定。
提示:不要在一个项目里同时装两个版本,也不要在 Vue3 项目里硬塞 Vue2 版本的包。编译期报错还算好的,最麻烦的是编译通过但运行时组件注册不上,页面一片空白,排查半天才发现是版本错配。
1.4 项目结构上的整体设计
决定用 uView 之后,我会在动手写业务之前先把三件事定下来,这三件事决定了后面几个月会不会天天改样式。第一件是主题变量落在哪:所有品牌色、圆角、字号统一收敛到uni.scss里,业务页面只允许引变量,不允许写死色值。第二件是组件前缀约定:如果项目里同时有原生自定义组件或者其他库,就要用 uView 2.x 推荐的双横线前缀写法来隔离命名空间,避免 easycom 匹配错组件。第三件是页面骨架分层:把“导航栏 + 内容区 + 底部安全区”这套壳抽成一个布局组件,业务页面只关心内容区,这样以后换皮肤、改沉浸式状态栏都只改一个地方。
这三件事听起来像老生常谈,但我见过太多项目在第三个月才开始收拾样式,那时候已经有八十个页面写死了#2979ff,改一次品牌色要全局搜索替换,还要祈祷没有拼写变体。前期花半天定规矩,后面能省几十个小时。
2. 接入实操:从空项目到 uView 跑通的第一屏
2.1 安装依赖的两种路径与各自的适用场景
uView 的引入方式主要有两种,选哪种取决于团队协作方式和构建流程。第一种是通过插件市场导入,适合不熟悉 npm、或者项目本身就是用 HBuilderX 可视化创建的情况,导入后会以独立目录的形式存在于项目里,好处是源码可见、改起来直接,坏处是升级要靠手动覆盖,多人协作时容易产生冲突。第二种是走 npm 安装,适合有完整 CI 流程、用命令行构建的团队,依赖版本可以锁在package.json里,升级和回滚都清晰。
npm 方式的基本命令如下,注意装的是对应 Vue 版本的包名:
# Vue2 项目 npm i uview-ui # Vue3 项目(社区移植版) npm i uview-plus装完之后一定要做一次npm ls或者看package-lock.json确认版本,我遇到过一次依赖树里同时存在两个版本的情况,原因是某个子依赖里锁了旧版,结果编译时用的是旧版,页面上的组件渲染出来是上一代的样式,排查了快两个小时才发现问题在依赖树而不是代码。
2.2 全局注册与配置注入的标准写法
注册这一步看起来只是三行代码,但顺序和位置有讲究。Vue2 项目的写法是在main.js里引入并Vue.use(),Vue3 项目则是app.use()。
// Vue2: main.js import Vue from 'vue' import App from './App' import uView from 'uview-ui' Vue.use(uView) App.mpType = 'app' const app = new Vue({ ...App }) app.$mount()// Vue3: main.js import { createSSRApp } from 'vue' import App from './App.vue' import uviewPlus from 'uview-plus' export function createApp() { const app = createSSRApp(App) app.use(uviewPlus) return { app } }这里有个容易被忽略的点:Vue.use()必须写在创建根实例之前。有人把注册写在app.$mount()后面,本地开发热更新时看着是好的,一旦打包到生产环境就出现部分组件未注册。原因在于生产构建的代码压缩会调整执行顺序相关的作用域,这种“本地能跑线上不行”的问题最难查。
注册之后还要把主题变量注进去。在uni.scss里追加一行引入,这个文件会被 uni-app 自动注入到每个 SCSS 文件的头部,所以业务页面里可以直接用$u-primary这类变量而不用再手动 import:
/* uni.scss */ @import 'uview-ui/theme.scss';然后是在App.vue的全局样式里引入基础样式表,注意要放在<style lang="scss">而不是 scoped 里,否则只有根组件能用:
/* App.vue */ <style lang="scss"> @import 'uview-ui/index.scss'; </style>注意:
App.vue里引基础样式这一步千万不能省,也不能只引到某个页面里。很多人反馈“组件结构出来了但完全没有样式”,十有八九是漏了这一步,或者引入路径大小写不对。Windows 和 macOS 对文件名大小写的容忍度不同,本地不报错但换台机器打包就炸。
2.3 easycom 配置与前缀隔离
easycom 是 uni-app 提供的一个自动引入机制,只要组件路径符合约定,模板里写标签就能用,不用import也不用components注册。uView 完全基于这套机制设计,所以pages.json里的配置是必须项。
{ "easycom": { "autoscan": true, "custom": { "^u--(.*)": "uview-ui/components/u-$1/u-$1.vue", "^u-([^-].*)": "uview-ui/components/u-$1/u-$1.vue" } } }为什么要配两条规则?因为 uView 2.x 推荐用u--xxx这种双横线写法,目的是把命名空间和原生组件、其他组件库的u-前缀隔开。实际开发中你会发现两种写法都能用,但混用会让代码风格很乱,我的做法是团队内统一用单横线u-xxx,前提是确认项目里没有其他占用同名的组件。如果项目里确实有其他库,那就老老实实用双横线,多敲一个字符换来的命名安全性是值的。
还有一个隐藏问题:easycom 的custom规则是有匹配顺序的,如果同时配了^u-(.*)和^u--(.*),后者必须写在前面,否则u--button会被前面的规则先匹配到,去找一个不存在的u--button目录。
2.4 第一屏验证:把几个组件摞上去试
接入完成之后,别急着写业务,先做个验证页面,把最容易出问题的几个组件一次性摞上去:一个带校验的表单、一个弹层、一个图标、一个列表。这个页面的作用不是给别人看,是给自己一个“基线”——后面任何一次依赖升级、构建配置调整,都可以拿这个页面回归一次,确认基础能力没坏。
我一般会在这个验证页里放三个断言:图标能正常渲染(说明字体文件被正确打包)、弹层能盖住页面内容(说明样式层级正常)、表单校验能拦住空提交(说明校验逻辑和事件绑定正常)。这三条过了,说明接入无误,可以放心往下写了。如果图标显示成方块,通常是字体文件路径问题;如果弹层盖不住,多半是z-index或者父级overflow的问题;如果校验不生效,先看prop有没有对上。
3. 核心组件实操:把表单、列表、弹层这三块吃透
3.1 表单校验:u-form 的规则体系与常见误区
表单是后台类、业务类 uni-app 项目里最重的部分,也是 uView 最能体现价值的地方。u-form配合u-form-item做字段级校验,核心思路和常见的表单库一致:表单绑定model,每个字段声明prop,规则挂在rules里。
<template> <u-form :model="form" :rules="rules" ref="formRef" label-width="160rpx"> <u-form-item label="手机号" prop="phone"> <u-input v-model="form.phone" placeholder="请输入手机号" clearable /> </u-form-item> <u-form-item label="验证码" prop="code"> <u-input v-model="form.code" placeholder="6 位数字" maxlength="6" /> </u-form-item> <u-button type="primary" @click="submit">提交</u-button> </u-form> </template>export default { data() { return { form: { phone: '', code: '' }, rules: { phone: [ { required: true, message: '请输入手机号', trigger: ['blur', 'change'] }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: ['blur'] } ], code: [ { required: true, message: '请输入验证码', trigger: ['blur'] }, { len: 6, message: '验证码为 6 位数字', trigger: ['blur'] } ] } } }, methods: { submit() { this.$refs.formRef.validate().then(() => { // 校验通过,走提交逻辑 }).catch(() => { // 校验失败,这里的回调通常不需要额外交互 }) } } }几个实操中必须知道的点。第一,prop的值必须和model里的字段名完全一致,写错了不会报错,只是那条规则永远不触发,你会以为校验坏了。第二,trigger配成数组比配单个值更好用,['blur', 'change']意味着失焦和输入变化都校验,用户体验上更即时,但要注意change触发太频繁会导致输入过程中就飘红,所以格式类的规则我一般只挂blur,必填类的挂['blur', 'change']。第三,规则是支持自定义函数的,涉及“两次密码是否一致”这种联动校验时,必须用validator函数并且把依赖字段一起放进去,因为默认规则只认得自己那个字段。
const validateConfirm = (rule, value, callback) => { if (value !== this.form.password) { callback(new Error('两次输入的密码不一致')) } else { callback() } }联动校验还有一个坑:修改密码字段之后,确认密码那一栏的红字不会自动消失,需要手动触发一次重新校验,这时候要用到表单实例上的清除校验方法,把对应字段的错误状态重置掉。这个细节官网上写得比较简略,但真实表单里几乎都会遇到。
3.2 滚动列表与分页加载的配合
长列表是移动端的另一个高频场景。uView 2.x 提供了u-list组件,本质是一个带滚动容器的列表,配合u-loadmore处理加载状态。用法不算复杂,但有几个参数必须理解清楚。
<template> <u-list height="100vh" @scrolltolower="loadMore"> <u-list-item v-for="item in list" :key="item.id"> <view class="card">{{ item.title }}</view> </u-list-item> <u-loadmore :status="loadStatus" @loadmore="loadMore" /> </u-list> </template>export default { data() { return { list: [], page: 1, loadStatus: 'loadmore' // loadmore / loading / nomore } }, methods: { async loadMore() { if (this.loadStatus !== 'loadmore') return this.loadStatus = 'loading' try { const res = await this.fetchList(this.page) this.list = this.list.concat(res.rows) this.page += 1 this.loadStatus = res.rows.length < 10 ? 'nomore' : 'loadmore' } catch (e) { this.loadStatus = 'loadmore' } } } }这里的核心是状态机的守门逻辑。loadStatus !== 'loadmore'这一句是必须的,否则用户在触底瞬间连续滑动会触发多次请求,出现重复数据。判断是否到底用“返回条数小于页大小”是个经验做法,比用总数计算更省事,因为后端不一定每次都返回 total。异常分支里要把状态改回loadmore,否则一次网络抖动之后列表就再也加载不动了,这个 bug 很隐蔽,因为用户手动下拉刷新之后又恢复了,容易被当成偶发问题忽略。
u-list的height参数是个硬性要求,很多平台下不设置高度组件不会产生滚动,而是跟随页面滚动,这样一来scrolltolower事件就不会触发。如果在页面里希望整页滚动而不是局部滚动,那其实不需要用u-list,直接用普通view加onReachBottom生命周期更简单。选择哪个取决于设计稿:顶部有固定筛选栏、只让内容区滚动的,用u-list;整页一起滚的,用页面生命周期。
3.3 弹层、弹窗与反馈组件的组合套路
移动端的交互反馈基本围绕三种东西转:从底部升起的弹层、居中的确认框、轻提示。uView 分别对应u-popup、u-modal、u-toast。这三个组件的正确用法不只是“调出来”,更在于层级关系和状态同步。
u-popup的mode参数决定方向,round控制圆角,safeAreaInsetBottom控制底部安全区。安全区这个参数一定要开,尤其在有底部操作栏的弹层里,否则在全面屏设备上按钮会被系统手势条压住。这个问题在模拟器上完全看不出来,只有真机才会暴露。
弹层里嵌套弹层是常见的坑。比如一个选择器弹层里再弹出一个确认框,后者的层级要盖住前者。uView 内部维护了一个z-index递增机制,正常情况下能处理好,但如果你的页面里有原生组件(小程序的textarea、video、map等),这些原生组件的层级永远在最上面,任何 CSS 层级都盖不住。解决方案有两个:一是把原生组件换成组件库提供的替代实现,二是给原生组件加fixed属性并在弹层出现时隐藏它。
<u-popup v-model="showPicker" mode="bottom" :safeAreaInsetBottom="true" round="16"> <view class="picker-body"> <!-- 弹层内容 --> </view> </u-popup>u-toast有个使用习惯问题:它需要配合u-toast组件的引用,如果不希望在每个页面都写一遍组件标签,可以用页面级的uni.$u.toast()这种全局方法调用。这个方法在绝大多数场景够用,只是没法做自定义图标和复杂内容。我的习惯是:简单提示用全局方法,需要图标、多行文案、带按钮的用组件实例。
3.4 主题色、字号与暗色模式的落地方式
主题定制这件事,最容易出问题的地方不是“怎么改”,而是“改了没生效”。uView 的主题变量定义在theme.scss里,你想改主色,正确的做法不是在页面里覆盖,而是在引入之前重新赋值。
/* uni.scss */ $u-primary: #3c6ef0; $u-warning: #f0a020; $u-error: #e34d59; @import 'uview-ui/theme.scss';注意赋值要在@import之前,SCSS 的变量是“后定义覆盖先定义”,顺序反了就不生效。这个顺序问题是新手最常犯的错,改完刷新没变化,以为是缓存,清缓存、重启编辑器折腾半小时,最后发现是两行代码写反了。
暗色模式需要单独考虑。uView 的变量体系是编译期的,做不了运行时切换,所以如果产品要求跟随系统深色模式,只能靠 CSS 变量在运行时覆盖,或者干脆维护两套变量。我的实际做法是:把品牌色、背景色、文字色抽成自己的 CSS 变量挂在根节点上,uView 组件内部能覆盖的地方用:deep()覆盖,覆盖不到的(比如图表颜色)通过 props 传入。这套方案不优雅但能用,比起为了暗色模式换组件库,成本低得多。
4. 样式冲突与跨端差异:真正消耗时间的部分
4.1 scoped 样式穿透的正确写法与失效场景
组件库用起来最磨人的不是 API,是“我想改它内部的一个元素,但 style 是 scoped 的”。Vue 提供深度选择器解决这个问题,uni-app 里写法有好几种,效果和兼容性不一样。
/* 推荐写法,uni-app 与 Vue3 都支持 */ :deep(.u-input__inner) { font-size: 28rpx; } /* 老写法,Vue2 项目里常见 */ ::v-deep .u-input__inner { font-size: 28rpx; } /* 更老的写法,部分编译器已不支持 */ /deep/ .u-input__inner { font-size: 28rpx; }写法的选择要看项目用的编译版本。我在一个 Vue2 老项目里用:deep()是编译不过的,只能退回::v-deep。反过来,在 Vue3 项目里::v-deep作为组合器使用时会有警告。统一的做法是看团队的技术基线,新项目直接上:deep()。
还有一个更隐蔽的失效场景:小程序端的组件样式隔离。uni-app 编译到小程序时,每个组件会被编译成自定义组件,默认情况下自定义组件的样式和外部是隔离的,父组件的深度选择器可能打不进去。解决办法是在组件的options里开启样式共享,或者使用addGlobalClass。如果改不动组件源码(比如是 npm 包),那就只能把需要覆盖的样式写在全局样式文件里,靠类名优先级去压。
注意:全局覆盖组件样式这件事要有节制。今天为了一处 3 像素的间距写一条全局覆盖,明天为了一处颜色再写一条,三个月后你会发现项目里躺着两百条全局样式覆盖,谁也不敢删。我现在的规矩是:能用 props 解决的绝不用样式覆盖,必须覆盖的写在该页面的 scoped 里,全局覆盖只允许出现在主题文件里并加注释说明原因。
4.2 rpx 与 px 混用踩出来的坑
uni-app 默认的尺寸单位是rpx,750 设计稿下 1px 等于 1rpx,换算简单。uView 的组件内部大量使用rpx,但有些属性是数字型的size,单位由全局配置决定。问题就出在这里:当你从设计稿量了一个 32px 的间距,顺手写成padding: 32px,在 iPhone 上看着正常,到小屏安卓上就偏大,因为px不随屏幕宽度缩放。
我的经验是把规则定死:布局尺寸一律rpx,字体在 24 到 34 之间用rpx,超过 40 的标题可以考虑用固定值,因为超大字号在小屏上缩放过多会显得很怪。边框线用1px,因为1rpx在部分设备上会被渲染成不可见的亚像素。这个混合策略不是拍脑袋定的,是长期在十几种机型上对比出来的结果。
全局单位配置也值得改一下。uView 支持通过配置修改内部组件的默认单位,如果你的设计体系是全rpx的,建议显式设一次,避免默认值和设计稿对不上:
// App.vue 的 onLaunch 里 uni.$u.setConfig({ config: { unit: 'rpx' } })4.3 H5 与小程序的行为差异清单
同一套代码编译到不同端,行为差异是必然的,这里列几个我在项目里实际遇到的,都是会直接影响功能的那种。
| 差异点 | H5 表现 | 小程序表现 | 处理方式 |
|---|---|---|---|
| 弹层内滚动 | 需要单独锁定 body 滚动 | 页面滚动需手动禁用 | 用组件库的遮罩滚动锁定,必要时加 catchtouchmove |
| 原生组件层级 | 无原生组件概念 | textarea/video 永远置顶 | 弹层出现时隐藏或改用替代组件 |
| 路由参数长度 | 基本无限制 | 有长度约束 | 大对象走全局状态或本地存储传递 |
| 文件上传 | 走表单或 fetch | 需用平台上传 API | 封装统一的上传方法,内部按端分支 |
| 字体图标 | 网络字体加载有闪烁 | 本地字体打包 | H5 端做字体预加载或内联 |
| 长按与滚动冲突 | 事件冒泡可控 | 长按容易吞掉滚动 | 明确事件绑定层级,避免嵌套长按 |
这张表不是让你背下来,是让你在遇到“为什么 H5 好的小程序不行”时有个排查方向。我自己的习惯是每次发版前在三个端各过一遍核心流程,重点看弹层、表单、列表加载这三块,因为这三块最容易出端差异。
5. 体积与性能:组件库用久了必然要面对的问题
5.1 easycom 的按需引入到底省了多少
很多人以为用了 easycom 就是按需引入,体积一定小。这个理解对了一半。easycom 确实做到了“模板里没写这个组件,就不打包它的 JS 和模板”,但它省不掉样式。如果把基础样式表全量引在App.vue里,那所有组件的 CSS 都会进包,这部分在小程序端是实打实的体积。
我做过一次粗略对比,一个只用了表单、按钮、弹层的项目,全量引样式和只引必要样式,在小程序主包上的差距是几十 KB 级别。听起来不多,但小程序主包有硬性上限,几十 KB 可能就是能不能塞下另一个页面的差别。优化办法是按需引入样式文件,只引自己用到的组件样式,代价是要维护一份清单,新增组件时记得补上。这个取舍看项目阶段:早期求快就全量,临近上限再动手拆。
5.2 打包体积的分析方法
不要凭感觉猜哪里大。uni-app 打包之后可以看产物目录,小程序端的common目录下是按 chunk 分的 JS,能直观看到哪个包最大。再细一点的做法是开构建分析,看依赖树里谁占了主要份额。我遇到过最夸张的一次,是某个工具库被两个依赖各引了一份不同的大版本,导致重复打包,光这一项就多出几百 KB。
排查顺序一般是:先看静态资源(图片、字体),再看第三方库,最后看自己的业务代码。图片往往是被低估的大头,一张没压缩的背景图能顶几十个组件。字体文件也一样,图标字体如果全量打包,动辄上百 KB,实际上项目里可能只用了二十个图标。
5.3 长列表与图片的渲染优化
列表卡顿这件事,在低端安卓机上特别明显。原因通常不是组件库的锅,而是渲染节点太多。一个列表项里嵌了五六层view,一百条数据就是五六百个节点,加上图片,滚动必然掉帧。
能落地的优化手段有这么几个。列表项结构尽量扁平,能用一层view加背景图解决的就不要嵌套三层;图片开懒加载并指定固定宽高,避免图片加载完成后触发重排;长列表用虚拟列表或者分页,不要一次性渲染几百条;v-for的key用业务主键而不是索引,索引作为 key 在数据变动时会导致整段重渲染。
<u-image :src="item.cover" width="200rpx" height="200rpx" mode="aspectFill" :lazyLoad="true" :fade="true" />u-image相比原生image标签多了加载态、错误态、圆角、淡入这些能力,代价是每个图片多几个节点。数据量大的列表里我会权衡一下,超过五十条的场景可能还是用原生image加自己写的占位更划算。
6. 问题排查:那些查了半天才发现原因的小毛病
6.1 组件不渲染、样式错乱的排查路径
这类问题占了日常调试的一大半,我整理了一条从快到慢的排查顺序,基本能覆盖九成情况。
先确认组件标签名是否拼写正确。uView 的组件名和目录名是强绑定的,u-form-item写成u-formitem不会报错,只是渲染不出来,因为在 easycom 规则下它去找一个不存在的路径,而 uni-app 对这种失败是静默处理的。
再确认pages.json里的 easycom 配置有没有被其他配置覆盖。有些项目在pages.json里同时配了easycom和usingComponents,两者冲突时以usingComponents为准,导致自定义规则失效。
然后看样式文件是否引入。基础样式没引,组件结构在但完全没样式,表现是“文字堆在一起”。主题变量没引,表现是用了变量的地方颜色变成默认值或者编译报错。
最后看编译产物。真机上出问题时,把编译后的小程序代码打开,搜一下组件名,看它到底有没有被打进包里。这一步能快速区分“代码问题”和“构建问题”,省下大量猜测时间。
6.2 表单校验失效的几种典型原因
校验不触发的原因就那么几个,但表现各不相同,值得单独说。
一是prop和字段名对不上,表现为完全没反应。二是rules里的trigger配的值和实际事件不匹配,比如只配了change但用户只是失焦没改内容,就不会校验。三是动态增删表单项之后规则没同步,u-form在初始化时收集了一次规则,后续新增的字段需要主动刷新,用表单实例的规则设置方法重新挂一次。四是在弹层里的表单,弹层首次渲染时表单可能还没挂载完成,这时候调用校验方法拿不到实例,需要等弹层打开之后再操作。
还有一种很隐蔽的:表单外层套了scroll-view,在某些平台上滚动容器会拦截事件,导致输入框的blur不触发,校验自然不执行。这种情况只能通过在外层加事件监听或者改用其他容器解决。
6.3 常见问题速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 图标显示为方块 | 字体文件未打包或路径错误 | 检查字体引入配置,确认打包产物含字体 |
| 弹层被内容盖住 | 页面内有原生组件 | 隐藏原生组件或改用组件库替代实现 |
| 表单提交无反应 | 校验未通过但无提示 | 检查 rules 与 prop 对应关系 |
| 列表触底不加载 | 容器未设高度 | 给滚动容器设置明确高度 |
| 按钮点击无响应 | 被透明遮罩拦截 | 检查弹层是否未正确关闭 |
| 样式改了不生效 | 引入顺序错误或缓存 | 调整变量定义顺序,清理构建缓存 |
| 真机字体偏大 | 单位混用 | 统一改为 rpx |
| 页面白屏 | 组件注册失败 | 检查版本匹配与全局注册位置 |
这张表我一般会放在项目的维护文档里,新人接手时先看一遍,能省掉不少在群里问“为什么我的按钮点不动”的时间。
6.4 几个我踩过之后才记住的经验
第一个经验关于升级。组件库不要随手升小版本,尤其是跨了多个补丁版本的时候。我有一次为了修一个弹层的 bug 顺手升级,结果新版本改了某个组件的默认样式,项目里三十多个页面全部错位。后来定的规矩是:升级前先在分支上跑一遍回归页面,确认没有视觉变化再合。
第二个经验关于自定义。能通过 props 和插槽解决的需求,就不要去改组件源码。改源码当时爽,下次升级就是灾难,而且团队其他人根本不知道你改过。实在要改,就在项目里包一层自己的组件,把定制逻辑放在外层,这样组件库本身保持干净。
第三个经验关于文档。组件库的在线文档有时候和实际版本有细微出入,尤其是参数的默认值。判断默认值最靠谱的方式是直接看源码里的props定义,比查文档快也准。这个习惯让我少走了很多弯路,因为文档更新总是滞后的。
第四个经验关于团队约定。组件库用得好不好,很大程度上不取决于库本身,而取决于团队有没有约定。前缀怎么写、主题变量在哪改、样式能不能覆盖、单位用哪个,这几条定下来,代码风格就统一了。定不下来,半年后项目里会同时存在三种写法,维护成本翻倍。
最后分享一个我常用来判断“该不该用组件库组件”的小方法:如果这个交互涉及超过三个状态(默认、悬停、禁用、加载、错误),或者涉及平台差异,就用组件库的;如果只是一个静态展示的方块,自己写三个view加样式反而更快更可控。组件库不是越多用越好,用在对的地方才是。