项目实战之vue掘金小册`WebApp`

掘金小册WebApp

(一)功能实现

  • Vue-cli/vue/vant等技术实战应用
  • Vue-Router路由加载和组件的动态缓存
  • 基于骨架屏技术来优化移动端白屏时间
  • 上拉刷新/下拉加载功能实现及无线列表性能优化
  • 小册首页/产品详情/内容阅读等功能的开发

(二)关键技术分析

1.通过路由信息判断头部和尾部的显示和隐藏,与传统的组件化处理不太一样。

\*APP.vue*\
<template>
  <div id="app">
    <!-- 头部 -->
    <van-nav-bar v-if="isNav" :title="navText" left-text="返回" left-arrow @click-left="onClickLeft" />

    <!-- 内容 -->
    <div class="mainBox" :style="sty">
      <keep-alive include="Home">
        <router-view></router-view>
      </keep-alive>
    </div>

    <!-- 底部 -->
    <van-grid :column-num="3" v-if="isFooter">
      <van-grid-item icon="home-o" text="全部小册" to="/" />
      <van-grid-item icon="shopping-cart-o" text="购买" to="/buy" />
      <van-grid-item icon="contact" text="我" to="/info" />
    </van-grid>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isFooter: true,
      isNav: true,
      navText: "",
      sty: {
        paddingBottom: "1.4rem",
        paddingTop: "0rem"
      }
    };
  },
  methods: {
    // 返回上级路由
    onClickLeft() {
      this.$router.go(-1);
    },
    // 控制头尾样式
    show() {
      let { path } = this.$route;
      if (/(content|detail)/.test(path)) {
        this.isFooter = false;
        this.isNav = true;
        this.sty = {
          paddingBottom: "0rem",
          paddingTop: "1.12rem"
        };
        this.navText = path.includes("/detail") ? "内容详情" : "章节阅读";
        return;
      }
      this.isFooter = true;
      this.isNav = false;
      this.sty = {
        paddingBottom: "1.4rem",
        paddingTop: "0rem"
      };
    }
  },
  // 第一次加载 或者 路由改变  都去计算一下头部底部
  watch: {
    $route() {
      this.show();
    }
  },
  beforeMount() {
    this.show();
  }
};
</script>

2.解决图片防盗问题。

数据是直接调用掘金官方的,而掘金官方对图片做了限制,直接访问会报403错误,以后在真实项目中你也想用别的网站图片,直接通过网站处理的时候,可能都会导致图片不能展示,报403权限问题,原因是什么?原因是在我们当前127.0.0.1:8082本地域名端口号向掘金发请求的时候,默认会在请求头里面加一个referer:http://127.0.0.1:8082/,这里面记录的是当前的这个源,我们向掘金服务器发请求,我们通过referer,把信息传给掘金,掘金拿到信息后,发现我们在的域名并不在它的白名单里,它是不允许我们访问图片的,这叫图片防盗。如何能在别人设置图片防盗的时候取消防盗?很简单,以后发请求不加referer,如何设置,在public/index.html文件头部添加下面代码即可。

/*public/index.html*/
<meta name="referrer" content="no-referrer">

在这里插入图片描述

3.一般来说通过脚手架生成的Webpack配置项并不是符合要求的,拿我们今天项目来说,通过改一些配置来完成我们需求。默认情况下,会加一个ESlint的东西,这个东西在开发的时候特别恶心。定义一个变量给你报错,一个空格也给你报错,不能让你编译成功,一般习惯把它去掉,所以新建一个vue.config.js文件修改vue脚手架配置,通过lintOnSave=false,在开发环境下取消 eslint的检测功能,不仅能使我们的开发速度更快一些,同时可以避免一些非常恶心的操作。

module.exports = {
    lintOnSave:false,
};

4.通过 devServer基于proxy代理实现跨域

module.exports = {
    lintOnSave:false,
    devServer:{
        proxy:{
            '/':{
              target:'https://xiaoce-timeline-api-ms.juejin.im/v1',
              changeOrigin:true
            }
        }
    }
}

