经过前面四个章节,我们已经完成了 微信小程序 的学习。那么从这一章开始我们就进入 uniapp 的项目开发之中。
整个 uniapp 阶段我们会完成一个正式的项目 慕课热搜 ,以这个项目来作为 uniapp 学习阶段的的最终产出,同时通过这个项目来贯穿所有的 uniapp 知识点,可以让大家在学习的过程中不至于那么枯燥。
目前我们的项目已经上线了:
H5 :https://imooc.blog.lgdsunday.club/#/那么现在:
各种前置条件已经全部准备就绪,项目开发即可开始!
《慕课热搜》基于 uniapp 进行开发,关于 uniapp 的优点 -> 在【课程导学】阶段已经描述过了,如果你忘了,那么你可以回过头去看一下。
这一小节我们来点实在的,光知道它好,不行。因为不是你的,它再好,对你而言也是没啥用的……
那么怎么才能学会它呢?或者说它难学吗?这才是这一小节我们需要说明的内容。
想要学习 uniapp 那么需要有三个前置条件:
具备了这 3 个条件,学习 uni-app ,对你而言就是小菜一碟
html + css + js: 这个相信大家都没有问题vue:可能有很多同学一看这个,心就凉了一截。我不会 vue 咋办啊。..
没有关系!
我敢把这个列出来,肯定就已经为大家想到了这么一点。
vue 的理念和 微信小程序 的理念有非常多相同的地方,在我们后面进行项目开发的过程中,遇到一些个别的语法时,我会为大家进行介绍的。总之,对于大家来说,这三个条件,如果你全部具备,那自然是最好的。
如果你只具备前两个条件,也不要担心,甚至可以说是更加幸运 -> 为啥这么说? -> 因为接下来你将会在学会 uniapp 的同时,也会掌握 vue 的核心使用!
学会 uni-app 难吗? -> uni-app 的语法可以简单理解成「vue 语法」和「小程序语法」的结合体 -> 这显然不难!
之前开发微信小程序之前,需要:
同样,在进行 uniapp 开发之前,我们也需要做类似这样的事情,也就是需要配置项目的开发环境
uniapp 项目的开发环境主要有两点:
uniapp 同样提供了一个专门的开发工具 HBuilder X -> HBuilderX 下载页面DOWNLOADApp 开发版本(因为我们要实现是慕课热搜) -> 推荐使用正式版Windows 版本下载完成之后会得到一个 zip 的压缩包文件,解压完成即可使用 -> 是一个便携版MacOS 版本下来完成会得到一个 dmg 的安装包,直接安装即可因为我们的项目开发会使用 sass,所以需要为 HBuilder X 安装 sass 编译器。
HBuilder XHBuilderX 导入插件】 -> 一定要去掉你浏览器的广告插件,不然,这是不会出现这个按钮的HBuilderX】HBuilderX 右下角,提示你【正在下载】
本小节做两件事:
打开 HBuilderX -> 文件 -> 新建 -> 项目:

效果:

├─pages // 页面存放文件夹,等同于 微信小程序中的 pages
│ └─index.vue // 默认生成的页面
├─static // 静态资源存放文件夹
└─uni_modules // uni-app 组件目录
│ └─uni-xxx // uni-app 所提供的业务组件,等同于 微信小程序中的组件
├─App.vue // 应用配置文件,用来配置全局样式、生命周期函数等,等同于 微信小程序中的 app.js
└─main.js // 项目入口文件 -> 初始化 Vue
├─mainfest.json // 配置应用名称、appid、logo、版本等打包信息,
└─pages.json // 配置页面路径、窗口样式、tabBar 等页面类信息,等同于 微信小程序中的 app.json
└─uni.scss // uni-app 内置的常用样式变量
uniapp 支持 10 个平台,我们以 微信小程序 和 h5 平台为例子,进行演示。
为啥只适配这两个端? -> 因为其它平台很少使用,比如 360 小程序
D:/微信 web 开发者工具」HBuilder X 中双击打开你项目中的某一个文件(比如:App.vue)第一个问题:

在小程序开发者工具里边,把安全设置的服务端口给开启了:

打开 IDE 成功:

效果:

