TOP ⬆

讲一讲「My_little_airplay」

关于这个前端项目虽然小且简单,但其功能实现所使用的方法(库)都还是蛮实用的,如统一包装 Axios 的 API 设计、$EventBus的封装(观察者模式)等还是值得总结提炼一下帮助加深记忆的,最近面临实习、秋招也顺带借这篇文章梳理一下项目的结构。由于本人技术尚浅,肯定有很多不足的地方可以提炼重构,请多担待~​

从项目根目录下的配置文件们讲起

该项目使用的@vue-cli构建的,因此在项目创建之初会依照我们的选择来自动创建若干个配置文件,比如从package.json.browserslistrc等若干文件就对应的配置好了项目所依赖的插件,从最熟悉的package.json开始

package.json

vue.config.js

browserslistrc & editorconfig

eslintrc & babel.config

「网络」 Axios 和 API 的封装

axios

axios 的使用就不用再赘述了,这里主要讲项目是如何将 axios 封装起来进行异常处理的。

**问题:**在项目初期 axios 是被各个组件中直接引入使用的,没有考虑统一的异常处理,那时组件少,使用 catch 来捕捉异常的工作量完全可以应付,但当组件变多后逐一添加 catch 就力不从心了。

分析:据此分析我们需要将 axios 先进行一次的封装(代理 \ 切面)以便出错时能弹出默认提醒,让组件使用 axios 时自动拥有最基本异常处理功能,同时为了让每个组件不丢失自定义异常处理的功能,我们应当让 axios 的回调能继续被调用。观察 axios 的大致逻辑我们就可以找到关键的切入点,构建 axios>>发送请求>>收到响应>>执行回调,答案呼之欲出!我们只需要在收到响应后做一层封装再让其继续执行指令即可,axios 也提高了对应的钩子来给我们使用,即响应拦截器。

实现: talk is cheap, show me the code

import axios from 'axios';
import { Notify } from 'vant';

//  网络失败的警示,使用vant的通知组件
const dangerTip = (msg) => {
	Notify({
		background: '#fe5f64',
		message: msg,
	});
};

//  错误处理函数
const errorHandler = (status, other) => {
	switch (status) {
		case 404:
			dangerTip('【请求失败】请求内容不存在');
			break;
		case 500:
			dangerTip('【请求失败】服务器错误');
			break;
		default:
			dangerTip(other);
	}
};

//  axios的默认配置
const instance = axios.create({
	//  超时时间为10s
	timeout: 1000 * 10,
});
instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';

//  重点:拦截器!
instance.interceptors.response.use(
	(res) => {
		if (res.status === 200) {
			return Promise.resolve(res);
		}
		return Promise.reject(res);
	},
	(error) => {
		const { response } = error;
		if (response) {
			errorHandler(error.status, error.data.message);
		} else {
			dangerTip('【网络错误】网络连接失败');
		}
		return Promise.reject(response);
	},
);
export default instance;

在拦截器中我们先判断 axios 的请求是否执行成功,如果成功收到响应了则还要判断收到的响应状态码以确认服务器响应的是否有效,这里使用的提醒是 vant 中的全局组件,其他的 axios 详细配置可以参照axios 文档,这样我们就封装好了 axios,在将其挂载到 Vue 实例的原型链上就成了一个全局方法,将其命名为$http你就自己实现了一个简单的vue-axios​​(不是)。

**思考:**但这样的方法还是要在组件中直接使用 axios,我们能不能再进行一层封装呢?如果组件中只需要把后端看成数据库,通过 DAO(概念)就可以直接拿到数据不就更好了吗?这就是接下来我要说的 API 封装了。

**唬烂三小:**每每这个时候,我都要想起组网老师对我的一句教导:“同学,你要不就再多套几层吧?” 很感慨,时光荏苒,我终于还是到了听懂这句话的年纪。

API

在用 Spring 写接口的时候我们用到controller 来将请求分成一类,再由 service 等一层一层地去处理数据,那我们写前端的时候还有什么理由不把像这样将 API 归类管理起来呢?更何况这样的操作实际并不复杂。

首先我们把请求分类,项目中的专辑、歌曲就是这两个类,然后在这两个 JS 文件引入封装好的 axios 来发送请求,将不同请求抽离成函数,最后函数返回 axios.method()的返回值即可(即 Promise 对象),我们把这两个类暴露出去,再使用一个统一的 API 类来向项目提供这两个类,把 API 类挂载到全局对象即可全局使用了。

以歌曲 API 为例:

//	song.js
import axios from '@/utils/http';
import base from './base';

