本节导读:上一章把语言层钉牢,本节进入界面的第一站:内置组件是编译器翻译的基本单位,选型即产物。本节过一遍高频组件的职责与端差异,重点是 button 的 open-type、image 的 mode、scroll-view 的滚动参数这三处端敏感区,末尾给出可直接用于评审的组件选型清单。
商城首页的设计稿摊开来,团队先把视觉块翻译成组件清单:轮播位用 swiper,金刚区是一排 view 包 image 与 text,商品瀑布流是 scroll-view 或者普通 view 加分页,悬浮购买栏用 fixed 定位的 view。这个翻译动作看起来平淡,却是界面跨端的第一道决策——内置组件在编译时会被改写成各端的目标标签,view 在 H5 端落成带样式约束的块级元素,在微信端落成原生 view 组件;你选的每一个组件,都在替你决定三端产物的形态。选型错误的代价不对称:用 view 能实现的地方换了 scroll-view,只是多了滚动语义;但用错 button 与 image 的属性,会出现"一端正常、另一端静默失效"的局面。
view 是布局容器,text 是文本载体。分界线值得认真画:只有 text 内的文本才能长按选中、才能被屏幕阅读器识别,直接把中文塞进 view 里在多数端也能显示,但复制、无障碍、超长省略号的行为都会打折扣。长文本截断的写法各端通用,但要求类名挂在 text 上:
<template> <view class="goods-row"> <view class="left"> <!-- 文本类样式写在 text 上,overflow 生效才有保证 --> <text class="goods-name">{{ goods.name }}</text> </view> <view class="right"> <text class="price">¥{{ goods.price }}</text> </view> </view> </template> <style> .goods-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } </style>
另一个容易忽略的差别是嵌套规则:text 里可以再放 text 做行内混排(一段话里部分高亮),view 里放 text 是标准姿势,反过来 text 里塞 view 在小程序端直接编译报错。商城的商品标题里要把"限时"两个字标红,用嵌套 text 一行搞定,不必拆结构。
image 是端差异最典型的媒体组件。不写 mode 时各端默认行为不一致,老项目里"图片变形"的 bug 多半源于此;工程公约应强制每个 image 显式声明 mode。常用模式分两类:缩放类(scaleToFill 拉伸铺满、aspectFit 完整 contain、aspectFill 裁剪 cover)与裁剪类(top、bottom、center 等九宫格定位)。商城的两处用法可以当模板记:
<template> <!-- 商品主图:等比裁剪填满,不变形不留白 --> <image class="cover" :src="goods.cover" mode="aspectFill" /> <!-- 分享水印:完整显示,允许留白 --> <image class="badge" src="/static/badge.png" mode="aspectFit" /> </template> <style> .cover { width: 340rpx; height: 340rpx; border-radius: 12rpx; } .badge { width: 120rpx; height: 60rpx; } </style>
懒加载属性 lazy-load 在微信端生效良好,H5 端编译为浏览器原生 loading 行为,App 端支持程度视基座版本而定——它是"端敏感属性"的又一例:写了不会错,但不能假设三端行为一致。此外 image 的 src 支持网络地址、base64 与 static 相对路径;网络图片要在小程序后台配置下载域名白名单,这件事不属于样式却经常在联调第一天爆发。
button 远不止"长得像按钮":它的 open-type 属性是各端开放能力的入口,也是本节最需要逐端核对的地方。

把矩阵变成代码,商城的"联系客服"按钮这样处理三端差异:
<template> <!-- #ifdef MP-WEIXIN --> <button class="cs" open-type="contact" @contact="onContact">联系客服</button> <!-- #endif --> <!-- #ifndef MP-WEIXIN --> <button class="cs" @tap="onFallbackContact">联系客服</button> <!-- #endif --> </template> <script> export default { methods: { onContact(e) { console.log('会话路径:', e.detail.path); }, onFallbackContact() { // H5 与 App 的降级:H5 唤起拨号,App 走内置客服页 // #ifdef H5 window.location.href = 'tel:4000000000'; // #endif // #ifdef APP-PLUS uni.navigateTo({ url: '/pages/service/index' }); // #endif } } }; </script>
注意这段代码里跨端分叉被圈在按钮这一层,页面其余部分对"客服"能力只面向 联系客服 这个交互本身——这是界面层使用条件编译的克制用法。
商品瀑布流选 scroll-view 时,三个参数决定成败:scroll-y 开纵向滚动(小程序端必须显式声明才滚)、配一个确定的高度(高度不定时滚动不会触发,用 flex 布局算高度或直接量节点)、lower-threshold 控制触底提前量让分页请求提前发起。下拉刷新优先用页面级的 onPullDownRefresh(第 4 章),scroll-view 内置的 refresher 系列属性适合"页面内局部刷新"的场景,比如订单列表嵌在 Tab 内容里时。
swiper 承载轮播,circular 无缝衔接、autoplay 加 interval 控制节奏; indicator-dots 的样式定制在小程序端受限,设计稿对圆点样式苛刻时,用普通 view 手写指示器反而省事。轮播里放 image 时记得同时给 swiper 与 image 定高,否则部分端会把高度算成零——这是新手轮播"消失"的头号原因。
组件选定了,下一节把它们封装成商城自己的组件库:props、事件与插槽的通信设计,以及动态组件在小程序端的边界。