HBuilder X 中双击打开你项目中的某一个文件(比如:App.vue) -> 不然浏览器是不认识的
一般切换到手机调试模式
在实际项目开发中,微信小程序占据了绝大多数的用户,而 H5 端是我们 web 前端常见的一种形式,所以,在以后的正常开发之中,我们项目多以微信小程序和 H5 端的适配为主
虽说 HBuilder X 开发体验还算不错,但是有时候金窝银窝不如自己的狗窝,当我们习惯了 VSCode 之后,有时候不太愿意换开发工具。
那么怎么使用 VSCode 来开发 uniapp 呢? 其实是有办法的。
HBuilder X 运行项目(运行方式,参考上一小节)VSCode 打开项目VSCode 中安装插件:
uni-* 的体验尽可能好。pages.json 和 manifest.json 简单的格式校验VSCode 中修改代码,运行结果自动发生变化让
HBuilder X作为中介,我们在 VSCode 中写代码 -> 为了提高自己的开发体验(比如 API 提示等),可以安装插件 -> 安装一个uni-helper插件,其它三个插件自动安装
💡:uniapp 的 uni_modules 目录需要提交到 Git 吗?
需要提交!
目前,项目的运行以及开发环境都搞定了,就下来我们要做的就是开发这个项目!
先搞定这个项目的结构 -> 也就是 tabBar 切换 -> uniapp 模仿微信小程序,也就是创建姿势,跟微信小程序一样
把默认的页面删了,由于安装了插件,VSCode 也可以右键目录名创建页面 -> 但建议还是用 HBuilder 创建页面
pages 下的 index 文件夹pages 文件夹处,右键 -> 选择新建页面hot、hot-video、my 三个页面的创建
效果:

index 路径tabBar 节点tab-icons 文件夹到 static 文件夹中 -> 删掉原先存在的图片tabBar代码如果修改完成之后,依然得到了以下错误,那么可以在 HBuilder X 中重新运行项目到微信开发者工具解决 -> 这是小程序开发者工具的 bug(无法更新已经删除了文件这种情况,似乎有缓存啊) -> 重新运行项目就可以解决这个 bug 了

完整tabBar代码:

效果:


微信小程序默认开启了索引功能,但是因为我们没有配置索引策略,导致出现了这么一个警告的问题。具体情况可以参考:https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html
而如果想要解决这个警告也非常简单,因为一般情况项目不需要被微信索引,所以我们只需要 关闭 默认索引功能即可!

双击打开
manifest.json-> 点击源码视图 -> 下翻找到mp-weixin配置节点 -> 在settings下新增"checkSiteMap" : false

这个错误的原因非常简单 -> 因为我们没有为项目配置 AppID 的原因,所以只需要完成 AppID 配置即可。 -> 我们说过开发一个微信小程序项目,首先得有微信开发者工具,其次得有AppId

错误处理完成后,就可以进入到我们的页面开发了!
整个项目分为三大模块,我们首先去开发第一个模块——热搜模块
我们会把「热搜模块」分成四部分去进行开发:


template:定义当前页面的结构。相当于 wxmlscript:定义当前页面的逻辑。相当于 jsstyle:定义当前页面的样式。相当于 wxss
style 标签增加 scoped 属性:表示当前样式只在当前页面生效注意点:
@开头 -> @表示项目的根路径,也就是从项目的根路径开始找文件 -> src="@/static/images/logo.png"scss -> 无须导入这个uni.scss -> uniapp 帮我们自动引入了,我们可以直接使用这些全局样式变量mode="aspectFit"代码:

效果:

👇:logo 处理完成,处理搜索框
.vue文件,里边分为三块做法:
components 文件夹右键components文件夹 -> 新建组件

my-search 组件的能力(暂时不需要考虑太多之后的能力)
placeholder 内容可以在父组件定义 -> 不是在组件内写死的props -> 小程序是用properties代码:创建搜索框组件 · ppambler/imooc-uni-app@651bcb6
目前这个my-search组件的功能是非常非常简单的,后边会赋予my-search更多的一些能力,这样它就会变得非常非常的复杂了
比如:

