Skip to content

复习与微信小程序不同设备适配(Day2 1/2) ​

本页对应 day2,共 5 段视频,约 1 小时 38 分。

学习目标 ​

  • 能够从零创建微信小程序项目骨架:正确创建 app.js、app.json、app.wxss,注册首页、分类、购物车、我的 4 个页面并配置路由与 tabBar(含图标和选中图标),启动后能正确显示 4 个底部导航项。
  • 能够使用 swiper、swiper-item、image 与 Flex 布局完成首页轮播和商品分类静态结构,并通过作用域类名及条件类名实现分类项首行与后续行不同尺寸的样式效果。
  • 能够说明并演示 wx.request 的基本用法:发起 HTTPS 请求,正确处理 success、fail、complete 回调,并从 res.data 中读取接口返回数据。
  • 能够完成网络请求二次封装:在 utils/request.js 中基于 Promise 封装 wx.request,统一基础路径、请求方式与参数,控制 loading 显示与隐藏,并返回可 then/await 的请求结果。
  • 能够建立 api/index.js 统一管理接口,封装获取首页 banner 与分类数据的函数,并在首页 onLoad 中调用接口、通过 setData 将结果保存到页面 data。
  • 能够使用 wx:for、wx:key 和插值表达式将 banner 与分类数据渲染到轮播图和分类列表,并在模拟器或真机中验证图片、文字及页面展示正常。

知识点 ​

一、复习:应用级文件与页面级文件 ​

为什么需要:小程序项目不是只写一个页面,而是“全局应用 + 多个页面”的结构。先把文件职责分清,后面搭项目才不会乱。

怎么想:应用级文件管全局:app.js 创建应用实例,app.json 配导航栏、路由、tabBar,app.wxss 写全局样式;页面级文件管局部:js 写逻辑,wxml 写结构,wxss 写样式,json 写页面配置。

怎么做:新建项目后先认识这四类页面文件,再删掉示例默认内容,从零搭骨架。app.json 是 JSON,键必须用双引号。

易错点:app.js 不是 App.js;app.json 不要写注释、不要用单引号;页面样式不要全塞进 app.wxss,否则容易全局污染。

二、项目起步:先跑示例,再配 tabBar ​

为什么需要:先看已写好的示例项目,能明确首页轮播、分类、购物车、个人中心等业务,再决定先做哪块。转写稿里明确说“以它为原型去开发”,今天先搞定首页。

怎么想:整体是 4 个 tabBar 页面:首页、分类、购物车、我的。tabBar 至少 2 个、最多 5 个,所以 4 个合法。先建页面,再配路由和图标。

怎么做:右键新建页面,得到 index/category/cart/my 四套文件;在 app.json 配 tabBar.list,每项包含 pagePath、text、iconPath、selectedIconPath。页面路由要以 / 开头,图标按规范准备。

json
{
  "tabBar": {
    "list": [
      { "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tabbar/1.png", "selectedIconPath": "static/tabbar/1-2.png" },
      { "pagePath": "pages/category/category", "text": "分类", "iconPath": "static/tabbar/2.png", "selectedIconPath": "static/tabbar/2-2.png" },
      { "pagePath": "pages/cart/cart", "text": "购物车", "iconPath": "static/tabbar/3.png", "selectedIconPath": "static/tabbar/3-2.png" },
      { "pagePath": "pages/my/my", "text": "我的", "iconPath": "static/tabbar/4.png", "selectedIconPath": "static/tabbar/4-2.png" }
    ]
  }
}

易错点:pagePath 不要写成 /pages/... 开头;图标建议 81×81、体积不超 40KB;配完要刷新看底部是否出现。

三、首页轮播静态:先搭结构,再调属性 ​

为什么需要:首页第一屏是轮播图,先静态搭出来,才能接接口数据。

怎么想:外层 swiper 控制轮播,里面每个 swiper-item 放一张图。先用三条静态数据跑通,再换成接口数据。

怎么做:写 swiper,加 indicator-dots、autoplay、interval="2000"、circular;样式上轮播宽 750rpx、高约 300rpx。

xml
<swiper class="swiper" indicator-dots autoplay interval="2000" circular>
  <swiper-item wx:for="{{bannerArr}}" wx:key="id">
    <image src="{{item.imageUrl}}" class="banner"></image>
  </swiper-item>
</swiper>

易错点:swiper-item 必须直接放在 swiper 里;wx:for 要加 wx:key;先检查图片路径和高度,不然容易“看起来没渲染”。

四、分类静态:flex 两行与 active 高亮 ​

为什么需要:分类是 10 个入口,要两行排列,且后 5 个样式不同,需要用 flex 和条件类名。

怎么想:大盒子 category 开 display:flex、flex-wrap:wrap;每个 item 宽 150rpx,5 个一行;每个 item 内部再纵向居中。第 5 个之后加 active,用于改图片大小和文字样式。

