掘金小册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.阻碍热更新的错误,一般来说不会阻碍项目代码的处理,但是如果老出现会特别恶心,出现问题的原因,我们现在预览的项目是基于webpack
的devServer
在本地搭建的,并且代码改变时候能随时编译,刷新浏览器来实现,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>