5.阻碍热更新的错误,一般来说不会阻碍项目代码的处理,但是如果老出现会特别恶心,出现问题的原因,我们现在预览的项目是基于webpackdevServer在本地搭建的,并且代码改变时候能随时编译,刷新浏览器来实现,devServer和本地服务的是如何处理的,其实是基于socket技术来实现的相对来说比较好的效果,但由于是基于soket的源默认是本地IP,一般网上解决方案是把node-modules里热更新的关掉,如果想让它热更新但不想让它报错,如果对webpack相对来说比较熟,可以配置scokHost: '127.0.0.1',指定热更新的源。之所以出问题的原因是我们预览的时候用的是 loaclhost,但是代码 热更新默认是基于本地IP。当两者不一样的时候就会出现这个问题。我们可以收动配置scokHost即可。

在这里插入图片描述

 scokHost: 'localhost'

在这里插入图片描述

5.Axios的二次配置。比较大的项目中,我们需要向很多后台发请求,可能需要传递的格式等都不太一样,创建一个axios.create()单独的实例。

/*axios.js*/
import axios from 'axios';
import qs from 'qs';

// 比较大的项目中,我们需要向很多后台发请求,可能需要传递的格式等都不太一样
// 创建一个axios.create()单独的实例
const instance = axios.create();
instance.defaults.baseURL = "";
instance.defaults.withCredentials = true;
//post请求下headers应该改的
instance.defaults.headers.post['Content-Type'] = "application/x-www-form-urlencoded";
//post请求下通过请求主体传给服务器的信息
instance.defaults.transformRequest = data => qs.stringify(data);
instance.interceptors.response.use(response => {
    return response.data;//只把响应主体信息交给响应的逻辑层
}, reason => {
    //失败的原因做一些统一操作
    return Promise.reject(reason);
});

export default instance;
/*book.js*/
import axios from './axios';
function getListByLastTime(options = {}){
   options = Object.assign({
       src:web,
       alias:'',
       pageNum: 1
   },options);

   return axios.get('/getListByLastTime',{
       params:options
   });
}

export default{
    getListByLastTime
};
/*index.js*/
import book from './book';
const api = {
    book
};
export default api;

因为所有组件都是Vue这个类的实例,所以我们可以把api这个变量在main.js中挂在vue原型上,这样在每个组件通过this.$api来调用。

import api from './api';

Vue.prototype.$api = api;

6.性能优化点:路由懒加载

标准化路由,进来之后导入所有组件,我们通过地址调入路由给不同组件进行渲染,但这种方案会有一个问题,这种方案会默认把所有组件中的代码最终会合并成一个JS,但是如果项目特别大,会导致js特别大,刚开始第一次请求页面的时候,要拉一个很大JS,会让我们页面首次加载速度变慢,导致很长时间白屏,这都是很好的方式,我们一般都用路由懒加载,这时候完全可以把home这个组件处理了,剩下的组件只有我点它,它才会处理。

路由懒加载: 文件的切割,把组件的代码单独打包为独立的JS =>对页面第一加载的性能体验有很大的帮助。

懒加载有三种方式: ①常用的import方式。②require的方式。③插件的方式。

import Vue from 'vue';
import VueRouter from 'vue-router';

/* 导入需要渲染的组件 */
import Home from "../views/Home.vue";

/* 路由懒加载:文件的切割,把组件的代码单独打包为独立的JS  =>对页面第一加载的性能体验有很大的帮助 */

Vue.use(VueRouter);
const router = new VueRouter({
	mode: 'hash',
	routes: [{
		path: '/',
		component: Home
	}, {
		path: '/detail/:id',
		component: () => import( /*webpackChunkName:"component"*/ '../views/Detail.vue')
	}, {
		path: '/content/:id/:sectionId',
		component: () => import( /*webpackChunkName:"component"*/ '../views/Content.vue')
	}, {
		path: '/info',
		component: () => import( /*webpackChunkName:"component"*/ '../views/MyInfo.vue')
	}, {
		path: '/buy',
		component: () => import( /*webpackChunkName:"component"*/ '../views/MyBuy.vue')
	}, {
		path: '*',
		redirect: '/'
	}]
});
export default router;