my-tabs 组件my-tabs 组件的能力 -> 希望创建一个通用的 my-tabs 组件,可以满足各个应用中的需求 -> 既然是通用的,内容就不能写死了
my-tabs 样式 -> 比如下划线颜色tab 的内容是啥啥叫通用的组件? -> 这个项目里边能用,其它项目里边导入该组件也能用它 -> 说白了就是「轮子」呗! -> 既然想要把
my-tabs开发成一个轮子 -> 那么my-tabs的复杂端将远远超过我们之前所开发的my-search组件 -> 你可以认为my-tabs是我们这个项目中第一个比较复杂的自定义组件
至此,我们指定了三个可定制的内容
本小节我们创建了my-tabs组件 -> 分析了my-tabs组件中所具备的能力
👇:my-tabs组件中的内容开发
要使用my-tabs,就需要把tabs的数据给展示出来 -> 想要展示tabs数据,就得调用封装的接口来获取数据 -> 接口文档:热搜 -> 热搜文章类型
发请求在微信小程序里边是wx.request,那在 uni-app 里边呢? -> uni.request
utils 文件夹request.js ,封装请求对象api 文件夹 -> 放置所有的网络请求的相关方法hot.js 文件,封装 hot 相关的请求方法:getHotTabshot.vue里边使用getHotTabs -> 在loadHotTabs方法里边发起请求
loadHotTabs方法? -> 在created里边:组件实例配置完成,但 DOM 未渲染,我们可以在这个钩子里边进行网络请求,配置响应式数据 -> 这跟 Vue 是一样的至此,一个基本的网络请求代码就已经完成了!
scroll-x:允许横向滚动 -> 默认值是falsescroll-with-animation:在设置滚动条位置时使用动画过渡 -> 默认值是falsehot 中使用my-tabs组件 -> 父子通信 -> 传递两个参数:
tabData:tabs 数据源defaultIndex:当前的切换 indexmy-tabs 组件中展示
scroll-view<block v-for="(item, index) in tabData" :key="index"></block> -> uni-app 遵循 vue 的 v-for 指令,小程序是wx:for="🟡🟡arr🟡🟡",默认变量名是item,下标是index效果:

这个样式的实现过程是很丑陋的…… -> 我很难理解为啥要嵌套那么多层……

头两个能力已经实现了,并且把数据给展示出来了,那么现在第三个能力「在父组件中选中项」 -> 这该如何实现呢?目前,我们已经给了这个激活项一个对应的props(defaultIndex)了 -> 有了这个props该如何实现激活项呢?
在开始下一小节前,请先自己去实现一遍(唯有自己尝试实现一遍,你才会发现这里边所存在的问题) -> 实现失败有失败的问题,实现成功也有成功的问题 -> 带着问题去看下一小节,唯有这样你才会收获更多!
要实现的效果:

自己实现一遍:
defaultIndex是否和index相等 -> 相等即给item添加一个active类defaultIndex的值 -> 如何为my-tabs组件绑定点击事件? -> 子向父传参注意点:
@click,微信小程序则是用bind:tap
activeIndex的值tabClick事件watch,微信小程序则是用observers
defaultIndex这个数据 -> 必须添加immediate: true,表示defaultIndex第一次赋值(默认值或者父传递过来的数据)也要被监听到 -> 为了更新activeIndex的值而服务 -> 这就是「父组件传递的数据」不应该在子组件中进行修改$emit ,微信小程序则是用triggerEvent 关于老师的代码实现:active的切换是在子组件内部通过修改activeIndex来完成的,而我的是在父组件通过修改defaultIndex的值来完成的
实现效果:

👇:实现激活项下边的滑块效果
style添加内联样式slider数据,它旗下有个left属性,用来指定这个滑块距离左侧的距离是多少,默认是0
👇:实现滑块的滚动效果
想要实现滑块的滚动:
this.slider.left的值如何计算滑块滚动的距离?

我们要知道tabItem的宽度,tabItem的left,slider 的width
一个标准的数学公式:
left = tabItem.left + (tabItem.width - slider.width) / 2

