目录
-
为何优化项目
- 我们的项目写好之后,为了优化用户的体验和减轻服务器的压力,我们回头一系列的优化,使自己的项目加载更快,所以要进行代码的优化
- 而优化的方式有很多,具体如下
- 路由懒加载
- 组件缓存
- 用户无感觉刷新token(登录后完成未完成的操作)
- 图片懒加载
- 减少代码的冗余(复用的代码封装函数)
- 为了后期的维护,可以封装以下工具文件
- 增删改查token
- 封装api接口(各种接口可以再分文件)
- 引入的第三方工具包(类似于day.js,moment..)
- 本地储存增删改
- 引入的组件库(vant..)
- 各种功能组件文件(首页为首页组件,个人信息为个人信息组件...)
- 自定义指令
- 网络请求(axios,jq...)
- Vuex , router
- 全局前置守卫(后置守卫)
-
路由懒加载
- 使用路由懒加载来进行优化, 详细方式见作者博客路由懒加载(链接)
-
组件缓存
- 组件缓存在路由挂载点router-view外层套一个keep-alive组件
- 一级路由和二级路由的挂载点不同 , 根据需求加缓存
- 缓存之后会出现的问题
- 文章列表也会被缓存 , 多次进入同一篇 , 会出现内容一致
- 缓存之后的个人信息 , 下次登陆的时候会是上次登陆的信息
- 搜索结果组件搜索到的为同一个结果
- 解决方法
-
exclude 不缓存的组件 include 缓存的组件 ArticleDetail,Login,Search,SearchResult 为 组件内部的name名字 <template> <div> <keep-alive exclude="ArticleDetail,Login,Search,SearchResult"> <router-view></router-view> </keep-alive> </div> </template> 组件缓存后会生成两个生命周期钩子函数 activated(组件激活) , deactivated(失去激活) 将初始化换成activated也可以
- 组件缓存之后 , 会造成一系列可能存在的问题 , 针对不同的问题 , 通过不进行缓存或者修改生命周期函数 , 类似于以下的用户头像不更新问题
- 组件缓存之后头像不更新问题
- 解决方案一 : 把created换成activated钩子函数即可
- 解决方案二
- 将头像变量保存到vuex中,定义mutations修改头像函数
- 在请求到头像数据之后 , 存到vuex变量之中
- 引申:头像 , 昵称 , 手机号 , 性别 也是同样道理
-
文章内容 代码高亮
- 获取的文章列表之后没有代码高亮的效果
- 想要让代码高亮怎么办 , 必须在用户发布文章的时候,就要使用富文本编辑器,将想要的代码分段用pre+code标签包裹
- 前端可以通过获取这些标签名,指定类名 , 分别给予相应的样式
- 解决: 基于highlight.js 美化详情页的代码片段(highlight.js中文网 )
- 使用方式
- 下载此插件到项目之中
-
yarn add highlight.js -D
- 在入口文件main.js引入
-
import hljs from 'highlight.js' // hljs对象 import 'highlight.js/styles/default.css' // 代码高亮的样式
- 注册高亮代码(自定义指令)
-
Vue.directive('highlight', function (el) { // 自定义一个代码高亮指令 const highlight = el.querySelectorAll('pre, code') // 获取里面所有pre或者code标签 highlight.forEach((block) => { hljs.highlightElement(block) // 突出显示这些标签(以及内部代码, 会自动识别语言) }) })
- 在铺设文章的标签上使用自定义指令 v-highlight 指令即可
-
优化请求时给用户一个反馈
- 在网络较慢的时候 , 可以给用户一个提示正在加载中
- 在没有文章的时候 , 提示文章正在加载中
- 使用组件库中特有的加载效果提示(vant , element-ui)
- 以vant为例子
- 找到组件库的加载效果提示组件 , 在main.js全局注册
-
import { Loading } from 'vant'; Vue.use(Loading);
- 思路 : 当发起网络请求加载的时候 , 这个时候文章和加载组件应该你死我活,当文章没有全部加载的时候,这个时候显示loading组件,加载完毕之后loading应该消失,文章显示 , 所以使用v-if和v-else , 而进行v-if判断的时候 , 条件使用文章内容请求的数据进行判断 , 当数据为undefined的时候 , 文章不显示 , 请求完毕后 数据已经不为undefined , 即文章显示
-
artObj.title 为请求过来的文章信息 <!-- 文章加载中... --> <van-loading color="#1989fa" class="loading" v-if="artObj.title === undefined">文章疯狂加载ing...</van-loading> <div v-else> <!-- 文章信息区域 --> </div>
- 注意 : 使用组件的时候记得看一下div结构 , 组件不显示可能是上面的元素覆盖了组件
-
无感知刷新token
- 无感知刷新token , 需要后台有对应的接口 , 登陆的时候收到两个token , 一个用于用户认证 , 另一个用于登陆过期 , 无感刷新token
- 当用户认证失败之后 , 用户会跳转到登录页
- 有些地方使用不到用户认证 , 但执行了登陆后的操作 , 就需要用户去登录页面 , 登陆完成之后 , 继续执行刚才的操作
- 思路 : 在发送网络请求的时候 , axios提供了请求响应器和拦截响应器 , 只需要在响应错误的时候 , 进行相应的判断即可
-
// axios添加响应拦截器 axios.interceptors.response.use(function (response) { // 对响应数据做点什么 return response }, async function (error) { // 打印错误信息 寻找需要判断的条件 console.dir(error) // 对响应错误做点什么 if (error.response.status === 401) { // 清除token removeToken() // 刷新token的请求 const res = await newToken() // 拿到刷新后的token 继续设置新的token setToken(res.data.data.token) // 携带请求头 , 这个请求头为刷新token后后台给予的新的token // 不加请求头 , 会进入死循环 error.config.headers.Authorization = 'Bearer ' + res.data.data.token // error.config 存储了用户上述操作的请求 , 过期之后登录token继续完成上次未完成的操作 // 记得return出去 才可以执行 return axios(error.config) // router.replace('/login') } else if (error.response.status === 500 && error.response.config.url === '/v1_0/authorizations') { // 刷新token和retoken都过期之后 , 进行的判断 // 清除token removeToken() // 将获取到的存储到本地 localStorage.removeItem('refresh_token') // 跳转页面 携带失败后的路由地址和参数 // 在登陆的同时 进行判断 router.push({ path: '/login', query: { pa: router.currentRoute.fullPath } }) } return Promise.reject(error) })
- 登录的时候 进行判断 路由地址是否有参数
-
this.$router.push({ path: this.$route.query.pa || '/layout' // 有未遂的地址就回去, 如果没有就去/layout })
-
图片懒加载
- 当图片标签进入到视口才加载图片
- 图片的src会调用浏览器请求的图片资源
- 将src属性替换掉 , 在恰当的时机 , 即图片到了视口之中 才开始加载图片
- 原生实现图片懒加载的原理 : 将src替换为另外的属性data-src , 监听用户的滚动事件 , 当dom文档内卷的距离加上窗口的距离, 大于了图片到文档顶端的距离 说明图片已经存在到视口当中,这个时候将data-src替换为src,既可以加载图片
- 组件使用(vant为例子),查阅文档 , 找到图片懒加载组件 , 引入到main.js使用
- 使用方式 : 将所有的img的src换成v-lazy指令即可
-
<!-- 标题区域的插槽 --> <template #title> <div class="title-box"> <!-- 标题 --> <span>{{ obj.title }}</span> <!-- 单图 --> <img class="thumb" v-lazy="obj.cover.images[0]" v-if="obj.cover.type === 1" /> </div> <!-- 三张图片 --> <div class="thumb-box" v-if="obj.cover.type > 1"> <img class="thumb" v-for="(imgUrl, index) in obj.cover.images" :key="index" v-lazy="imgUrl" /> </div> </template>
-
Vue.use(Lazyload, { preLoad: 1.0 // 图片开始加载判断范围 // 1.0 (出现在视口就加载) // 1.3 (多往下加载30%范围) }) // 全局入口main.js文件配置 // 根据vant文档使用
-
自定义指令 (自动聚焦)
- 用户修改弹框类型的内容时候 , 类似于 发表评论 , 修改昵称 姓名 手机号 ...
- 只有第一次自动聚焦的问题(点击第二次没有自动聚焦了)
- 自动聚焦依赖自定义指令inserted执行
- 而Dialog只有第一次出现是插入到真实DOM , 才会触发inserted方法
- 而Dialog以后初选是css层面的县市出现 , 不会触发inserted方法
- 解决 : 给自定义指令添加updata方法 , 指定所在DOM更新时执行
-
export default function (Vue) { Vue.directive('fofo', { // 指令所在标签, 插入到真实DOM时, 才触发 inserted (el) { // van-search组件封装一套div里包含input // el是原生的div标签 // 往里获取到input // JS触发标签事件, 直接.事件名()\ if (el.nodeName === 'TEXTAREA' || el.nodeName === 'INPUT') { el.focus() } else { // el不是输入框, 尝试往里查找 const inp = el.querySelector('input') const textA = el.querySelector('textarea') if (inp) { inp.focus() } if (textA) { textA.focus() } } }, // 指令所在标签, 发生更新时触发, (例如: display:none隐藏->出现) update (el) { if (el.nodeName === 'TEXTAREA' || el.nodeName === 'INPUT') { el.focus() } else { // el不是输入框, 尝试往里查找 const inp = el.querySelector('input') const textA = el.querySelector('textarea') if (inp) { inp.focus() } if (textA) { textA.focus() } } } }) }
-
抽离组件注册
- 入口文件main.js入口代码过多 , 将一致的分散出去 , 方便后期管理维护
- 以vant组件库为例子
- 将vant注册代码复制到另一个文件中
-
import Vue from 'vue' import { NavBar, Form, Field, Button, Tabbar, TabbarItem, Icon, Tab, Tabs, Cell, List, PullRefresh, ActionSheet, Popup, Row, Col, Badge, Search, Divider, Tag, CellGroup, Image, Dialog, DatetimePicker, Loading, Lazyload } from 'vant' ... // 省略上面的一些 Vue.use(Lazyload) Vue.use(Field) Vue.use(Button) Vue.use(NavBar)
- 在main.js引入 , 只需要将代码执行的话 就不用按需或者全部导出
-
import './VantRegister' // VantRegister js文件的名字
-
持久化储存方式
- 将本地持久化储存封装到一个文件下
- 创建utils/storage.js文件, 定义4个方法
-
// 本地存储方式 // 如果同时有sessionStorage和localStorage, 可以封装2份 // 现在我只封装一种统一的方式 export const setStorage = (key, value) => { localStorage.setItem(key, value) } export const getStorage = (key) => { return localStorage.getItem(key) } export const removeStorage = (key) => { localStorage.removeItem(key) } export const clearStorage = () => { localStorage.clear() }
- 把所有使用本地存储的地方, 都统一换成这里定义的方法
- 好处 : 以后如果切换本地存储的方式 , 可以直接修改utils/storage.js内的真正实现即可
-
封装统一的提示信息
- 以后的项目如果需要更换提示框 , 只需要在这里统一修改即可
- 类似于vant里的Notity提示
-
// 整个项目的统一的通知 // 先用vant组件库里的Notify方法 // import { Notify } from 'vant' import { Toast } from 'vant' export default { 引入到main.js, 用Vue.use注册导致install执行 install (Vue) { // Vue原型上, 添加属性$notify -> 通知方法 // 后期只需要修改原型的组件库方法即可 Vue.prototype.$notify = Toast } } /* export const MyNotify = ({ type, message }) => { // Notify({ // type: type, // message: message // }) if (type === 'warning') { Toast({ type: 'fail', message }) } else if (type === 'success') { Toast({ type, message }) } } */
- 使用的时候只需要调用原型上的方法即可
- 封装统一UI弹窗, 以后更改封装的自定义函数内部实现, 整个项目所有UI弹窗都换掉
-
滚动条位置
- 用户滚动之后 , 切换其他的组件 , 仍然可以回到用户移动到的地方
- 如果不进行优化 , 用户切换之后仍然回到第一条上面,用户体验不好
- 原生的思路 : 监听window的滚动事件 ,记录滚动的位置 , 当用户离开之后回来 , 将滚动的值重新赋值给html的scrollTop即可
-
created () { window.addEventListener('scroll', () => { console.log('滚动了') // 滚动事件window/document // 获取/设置滚动位置 -> html // console.log(document.documentElement.scrollTop) this.$route.meta.scrollT = document.documentElement.scrollTop }) }, // 组件激活 activated () { // 路由对象上存额外信息, 滚动位置设置回来即可 document.documentElement.scrollTop = this.$route.meta.scrollT }, 特别注意 : 重点重点 1. 不能将滚动的值赋给变量 , 因为keep-alive组件缓存之后 , 会将变量的值直接改为0 2. 那么问题 将变量存到哪里去呢 , 路由元信息 meta:{ } 中 { path: 'home', meta: { scrollT: 0 // 首页滚动的位置 }, // 路由元信息(路由对象里保存更多有用的数据) component: () => import('@/views/Home') }
-
与上述对应(频道列表滚动条位置)
- 明确结构 , 当我们点击不同的频道时 , 滚动后将值存到相对于的频道中
- 使用对象的一一映射关系
-
// “频道名称”和“滚动条位置”之间的对应关系, // 格式 { '推荐ID': 211, 'htmlID': 30, '开发者资讯ID': 890 } const nameToTop = {}
- 查阅vant组件库文档 , 找到对于tabs对应方法
-
// tabs切换事件 tabsChangeFn () { // 2. 把频道ID对应的滚动条位置 (在nameToTop对象上), 设置上 // active 为频道的id信息 和vant组件进行的绑定 const st = nameToTop[this.active] // 注意: 切换tab栏的, DOM更新是异步的, 所以滚动位置, 在DOM缓过来以后, 再执行滚动 setTimeout(() => { document.documentElement.scrollTop = st }, 0) }, // tabs切换"前"的回调函数 // return false阻止tabs切换 beforeChangeFn () { // tabs切换, 如果从底下 切换到别的tab上, 滚动条会上来, 会拿到0的位置覆盖进去 // 所以不能在这里存值 // 1. 再切换频道之前, 把当前频道滚动位置, 先保存到对象里 nameToTop[this.active] = document.documentElement.scrollTop return true }