怎么做:

xml
<view class="category">
  <view class="item {{index > 4 ? 'active' : ''}}" wx:for="{{categoryArr}}" wx:key="id">
    <image src="{{item.imageUrl}}" class="image"></image>
    <text class="title">{{item.name}}</text>
  </view>
</view>
css
.category { display: flex; flex-wrap: wrap; margin-top: 20rpx; }
.category .item { width: 150rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; margin: 10rpx 0; }
.category .image { width: 88rpx; height: 88rpx; }
.category .active .image { width: 40rpx; height: 40rpx; }
.category .title { margin-top: 10rpx; font-size: 24rpx; }

易错点:类名要由外到内写,避免和别的页面重名;active 判断用 index > 4,别写成 >= 4;改完刷新看控制台是否有 wx:key 警告。

五、wx.request:小程序里不能用 axios ​

为什么需要:页面数据来自服务器,必须发网络请求。转写稿强调:小程序里用不了 axios,要用微信提供的 wx.request。

怎么想:先在 onLoad 里发一次请求,打印 res 看结构;接口成功走 success,失败走 fail,无论成败都走 complete。

怎么做:

js
wx.request({
  url: '接口地址',
  method: 'GET',
  success(res) {
    console.log(res)
  },
  fail(err) {
    console.log(err)
  },
  complete() {
    console.log('无论成功失败都执行')
  }
})

易错点:小程序请求默认要求 HTTPS;本地调试 HTTP 可在开发者工具勾“不校验合法域名”,但上线仍要 HTTPS;wx.request 是异步的,不能在后面立刻拿结果。

六、二次封装 request:Promise + loading ​

为什么需要:每个页面都写一遍 wx.request 太重复,加载提示、成功失败处理也不统一。转写稿用 Promise 解决异步结果返回问题。

怎么想:在 utils/request.js 暴露一个函数,统一拼基础路径、统一显示/隐藏 loading,成功 resolve、失败 reject。

怎么做:

js
const baseURL = '服务器基础路径'

export default function request({ url, method = 'GET', data = {} }) {
  wx.showLoading({ title: '加载中' })
  return new Promise((resolve, reject) => {
    wx.request({
      url: baseURL + url,
      method,
      data,
      success(res) {
        resolve(res.data)
      },
      fail(err) {
        reject(err)
      },
      complete() {
        wx.hideLoading()
      }
    })
  })
}

易错点:resolve 返回的是接口响应对象,页面里要按接口结构取 result.data;hideLoading 要放 complete,否则失败时 loading 不消失;不要忘了 return new Promise。

七、统一 API 与首页数据渲染 ​

为什么需要:接口多时,地址散落在页面里,服务器地址一换就要到处改。转写稿要求把接口统一放 api/index.js。

怎么做:

js
import request from '../utils/request'

export const getBanner = () => request({ url: '/banner', method: 'GET' })
export const getCategory = () => request({ url: '/category', method: 'GET' })

页面里:

js
import { getBanner, getCategory } from '../../api/index'

Page({
  data: { bannerArr: [], categoryArr: [] },
  onLoad() {
    this.getBannerData()
    this.getCategoryData()
  },
  async getBannerData() {
    const result = await getBanner()
    if (result.code === 200) {
      this.setData({ bannerArr: result.data })
    }
  },
  async getCategoryData() {
    const result = await getCategory()
    if (result.code === 200) {
      this.setData({ categoryArr: result.data })
    }
  }
})

易错点:页面不要直接写接口路径;wx:key 用 id;调试时先打印 result,确认 code 和 data 结构;最后一定真机预览,转写稿特别提醒模拟器和真机、iOS 与安卓样式可能有差异。

为什么小程序必须做设备适配 ​

本节笔记的核心只有一句:rpx 是微信小程序新增的响应式单位,并给出换算关系 1px = 2rpx。但真正写项目时,不能只背这句话,要先想清楚“为什么需要它”。

为什么需要:手机屏幕宽度并不一样。如果首页轮播图写死 width: 375px,在窄屏上可能撑不满,在宽屏上又可能留白;分类图标、字号也会忽大忽小。转写稿里搭建首页时反复用 750rpx、150rpx、88rpx,就是在解决这个问题。

怎么想:小程序规定屏幕宽度为 750rpx。所以在 iPhone 6/7/8 这类 375px 逻辑宽度的设备上,750rpx = 375px,于是 1px = 2rpx。注意,这个等式是在这个基准下成立,不是所有设备永远不变。

怎么做:保留笔记里的代码,先记住单位关系:

text
rpx: 微信小程序新增响应式单位
1px = 2rpx

实际写样式时,优先用 rpx 描述与屏幕宽度相关的尺寸,例如轮播、分类项、图片宽高。

