代碼連接:GitHubjavascript
預覽連接:Git Pagescss
本項目的開發讓我瞭解並學習到如下幾點:html
1.在真實的開發工做環境與流程,一些項目結構的處理,讓其更容易維護前端
2.數據接口的封裝與切換,與上下游更好地協做vue
3.webpack 配置參數的一些原理和技巧java
4.在前端開發過程當中 mock 數據,更好地進行測試webpack
5.更全面地瞭解 Vue / vue-router / vuex 等ios
6.在項目開發過程使用了一些庫:qs / Swiper / mint-ui / ...git
7.把靜態頁面使用 Vue 重構github
實現功能:
首頁 展現輪播圖和商品列表
分類頁 展現不一樣商品的推介列表
商品詳情頁 顯示商品信息(包括價格、圖片、詳情等),可增長商品數量並加入購物車
購物車 可增長商品數量,對商品可刪除、批量刪除,價格實時演算
我的頁面 可管理我的收收貨地址(包括刪除、增長、修改、設爲默認地址等)
頁面渲染流程:
API 拿到數據 -> 渲染頁面
沒有真實數據的狀況下 -> Mock 數據 -> 使用 API 拿到數據 -> 渲染頁面
頁面重構:
把原 HTML 的內容放進對應的 Vue 組件中,引入 CSS,肯定樣式,再獲取數據,渲染頁面。
接下來概括整理一下開發過程當中學習到的知識點和踩的坑。
項目構建方面處理:在使用 vue-cli 構建項目後對目錄結構和 webpack 配置作一個調整。
基於 vue-cli 把單頁面應用搭建成多頁面應用:
修改目錄結構
修改 webpack 配置
參考:
在 build/webpack.base.conf.js
中的 resolve 能夠設置路徑或模塊的別名:
...... resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), 'components': '@/components', 'pages': '@/pages', 'js': '@/modules/js', 'css': '@/modules/css', 'sass': '@/modules/sass', 'imgs': '@/modules/imgs' } } ......
在其餘地方引用:
import Hello from 'components/Hello'
參考:[webpack resolve]
<!-- DNS預解析 --> <link rel="dns-prefetch" href="https://dn-kdt-img.qbox.me/"> <link rel="dns-prefetch" href="https://img.yzcdn.cn/"> <link rel="dns-prefetch" href="https://b.yzcdn.cn/"> <link rel="dns-prefetch" href="https://su.yzcdn.cn/"> <link rel="dns-prefetch" href="https://h5.youzan.com/v2/"> <link rel="dns-prefetch" href="https://h5.youzan.com/">
可以減小用戶點擊連接時的延遲。
在真實開發環境中,前端須要經過 API 接口獲取數據,從而把數據渲染在頁面上,那麼能夠這樣寫:
// api.js // 開發環境和真實環境的切換 let url = { hotLists:'/index/hotLists', banner:'/index/banner' } let host = 'http://rap2api.taobao.org/app/mock/7058' for (let key in url){ if(url.hasOwnProperty(key)){ url[key] = host + url[key] } } export default url
先使用 mock 數據的接口獲取數據,進行開發和測試,在與後端對接的時候再替換真實的數據接口。
問題 使用命令 npm i mint-ui -S
安裝了 mint-ui 後,在 babelrc 中作了相應的配置,引用後報錯,提示找不到模塊:
解決辦法:npm start
重啓服務器。
使用 mint-ui 的 Infinite scroll,使頁面的推薦商品列表下拉到底部時能夠自動獲取並加載數據,實現無限滾動。
使用 Swiper 實現首頁輪播組件:
1.在首頁組件中,在 created 階段獲取 banner 的數據
2.經過 props 傳遞數據給 swipe 組件
3.swiper 接收數據,渲染到模板中,完成輪播
可是其中要注意數據獲取和生命週期的問題:
由於 swipe 組件中的 Swiper 插件依賴於 dom 節點,而 dom 節點是在 mounted 時被掛載的,這也就要求了在 swipe 組件中,當生命週期來到 mounted 的時候,他必須拿到數據,才能使 Swiper 組件拿到 dom 節點,操做輪播;當父組件中經過(異步)獲取到 banner 的數據並傳遞給 swipe 組件時,能夠在父組件中作以下設置:
<!-- index.html --> <swipe :lists='bannerLists' v-if='bannerLists'></swipe>
只有在 bannerLists 數據不爲 null 的時候,這個 swipe 的組件才能夠顯示,這也就保證了數據能夠正常傳遞, Swiper 也能夠在 mounted 的時候拿到 dom 節點。
問題 使用 npm run dev
打開 http://localhost:8080/#/ 調試代碼時,老是一刷新就進入 debugger 狀態:
解決辦法:
1.打開 source 面板,把 Any XHR 勾選去掉
2.paused on exception
從分類頁跳轉到列表頁:
1.傳遞參數及跳轉
// category.js toSearch(list){ location.href = `search.html?keyword=${list.name}$id=${list.id}` }
2.使用 qs 讀取url參數:
// search.js import qs from 'qs' let {keyword,id} = qs.parse(location.search.substr(1))
混入 (mixins) 是一種分發 Vue 組件中可複用功能的很是靈活的方式。混入對象能夠包含任意組件選項。當組件使用混入對象時,全部混入對象的選項將被混入該組件自己的選項。
把一些公用的函數/方法抽離出來,放進 mixin.js:
// mixin.js import Foot from 'components/Foot.vue' let mixin = { filters:{ number(price){ return price = price.toFixed(2) } }, components:{ Foot, }, } export default mixin
在組件中引用 mixin:
import mixin from 'js/mixin' new Vue({ ... mixins:[mixin] ... })
這樣就能夠直接在組件中對函數/方法進行復用了。
使用 velocity 實現「回到頂部」動畫過渡:
安裝:npm i velocity-animate
引用:import Velocity from 'velocity-animate'
使用:
new Vue({ ... methods:{ toTop(){ // 第一個參數:動做元素 第二個參數:動做事件 Velocity(document.body,'scroll',{duration:1000}) } } })
問題:使用 touchmove 監聽頁面:
<div class="container with-top-search" style="min-height: 667px;" @touchmove='move'>...</div>
根據距離頁面頂部距離的大小,肯定某個元素是否展示:
data:{ toShow:false }, move(){ if(document.documentElement.scrollTop > 100){ console.log(1) this.toShow = true }else{ console.log(2) this.toShow = false } },
頁面划動是有效的,可是結果一直取不到 document.body.scrollTop 的值。
解決方法:使用 document.documentElement.scrollTop
因爲在不一樣狀況下,document.body.scrollTop與document.documentElement.scrollTop都有可能取不到值
參考文章:https://segmentfault.com/a/1190000008065472
在項目首頁中,有一個圖片輪播組件,用於展現一個具體商品,點擊會跳轉到不一樣的頁面;
而在詳情頁中,也有一個商品圖片輪播,項目須要這個組件繼續沿用首頁的輪播組件,可是他的圖片、點擊後跳轉、經過 API 所獲取的數據結構均和首頁輪播組件不一樣,這時候該怎麼處理傳入輪播組件的數據:
1.首先應該分析一下輪播組件須要接收的數據:一個數組,數組裏包含 N 個對象,包含鍵 clickUrl(值爲點擊圖片後跳轉的的url)和鍵 img(值爲圖片url)
2.對 API 獲取的將要傳入的數據作一層處理,讓輪播組件只接收一種統一的格式:
new Vue({ el:'#app', data:{ details:null, detailTab, currentTab:0, dealList:null, bannerLists:null }, created(){ this.getDetails() }, methods:{ getDetails(){ axios.get(url.details,{id}).then(res=>{ // 經過API獲取的原數據 details this.details = res.data.data // 須要傳入組件的數據 bannerLists this.bannerLists = [] this.details.imgs.forEach(item => { // 把 bannerLists 數組中的值改成對象 this.bannerLists.push({ clickUrl:'', img:item }) }) }) }, }, })
最後再把數據傳遞給輪播組件:<swipe :lists='bannerLists' v-if='bannerLists'></swipe>
當線上接口平臺鏈接不穩定的時候,可使用 mockjs 模擬 mock 數據。
安裝:npm i mockjs
引入:
import Mock from 'mockjs' let Random = Mock.Random let data = Mock.mock({ 'cartList|3':[{ 'goodsList|1-5':[{ id:Random.int(10000,100000), img:Mock.mock('@Img(90x90,@color)') }] }] }) console.log(data)
場景:在購物車頁面,向左划動商品欄時出現相關操做按鈕(增減商品數量,刪除);向右划動恢復原狀。
在元素上綁定 touchstart 和 touchend 事件,並設置 ref 值用於獲取須要操做的商品節點:
<li class="block-item block-item-cart " v-for="(good,goodIndex) in shop.goodsList" :class="{editing:shop.editing}" :ref="'goods-'+ shopIndex + '-' + goodIndex" @touchstart="start($event,good)" @touchend="end($event,shopIndex,good,goodIndex)">...</li>
配合 velocity ,根據划動距離操做節點:
methods:{ ... start(e,good){ // 拿到初始值的座標 good.startX = e.changedTouches[0].clientX }, end(e,shopIndex,good,goodIndex){ // 拿到結束值的座標 let endX = e.changedTouches[0].clientX let left = '0' if(good.startX - endX > 100){ left = '-60px' } if(endX - good.startX > 100){ left = '0px' } // 使用 velocity 操做節點 Velocity(this.$refs[`goods-${shopIndex}-${goodIndex}`], {left}) } ... }
問題:當商品列表中的某款商品被刪除後,某些樣式會繼續殘留在該列表的下一款商品中,如:
問題緣由: 商品列表使用了 v-for 來渲染,而v-for 模式使用「就地複用」策略,簡單理解就是會複用原有的dom結構,儘可能減小dom重排來提升性能,當商品刪除後,列表中的剩餘商品就會複用被刪除商品的 dom 結構,因此會產生這種現象。
當 Vue.js 用 v-for 正在更新已渲染過的元素列表時,它默認用「就地複用」策略。若是數據項的順序被改變,Vue 將不會移動 DOM 元素來匹配數據項的順序, 而是簡單複用此處每一個元素,而且確保它在特定索引下顯示已被渲染過的每一個元素。
解決方法:
1.在刪除了商品後,從新操做節點,返回原來的位置(還原dom)。this.$refs[`goods-${shopIndex}-${goodIndex}`][0].style.left = '0px'
2.給遍歷的節點設置一個惟一的 key 屬性:
<li v-for="(good,goodIndex) in shop.goodsList" :key="good.id"></li>
爲了給 Vue 一個提示,以便它能跟蹤每一個節點的身份,從而重用和從新排序現有元素,你須要爲每項提供一個惟一 key 屬性。理想的 key 值是每項都有的惟一 id。
在真實開發過程當中,對請求接口進行封裝,方便調用。
// fetch.js import url from 'js/api.js' import axios from 'axios' function fetch(method='get',url, data) { return new Promise((resolve, reject) => { axios({method, url, data}).then(res => { let status = res.data.status if (status === 200) { resolve(res) } if (status === 300) { location.href = 'login.html' resolve(res) } }).catch(err => { reject(err) }) }) } export default fetch
在具體場景中,把對於數據請求的操做放在 Service 中,在別的地方調用的時候傳參便可:
// cartService.js import url from 'js/api.js' import fetch from './fetch.js' class Cart { // 增長商品數量 static add(id){ return fetch('post',url.cartAdd,{ id, number:1 }) } // 減小商品數量 static reduce(id){ return fetch('post',url.cartReduce,{ id, number:1 }) } // 刪除商品 static remove(id){ return fetch('post',url.cartRemove,{id}) } } export default Cart
這樣就能夠省略不少步驟,也讓流程更爲清晰:
import Cart from 'js/cartService.js' add(good){ // axios.post(url.cartAdd,{ // id:good.id, // number:1 // }).then(res=>{ // good.number++ // }) Cart.add(good.id).then(res=>{ good.number++ }) },
路由管理 / 嵌套路由:
在「會員頁面」下有「個人設置」和「收貨地址管理」,「收貨地址管理」下有子路由「地址列表」和「新增/編輯地址」,進入「收貨地址管理」默認重定向到「收貨地址列表」:
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) let routes = [ { // 默認顯示頁面 path:'/', components:require('./components/member.vue') }, { // 收貨地址管理 path:'/address', components:require('./components/address.vue'), children:[ { path:'', redirect:'all' }, { // 地址列表 path:'all', components:require('./components/all.vue') }, { // 新增/編輯地址 path:'form', components:require('./components/form.vue') } ] } ] let router = new Router({ routes }) new Vue({ el:'#app', router })
由於「新增地址」和「編輯地址」所用的組件時同一個,因此就要在進入組件的路由參數上作一些設置,讓組件能夠區分用戶是須要「新增地址」仍是「編輯地址」。
1.首先完善路由信息,增長 name 字段:
{ path:'form', name:'form', components:require('./components/form.vue') }
2.根據不一樣的需求,路由跳轉攜帶不一樣的參數:
// 新增地址 type 爲 add <router-link :to="{name:'form',query:{type:'add'}}" >新增地址</router-link> // 編輯地址 type 爲 edit,同時接收一個實例參數:選擇須要修改的地址信息 <a @click="toEdit(list)"></a> toEdit(list){ this.$router.push({name:'form',query:{ type:'edit', instance:list }}) }
3.同時給組件設置一些初始值,用於 v-model 綁定數據,提交修改:
export default { data(){ return { name:'', tel:'', provinceValue:-1, cityValue:-1, districtValue:-1, address:'', id:'', type:'', instance:'' } }, created() { let query = this.$route.query this.type = query.type this.instance = query.instance if(this.type === 'edit'){ let ad = this.instance this.provinceValue = parseInt(ad.provinceValue) this.name = ad.name this.tel = ad.tel this.address = ad.address this.id = ad.id } }, }
接着根據需求渲染數據便可。
在「我的地址管理頁面」中使用 vuex 管理狀態和數據:
1.首先建立 store,其中包含一些初始值的設置、獲取數據的方法、更改狀態和數據的方法
// vuex/index.js import Vue from 'vue' import Vuex from 'vuex' import Address from 'js/addressService.js' Vue.use(Vuex) const store = new Vuex.Store({ state:{ lists:null }, mutations:{ init(state,lists){ state.lists = lists } }, actions:{ getLists({commit}){ Address.list().then(res=>{ // this.lists = res.data.lists store.commit('init',res.data.lists) }) } } }) export default store
2.注入 Vue 實例:
import Vue from 'vue' import router from './router/index.js' import store from './vuex' import './member.css' new Vue({ el:'#app', router, store })
3.先在 created 階段執行this.$store.dispatch('getLists')
,更新數據到 state,而後經過 computed 拿到 state 中的 數據,在組件中渲染數據渲染:
created() { // Address.list().then(res=>{ // this.lists = res.data.lists // }) this.$store.dispatch('getLists') }, computed:{ lists(){ return this.$store.state.lists } }
需求:在使用 vuex 管理狀態和數據的過程當中,有一些對於數據列表的增刪改的操做,每當完成這些操做後頁面須要跳轉到某個頁面。
方法:使用 watch 監聽數據列表,一旦監測到數據列表增減,則跳轉。
在實際過程當中,數據的增減確實是能夠引起跳轉行爲,可是列表中(列表項是對象)某個屬性的更改則不會引起跳轉。
解決方法:
1.對數據列表進行深度監聽
爲了發現對象內部值的變化,能夠在選項參數中指定 deep: true 。注意監聽數組的變更不須要這麼作。
watch:{ lists:{ handle(){ this.$router.go(-1) }, deep:true }, }
在設置了深度監聽後,發現問題仍是沒有獲得解決,那是由於監聽對象是從 state 獲得的 lists,當在 mutations 裏對這個 lists 的成員進行其屬性的某些操做的時候,依然沒有監聽到屬性值的改變。
因此,須要對這個 lists 進行深拷貝,當拷貝對象完成對數據的處理後,再把他賦值給 state.lists:
2.對監聽對象進行深拷貝
// vuex/index.js update(state,instance){ // 經過 instance 的 id 找到 let lists = JSON.parse(JSON.stringify(state.lists)) let index = lists.findIndex(item =>{ return item.id === instance.id }) lists[index] = instance state.lists = lists },
vuex 配合 webpack 實現熱重載功能,提升開發效率(前提:state/mutations/actions 被作爲模塊引入 store):
好比配置了 mutations 的熱重載,你添加新的 mutations 方法的時候就不會刷新頁面,而是加載一段新的js,不配頁面就會刷新
/ store.js import Vue from 'vue' import Vuex from 'vuex' import mutations from './mutations' import moduleA from './modules/a' Vue.use(Vuex) const state = { ... } const store = new Vuex.Store({ state, mutations, modules: { a: moduleA } }) if (module.hot) { // 使 action 和 mutation 成爲可熱重載模塊 module.hot.accept(['./mutations', './modules/a'], () => { // 獲取更新後的模塊 // 由於 babel 6 的模塊編譯格式問題,這裏須要加上 `.default` const newMutations = require('./mutations').default const newModuleA = require('./modules/a').default // 加載新模塊 store.hotUpdate({ mutations: newMutations, modules: { a: newModuleA } }) }) }
在將項目部署到 Git Pages 的時候,出現了一個問題:
緣由是 GitPages 是 HTTPS 頁面的,而調用接口獲取數據的 API 是 HTTP 的,HTTPS 頁面裏動態的引入 HTTP 資源,好比引入一個js文件,會被直接block掉的.在 HTTPS 頁面裏經過 AJAX 的方式請求 HTTP 資源,也會被直接block掉的。
搜索了一下資料,按照 stackoverflow 的答案,給 index.html 的 head 加上了一個 meta 標籤,意思是自動將http的不安全請求升級爲https:
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
產生了兩個結果:
1.本地調試獲取不到 index.js:
2.GitPages 中的接口轉換成了 HTTPS,可是接口沒有對應的 https 資源,於事無補:
因此只能買一個域名,而後配置 http 的協議,再解析到 Git Pages 上。
參考: