這是我第一個基於 Vue 的項目做品,目的很簡單,學以至用,將以前的前端知識積累加上目前流行的前端框架,以項目的形式展示出來。css
源代碼:https://github.com/nanyang24/...
演示地址:https://ele.n-y.io/html
Vue有本身的腳手架構建工具vue-cli,使用起來很是方便,使用webpack來集成各類開發便捷工具,好比:前端
…vue
除此以外,vue-cli已經使用node配置了一套本地服務器和安裝命令等,本地運行和打包只須要一個命令就能夠搞定,很是的方便node
├──app.vue │ ├──header.vue--頭部組件 │ │ ├──star.vue--星星評分組件 │ ├──goods.vue--商品組件 │ │ ├──shopcart.vue--購物車組件,包括小球飛入購物車動畫 │ │ ├──cartcontrol.vue--購買加減圖標控件--選中數量返回給父組件goods,goods響應後,從新計算選中數量,將數據發送給購物車組件, │ │ ├──food.vue--商品詳情頁 │ │ │ ├──ratingselect.vue--評價內容篩選組件 │ ├──ratings.vue--評論組件 │ │ ├──ratingselect.vue--評價內容篩選組件 │ ├──seller.vue--商家組件 獨立組件 ├──split.vue--關於分割線組件
common/---- 文件夾存放的是通用的css和fonts components/---- 文件夾用來存放 Vue 組件 router/---- 文件夾存放的是vue-router相關配置(linkActiveClass,routes註冊組件路由) build/---- 文件是 webpack 的打包編譯配置文件 config/---- 文件夾存放的是一些配置項,好比咱們服務器訪問的端口配置等 dist/---- 該文件夾一開始是不存在,在項目通過 build 以後纔會生成 prod.server.js---- 該文件是測試是模擬的服務器配置,用來運行dist裏面的文件,在config/index.js中,build對象中添加一條端口設置port:9000, App.vue---- 根組件,全部的子組件都將在這裏被引用 index.html---- 整個項目的入口文件,將會引用咱們的根組件 App.vue main.js---- 入口文件的 js 邏輯,在 webpack 打包以後將被注入到 index.html 中
當樣式像素必定時,因手機有320px,640px等.各自的縮放比差別,因此設備顯示像素就會有1Npx,2Npx。android
公式:設備上像素 = 樣式像素 * 設備像素比
webpack
爲了保證設計稿高度還原,採用 media + scale
的方法解決ios
屏幕寬度: 320px 480px 640px 設備像素比: 1 1.5 2 經過查詢它的設備像素比 devicePixelRatio 在設備像素比爲1.5倍時, round(1px 1.5 / 0.7) = 1px 在設備像素比爲2倍時, round(1px 2 / 0.5) = 1px
實現代碼css3
// SCSS 語法 @mixin border-1px($color) { position: relative; &::after { display: block; position: absolute; left: 0; bottom: 0; width: 100%; border-top: 1px solid $color; content: ''; } } @mixin border-none() { &::after{ display: none; } } @media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5) { .border-1px { &::after { -webkit-transform: scaleY(0.7); transform: scaleY(0.7); } } } @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) { .border-1px { &::after { -webkit-transform: scaleY(0.5); transform: scaleY(0.5); } } }
在 header 組件的詳情頁採用 sticky-footer 佈局,主要特色是若是頁面內容不夠長的時候,頁腳塊粘貼在視窗底部;若是內容足夠長時,頁腳塊會被內容向下推送git
父級 position:fixed,內容設 爲padding-bottom:64px,頁腳相對定位,margin-top:-64px,clear:both
爲了保證兼容性,父級要清除浮動
參考:
https://www.cnblogs.com/shico...
https://www.w3cplus.com/css3/...
// 左側固定width:80px,右側自適應 parent: display:fiexd; child-left: flex:0 0 80px child-right: flex:1
例如:商品詳情頁面的商品圖片展現樣式
// stylus語法 .img_header { position:relative width:100% // width是 設備寬度 height:0 padding-top:100% // 高度設爲0,使用padding撐開 .img { position:absolute //定位佈局 top:0 left:0 width:100% height:100% } }
filter:blur(10px),注意,全部在內的子元素也會模糊,包括文字,因此採用定位佈局,背景單獨佔用一個層,ios有一個設置backdrop-filter:blur(10px),只會模糊背景,但不支持android
在購買控件中使用transition過渡效果,實現添加減小按鈕的動效,和小球飛入購物車的動效(模仿貝塞爾曲線的效果)
vue2.x裏面定義了transition過渡狀態,
name - string, 用於自動生成 CSS 過渡類名。
例如:name: 'fade' 將自動拓展爲.fade-enter,.fade-enter-active等。默認類名爲 "v" fade-enter fade-enter-active fade-leave fade-leave-active
包括transition過渡的鉤子函數
before-enter before-leave before-appear enter leave appear after-enter after-leave after-appear enter-cancelled leave-cancelled (v-show only) appear-cancelled
解決方案:每一個 li 要 display:inline-block,由於width不會自動撐開父級ul,因此須要將計算後的寬度賦值給ul的width,(每一張圖片的width+margin)*圖片數量-一個margin,由於最後一張圖片沒有margin
同時new BScroll裏面要設置scrollX: true,eventPassthrough: 'vertical', // 滾動方向橫向
問題分析:出現這種現象是由於better-scroll插件是嚴格基於DOM的,數據是採用異步傳輸的,頁面剛打開,DOM並無被渲染,因此,要確保DOM渲染了,才能使用 better-scroll,
解決方案:用到mounted鉤子函數,同時必須搭配this.$nextTick()
問題分析:出現這種狀況是由於mounted函數在整個生命週期中只會只行一次
解決方案:使用watch方法監控數據變化,並執行滾動函數 this._initScroll();this._initPicScroll();
使用window.localStorage保存和設置緩存信息,封裝在store.js文件內
//將頁面信息保存到localStorage裏 export function saveToLocal(id, key, value) { let store = window.localStorage._store_; // 新定義一個key值_store_,存放要保存的數據對象 // _store_ { // store[id]: { // key: value // } // } if (!store) { store = {}; store[id] = {}; } else { store = JSON.parse(store); // String格式--> json格式 if (!store[id]) { store[id] = {}; } } store[id][key] = value; window.localStorage._store_ = JSON.stringify(store); // 將json格式轉成String格式,存放到window.localStorage._store中 } //將localStorage信息設置到頁面中 export function loadFromLocal(id, key, defaults) { let store = window.localStorage._store_; if (!store) { // 一開始是沒有的,由於沒有點擊事件,因此顯示默認數據 return defaults; } store = JSON.parse(store)[id]; // 將json格式-->String格式 // console.log(store); // {"isFavorite":true} if (!store) { return defaults; } let ret = store[key]; return ret || defaults; }
使用window.localStorage.search獲取url地址,並進行解析
封裝在util.js文件內
/** * 解析URL參數 * @example ?id=12345&a=b * @return Object {id:12345, a:b} **/ export function urlParse() { let url = window.location.search; let obj = {}; let reg = /[?&][^?&]+=[^?&]+/g; let arr = url.match(reg); // ['?id=12345', '&a=b'] if (arr) { arr.forEach((item) => { let temArr = item.substring(1).split('='); let key = decodeURIComponent(temArr[0]); let value = decodeURIComponent(temArr[1]); obj[key] = value; }); } return obj; };
咱們須要將獲得的 id 和 name 帶到數據中,實際上在獲取數據的時候,並無帶着id和name,這時就要用到 es6 語法中Object.assign()
,官方解釋爲:能夠把任意多個的源對象自身的可枚舉屬性拷貝給目標對象,而後返回目標對象。
this.seller = Object.assign({}, this.seller, response.data); //即將vm.seller屬性和請求返回數據對象合併到空對象,而後賦值給vm.seller,這裏加上this.seller即提供了一種可擴展的機制,假若原來的屬性中有預約義的其餘屬性。
解決方案:在 app.vu 內使用 keep-alive,保留各組件狀態,避免從新渲染
<keep-alive> <router-view :seller="seller"></router-view> </keep-alive>
使用<router-link>
組件完成導航,<router-link>
默認會被渲染成一個 <a>
標籤,但必須使用 to屬性
,指定鏈接
// app.vue <!-- 導航 --> <router-link to="/home">home</router-link> <router-link to="/about">about</router-link> <!-- 路由出口 組件渲染容器 --> <router-view></router-view>
// router: index.js import Vue from 'vue'; import Router from 'vue-router'; import goods from 'components/goods/goods.vue'; import ratings from 'components/ratings/ratings.vue'; import seller from 'components/seller/seller.vue'; Vue.use(Router); const routes = [{ path: '/', redirect: '/goods' }, { path: '/goods', component: goods }, { path: '/ratings', component: ratings }, { path: '/seller', component: seller }]; export default new Router({ routes, linkActiveClass: 'active' });
在vue1.x的時候,vue的官方推薦HTTP請求工具是vue-resource,可是在vue2.0的時候將推薦工具改爲了axios。
若是想像之前使用 vue-resource 那樣 this.$http.get 調用,要這樣定義:
Vue.prototype.$http = axios;
經過 this.$http.get 來定義經過vue實例來發送get請求,而後經過then後面的回調函數將請求成功的數據接收,經過狀態碼來判斷是否成功以及複製給vue的數據對象。因爲這裏是用的mock數據(模擬後臺數據),因此用的模擬狀態碼。
const ERR_OK = 0;//表示沒有錯誤信息,即獲取數據成功 this.$http.get('/api/seller').then((response) => { response = response.data; if (response.errno === ERR_OK) { this.seller = Object.assign({}, this.seller, response.data); } });
vue是組件式開發,因此組件間通信是必不可少的
vue提供了一種方式,即在子組件定義 props 來接受父組件傳遞來的數據對象。
// 父組件 <v-header :seller="seller"></v-header> // 子組件 header.vue props: { seller: { type: Object } }
若是是子組件想傳遞數據給父組件,須要派發自定義事件,使用 $emit 派發,
父組件使用v-on接收監控(v-on能夠簡寫成@)
// 子組件 RatingSelect.vue,派發自定義事件isContent,將this.onlyContent數據傳給父級 this.$emit('isContent', this.onlyContent); this.$emit('selRatings', this.selectType); // 父組件 foodInfo.vue 在子組件的模板標籤裏,使用v-on監控isContent傳過來的數據 <v-ratingselect @selRatings="filterRatings" @isContent="iscontent"></v-ratingselect>
父組件再利用 $refs 直接訪問子組件B的方法,間接實現數據從子組件A傳遞至子組件B
將相一樣式或功能的區塊單獨提出來,做爲一個組件。
另外組件中用到的圖片等資源就近維護,便可以考慮在組件文件夾中新建images文件夾。
抽離組件遵循原則:
要儘可能遵循單一職責原則,複用性更高,不要設置額外的margin等影響佈局的東西
想要達到這種目的,有兩種方法,一種是利用重定向,另外一種是利用vue-router的導航式編程。
//在router的index.js文件中設置,要多寫一個對象,指向目標組件 Vue.use(Router); const routes = [{ path: '/', redirect: '/goods' // 重定向 }, { path: '/goods', component: goods }, { path: '/ratings', component: ratings }, { path: '/seller', component: seller }]; export default new Router({ routes, linkActiveClass: 'active' });
router.push('/Goods');
<div class="ball-container"> <div v-for="ball in balls"> //用了兩種方式的動畫,css和js鉤子 <transition name="drop" @before-enter="beforeDrop" @enter="dropping" @after-enter="afterDrop"> //外層動畫 <div class="ball" v-show="ball.show"> //內層動畫 <div class="inner inner-hook"></div> </div> </transition> </div> </div>
data(){ return { balls: [ {show: false}, {show: false}, {show: false}, {show: false}, {show: false} ], dropBalls: [] } },
只要觸發了drop事件,不止是drop事件裏面的代碼會執行,另外幾個vue的js監聽鉤子也會一塊兒按順序執行
drop 事件的觸發能夠經過點擊 cartcontrol 組件的添加小球按鈕 addCart 事件觸發使用 $emit
,也能夠父組件 this.$refs.shopcart.drop(target);
直接觸發
$emit
是觸發當前實例上的事件。附加參數都會傳給監聽器回調。methods: { drop(el) { //觸發一次事件就會將全部小球進行遍歷 for (let i = 0; i < this.balls.length; i++) { let ball = this.balls[i]; if (!ball.show) { //將false的小球放到dropBalls ball.show = true; ball.el = el; //設置小球的el屬性爲一個dom對象 this.dropBalls.push(ball); return; } } }, beforeDrop(el){ //這個方法的執行是由於這是一個vue的監聽事件 let count = this.balls.length; while (count--) { let ball = this.balls[count]; if (ball.show) { let rect = ball.el.getBoundingClientRect(); //獲取小球的相對於視口的位移(小球高度) let x = rect.left - 32; let y = -(window.innerHeight - rect.top - 22); //負數,由於是從左上角往下的的方向 el.style.display = ''; //清空display el.style.webkitTransform = `translate3d(0,${y}px,0)`; el.style.transform = `translate3d(0,${y}px,0)`; //處理內層動畫 let inner = el.getElementsByClassName('inner-hook')[0]; //使用inner-hook類來單純被js操做 inner.style.webkitTransform = `translate3d(${x}px,0,0)`; inner.style.transform = `translate3d(${x}px,0,0)`; } } }, dropping(el, done) { //這個方法的執行是由於這是一個vue的監聽事件 /* eslint-disable no-unused-vars */ let rf = el.offsetHeight; //觸發重繪html this.$nextTick(() => { //讓動畫效果異步執行,提升性能 el.style.webkitTransform = 'translate3d(0,0,0)'; el.style.transform = 'translate3d(0,0,0)'; //處理內層動畫 let inner = el.getElementsByClassName('inner-hook')[0]; //使用inner-hook類來單純被js操做 inner.style.webkitTransform = 'translate3d(0,0,0)'; inner.style.transform = 'translate3d(0,0,0)'; el.addEventListener('transitionend', done); //Vue爲了知道過渡的完成,必須設置相應的事件監聽器。 }); }, afterDrop(el) { //這個方法的執行是由於這是一個vue的監聽事件 let ball = this.dropBalls.shift(); //完成一次動畫就刪除一個dropBalls的小球 if (ball) { ball.show = false; el.style.display = 'none'; //隱藏小球 } } }
關於 getBoundingClientRect
(位移的計算是從左上角開始)
getBoundingClientRect
獲取到當前元素的座標,而後須要位移的left減去元素的寬獲取真正的最終位移x座標getBoundingClientRect
獲取到當前元素的座標,而後須要當前屏幕的高度減去元素的 top 再減去元素自己的高度獲取到真正的最終位移 y 座標,而且這個是負數,由於是從左上角往下的方向關於html重繪
let rf = el.offsetHeight;
這是一個手動觸發html重繪的方法 .ball-container .ball position: fixed //小球動畫必須脫離html佈局流 left: 32px bottom: 22px z-index: 200 transition: all 0.4s cubic-bezier(0.49, -0.29, 0.75, 0.41) .inner width: 16px height: 16px border-radius: 50% background: rgb(0, 160, 220) transition: all 0.4s linear
整個流程是:
最後造成的星星html就相似這樣
<div class="star star-48"> <span class="star-item on"></span> <span class="star-item on"></span> <span class="star-item on"></span> <span class="star-item on"></span> <span class="star-item half"></span> </div>
<template> <div class="star" :class="starType"> <span v-for="itemClass in itemClasses" :class="itemClass" class="star-item"></span> </div> </template>
星星計算比較巧妙(根據分數轉換爲星星數)
<script> //設置常量 const LENGTH = 5; const CLS_ON = 'on'; const CLS_HALF = 'half'; const CLS_OFF = 'off'; export default{ props: { size: { //傳入的size變量 type: Number //設置變量類型 }, score: { //傳入的score變量 type: Number } }, computed: { starType(){ //經過計算屬性,返回組裝過的類型,用來對應class類型 return 'star-' + this.size; }, itemClasses(){ let result = []; //返回的是一個數組,用來遍歷輸出星星 let score = Math.floor(this.score * 2) / 2; //計算全部星星的數量 let hasDecimal = score % 1 !== 0; //非整數星星判斷 let integer = Math.floor(score); //整數星星判斷 for (let i = 0; i < integer; i++) { //整數星星使用on result.push(CLS_ON);//一個整數星星就push一個CLS_ON到數組 } if (hasDecimal) { //非整數星星使用half result.push(CLS_HALF);//相似 } while (result.length < LENGTH) { //餘下的用無星星補全,使用off result.push(CLS_OFF);//相似 } return result; } } } </script>
star48_half@2x.png star48_half@3x.png star48_off@2x.png star48_off@3x.png star48_on@2x.png star48_on@3x.png
<style lang="scss" rel="stylesheet/scss"> @import "../../common/css/mixin"; .star { font-size: 0; .star-item { display: inline-block; background-repeat: no-repeat; } &.star-48 { //48尺寸的星星 .star-item { //每個星星的基本css信息 width: 20px; height: 20px; margin-right: 22px; //每個星星dom都有外邊距 background-size: 20px 20px; &:last-child { //最後一個的外邊距就是0 margin-right: 0; } &.on { //全星狀態的class @include bg-img('star48_on') } &.half { //半星狀態的class @include bg-img('star48_half') } &.off { //無星狀態的class @include bg-img('star48_off') } } } &.star-36 { .star-item { width: 15px; height: 15px; margin-right: 6px; background-size: 15px 15px; &:last-child { margin-right: 0; } &.on { @include bg-img('star36_on') } &.half { @include bg-img('star36_half') } &.off { @include bg-img('star36_off') } } } &.star-24 { .star-item { width: 10px; height: 10px; margin-right: 3px; background-size: 10px 10px; &:last-child { margin-right: 0; } &.on { @include bg-img('star24_on') } &.half { @include bg-img('star24_half') } &.off { @include bg-img('star24_off') } } } } </style>
備註:父組件food.vue傳入的數據
<ratingselect @select="selectRating" @toggle="toggleContent" :selectType="selectType" :onlyContent="onlyContent" :desc="desc" :ratings="food.ratings"></ratingselect>
@select="selectRating" @toggle="toggleContent"
,經過將字組件的方法和父組件的方法進行關聯,這樣就可以實現跨組件通信和操做:selectType="selectType":onlyContent="onlyContent" :desc="desc":ratings="food.ratings"
,這是經過pros傳入到子組件的屬性,將父組件的數據傳到子組件裏面,也帶有一種經過父組件來初始化子組件屬性的意思.<div class="ratingselect"> <!--有使用一個border-1px的mixin--> <div class="rating-type border-1px"> <!--綁定一個select方法控制切換,綁定class控制切換以後的按鈕樣式顯示--> <span @click="select(2,$event)" class="block positive" :class="{'active':selectType ===2}">{{desc.all}}<span class="count">{{ratings.length}}</span></span> <span @click="select(0,$event)" class="block positive" :class="{'active':selectType ===0}">{{desc.positive}}<span class="count">{{positives.length}}</span></span> <span @click="select(1,$event)" class="block negative" :class="{'active':selectType ===1}">{{desc.negative}}<span class="count">{{negatives.length}}</span></span> </div> <!--綁定一個toggleContent方法來控制有內容和無內容的顯示--> <div @click="toggleContent" class="switch" :class="{'on':onlyContent}"> <span class="icon-check_circle"></span> <span class="text">只看有內容的評價</span> </div> </div>
@click="select(2,$event)"
select方法傳入類型和事件,而後在methods裏面調用父組件的方法,實現子組件控制父組件的目的:class="{'active':selectType ===2}"
根據類型來肯定顯示的class,實現不一樣類型顯示不一樣樣式的目的positives.length
使用計算屬性自動計算類型數組的長度,用來顯示不一樣類型的數量@click="toggleContent" :class="{'on':onlyContent}"
toggleContent
控制是否展現有內容的rate,也是在methods裏面調用父組件的方法,實現子組件控制父組件的目的on
這個class來控制該按鈕的樣式const POSITIVE = 0; //設置顯示常量 const NEGATIVE = 1; const ALL = 2; export default{ props: { ratings: { //傳入ratings數組,跟food.ratings關聯 type: Array, default(){ return []; } }, selectType: { //跟selectType關聯,經過在父組件裏面設置這3個值來實現控制子組件的操做 type: Number, default: ALL }, onlyContent: { //跟onlyContent關聯 type: Boolean, default: true }, desc: { //跟desc關聯 type: Object, default(){ return { all: '所有', positive: '滿意', negative: '不滿意' } } } }, computed: { positives(){ //自動過濾rateType(正面的rate) return this.ratings.filter((rating) => { //js的filter函數會返回一個處理後的(爲true)結果的結果數組 return rating.rateType === POSITIVE; }) }, negatives(){ //自動過濾rateType(反面的rate) return this.ratings.filter((rating) => { return rating.rateType === NEGATIVE; }) } }, methods: { select(type, event) { // 選擇rateType而且通知父組件 if (!event._constructed) { return; } this.$emit('select', type); // 派發事件,父組件監聽此事件 }, toggleContent(event) { // 選擇是否顯示有內容的rate,而且通知父組件 if (!event._constructed) { return; } this.$emit('toggle'); } } }
(item,index) in goods
:class="{'current':currentIndex === index}"
是vue的綁定class的使用方法,經過綁定一個class變量來直接操做,而且這裏的邏輯會跟js代碼裏面對應
v-show
和 v-if
的區別官網已經說過
通常來講, v-if 有更高的切換開銷,而 v-show 有更高的初始渲染開銷。所以,若是須要很是頻繁地切換,則使用 v-show 較好;若是在運行時條件不太可能改變,則使用 v-if 較好。
$refs
的使用是vue操做dom的一種方式:
<food @add="addFood" :food="selectedFood" ref="food">
是經過selectFood方法寫入到vue實例裏面,而後傳給子組件food<shopcart ref="shopcart" :selectFoods="selectFoods"
這裏selectFoods被自動添加了count屬性,是爲了讓購物車更加簡單的計算已選擇的food這裏最關鍵的是menu和food兩個區域的對應處理:
_initScroll
和_calculateHeight
_calculateHeight
計算foods內部每個塊的高度,組成一個數組listHeight_initScroll
裏面,設置了bscroll插件的一個監聽事件scroll,將food區域當前的滾動到的位置的y座標設置到一個vue實例屬性 scrollY this.scrollY = Math.abs(Math.round(pos.y));
:class="{'current':currentIndex === index}
,實現聯動this.foodsScroll.scrollToElement(el, 300);
關於在selectMenu
中點擊,在pc界面會出現兩次事件,在移動端就只出現一次事件的問題:
緣由:
解決:
_constructed: true
,因此作處理,return 掉非bsScroll的事件index.scss是SCSS文件的入口文件,裏面使用 @import 引入各類SCSS文件
@import "./base"; @import "./mixin"; @import "./icon.css";
在入口文件main.js中全局引用index.scss
import 'common/css/index.scss';
eslint 是一個js代碼風格檢查器,配合vue-cli腳手架中的熱更新,能夠很方便的定位和提示錯誤。在公司多人協做開發時能夠確保代碼風格保持一致,能夠很方便的閱讀他人的代碼。
將 localhost 換成本身的ip (Windows在命令行執行ipconfig查看,mac執行ifconfig查看)
而後複製地址欄地址,進入草料二維碼,而後生成二維碼,而後用手機掃一掃就能夠查看了,前提是,你手機和電腦必須在同一個局域網。
克隆項目到本地 git clone git@github.com:nanyang24/eleme-vue.git 安裝依賴 npm install 本地開發,開啓服務器,瀏覽器訪問http://localhost:8080 npm run dev 構建生產 npm run build 運行打包文件 node prod.server.js 會看到 Listening at http://localhost:9000 在瀏覽器中打開便可