• Vue 重构有赞商城


    Vue.js 重构移动端有赞商场

    代码链接:GitHub

    预览链接:Git Pages

    本项目的开发让我了解并学习到以下几点:

    1.在真实的开发工作环境与流程,一些项目结构的处理,让其更容易维护

    2.数据接口的封装与切换,与上下游更好地协作

    3.webpack 配置参数的一些原理和技巧

    4.在前端开发过程中 mock 数据,更好地进行测试

    5.更全面地了解 Vue / vue-router / vuex 等

    6.在项目开发过程使用了一些库:qs / Swiper / mint-ui / ...

    7.把静态页面使用 Vue 重构

    实现功能:

    首页 展示轮播图和商品列表

    分类页 展示不同商品的推介列表

    商品详情页 显示商品信息(包括价格、图片、详情等),可增加商品数量并加入购物车

    购物车 可增加商品数量,对商品可删除、批量删除,价格实时演算

    个人页面 可管理个人收收货地址(包括删除、增加、修改、设为默认地址等)

    页面渲染流程:

    API 拿到数据 -> 渲染页面

    没有真实数据的情况下 -> Mock 数据 -> 使用 API 拿到数据 -> 渲染页面

    页面重构:

    把原 HTML 的内容放进对应的 Vue 组件中,引入 CSS,确定样式,再获取数据,渲染页面。

    接下来归纳整理一下开发过程中学习到的知识点和踩的坑。


    项目构建方面处理:在使用 vue-cli 构建项目后对目录结构和 webpack 配置做一个调整。

    多页面应用调整

    基于 vue-cli 把单页面应用搭建成多页面应用:

    • 修改目录结构

    • 修改 webpack 配置

    commit

    参考:

    基于vue-cli搭建一个多页面应用

    基于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/">
    

    能够减少用户点击链接时的延迟。

    • mock 数据接口的处理

    在真实开发环境中,前端需要通过 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 数据的接口获取数据,进行开发和测试,在与后端对接的时候再替换真实的数据接口。

    • mint-ui

    问题 使用命令 npm i mint-ui -S 安装了 mint-ui 后,在 babelrc 中做了相应的配置,引用后报错,提示找不到模块:

    报错

    解决办法:npm start 重启服务器。

    • Infinite scroll

    使用 mint-ui 的 Infinite scroll,使页面的推荐商品列表下拉到底部时可以自动获取并加载数据,实现无限滚动。

    commit

    • 轮播组件

    使用 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


    URL跳转

    从分类页跳转到列表页:

    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))
    

    mixin

    混入 (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

    使用 velocity 实现「回到顶部」动画过渡:

    安装:npm i velocity-animate

    引用:import Velocity from 'velocity-animate'

    使用:

    new Vue({
        ...
        methods:{
            toTop(){
                // 第一个参数:动作元素 第二个参数:动作事件
                Velocity(document.body,'scroll',{duration:1000})
            }
        }
    })
    

    touchmove

    问题:使用 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)
    

    mockjs

    • $refs

    场景:在购物车页面,向左划动商品栏时出现相关操作按钮(增减商品数量,删除);向右划动恢复原状。

    在元素上绑定 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)

    在「个人地址管理页面」中使用 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 上。

    • Git Pages 配置 http 域名

    参考:

    GitHub 绑定域名

    segmentfault 绑定域名

    GitHub 绑定域名

  • 相关阅读:
    Oracle-增加字段
    Oracle数据库将varchar类型的字段改为Clob类型
    将Oracle数据库字段长度进行修改
    http请求util
    读取excel文件后,将一行数据封装成一个对象,多行返回一个map对象即可
    使用tushare 库查阅交易日历
    python winsound模块
    python可视化:matplotlib系列
    期货、股指期权、ETF期权
    股指期货
  • 原文地址:https://www.cnblogs.com/No-harm/p/9942838.html
Copyright © 2020-2023  润新知