易错点:把 1px = 2rpx 当成“所有设计稿都直接乘 2”。要先确认设计稿基准:如果设计稿按 375px 宽画,1px 标注可换 2rpx;如果按 750px 宽画,标注的 px 往往可直接写 rpx。基准没确认,尺寸一定乱。

rpx 怎么落到首页轮播上 ​

为什么需要:轮播图通常要横向铺满屏幕,用固定 px 不适合多设备。

怎么想:转写稿中首页轮播写的是 width: 750rpx,高度写 300rpx。因为小程序屏幕宽就是 750rpx,所以宽度写 750rpx 就能铺满;高度按设计比例给 rpx。

怎么做:可以写成类似:

css
.home {
  width: 750rpx;
}
.swiper {
  width: 750rpx;
  height: 300rpx;
}

调试时,转写稿特别提醒:开发者工具设备最好调成 iPhone 6/7/8,缩放保持 100%,不要调成 85% 去看,否则视觉判断会偏。

易错点:只写 width: 100% 却忘记高度和图片比例;或者轮播容器写 750rpx,内部图片仍写死 px,导致图片与容器不匹配。轮播的指示器、自动轮播等属性属于组件配置,不要和尺寸适配混在一起。

分类区:用 rpx 算出一行几个 ​

为什么需要:首页分类通常一行五个,每个分类有图片和文字。不同屏幕下如果每个分类写固定 px,很容易一行放不下或留大缝。

怎么想:转写稿里分类区每个 item 写 150rpx,因为 150rpx × 5 = 750rpx,正好等于屏幕宽,所以一行五个。图片写 88rpx × 88rpx,文字与图片再用 flex 竖向排列并居中。

怎么做:

css
.category {
  display: flex;
  flex-wrap: wrap;
}
.category .item {
  width: 150rpx;
}
.category .item image {
  width: 88rpx;
  height: 88rpx;
}

转写稿中还提到:前五个和后五个样式可能不同,后五个可通过动态类名控制,例如给大于第四个索引的项加 active 类,再在 active 下调整图片宽度或字号。这里的重点仍是:尺寸变化用 rpx 控制,而不是在页面里到处写 px。

易错点:分类项宽度相加超过 750rpx,导致换行错位;忘记 flex-wrap: wrap,十个分类挤成一行;图片和文字没有用 flex 居中,看起来偏上偏下。还有一点:类名最好带页面前缀,例如 category 下的 item,避免不同页面样式重名。

调试与真机复核 ​

为什么需要:模拟器只能看个大概,转写稿多次强调“写完一定要真机测试”。有些 API 和真机表现,模拟器并不完全一致,支付等能力更是如此。

怎么想:先在开发者工具里选 iPhone 6/7/8、缩放 100%,检查 750rpx 轮播是否铺满、五个 150rpx 分类是否正好一行、88rpx 图片是否清晰不变形;再用真机看字号、间距和点击区域。

怎么做:按“模拟器看结构 → 真机看实际尺寸 → 回到样式调 rpx”的顺序排查。若发现分类换行,先算 item 宽度总和;若发现图片模糊,检查原图尺寸和 image 宽高是否匹配;若发现整体偏大偏小,先确认设计稿基准和设备宽度。

易错点:只在模拟器 85% 缩放下看效果;把 wx.request 的异步数据渲染问题误认为样式适配问题;真机上安卓和苹果表现有差异却不复核。转写稿里还提到 tabBar、flex、wx.request、complete、Promise 等词,注意正确拼写,别把适配问题和请求封装问题混为一谈。

本节小结 ​

rpx 的用法不复杂,关键是先确认设计稿基准,再记住小程序屏幕宽为 750rpx;在 iPhone 6/7/8 的 375px 逻辑宽度下,才有笔记里的 1px = 2rpx。写首页时,轮播用 750rpx 铺满,分类每项 150rpx 保证一行五个,图片用 88rpx 控制大小,最后一定用 100% 缩放和真机复核。这样既保留了笔记里的单位结论,也能把它落到真实页面尺寸上。

先明确:项目里的图标从哪里来 ​

为什么需要:底部 tabBar 是用户进入首页、分类、购物车、我的四个主入口。只有文字时识别慢,配上图标后更符合小程序习惯。转写稿里反复说“图标从写好那块直接拿过来”,说明项目里并不是临时上网找图,而是把阿里图标库里的图标下载成图片,放进项目本地使用。

怎么想:先区分“页面内容图标”和“tabBar 图标”。tabBar 是原生组件,iconPath、selectedIconPath 只认本地图片路径,不认字体图标,也不认网络图片。所以阿里图标库在这里的角色是“图标资源来源”,不是直接在小程序里引线上字体。转写稿中“住本地的”应纠正为“用本地的”。

怎么做:在阿里图标库选好图标,下载为 png,准备“未选中”和“选中”两套,例如 home.png、home-active.png;统一放进 static/tabbar/。四个 tab 共 8 张图,命名要能对应页面。

易错点:不要直接把阿里图标库的在线字体链接塞进 tabBar;不要用中文文件名;选中态和未选中态要成对准备,否则切换时没有高亮变化。

从阿里图标库到 static/tabbar 目录 ​

为什么需要:tabBar 图标会被所有页面共用,属于静态资源。转写稿里说“把这个文件夹直接扔到我们的这里”,就是为了让路径稳定、所有页面都能引用。

怎么想:项目根目录下建 static/tabbar,把首页、分类、购物车、我的四组图标放进去。转写稿里“推过腕”“推播半”“黑猫”都是 tabBar 的口误,实际配置项是 tabBar。

怎么做:目录可以整理成:

text
static/
  tabbar/
    home.png
    home-active.png
    category.png
    category-active.png
    cart.png
    cart-active.png
    mine.png
    mine-active.png

易错点:转写稿说“81乘81嘛,不能超出40”,这里要纠正为:微信小程序 tabBar 图标建议尺寸 81px × 81px,单个图片文件不超过 40KB。不要把“40KB”误听成“40px”。图片过大或尺寸离谱时,可能出现不显示、模糊或变形。

在 app.json 里配置 tabBar 图标 ​

为什么需要:图标放进项目还不够,必须让小程序知道每个 tab 用哪张未选中图、哪张选中图、跳到哪个页面。

怎么想:tabBar.list 里一项对应一个页面。pagePath 写页面路径,text 写文字,iconPath 写未选中图,selectedIconPath 写选中图。转写稿强调“页面路由不能以杠开头”,这里要纠正:pagePath 是页面路径,不要写成 /pages/...;而图片路径在项目里可采用以 / 开头的根路径,如 /static/tabbar/home.png。

怎么做:在 app.json 中配置,页面路径按你项目实际目录替换:

json
{
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/home/home",
        "text": "首页",
        "iconPath": "/static/tabbar/home.png",
        "selectedIconPath": "/static/tabbar/home-active.png"
      },
      {
        "pagePath": "pages/category/category",
        "text": "分类",
        "iconPath": "/static/tabbar/category.png",
        "selectedIconPath": "/static/tabbar/category-active.png"
      },
      {
        "pagePath": "pages/cart/cart",
        "text": "购物车",
        "iconPath": "/static/tabbar/cart.png",
        "selectedIconPath": "/static/tabbar/cart-active.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "/static/tabbar/mine.png",
        "selectedIconPath": "/static/tabbar/mine-active.png"
      }
    ]
  }
}

易错点:pagePath 对应页面必须先在 pages 中注册;tabBar 至少 2 个、最多 5 个;iconPath 不要写 http:// 网络地址;文件名大小写要和磁盘一致。转写稿说“写完一个之后会出来吗?不会,至少两个”,说的就是 tabBar 数量限制。

调试:图标不显示时怎么查 ​

为什么需要:配置完刷新后,底部应出现四个 tab,切换时选中图标变高亮。如果没出现,通常是路径、图片大小或页面注册问题。

怎么想:按“先看控制台报错 → 再看图片路径 → 再看图片大小 → 最后看 pagePath 是否注册”的顺序排查。转写稿里配置后“刷新去看一下整体 tabBar 是不是配好了”,就是最直接的验证。

怎么做:改完 app.json 重新编译;在开发者工具里切换四个 tab,观察 selectedIconPath 是否生效;再用真机预览确认图标清晰度。转写稿也强调“写完之后自己一定要真机测试”,因为部分表现模拟器不完全一致。

易错点:只配了 iconPath 没配 selectedIconPath,切换无高亮;图片超过 40KB 或路径写错,图标空白;pagePath 多写 / 导致找不到页面;把 tabBar 图标和首页轮播、分类图片混在一起管理,后期替换容易漏改。

列表渲染 wx:for:为什么需要 ​

本节笔记的核心是两条小程序指令:wx:for 列表渲染,以及 wx:if / wx:else / hidden 显示与隐藏。转写稿里项目首页有轮播图、商品分类,分类一次要渲染约 10 项,轮播图有 3 张。如果每张图、每个分类都手写一遍结构,代码会又长又难维护;接口数据回来后也无法自动对应页面。因此需要“数据驱动”:数据是数组,模板只写一份重复结构,用 wx:for 遍历。转写稿中把“遍历”说成“便利”,这里要纠正为“遍历”。

列表渲染 wx:for:怎么想、怎么做 ​

先判断数据是不是数组:轮播图 bannerAR、分类 categoryAR 都是接口返回后存进 data 的数组。怎么想?页面只关心“每一项长什么样”,所以模板里写一项的结构,再用 wx:for 循环。怎么做?最小示例如下:

html
<!-- 轮播图:bannerAR 为数组 -->
<swiper indicator-dots autoplay interval="2000" circular>
  <swiper-item wx:for="{{bannerAR}}" wx:key="id">
    <image src="{{item.imageUrl}}" />
  </swiper-item>
</swiper>

<!-- 分类:categoryAR 为数组 -->
<view class="category">
  <view class="item" wx:for="{{categoryAR}}" wx:key="id">
    <image src="{{item.imageUrl}}" />
    <text>{{item.name}}</text>
  </view>
</view>

转写稿的调试过程是:先 console.log(res) 看接口返回,发现真正数据在 res.data,再判断 res.code === 200 后 setData,例如把轮播数据存入 bannerAR、分类数据存入 categoryAR;模板里再用 wx:for 渲染。wx:key 要写唯一字段,转写稿中使用 id,不要图省事乱写,否则会有警告并影响更新。

列表渲染易错点 ​

第一,wx:for 的数组名要和 data 中一致,bannerAR 写成 banner 就不会渲染。第二,默认项变量是 item,索引是 index;嵌套循环要改名,避免内外层冲突。第三,wx:key 不要用没有唯一性的值;接口有 id 就用 id。第四,图片不显示先查字段名,转写稿里类似 imageUrl 的字段要按接口文档写,不能凭感觉。第五,数据回来后必须 setData,只 console.log 不会更新页面。

显示与隐藏 wx:if、wx:else、hidden:为什么需要 ​

列表解决“重复渲染”,条件指令解决“某块内容要不要出现”。例如接口未回来时不显示列表、显示提示;有数据时显示内容;登录前后展示不同区域。wx:if 是根据条件决定是否渲染,wx:else 必须紧跟 wx:if;hidden 是元素已渲染但隐藏。转写稿中首页分类有“前 5 个样式、后 5 个样式不同”的处理,那里是通过动态类名控制样式,不是 wx:if,不要混为一谈。

显示与隐藏:怎么做 ​

基本写法:

html
<view wx:if="{{categoryAR.length > 0}}">
  有分类数据
</view>
<view wx:else>
  暂无分类数据
</view>

<!-- hidden:true 隐藏,false 显示 -->
<view hidden="{{loading}}">内容区域</view>

怎么想?如果切换频繁、只是视觉隐藏,用 hidden;如果条件不成立时完全不想创建节点,用 wx:if。wx:else 不能中间插别的节点。调试时先打印条件值,确认是 true/false,再检查 wx:else 是否紧跟 wx:if。

易错点与项目落地 ​

wx:if 和 hidden 语义不同:前者是“渲染不渲染”,后者是“显示不显示”。hidden 的值要传布尔值,写成字符串可能不符合预期。wx:else 必须和 wx:if 相邻。列表渲染后如果样式不对,先查数组是否为空、wx:key 是否重复、字段名是否正确;转写稿强调写完要真机测试,因为模拟器和真机在部分 API、样式上会有差异。把 wx:for 用在轮播和分类上,把 wx:if / wx:else / hidden 用在数据有无、区域显隐上,页面才能从静态结构变成由接口数据驱动的页面。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

1. 项目骨架:app.json(tabBar + window)与 app.wxss(全局样式) ​

app.json

json
{
  "pages": [
    "pages/index/index",
    "pages/category/category",
    "pages/shopcart/shopcart",
    "pages/mine/mine"
  ],
  "window": {
    "navigationBarTitleText": "木想花坊",
    "navigationBarTextStyle": "white",
    "navigationBarBackgroundColor": "#f39c3c",
    "backgroundColor": "#f8f8f8"
  },
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#f39c3c",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/tabbar/1.png",
        "selectedIconPath": "static/tabbar/1-2.png"
      },
      {
        "pagePath": "pages/category/category",
        "text": "分类",
        "iconPath": "static/tabbar/2.png",
        "selectedIconPath": "static/tabbar/2-2.png"
      },
      {
        "pagePath": "pages/shopcart/shopcart",
        "text": "购物车",
        "iconPath": "static/tabbar/3.png",
        "selectedIconPath": "static/tabbar/3-2.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "static/tabbar/4.png",
        "selectedIconPath": "static/tabbar/4-2.png"
      }
    ]
  },
  "sitemapLocation": "sitemap.json"
}

app.wxss

css
page {
  width: 100%;
  height: 100%;
}

/* 全局弹性布局,哪个页面要就直接加类名 */
.flex {
  display: flex;
}

.flex-col {
  display: flex;
  flex-direction: column;
}

app.js

js
App({
  onLaunch() {},
  onShow() {},
  onHide() {},
  onError() {}
})