这个公式似曾相识
width很容易确定,因为这是固定的 -> 添加默认配置数据:defaultConfig
view.underLine的styletabList,相较于父组件传过来的tabData,每个item多了一个_slider属性,该属性有个left属性,存储的是滑块距离左侧的距离值tabData的变化,再次强调,设置了immediate:true,第一次的初始值,也是会执行它的handler的,所以在updateTabWidth和tabToIndex会有一个判断tabList是否为空数组的语句
updateTabWidth没啥反应 -> 因为此时tabData为空,所以tabList也为空updateTabWidth,获取每个 tabItem DOM 元素的信息,计算每个 tabItem 距离左侧的距离,也就是给tabList里边的每个item添加一个_slider.left -> 因为父组件传递了tabData,所以tabData不为空 -> 默认会有一次计算「滑块」的位置,比如初始的第一次tabItem -> 点击事件触发 -> 更新this.slider.left的值,好让view.underLine的transform根据这个this.slider.left产生水平位移一些小技巧:
handler里边用setTimeout -> $nextTick()兼容性不好tabItem添加了:id="'_tab_' + index"const query = uni.createSelectorQuery().in(this);query.select("#_tab_" + index).boundingClientRect((res) => { // 获取每个元素在这个页面的空间信息 }).exec()
效果:

需求:当【选中项】发生变化时,希望 scrollView 也进行对应的位移 -> 说白了,想选最后一个,不让用自己拖动这个 scroll 视图容器
用代码模拟我们用鼠标滑动的滚动视图容器的效果

关键代码:
:scroll-left="scrollLeft"this.scrollLeft = this.activeIndex * this.defaultConfig.underLineWidth;需求:可在父组件中定制 my-tabs 样式

做法:
data的defaultConfig追加几项有关样式的配置props——config -> 让内部维护的数据对象defaultConfig跟父组件传递过来的数据合并一下 -> 说白了,子组件内部默认的样式配置作为兜底值style属性指定每个tabItem的样式至此,我们就已经完成了tabs组件的开发了 -> 整个tabs组件从创建开始到最后完成,总共经历了 9 小节内容
tabs 组件是我们这个项目中第一个复杂的组件,并且我们希望把这个tabs组件做成一个轮子,可以满足更多应用的需求 -> 所以我们不光要实现功能就完事儿了,我们还要提供定制化的一些特性(比如样式),这样的话,才能让我们这个组件来满足各个项目的各个场景里边去
这是首页的最后一个功能!
分析完成品里边 List 组件的效果,得出我们要做以下几个步骤才能实现它:
组件的根元素 -> 控制同类盒子之间的间隙;盒子容器 -> 控制盒子的外观;内容容器 -> 控制元素
有多个子元素就用一个容器把它们给包裹了
把每个item项抽离成单独的组件——hot-list-item
使用假数据把组件的基本结构给渲染出来

item的结构划分:

左侧展示的索引图标,而且每个item都有,并且都不一样,所以我们可以把它封装成一个组件——hot-ranking
注意,这个图标只是图片,这个图片是包含数字内容的

目前这个结构有了,就是样式长得太丑了:

效果:

所谓的真实数据指的是后端的接口已经写好了!你可以根据这个接口的定义去获取数据,之前的假数据只是为了完成这个
List组件的结构以及样式!
tab切换
my-tabs组件 -> 发送了一个tabClick通知index和父组件的currentIndex绑定hot.js封装请求接口loadHotListFromTab -> 该方法的作用是「获取 List 列表数据」 -> 什么时候调用这个方法 -> 在调用loadHotTabs的时候,因为,我们获取 list 数据时,需要 tab 中对应的 id -> 这是初始化列表的第一次数据请求关键点:
listData: {} -> key 是 tabItem 的 id,value是这个tabItem所对应的list数据注意点:
bind监听事件,uniapp 遵循 vue 规则,通过 @ 监听事件uni-load-more组件 -> 用来专门展示加载动画的组件效果:

👇:渲染真实数据
v-for="(item, index) in 50" -> v-for="(item, index) in listData[currentIndex]"list数据传给 List 组件 -> 给 List 组件定义两个props:data(循环列表的 item 数据)、ranking(排名次序数据)hot传递数据给了子组件list -> 子组件得把数据给显示出来 -> 根据接口文档来确定有哪些数据需要被展示:比如title、nicknamestyles文件夹,在这个文件夹下边创建一个global.scss -> 定义公共样式的地方 -> 展示两行的样式是固定的 CSS 写法 -> 需要做兼容处理 -> 在main.js引入公共样式 -> 谁要添加这个类?
hot-ranking组件添加props
require:class
效果:

前四步已经完成:
接下来实现:
💡:关于文本的截断
想要让 list 具备【横向翻页】的效果,那么可以使用 swiper 对其进行改造!
swiper组件 -> 具备滚动能力:
swiper-item下边都是一个遍历完成的一个个hot-list-item -> 循环次数由listData决定 -> 此时用的是tabIndex属性(tabData的0~6),而不是之前的currentIndex属性,因为swiper滑动的时候,也是需要变化的 -> 需要给swiper组件指定current属性(值是currentIndex,也就是我们点击某个tab可以切换列表数据),它决定当前展示哪个 swiperItem,不然这个tabIndex永远为0swiper-item的数量取决于my-tabs里边的tabItem数量 -> tabData决定 -> v-for处理一下
current属性:默认值是0,表示当前所在滑块的index
效果:
swiper-item默认展示第一个 list 的数据,其它 6 个 swiper-item,你在滑动的时候,你会发现这是空的:

当你第一次点击其它tab时,会加载这个tab对应的list数据,这也就意味着,这个「空」就被填上了这个list数据!

要把这些「空」都填完,你得把把其余的没有点过的tab都给点一遍……这样左右滑的时候都会看到有数据存在了! -> 为啥这样点就会有数据? -> 因为我们点击tab,就是在请求这个tab所对应的list数据啊,而这写数据会被缓存到listData中
目前存在的问题:
list列表的高度展示错误 -> 也就是列表数据没有展示全
应该是 list 的数据渲染完再滑?还是滑完后再渲染? -> 等大风小了再走,还是不管大风直接走呢?
swiper 指定高度可这个高度该给多少呢?1000px?2000px?


你要知道每个tabs所对应的list数据的高度是不一样的! -> 这个接口给出来的每个tab都是20条数据,不过我们假设都不一样!
最终的解决方案:计算出每个 listItem 的高度,然后叠加到一起,就可以得到 swiper 的高度了!
这个方案的实现逻辑:
currentSwiperHeight:当前 swiper 的高度swiperHeightData:缓存高度的计算结果:以 index 为 key,对应的 swiper 的高度 为 val -> 每次计算太耗性能了,你得把计算结果给缓存下来getCurrentSwiperHeight -> 用来帮我们计算当前 swiper 的高度
item -> 这是一个异步操作(节点信息 - uni-app 官网)
item 的高度 -> 来自一个个的节点信息this.$nextTick 存在一定的兼容性问题,所以更加推荐使用传统的方式 setTimeoutgetHotListFromTab里边的setTimeout的回调里调用
getCurrentSwiperHeight的结果值,也就是说swiper的高度swiperHeightData缓存中 -> 当前选中的tabItem索引值作为key,而对应的value就是这个高度值currentSwiperHeight注意:

效果:

💡:为什么会出现这个卡顿问题?
原因:swiper 动画未完成时,就获取数据,渲染 DOM
具体来说就是:点击 tab 切换 -> 修改currentIndex -> currentIndex绑定到了swiper的current属性 -> 也就说 tab 一切换,swiper就会发生对应的切换/滑动效果 -> 在发生切换效果时,swiper会执行一个动画效果,但是我们的切换动画和我们的数据获取渲染列表是同步进行的,也就说 swiper 动画未完成时,就获取数据,渲染 DOM -> 所以这就导致了卡顿问题?
💡:如何解决呢?
有了原因之后,这解决方案就非常简单了!
解决方案:swiper 动画完成之后,再去获取数据,渲染 DOM
💡:如何监听 swiper 的动画完成?
swiper绑定一个animationfinish事件 -> 表示我们当前动画完成之后的回调 -> onSwiperEndonSwiperEnd这个方法的逻辑:
onTabClick里边的获取列表数据的方式就不需要了loadHostListFromTab里边的判断缓存不需要了,直接获取数据即可return -> e.detail.current也可以拿到当前切换的currentIndexreturn,则证明存在数据缓存,存在数据缓存,则同时存在 height 的缓存数据 -> 设置currentSwiperHeight这个高度值效果:

至此:
这两个问题就已经全部搞定了
这也就意味着让 list 具备左右切换的能力也已经全部搞定了
六个步骤已经完成了其中的五个步骤 -> 我们需要完成第六步:「list 与 tabs 联动的能力」,也就是完成它们俩之间的联动效果
所谓的联动能力:
tabs 切换时,swiper 联动切换 -> 这一步已经完成了,点击某个tab,swiper会自动滑动到相应的listswiper 切换时,tabs 联动切换 -> 这一步未完成实现逻辑:
swiper切换的事件 -> @change -> 通过e.detail.current获取切换完成后此时的swiper下标 -> onSwiperChangeonSwiperChange:更新currentIndex的值效果:

目前的问题:激活的tabItem,其底部没有那个滑块跟随着
为啥不跟着滚动? -> 分析一下这个原因
我们知道滑块的滚动依赖my-tabs组件里边的tabToIndex这个方法来进行计算的
当activeIndex发生切换的时候 -> 我们需要重新调用这个tabToIndex
实现逻辑:
hot.vue -> currentIndex变化,defaultIndex这个传给my-tabs组件的props更新了my-tabs -> watch -> defaultIndex -> handler -> tabToIndex()(注意,第一次tabList为空时,直接返回就好了)效果:

至此,我们的第六步代码就已经完成了,整个list组件的功能就已经全部搞定了
不过,目前还有一些小问题,比如:
list 到底部,希望有吸顶的效果4k这样的效果,而不是4000
我发现一个问题,
list的数据都缓存了,swiper的animationfinish事件还是会被触发 -> 我突然明白这个动画指的是swiperItem左右切换的动画,而不是loading动画
目前,hot这首页的主要功能就已经完成了,剩下的都是一些边边角角的修复
💡:功能补充:让 tabs 具备吸顶的效果
my-tabs标签包装一层view.tab-stickytab-sticky这个类添加样式💡:功能补充:控制列表滚动位置
这是啥功能?

这个交互我不是很认可啊,我对比华为的应用市场 App 软件,和极客时间的 App,都发现每个
tabItem所对应的list列表的滚动位置是会保存的,也就是第一个tabItem,你在1000px高度处,你切换到第二个tabItem,此时你滚动到2000px高度处,你再切回来,这第一个tabItem还是在1000px高度处 -> 不过,那个打卡小程序,也没有做这样的效果!
实现逻辑:
currentPageScrollTop数据:表示当前的滚动距离onPageScroll -> 这是关于 uni-app 的页面生命周期
130px这个位置正好是触发吸顶的位置uni.pageScrollTo控制列表的滚动位置👇:搞定这两个补充功能后,接下来处理热度显示的问题
用过滤器
实现逻辑:
filters目录 -> 包含所有的过滤器
index.js -> 暴露一个hotNumber方法 -> 用来把大于1000的字符串数字转化成以k结尾的字符串数字main.js -> 会把所有过滤器都给注册了hot-list-item.vue效果:

至此,我们这个首页的功能开发就基本完成了!
用了 36 节,4 个多小时!
这个首页内容是我们接触 uni-app 后所完成的第一个比较复杂的页面 -> 这个页面的复杂度只要体现在tabs和list这两个位置

首页内容完成:
uniapp 进行了基础的了解 -> uni-app 结合了微信小程序语法和 Vue 语法的一个结合体imooc-blog 的项目tabbar 的搭建 -> 小程序里边的搭建规则.vue 的单文件组件 -> 由哪几部分组成? -> 在这种文件里边,我们实现了很多的页面以及对应的组件request API 请求模块 -> 使用 Promise 自己封装,而不是网络上现成的库tabs和list组件 -> 在实现的过程中,抛出了很多问题,通过思考这些问题,然后解决这些问题,这会让你收获很多!tabs 和 基于 swiper 的列表联动👇:完成新的页面:慕课搜索页面