/*webpackChunkName:"component"*/的原因,现在每个组件都单独打包个js,但是项目中如果有1000个呢?要单独打包1000个js,这样不好。有些东西直接合并到app.js,比如本项目中的home组件的js,把剩下的所有组件统一打包到一个JS中,不是每个组件一个js,需要我们配置/*webpackChunkName:"component"*/”**这是项目中为数不多的注释还有作用的。

7.首页开发及优化项

(1)Object.freeze性能优化

如下图你发现数组里的每一项都getter和setter了,因为vue2.0响应式原理做了object.deferproperty用于监听和数据劫持,进行数据劫持的时候会深度劫持,例如本例中会对types的每个属性和成员都进行getter和setter,好处是以后通过type[].xx=xx更改值的时候通知组件重新渲染。坏处是深度监听,每次处理都拿过来校验,浪费时间。但是本例中的types值不会更改,只是为了能够第一次渲染出来,不需要后期更改,还需要监听劫持它吗?把数据放到状态里的第一个作用是data里状态的信息是能够放到组件里重新渲染的,第二个作用是我修改信息能通知组件重新渲染,而且我在组件视图中更改一些信息,也能控制状态更改,MVVM。通过底层数据劫持才能实现双向数据绑定,现在只想让它渲染,我不需要更改状态data使组件重新渲染,这时候就没有必要劫持了,这时候就可以通过Object.freeze把不需要后期更改状态通知组件重新渲染的数据冻结了,types本身还是要冻结的,只是把types内层的数据冻结了。
在这里插入图片描述

在这里插入图片描述
(2)骨架屏技术

骨架屏技术的目的:让页面第一次渲染速度更快,在没有加载出页面内容的时候有一个占位或者loading的效果,减少白屏的时间。

【服务器骨架屏】

  • 服务器渲染 SSR (项目是办分离开发:首屏服务器渲染,其余屏幕在首屏加载完成后,由客户端渲染)。
  • 首屏数据是我们直接基于ajax从服务器获取的,但是获取的结果中就包含了样式结构和数据(完全分离)。

【客户端骨架屏】

  • 其实就是一个loading和占位图。
  • 开始也只请求首屏的数据(图片或者其他屏幕的数据都延迟一下)。

(3)下拉刷新,通过vant组件库中

<van-list
      v-else
      v-model="loading"
      :finished="finished"
      finished-text="数据已经全部加载完毕"
      @load="loadMore"
    >
 </van-list>

(4) 长列表优化项

从服务器拿回来的数据每一项不需单独要修改,不需要把每一项都get和set进行劫持,所以把从服务器拿到的数据每一项都冻结,冻结之后再处理。

在这里插入图片描述

d = d.map(item => Object.freeze(item));

(5)优化项:keep-alive组件缓存

当从首页点到详情,从详情页返回到首页,你会发现数据会重新拉一次,因为会有组件重新渲染的过程。跳转路由,当前组件销毁坏,再跳转回来组件重新渲染,组件重新渲染,请求也会重新发起。所以要对组件做缓存,则应该设置keep-alive,只对首页缓存则应该设置include = “Home”,动态组件缓存怎么做?

    <div class="mainBox" :style="sty">
      <keep-alive include="Home">
        <router-view></router-view>
      </keep-alive>
    </div>
<template>
  <div class="homeBox">
    <!-- NAV -->
    <nav class="navBox">
      <div class="wrapper">
        <a
          href="javascript:;"
          v-for="item in types"
          :key="item.value"
 <!-- 当前循环的value和当前当前选中的一不一样,如果一样,就是true,就有active样式,如果没有,就是false,就没有active样式。 -->
          :class="{active:item.value===active}"
          @click="changeNav(item.value)"//把当前点击的value值传过去。
        >{{item.name}}</a>
      </div>
    </nav>

    <!-- LIST -->
    <van-skeleton title avatar :row="4" v-if="bookData.length===0"></van-skeleton>
    <van-list
      v-else
      v-model="loading"
      :finished="finished"
      finished-text="数据已经全部加载完毕"
      @load="loadMore"
    >
      <ul class="listBox">
        <li class="item" v-for="item in bookData" :key="item.id">
          <router-link :to="{path:`/detail/${item.id}`}">
            <div class="pic">
              <img :src="item.img" alt />
            </div>
            <div class="con">
              <h4>Python数据分析实战:构建股票量化交易系统</h4>
              <p>walfud</p>
              <p>15小节 ▪ 186人购买</p>
            </div>
            <div class="price">129.9</div>
          </router-link>
        </li>
      </ul>
    </van-list>
  </div>