关键行 / 易错点

  • app.json、app.wxss、app.js 文件名必须小写,是应用级文件:app.js 创建应用实例,app.json 管导航栏和路由,app.wxss 是全局样式。
  • app.json 是严格 JSON:key 必须双引号、不能写单引号、不能写注释、最后一项不能带逗号。
  • pages 数组第一项就是小程序打开时的默认页;pagePath 不能以 / 开头,且必须是已创建的页面路径,否则 tabBar 点不动。
  • tabBar 的 list 至少 2 个、最多 5 个;图标建议 81×81、不超过 40kb,不支持网络图片;iconPath/selectedIconPath 成对写,只写一个不会报错但不显示。
  • navigationBarTextStyle 只能是 white / black,写别的颜色值无效;背景色是十六进制。
  • 全局样式 page { width:100%; height:100% } 是页面根节点的宽高,app.wxss 里的 .flex 任何页面都能直接用,不用每个页面重复写。

2. 首页轮播图 + 商品分类静态搭建(wxml + wxss + js) ​

pages/index/index.wxml

html
<view class="home">
  <!-- 轮播图 -->
  <swiper
    class="swiper"
    indicator-dots
    indicator-color="rgba(255,255,255,0.6)"
    indicator-active-color="#ff6699"
    autoplay
    interval="2000"
    circular
  >
    <swiper-item wx:for="{{bannerArr}}" wx:key="id">
      <image class="banner" src="{{item.imageUrl}}" mode="widthFix"></image>
    </swiper-item>
  </swiper>

  <!-- 分类 -->
  <view class="category">
    <view
      class="item {{index > 4 ? 'active' : ''}}"
      wx:for="{{categoryArr}}"
      wx:key="id"
    >
      <image class="image" src="{{item.imageUrl}}" mode="aspectFit"></image>
      <text class="title">{{item.name}}</text>
    </view>
  </view>
</view>

pages/index/index.wxss

css
/* 页面根节点:宽度跟设备一边宽 */
.home {
  width: 750rpx;
}

/* 轮播图 */
.home .swiper {
  width: 750rpx;
  height: 300rpx;
}

.home .banner {
  width: 750rpx;
}

/* 分类大盒子 */
.category {
  display: flex;
  flex-wrap: wrap;
  margin-top: 20rpx;
}

.category .item {
  width: 150rpx;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  margin: 10rpx 0;
}

.category .image {
  width: 88rpx;
  height: 88rpx;
}

.category .title {
  margin-top: 10rpx;
  font-size: 24rpx;
  font-weight: 700;
}

/* 第二行 5 个:图标更小、字更大 */
.category .active .image {
  width: 40rpx;
  height: 40rpx;
}

.category .active .title {
  font-size: 32rpx;
}

pages/index/index.js(先用死数据验证静态)

js
Page({
  data: {
    bannerArr: [
      { id: 1, imageUrl: '/static/images/banner1.png' },
      { id: 2, imageUrl: '/static/images/banner2.png' },
      { id: 3, imageUrl: '/static/images/banner3.png' }
    ],
    categoryArr: [
      { id: 1, name: '爱礼精选', imageUrl: '/static/images/cate1.png' },
      { id: 2, name: '分类2', imageUrl: '/static/images/cate2.png' },
      { id: 3, name: '分类3', imageUrl: '/static/images/cate3.png' },
      { id: 4, name: '分类4', imageUrl: '/static/images/cate4.png' },
      { id: 5, name: '分类5', imageUrl: '/static/images/cate5.png' },
      { id: 6, name: '分类6', imageUrl: '/static/images/cate6.png' },
      { id: 7, name: '分类7', imageUrl: '/static/images/cate7.png' },
      { id: 8, name: '分类8', imageUrl: '/static/images/cate8.png' },
      { id: 9, name: '分类9', imageUrl: '/static/images/cate9.png' },
      { id: 10, name: '分类10', imageUrl: '/static/images/cate10.png' }
    ]
  }
})

关键行 / 易错点

  • 单位用 rpx:设计稿 750px 宽 = 750rpx,即 1px = 2rpx,自动适配不同机型(笔记第二条)。
  • swiper 里只能放 swiper-item,内容写在 swiper-item 内部;autoplay 默认 5000ms,用 interval="2000" 改 2 秒;要无缝滚动必须加 circular。
  • wx:for 一定要写 wx:key="id"(写属性名,不写 {{item.id}}),否则控制台警告、列表更新出错。
  • 一行 5 个:750 ÷ 5 = 150rpx,并且 .category 必须 flex-wrap: wrap 才会换行,不然会挤成一行。
  • .item 自身也要 display:flex; flex-direction:column; justify-content:center; align-items:center,图片和文字才能上下居中。
  • 类名从外到内写(.category .item .image),避免和其他页面同名样式冲突;.active 用三元表达式 {{index > 4 ? 'active' : ''}} 动态加,index 从 0 开始,大于 4 就是第 6 个开始。
  • image 不写宽高会按原图撑开,轮播图用 mode="widthFix",小图标用 aspectFit 保持比例。

3. 网络请求二次封装(utils/request.js)+ 接口统一管理(api/index.js)+ 首页调用 ​

utils/request.js

js
// 所有请求的公共基础路径,只在这里改一次
const BASE_URL = 'https://你的服务器域名'

// 默认暴露一个函数,页面/接口层直接 import 使用
export default function request({ url, method = 'get', data = {} }) {
  // 发请求之前先弹出加载提示
  wx.showLoading({
    title: '加载中',
    mask: true
  })

  // 必须 return Promise,否则页面拿不到异步结果
  return new Promise((resolve, reject) => {
    wx.request({
      url: BASE_URL + url,   // 基础路径 + 接口路径
      method,                // get / post / put / delete
      data,                  // 请求携带的参数
      success: (res) => {
        // res.data 才是服务器真正返回的数据
        resolve(res.data)
      },
      fail: (err) => {
        reject(err)
      },
      complete: () => {
        // 成功失败都要关闭 loading,否则失败时一直转圈
        wx.hideLoading()
      }
    })
  })
}

api/index.js(统一管理接口地址)

js
import request from '../utils/request'

// 首页轮播图
export const reqBanner = () => {
  return request({
    url: '/banner',
    method: 'get'
  })
}

// 首页一级分类
export const reqCategory = () => {
  return request({
    url: '/category',
    method: 'get'
  })
}

pages/index/index.js(页面里调用)

js
import { reqBanner, reqCategory } from '../../api/index'

Page({
  data: {
    bannerArr: [],
    categoryArr: []
  },

  onLoad() {
    // 生命周期里只调方法,不直接写 wx.request
    this.getBanner()
    this.getCategory()
  },

  // 获取首页轮播数据
  async getBanner() {
    const result = await reqBanner()
    if (result.code === 200) {
      this.setData({
        bannerArr: result.data
      })
    }
  },

  // 获取首页分类数据
  async getCategory() {
    const result = await reqCategory()
    if (result.code === 200) {
      this.setData({
        categoryArr: result.data
      })
    }
  }
})

关键行 / 易错点

  • 小程序里用不了 axios,只能用平台能力 wx.request;它默认发 HTTPS,本地调试 HTTP 接口要在开发者工具「详情 → 本地设置」勾选「不校验合法域名」。
  • wx.request 是回调式写法,容易回调地狱,所以用 new Promise 包一层;一定要 return 这个 Promise,否则页面里 await 拿到的是 undefined。
  • 成功回调里 resolve(res.data),注意是 res.data(服务器返回体),不是 res;页面里判断 result.code === 200 再 setData。
  • wx.showLoading 必须配 wx.hideLoading,且写在 complete 里 —— 写进 success 的话,请求失败 loading 永远不消失。
  • 基础路径 BASE_URL 只写在 request.js 一处,接口路径只写在 api/index.js 一处,页面里不出现任何 URL,以后换服务器只改一个地方。
  • 页面里用 await 的函数必须声明 async;不要在 onLoad 上直接写 async,单独封装方法再在生命周期里调用。
  • import 的路径是相对当前文件:api/index.js 引 ../utils/request,页面引 ../../api/index,层级写错会直接报模块找不到。
  • 封装完之后,真实数据替换第二段里的死数据即可,wxml 的 wx:for、wx:key、{{item.imageUrl}}、{{item.name}} 一个字都不用改。