const song = {
  //  搜索歌曲功能
  searchSongByName(name) {
    return axios.get(`${base.mlaUrl}/song/name/${name}`);
  },
  getRandomSongsWithLimit(limited) {
    return axios.get(`${base.mlaUrl}/song/random/${limited}`);
  },
};

export default song;

//	api.js
import song from '@/api/song';
import album from '@/api/album';

export default {
  song,
  album,
};

//	main.js
import api from './api';
Vue.prototype.$api = api;

//	组件中使用
this.$api.song.searchSongByName(this.searchName).then().catch();

这里还有一个问题,即请求的地址要怎么配置?这里有很多方式:如项目配置代理、硬编码、使用静态文件读取等… 这里我使用的是将其保留到 base.js 一个 js 文件中,然后引入使用,这样做其实是不太方便的,比如要修改的时候需要重新编译,很死板。另一个好方式的就是让项目去静态资源中读取,这样可以实现热更新这里不细讲了。

具体可以参考 如何修改 Vue 打包后文件的接口地址配置

「搜索框 - 动画」节流和防抖

搜索框

**需求:**我们在搜索的时候需要让用户在输入的时候能得到一定的响应,但又要限制用户在该时间段里不能发出太多请求,这时候我们就可以选择用节流来对搜索功能进行优化。

节流:使用阀门的概念来理解就是每隔一段时间泄一次洪,要让数据能流出去又不至于洪泛。

**实现:**节流功能需要借助一个状态来判断当前是否要响应事件(即阀门的开启与否),初始时阀门打开,确认阀门当前处于开启状态后就可以为响应目标函数做准备,准备期间需要先将阀门关闭屏蔽外界的响应(关中断…),执行目标函数后再将阀门打开即可,把要目标函数包装成定时任务就可实现每隔一段时间响应一次。

//  节流函数
function throttle(fn, delay = 500) {
	let timer = null; //	在下次执行前如果定时任务未完成则清楚定时任务
	let canRun = true; //	阀门

	return function () {
		//	当前不营业
		if (!canRun) return;
		canRun = false; //	关阀门
		clearTimeout(timer);
		timer = setTimeout(() => {
			fn();
			canRun = true; //	开阀门
		}, delay);
	};
}

直接使用的 fn() 是最简化的模式了,实际上我们在fn()中必须考虑使用 this 和传参数的问题,因此在这里我们要写成fn.apply(this, arguments) \ fn.call(this,...) \ fn.bind(this)

这里还有另一种写法是将fn()放置到定时器的外面变成立即执行的函数,具体采用哪种方式还是依据个人选择,功能上的差异是不大的。

​ 动画

需求:项目中使用了Animate.css动画库来实现某些组件在点击后触发某些动画,同时要屏蔽用户的重复点击事件,不然动画就会出现鬼畜效果。

**实现:**使用 css 动画库要求我们在处理点击事件的时要修改类名,触发事件时添加类名以触发动画,事件结束后删除类名以方便下次触发动画,要屏蔽用户重复点击事件使用定时任务即可,这里我想让用户有一个重复点击蓄能的体验,即事件不是第一时间响应的,要留有一定的缓冲期,在用户停止点击后再执行动画,这就需要使用到防抖了。(有种面向答案出题的感觉)

防抖:使用定时器来执行任务,在定时任务未被执行前又触发了事件则需要重设定时器。

//	防抖
function debounce(fn, delay) {
	let timer = null; //	闭包
	return function () {
		//	重置与否
		if (timer) {
			clearTimeout(timer);
		}
		timer = setTimeout(fn, delay); //	设置定时器
	};
}

可以看到防抖和节流其实很相似,其核心思想都是尽可能少的触发目标事件,节约资源。

操作类名的小知识点:classList

elementClasses 是一个 DOMTokenList 表示 elementNodeReference 的类属性 。如果类属性未设置或为空,那么 elementClasses.length 返回 0。虽然 element.classList 本身是只读的,但是你可以使用 add()remove() 方法修改它。 - MDN Web Docs

答应我,不要再用className了好吗?classList 提供了add() \ remove() \ replace() \ taggle()这四种方法大大方便了我们操作类名。不过 Vue 中是不推荐我们直接操作 dom 的,如果还有其他更好的实现方式都值得我们去了解一下。

自定义全局 API - eventBus & global

eventBus \ 观察者模式

**需求:**在项目中有一个播放器组件(vue-aplayer),我们需要在很多组件更新其播放数据,如在搜索组件中,用户搜索到歌曲后要添加到播放器组件中,专辑列表需要一次性大量添加歌曲,将通知绑定在父子孙组件上进行组件间通信的方式显示是过于繁琐不合适的,这时候我们可能会想如果组件间收发“短信”问题就容易解决了。

