饿了么vue-cli3.0+cube-ui笔记
1、目录结构
模板文件是public里的index.html,运行项目的时候,会引用src/main.js(入口文件)
详细文档在这里:https://cli.vuejs.org/zh/config/#pwa
public:放着html模板和静态资源,public/index.html
文件是一个会被 html-webpack-plugin 处理的模板。在构建过程中,资源链接会被自动注入。
.browserslistrc 指定浏览器版本。不同的浏览器会有兼容性问题,比如css,我们会给它们加上前缀,这个文件是为postcss.config.js的Autoprefixer 插件使用的,
Autoprefixer 插件依据browserslistrc 来添加前缀
postcss.config.js 里的autoprefixer就是依据.browserslistrc文件加前缀
.eslintrc.js eslint的相关配置
引入的一些vue插件,比如v-for模板没有使用key,就会报错
babel.config.js 预设
package.json 各种依赖
package-lock.json 锁版本 管理版本的文件
cube-ui插件
https://github.com/didi/cube-ui
后编译:就是我们做项目的时候使用的是 源代码,只有打包运行后,才会进行编译。好处是节省构建包的体积,做完项目后,可以把不用的引入删掉,这样打包的时候,
就只会打包那些我们使用的模块
在vue-cli3.0的项目里,直接使用vue add cube-ui 就可以安装
是否使用后编译
是用部分引入还是全部引入,上面的选择是部分引入
自定义主题是否需要(选择是,因为我们的项目的颜色一般都与插件的不一样)
安装完以后,下面是修改和添加的文件
cube-ui.js 管理cube-ui模块的引入
theme.styl 管理cube-ui的颜色(修改颜色可以在这里面进行修改)
表示可以直接引入cube-ui的源码,把cube-ui的组件直接引入项目中,不是用的编译后的代码
vue.config.js 类似以前的webpack.js文件,进行一些配置
2、2-3 api接口mock
现在的项目都是前后端分离,我们这个项目,现在就进行数据接口模拟
data.json里保存的所有数据,类似于后端的数据库
vue.config.js
// 引入data.json文件,获取对应的数据 const appData = require('./data.json') const seller = appData.seller const goods = appData.goods const ratings = appData.ratings devServer: { before (app) { app.get('/api/seller', function (req, res) { res.json({ errno: 0, data: seller }) }) app.get('/api/goods', function (req, res) { res.json({ errno: 0, data: goods }) }) app.get('/api/ratings', function (req, res) { res.json({ errno: 0, data: ratings }) }) } }
这里有一个devServer,表示本地服务器,里面有个before方法,参数是app,可以在这里面定义接口
例如里面定义的app.get('/api/seller',。启动服务后,在url输入http://localhost:8080/api/seller,可以看到下面的效果
2、
~表示绝对路径,要使用这个,先要在vue.config.js里面进行配置
webpack里的DevServer的before方法
https://webpack.js.org/configuration/dev-server/#devserver-before
发现的一个问题:
在tab组件的mounted,created,watch里面输出App.vue传入的tabs
export default { name: 'tab', props: { tabs: { type: Array, default() { return [] } }, initialIndex: { type: Number, default: 0 } }, data () { return { index: this.initialIndex, slideOptions: { listenScroll: true, // 是否监控scroll事件 probeType: 3, // 0 不派发scroll事件,1:非实时;2:滑动过程中;3:不仅在屏幕滑动的过程中,而且momentum 滚动动画运行过程中实时派发 directionLockThreshold: 0 } } }, created () { console.log(this.tabs) }, mounted () { console.log(this.tabs) this.onChange(this.index) },
发现输出的值里面,sellel不是真实的数据
经过研究,是因为selle在App.vue里面,是异步获取的,所以是这个样子
如果我们在App.vue里面写死selle,那在tab组件的mounted,created里面就可以输出我们想要的值
如果在watch里监控,那就正确
综上所述:如果我们把请求到的数据封装到一个对象里面(或者是复杂的数据里面),然后传到子组件,在created和mounted里面输出这个对象的话,请求的这部分会显示的不对
但是在template里面使用或者输出,那就没问题
如果直接把请求的数据传递到子组件,在created和mounted里输出,也有可能不对,最好的方法是在watch监控这个数据,这样就正确
3、
<!--注意这个写法,先判断seller.supports有没有,如果没有的话,seller.supports[0]会报错--> <div v-if="seller.supports" class="support"> <support-ico :size=1 :type="seller.supports[0].type"></support-ico> <span class="text">{{seller.supports[0].description}}</span> </div>
4、3-4 headerdetail组件交互
headerDetail组件是fixed,如果放在其他组件内部(有类似transition的样式),会对样式造成影响,所以我们可以直接把这种类型的组件放在body下
这里可以借助cube-ui的create-api 模块 https://didi.github.io/cube-ui/#/zh-CN/docs/create-api
该模块默认暴露出一个 createAPI
函数,可以实现以 API 的形式调用自定义组件
register.js里面
import { createAPI } from 'cube-ui' import Vue from 'vue' import HeaderDetail from 'components/header-detail/header-detail' createAPI(Vue, HeaderDetail)
main.js
import Vue from 'vue' import './cube-ui' import App from './App.vue' // 引入register.js import './register' import 'common/stylus/index.styl' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App) }).$mount('#app')
组件里面使用
showDetail() { // cube-ui的create-api把headerdetail组件变成了api实例,所以可以这样当成一个实例调用this.$createHeaderDetail this.headerDetailComp = this.headerDetailComp || this.$createHeaderDetail({ $props: { seller: 'seller' } }) this.headerDetailComp.show() }
5、4-1 tab组件基础实现
https://didi.github.io/cube-ui/#/zh-CN/docs/tab-bar
使用了cube-ui的TabBar组件
6、cube-ui的轮播图组件 https://didi.github.io/cube-ui/#/zh-CN/docs/slide
7、4-1 tab组件基础实现
.tab display: flex flex-direction: column height: 100% >>> .cube-tab padding: 10px 0 .slide-wrapper flex: 1 overflow: hidden
>>>是什么?
这与vue-loader有关,vue-loader是webpack的一个Loader,专门为了编写vue方便而出现的
参考地址:https://vue-loader.vuejs.org/zh/guide/scoped-css.html#子组件的根元素
深度作用选择器
如果你希望 scoped
样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>>
操作符:
<style scoped> .a >>> .b { /* ... */ } </style>
在这个项目中
cube-tab是tab子类中的子类,要想修改样式,只能用>>>
8、项目用了cube-ui库,如果要修改里面默认的颜色,可以这样做
我们安装cube-ui的时候,会出现一个文件:theme.styl,可以在这里面修改
项目中的做法是,我们新建common/stylus/variable.styl文件,在里面先定义好颜色
@import "~cube-ui/src/common/stylus/variable.styl" $color-background = rgba(7, 17, 27, 1) $color-background-s = rgba(7, 17, 27, 0.8) $color-background-ss = rgba(7, 17, 27, 0.5) $color-background-sss = rgba(7, 17, 27, 0.2) $color-background-ssss = #f3f5f7 $color-red = rgb(240, 20, 20) $color-blue = rgb(0, 160, 220) $color-light-blue = rgba(0, 160, 220, 0.2) $color-green = #00b43c $color-col-line = #d9dde1 $color-row-line = rgba(7, 17, 27, 0.1)
然后呢,在theme.styl中引入这个文件,然后替换定义好的颜色
引入这个文件
替换颜色
备注:修改的这些样式,要使用=,不要使用:=,不然不起作用
9、4-2 tab组件上下联动
利用cube-tab和cube-slide实现,当滚动slide的时候,对应的tab下划线也滚动
(1)、在cube-slide加入option属性
<cube-slide :loop=false :auto-play=false :show-dots=false :initial-index="index" ref="slide" :options="slideOptions" @scroll="onScroll" @change="onChange" >
slideOptions: { listenScroll: true, // 是否监控scroll事件 probeType: 3, // 0 不派发scroll事件,1:非实时;2:滑动过程中;3:不仅在屏幕滑动的过程中,而且momentum 滚动动画运行过程中实时派发 directionLockThreshold: 0 }
directionLockThreshold https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/options.html#directionlockthreshold
- 类型:Number
- 默认值:5(不建议修改)
- 作用:当我们需要锁定只滚动一个方向的时候,我们在初始滚动的时候根据横轴和纵轴滚动的绝对值做差,当差值大于
directionLockThreshold
的时候来决定滚动锁定的方向。 - 备注:当设置 eventPassthrough 的时候,
directionLockThreshold
设置无效,始终为 0。
如果项目中只是一个方向滚动,那就不用设置,现在这个项目是俩个方向滚动,所以要设置为0
(2)、cube-slide有scroll事件
滚动中实时派发,返回一个对象,包含滚动的坐标值
https://didi.github.io/cube-ui/#/zh-CN/docs/slide
onScroll (pos) { // cube-slide的scroll事件,滚动中实时派发,获取到滚动位置的坐标值 // 滚动slide的时候,tab实时改变 // 原理是:先获取tabBar和slide的宽度,然后获取到滚动位置的坐标值,坐标值/slideWidth得到滚动的比例,然后乘以tabBarWidth,实时得到 // tab下划线的滚动位置 // 调用cube-tab的setSliderTransform方法,参数就是上面得到的值 const tabBarWidth = this.$refs.tabBar.$el.clientWidth const slideWidth = this.$refs.slide.slide.scrollerWidth const transform = -pos.x / slideWidth * tabBarWidth this.$refs.tabBar.setSliderTransform(transform) }
(3)、调用cube-tab的setSliderTransform方法,实时改变tab的下划线
备注:tab里有useTransition参数,transition 过渡(默认为true),为了让效果好看,这里我们要把这个值设为false
<cube-tab-bar :useTransition=false :showSlider="true" v-model="selectedLabel" :data="tabs" ref="tabBar" class="border-bottom-1px" >
完整代码:
<template> <div class="tab"> <cube-tab-bar :useTransition=false :showSlider="true" v-model="selectedLabel" :data="tabs" ref="tabBar" class="border-bottom-1px" > </cube-tab-bar> <div class="slide-wrapper"> <cube-slide :loop=false :auto-play=false :show-dots=false :initial-index="index" ref="slide" :options="slideOptions" @scroll="onScroll" @change="onChange" > <cube-slide-item> <goods></goods> </cube-slide-item> <cube-slide-item> <ratings></ratings> </cube-slide-item> <cube-slide-item> <seller></seller> </cube-slide-item> </cube-slide> </div> </div> </template> <script> import Goods from 'components/goods/goods' import Ratings from 'components/ratings/ratings' import Seller from 'components/seller/seller' export default { name: 'tab', data () { return { index: 0, tabs: [{ label: '商品' }, { label: '评价' }, { label: '商家' }], slideOptions: { listenScroll: true, // 是否监控scroll事件 probeType: 3, // 0 不派发scroll事件,1:非实时;2:滑动过程中;3:不仅在屏幕滑动的过程中,而且momentum 滚动动画运行过程中实时派发 directionLockThreshold: 0 } } }, methods: { // silde 页面切换时触发change事件,返回当前的索引值,然后赋值给this.index // this.index改变的话,会触发selectedLabel重新计算,然后cube-tab就会进行新的计算,就可以完成切换了 onChange (current) { this.index = current }, onScroll (pos) { // cube-slide的scroll事件,滚动中实时派发,获取到滚动位置的坐标值 // 滚动slide的时候,tab实时改变 // 原理是:先获取tabBar和slide的宽度,然后获取到滚动位置的坐标值,坐标值/slideWidth得到滚动的比例,然后*tabBarWidth,实时得到 // tab下划线的滚动位置 // 调用cube-tab的setSliderTransform方法,参数就是上面得到的值 const tabBarWidth = this.$refs.tabBar.$el.clientWidth const slideWidth = this.$refs.slide.slide.scrollerWidth const transform = -pos.x / slideWidth * tabBarWidth this.$refs.tabBar.setSliderTransform(transform) } }, computed: { selectedLabel: { get() { return this.tabs[this.index].label }, set(newVal) { this.index = this.tabs.findIndex((value) => { return value.label === newVal }) } } }, components: { Goods, Ratings, Seller } } </script> <style lang="stylus" rel="stylesheet/stylus" scoped> @import "~common/stylus/variable" .tab display: flex flex-direction: column height: 100% >>> .cube-tab padding: 10px 0 .slide-wrapper flex: 1 overflow: hidden </style>
10、4-3、tab组件的抽象和封装
上一节是实现了tab组件的效果,所有的数据都是直接写在了tab组件,写在我们把数据抽离出来,只留下功能代码,这样我们以后新加/减少tabbar,都不用修改tab组件,只要修改
父组件传入的数据就行
在父组件的计算属性里,定义tabs,然后传入到tab中
computed: { tabs() { return [ { label: '商品', component: Goods, data: { seller: this.seller } }, { label: '评论', component: Ratings, data: { seller: this.seller } }, { label: '商家', component: Seller, data: { seller: this.seller } } ] } },
<tab :tabs="tabs"></tab>
在tba组件里
<cube-slide-item v-for="(tab, index) in tabs" :key="index"> <component ref="component" :is="tab.component" :data="tabs.data"></component> </cube-slide-item>
11、5-1 scroll-nav组件
使用cube-ui的scrollNav组件
<template> <div class="goods"> <div class="scroll-nav-wrapper"> <cube-scroll-nav :side=true :data="goods" :options="scrollOptions" v-if="goods.length" > <cube-scroll-nav-panel v-for="good in goods" :key="good.name" :label="good.name" :title="good.name" > <ul> <li v-for="food in good.foods" :key="food.name" class="food-item" > <div class="icon"> <img width="57" height="57" :src="food.icon"> </div> <div class="content"> <h2 class="name">{{food.name}}</h2> <p class="desc">{{food.description}}</p> <div class="extra"> <span class="count">月售{{food.sellCount}}份</span><span>好评率{{food.rating}}%</span> </div> <div class="price"> <span class="now">¥{{food.price}}</span> <span class="old" v-show="food.oldPrice">¥{{food.oldPrice}}</span> </div> <div class="cart-control-wrapper"> </div> </div> </li> </ul> </cube-scroll-nav-panel> </cube-scroll-nav> </div> </div> </template> <script> import { getGoods } from 'api' export default { name: 'goods', props: { data: { type: Object, default() { return {} } } }, data() { return { goods: [], selectedFood: {}, scrollOptions: { click: false, // 会点击俩次,底层用的是scroll,所以设置click为false directionLockThreshold: 0 } } }, methods: { fetch () { getGoods().then((goods) => { this.goods = goods }) } } } </script> <style lang="stylus" rel="stylesheet/stylus" scoped> .goods position: relative text-align: left height: 100% .scroll-nav-wrapper position: absolute width: 100% top: 0 left: 0 bottom: 48px >>> .cube-scroll-nav-bar width: 80px white-space: normal overflow: hidden >>> .cube-scroll-nav-bar-item padding: 0 10px display: flex align-items: center height: 56px line-height: 14px font-size: $fontsize-small background: $color-background-ssss .text flex: 1 position: relative .num position: absolute right: -8px top: -10px .support-ico display: inline-block vertical-align: top margin-right: 4px >>> .cube-scroll-nav-bar-item_active background: $color-white color: $color-dark-grey >>> .cube-scroll-nav-panel-title padding-left: 14px height: 26px line-height: 26px border-left: 2px solid $color-col-line font-size: $fontsize-small color: $color-grey background: $color-background-ssss .food-item display: flex margin: 18px padding-bottom: 18px position: relative &:last-child border-none() margin-bottom: 0 .icon flex: 0 0 57px margin-right: 10px img height: auto .content flex: 1 .name margin: 2px 0 8px 0 height: 14px line-height: 14px font-size: $fontsize-medium color: $color-dark-grey .desc, .extra line-height: 10px font-size: $fontsize-small-s color: $color-light-grey .desc line-height: 12px margin-bottom: 8px .extra .count margin-right: 12px .price font-weight: 700 line-height: 24px .now margin-right: 8px font-size: $fontsize-medium color: $color-red .old text-decoration: line-through font-size: $fontsize-small-s color: $color-light-grey .cart-control-wrapper position: absolute right: 0 bottom: 12px .shop-cart-wrapper position: absolute left: 0 bottom: 0 z-index: 50 width: 100% height: 48px </style>
知识点有俩个:
(1)、options参数配置
scrollOptions: { click: false, // 会点击俩次,底层用的是scroll,所以设置click为false directionLockThreshold: 0 }
(2)、获取数据的方法为fetch
fetch () { getGoods().then((goods) => { this.goods = goods }) }
什么是调用呢?我们一般是在组件的mounted里面调用,但是在这个项目中,如果我们在评论或者商家页面,商品页面有可能是在mounted,这时就会进行数据加载,这样的话,
会影响当前页面的显示,所以,我们应该在切换组件的时候调用这个方法
可以在Tab组件的onChange方法里调用
// 切换的时候,调用对应组件里面的fetch onChange (current) { this.index = current const instance = this.$refs.component[current] if (instance && instance.fetch) { instance.fetch() }
12、5-3 cart-control组件
add (event) { if (!this.food.count) { // food这个数据时由父组件传过来的,最开始里面是没有count属性的,我们要给里面添加,就需要是vue的$set this.$set(this.food, 'count', 1) } else { this.food.count++ }this.$emit(EVENT_ADD, event.target) },
这里面的this,food是父组件传到子组件里面的,可以用this.$set(this.food, 'count', 1)添加新的属性,并且赋值
而且可以修改里面值
修改完以后,在父组件里面的值也会随之改变
13、
14、ScrollNav 组件
在使用ScrollNav 组件的时候,不要用v-show,会出现有点地方没有加载进数据,可以使用v-if或者router跳转到新页面
15、https://didi.github.io/cube-ui/#/zh-CN/docs/picker picker选择器
可以自定义传入的内容