</template>

<script>
//类别数据:对于不需要更新或者更新后不需要通知组件渲染的数据,我们不让其数据劫持=>基于Object.freeze冻结它
let types = [
  {
    value: "",
    name: "全部"
  },
  {
    value: "frontend",
    name: "前端"
  },
  {
    value: "backend",
    name: "后端"
  },
  {
    value: "mobile",
    name: "移动开发"
  },
  {
    value: "blockchain",
    name: "区块链"
  },
  {
    value: "general",
    name: "通用"
  }
];

export default {
  name: "Home",
  data() {
    return {
      // 页卡切换
      types: Object.freeze(types),
      active: "",
      // 数据处理
      bookData: [],
      pageNum: 1,
      loading: false,
      finished: false
    };
  },
  methods: {
    // 页卡切换
    changeNav(val) {
      this.active = val;
      this.pageNum = 1;
      this.bookData = [];
      this.queryData();
    },
    // 请求数据
    async queryData() {
      this.loading = true;
      let { d } = await this.$api.book.getListByLastTime({
        pageNum: this.pageNum,
        alias: this.active
      });
      this.loading = false;
      if (d && d.length > 0) {
          console.log(d)
        d = d.map(item => Object.freeze(item));
        this.bookData.push(...d);//为什么不用concat合并数组?因为concat不属于vue为我们提供的响应式方法
        return;
      }
      // 加载完毕
      this.finished = true;
    },
    // 加载更多数据
    loadMore() {
      this.pageNum++;
      this.queryData();
    }
  },
  // 开始加载组件
  created() {
    this.queryData();
  }
};

</script>

<style lang="less" scoped>
.homeBox {
  background: #fff;
  min-height: 100vh;
}

.navBox {
  border-bottom: 0.02rem solid #ebedf0;
  overflow: auto;
  margin-bottom: 0.2rem;

  .wrapper {
    font-size: 0;
    width: 140%;

    a {
      display: inline-block;
      font-size: 0.38rem;
      color: #555;
      padding: 0 0.4rem;
      line-height: 1rem;

      &.active {
        color: #1989fa;
      }
    }
  }
}

.van-skeleton {
  margin: 0.4rem 0;
}

.listBox {
  background: #fff;
  .item {
    position: relative;
    padding: 0.28rem 0.32rem;
    border-bottom: 0.02rem solid #ebedf0;

    &:nth-last-child(1) {
      border-bottom: none;
    }

    a {
      display: block;
      display: flex;
      justify-content: flex-start;
      align-items: flex-start;
      color: #555;
    }

    .pic {
      box-sizing: border-box;
      width: 1.6rem;
      box-shadow: 0.1rem 0.1rem 0.1rem #aaa;
      background: #909090;

      .van-image,
      img {
        display: block;
        width: 100%;
      }
    }

    .con {
      margin-left: 0.3rem;
      width: 3.5rem;

      h4,
      p {
        line-height: 0.6rem;
      }

      h4 {
        font-size: 0.36rem;
      }

      p {
        font-size: 0.32rem;
      }
    }

    .price {
      position: absolute;
      top: 50%;
      right: 0.4rem;
      transform: translateY(-50%);
      padding: 0 0.3rem;
      height: 0.8rem;
      line-height: 0.8rem;
      text-align: center;
      background: #f0f7ff;
      font-size: 0.36rem;
      color: #1989fa;
      border-radius: 0.8rem;
    }
  }
}
</style>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值