**思考:**我们都学过计算机网络,那么模仿网络通信的方式我们可不可以也在 vue 中实现“端口”监听呢?学过 vue 的我们可能就会想起一个叫做 vuex 的工具,但也正如其文档所写:“如果您的应用够简单,您最好不要使用 Vuex。” 显然,mla 项目很符合「简单」的定义,为项目引入 vuex 远远超过了够用即可的原则(我的原则)。还好我们有另一种选择,vue.api 为我们提供的 $on$emit函数实现“端口”监听 vue-api

实现思路:既然 vue 的实例有提供用于监听的 API,那我们直接注册一个空的 vue 实例并将其挂载到 vue 原型链上,或者直接绑定到根实例的数据中,那我们不就可以在全局使用了吗?这个实例就像是邮差,其有个通用的名字叫eventBus 没错,其和计算机中的总线的概念是一致的!接下来的代码实现就简单了

//	main.js
Vue.prototype.$eventBus = new Vue();	//	挂载到原型链上

//	app.js
mounted(){
  this.$eventBus.$on('getRandomSong', (load) => this.addSong(load));
}

//	RandomPlay.vue
this.$api.song.getRandomSongsWithLimit(1).then((resp) => {
        this.$eventBus.$emit('getRandomSong', resp.data[0]);
});

这里要注意的重点问题是$on一定要比$emit先调用,必须先监听再触发,如果两个组件没有依赖关系都加载完了,则放在mounted()钩子上就好了,但如果是 A 组件先加载后才会加载 B 组件,是有顺序的,那么 A 中的监听必须放在mounted()钩子运行之前,而 B 中的必须放在beforeDestroy及之后,否则不起作用。

在项目中最好是使用一个 bus.js 来提供 vue 实例对象,这样通过统一的引入更规范也便于观察。同样简单场景直接用 this.$root.$emitthis.$root.$on 也是一样的,可以少初始化一个 Vue 对象

观察者模式(发布/订阅模式)就不展开了,看代码就能基本了解了,其在 vue 源码中也有大量的应用,如数据变化侦测的功能实现,因此这个设计模式还是很重要的~ 有兴趣推荐《深入浅出 vue.js》

global

这是一个工具类,其中存放了一些全局可使用的函数来简化编码操作,如防抖节流函数就很适合放在这里,还有上文提到的操作类名的方法也被抽离到了这里。在项目中对应是global.vue,这和 .js 文件 是一样的,在项目中纯粹是为了验证 .vue 文件 引入的效果。

全局即代表我们在main.js中将其绑定到了原型链上,这和$eventBus的操作是一样的,因此我们可以在多个组件中这样使用:

//	main.js
import global_ from './Global.vue';
Vue.prototype.$global = global_;

//	global.vue
function addAnimateClass(element, animateName, delay) {
	element.classList.add('animate__animated');
	element.classList.add(animateName);
	setTimeout(() => {
		element.classList.remove(animateName);
	}, delay);
}

function deBounceAddAnimate(element, animateName, canRun, delay = 2000) {
	debounce(
		() => {
			addAnimateClass(element, animateName, delay);
		},
		canRun,
		delay,
	);
}

//	组件中
this.$global.deBounceAddAnimate(
	event.srcElement.parentElement,
	'animate__rubberBand',
	this.canPulse, //	组件中的阀门
	2000,
);

将阀门保存在组件中的好处是,每个组件触发事件是受组件本身控制的,各个组件可以使用统一的函数又不至于互相干扰。

小结

css

在这个项目中我花在写 css 样式上的时间占比是很大的,要实现组件的通用要考虑的因素有很多,一下几点是我在项目中收获比较多的。

css 要学习的东西有很多,有时候遇到各种怪异的问题处理起来是很棘手的,在另一个项目中我就遇到了 img 元素 溢出 div 挤占了下行内容还解决不了的问题,对于各种 css 原理还是需要继续加强,这里推荐张鑫旭老师的《css 世界》系列书籍,属于细致到读每一章都能更新一次观念的书籍。

项目后端

项目的后端看 前端 api.js 就知道其实提供的接口并不多也不复杂,技术就是最常见的 springMVC+mybatis,redis 都没用上,因为没有什么可缓存的,如果一定要缓存甚至可以不用 mysql,单一个 redis 就足够存放所有数据了。作为歌迷我是打心底里希望 mla 多出点歌,多出点专辑

不过最后还是总结一下后端一些我觉得比较好玩的点吧

说到后端我们前端工程师绕不开的一个话题就是golang,go 确实是一个很好玩的语言开发 web 比 spring 要轻便很多,项目中之所以不用 GO 是因为我接近三四个月没写 GO 有些遗忘了… 菜是原罪。