代码连接: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 上。
参考: