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選擇器
可以自定義傳入的內容