经过前面四个章节,我们已经完成了 微信小程序 的学习。那么从这一章开始我们就进入 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 下载页面DOWNLOAD
App 开发版本
(因为我们要实现是慕课热搜) -> 推荐使用正式版Windows
版本下载完成之后会得到一个 zip
的压缩包文件,解压完成即可使用 -> 是一个便携版MacOS
版本下来完成会得到一个 dmg
的安装包,直接安装即可因为我们的项目开发会使用 sass
,所以需要为 HBuilder X
安装 sass 编译器
。
HBuilder X
HBuilderX
导入插件】 -> 一定要去掉你浏览器的广告插件,不然,这是不会出现这个按钮的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
:定义当前页面的结构。相当于 wxml
script
:定义当前页面的逻辑。相当于 js
style
:定义当前页面的样式。相当于 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
相关的请求方法:getHotTabs
hot.vue
里边使用getHotTabs
-> 在loadHotTabs
方法里边发起请求
loadHotTabs
方法? -> 在created
里边:组件实例配置完成,但 DOM 未渲染,我们可以在这个钩子里边进行网络请求,配置响应式数据 -> 这跟 Vue 是一样的至此,一个基本的网络请求代码就已经完成了!
scroll-x
:允许横向滚动 -> 默认值是false
scroll-with-animation
:在设置滚动条位置时使用动画过渡 -> 默认值是false
hot
中使用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
的style
tabList
,相较于父组件传过来的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
、nickname
styles
文件夹,在这个文件夹下边创建一个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
永远为0
swiper-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
存在一定的兼容性问题,所以更加推荐使用传统的方式 setTimeout
getHotListFromTab
里边的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
事件 -> 表示我们当前动画完成之后的回调 -> onSwiperEnd
onSwiperEnd
这个方法的逻辑:
onTabClick
里边的获取列表数据的方式就不需要了loadHostListFromTab
里边的判断缓存不需要了,直接获取数据即可return
-> e.detail.current
也可以拿到当前切换的currentIndex
return
,则证明存在数据缓存,存在数据缓存,则同时存在 height
的缓存数据 -> 设置currentSwiperHeight
这个高度值效果:
至此:
这两个问题就已经全部搞定了
这也就意味着让 list 具备左右切换的能力也已经全部搞定了
六个步骤已经完成了其中的五个步骤 -> 我们需要完成第六步:「list 与 tabs 联动的能力」,也就是完成它们俩之间的联动效果
所谓的联动能力:
tabs
切换时,swiper
联动切换 -> 这一步已经完成了,点击某个tab
,swiper
会自动滑动到相应的list
swiper
切换时,tabs
联动切换 -> 这一步未完成实现逻辑:
swiper
切换的事件 -> @change
-> 通过e.detail.current
获取切换完成后此时的swiper
下标 -> onSwiperChange
onSwiperChange
:更新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-sticky
tab-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
的列表联动👇:完成新的页面:慕课搜索页面