常见错误 ​

  1. 应用级文件名写成 App.js / App.json 现象:编译后提示找不到 app.json,或者 window、tabBar 配置完全不生效。 原因:沿用了“类名首字母大写”的习惯;Windows 不区分大小写会掩盖问题,真机和上传后就暴露。 修法:应用级三件套固定全小写:app.js、app.json、app.wxss,且必须放在项目根目录。

  2. tabBar 图标不显示,只剩文字 现象:底部导航只有文案,图标区域空白或控制台报错。 原因:图片体积超过 40KB,或尺寸不是 81×81。 修法:设计稿按 81×81 导出并压缩;选中态单独出一张(如 home-active.png),iconPath / selectedIconPath 分别指向。

  3. tabBar 的 pagePath 写错 现象:点 tab 无反应或白屏。 原因:写成了绝对路径 /pages/index/index,或目录层级漏写,或该页面没在 pages 数组里注册。 修法:pagePath 不带前导斜杠,值必须是 pages/xxx/xxx 且与 pages 数组中的条目完全一致。

  4. 新建页面后 pages 没注册 现象:编译报“页面不存在”,或页面能建但访问不到。 原因:部分版本的开发者工具不会自动往 app.json 的 pages 里补路径。 修法:手动补,且把首页放在数组第一项(决定启动页);tabBar 的四个页面顺序与数组保持一致,便于排查。

  5. swiper 不轮播、三张图叠在一起 现象:内容不切换,或只显示第一张。 原因:swiper 的直接子元素写成了 view 而不是 swiper-item。 修法:swiper 下只能直接放 swiper-item,样式宽高写在 swiper 和 swiper-item 上,图片 width:100%。

  6. wx:for 不写 wx:key 现象:控制台持续警告,列表数据更新后渲染错乱。 原因:列表渲染缺少唯一标识。 修法:wx:key="id"(写字段名,不写 {{}});接口没有 id 时用 wx:key="*this"。

  7. wx.request 封装后页面拿到 undefined 现象:console.log(getBanner()) 打印 undefined,res.data 取不到。 原因:wx.request 是异步的,在 success 回调外面 return 拿不到结果。 修法:工具函数 return new Promise((resolve, reject) => { ... }),成功 resolve(res.data.data),失败 reject(err);页面侧用 async/await 或 .then() 接。同时别忘了在 API 层把 promise return 出去,少写一个 return 页面同样拿不到。

  8. loading 转圈卡死 现象:弱网或接口失败后,加载提示一直不消失。 原因:只在 success 里调了 wx.hideLoading()。 修法:success 和 fail 里都调,或统一放到 complete;并且 showLoading 要在发请求之前调用。

  9. 样式串页,改了首页分类页跟着变 现象:调整首页图标大小,另一个页面同名元素也变了。 原因:类名过于通用(.item、.swiper),又写在全局 app.wxss 里。 修法:类名带页面前缀,如 home-swiper、cate-item;只有真正的公共布局(flex 容器类)才提到全局。

  10. 真机请求全部失败 request:fail 现象:模拟器能拿到数据,手机上一条都请求不回来。 原因:接口是 http 协议,或域名未在小程序后台配置。 修法:优先 https;开发阶段在“详情 → 本地设置”勾选“不校验合法域名”,但上线前必须换成已备案的 https 域名。

练习 ​

  1. 骨架搭建验收:从空目录起手,建齐 app.js / app.json / app.wxss 与四个 tabBar 页面(首页、分类、购物车、我的),配置 window 标题与导航栏配色、tabBar 图标。验收标准:四个 tab 可互相切换,图标亮暗态正确,控制台零警告。

  2. 手写 request 二次封装:在 utils/request.js 中默认导出一个函数,接收 { url, method = 'GET', data },内部完成:拼基础路径 → showLoading → wx.request → 成功 resolve(res.data.data)、失败 reject → hideLoading。故意断开网络跑一次,确认 loading 会消失、catch 能捕获。

  3. API 层统一管理:新建 api/index.js,把“首页轮播”“首页分类”两个接口各封装成一个函数并导出,函数内部 return request(...),路径写死在 API 层。页面里只 import 函数名,不允许出现任何 url 字符串。

  4. 首页数据渲染:在 onLoad 中并行拿到 banner 与分类数据,setData 后渲染到 swiper 与分类网格。验收标准:wx:for 均带 wx:key;分类共 10 项、两行五列,前 5 个与后 5 个图标尺寸不同(用下标判断加类名实现)。

  5. 真机回归:用手机预览首页,重点检查三件事——轮播是否自动播放且指示点颜色正确、分类两行是否按 750rpx 均分不错位、切到弱网后 loading 是否正常消失。发现样式偏差记录到笔记,回头统一调。

原视频对照 ​

#视频时长
1小程序项目的起步20:47
2首页轮播与分类静态的搭建18:36
3wx.request网络请求语法查看17:53
4网络请求的二次封装获取首页banner图34:46
5首页分类数据的展示06:22

小结 ​

  1. 项目起步的第一件事不是写业务,而是把“应用级三件套 + 页面四件套 + tabBar 注册”这套骨架搭对,骨架错一处,后面所有页面都跟着报错。
  2. tabBar 是配置驱动的,页面必须先在 pages 里注册、再在 tabBar 里引用,两处路径必须字面一致,且首页要排在数组第一位。
  3. 尺寸一律用 rpx,按 750 设计稿切图;只要出现 px 与 rpx 混用,换机型必错位。
  4. 类名要带页面前缀,公共布局才进全局 app.wxss;这是避免多人协作时样式互相污染的最低成本手段。
  5. wx.request 是回调式异步 API,直接 return 一定拿不到值,必须用 Promise 包一层,让“异步结果”变成“可 await 的返回值”。
  6. 封装要封两层:底层 utils/request.js 管协议、loading、错误;上层 api/index.js 管路径与参数。页面只认函数名,服务器地址变更时只改一个文件。
  7. 请求的三个出口都要处理:成功渲染数据、失败给用户反馈、无论成败都要关掉 loading——漏掉任何一个,线上就是白屏或卡死。
  8. 模拟器通过不等于完成,涉及支付、授权、图标、样式的功能必须真机回归;iOS 与 Android 的表现差异要在真机上才能看到。