避免大量 if...else...
if(x===a){
res=A
}else if(x===b){
res=B
}else if(x===c){
res=C
}else if(x===d){
//...
}
改写成 map 的写法:
let mapRes={
a:A,
b:B,
c:C,
//...
}
res=mapRes[x]
🌰再比如以下代码:
const isMammal = (creature) => {
if (creature === "human") {
return true;
} else if (creature === "dog") {
return true;
} else if (creature === "cat") {
return true;
}
// ...
return false;
}
改写成数组:
const isMammal = (creature) => {
const mammals = ["human", "dog", "cat", /* ... */];
return mammals.includes(creature);
}
uniapp微信小程序强制更新
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
console.log(res.hasUpdate);
});
updateManager.onUpdateReady(function (res) {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
}
});
});
updateManager.onUpdateFailed(function (res) {
// 新的版本下载失败
});
vue中 给元素添加鼠标移入,鼠标移出的效果的事件
使用方法如以下代码中的写法
<div class="subei1" id="subei1"
v-on:mouseover="changeActive()"
v-on:mouseout="removeActive()"
@click.stop="changeStyle()">
</div
方法如下:
changeActive(id) {
document.getElementById("subei1").classList.add("active");
document.getElementById("subei2").classList.add("active");
},
removeActive($event) {
if (this.flag) {
document.getElementById("subei1").classList.remove("active");
document.getElementById("subei2").classList.remove("active");
}
},
vue实现页面手动动滚动效果
let scrollTargetBox: any = document.getElementById('myTable');
scrollTargetBox.onscroll = () => {
var scrollHeight = scrollTargetBox.scrollHeight; //251元素内容高度,包含隐藏的
var scrollTop = scrollTargetBox.scrollTop; //0-18滚动条到顶边的距离
var clientHeight = scrollTargetBox.clientHeight; //233 元素内部高度
console.log(scrollHeight, scrollTop, clientHeight, '高度');
if (scrollHeight - clientHeight == scrollTop) {
//滚动条滚到最底部
console.log('底部');
page.value++;
scrollTargetBox.scrollTop = 0;
getQueryDataByMsId();
// if (pageData.integralList.length >= pageData.total) return;
// pageData.page++;
// pageData.getIntegralList();
}
};
vue实现页面自动滚动效果
<div id="printBox">
</div>
data中配置:
scrollTimer: null, // 滚动定时器
pauseTimer: null, // 暂停定时器
scrollDirection: 'down' // 滚动方向 up向上 down向下
mounted() {
this.dataCompleteFun()
},
destroyed() {
// 清理定时器
clearTimeout(this.pauseTimer)
this.pauseTimer = null
clearInterval(this.scrollTimer)
this.scrollTimer = null
// 清理点击监听
window.document.removeEventListener('click', this.pauseScroll)
},
// 数据加载完成方法
dataCompleteFun() {
// 开启滚动
console.log('开始滚动');
this.autoScroll()
},
autoScroll() {
const scrollHeight = document.getElementById('printBox').scrollHeight
const clientHeight = document.getElementById('printBox').clientHeight
const scroll = scrollHeight - clientHeight
// 滚动长度为0
if (scroll === 0) {
return
}
// 触发滚动方法
this.scrollFun()
// 去除点击监听
window.document.removeEventListener('click', this.pauseScroll)
// 重设点击监听
window.document.addEventListener('click', this.pauseScroll, false)
},
pauseScroll() {
// 定时器不为空
if (this.scrollTimer) {
// 清除定时器
clearInterval(this.scrollTimer)
this.scrollTimer = null
// 一秒钟后重新开始定时器
this.pauseTimer = setTimeout(() => {
this.scrollFun()
}, 2000)
}
},
scrollFun() {
// 如果定时器存在
if (this.scrollTimer) {
// 则先清除
clearInterval(this.scrollTimer)
this.scrollTimer = null
}
this.scrollTimer = setInterval(() => {
const scrollHeight = document.getElementById('printBox').scrollHeight
const clientHeight = document.getElementById('printBox').clientHeight
const scroll = scrollHeight - clientHeight
// 获取当前滚动条距离顶部高度
const scrollTop = document.getElementById('printBox').scrollTop
console.log('scrollTop',scrollTop);
// 向下滚动
if (this.scrollDirection === 'down') {
// 滚动速度
const temp = scrollTop + 10
document.getElementById('printBox').scrollTop = temp // 滚动
// 距离顶部高度 大于等于 滚动长度
if (scroll <= temp) {
// 滚动到底部 停止定时器
clearInterval(this.scrollTimer)
this.scrollTimer = null
}
}
}, 150)
}
关于输入框非空的判断
在处理输入框相关业务时,往往会判断输入框未输入值的场景。
if(value !== null && value !== undefined && value !== ''){
//...
}
ES6中新出的空值合并运算符了解过吗,要写那么多条件吗?
if((value??'') !== ''){
//...
}
vue定时刷新数据
data() {
return {
timer: null
}
},
mounted() {
// 每隔1分钟定时刷新
this.timer = setInterval(() => {
this.getFxItemlist();
}, 60000)
},
beforeDestroy() {
clearInterval(this.timer);
},
methods: {
getFxItemlist() {
...
}
}
//vue3
const timer = ref<any>(null);
onMounted(() => {
timer.value = window.setInterval(() => {
getQueryByMsId();
console.log('哈哈');
}, 60000);
});
onBeforeUnmount(() => {
window.clearInterval(timer.value);
console.log('清除',timer.value);
});
获取当前日期的周一、周末的日期
export function getWeeekdata(cdate){ //cdate 传来当前的时间
let now = new Date(cdate);
let year = now.getFullYear();
let month = now.getMonth() + 1;
let date = now.getDate();
let nowTime = now.getTime();
let day = now.getDay();
let oneDayTime = 24 * 60 * 60 * 1000;
//显示周一
var MondayTime = nowTime - (day - 1) * oneDayTime;
//显示周日
var SundayTime = nowTime + (7 - day) * oneDayTime;
//初始化日期时间
var monday = new Date(MondayTime);
var sunday = new Date(SundayTime);
return [format(monday), format(sunday)];
}
function format(date) {
var time = new Date(date);
var y = time.getFullYear();
var m =
time.getMonth() + 1 < 10 ? "0" + time.getMonth() + 1 : time.getMonth() + 1;
var d = time.getDate() < 10 ? "0" + time.getDate() : time.getDate();
//var h = time.getHours();
//var mm = time.getMinutes();
//var s = time.getSeconds();
return y + "-" + m + "-" + d;
}
根据当前时间获取当月的1号和最后一号
export function getcurentMonth(cdate) { //cdate传来的当前的时间
// 当天
let thatDay = "";
// 当月第一天
let oneDayTime = "";
// 当月最后一天
let zDay = "";
let date = new Date(cdate);
let curr_date = date.getDate();
let curr_month = date.getMonth() + 1;
let curr_year = date.getFullYear();
String(curr_month).length < 2 ? (curr_month = "0" + curr_month) : curr_month;
String(curr_date).length < 2 ? (curr_date = "0" + curr_date) : curr_date;
thatDay = curr_year + "-" + curr_month + "-" + curr_date;
String(curr_year).length < 2 ? (curr_year = "0" + curr_year) : curr_year;
var m = date.getMonth() + 1;
String(m).length < 2 ? (m = "0" + m) : m;
var d = "01";
oneDayTime = curr_year + "-" + m + "-" + d;
//结束时间
var currentMonth = date.getMonth();
var nextMonth = ++currentMonth;
var nextMonthFirstDay = new Date(date.getFullYear(), nextMonth, 1);
var oneDay = 1000 * 60 * 60 * 24;
var date1 = new Date(nextMonthFirstDay - oneDay);
var yy = date1.getFullYear();
String(yy).length < 2 ? (yy = "0" + yy) : yy;
var mm = date1.getMonth() + 1;
String(mm).length < 2 ? (mm = "0" + mm) : mm;
var dd = date1.getDate();
String(dd).length < 2 ? (dd = "0" + dd) : dd;
zDay = yy + "-" + mm + "-" + dd;
return [thatDay, oneDayTime, zDay];
}
判断文件上传的类型
/**
* @param: fileName - 文件名称
* @param: 数据返回 1) 无后缀匹配 - false
* @param: 数据返回 2) 匹配图片 - image
* @param: 数据返回 3) 匹配 txt - txt
* @param: 数据返回 4) 匹配 excel - excel
* @param: 数据返回 5) 匹配 word - word
* @param: 数据返回 6) 匹配 pdf - pdf
* @param: 数据返回 7) 匹配 ppt - ppt
* @param: 数据返回 8) 匹配 视频 - video
* @param: 数据返回 9) 匹配 音频 - radio
* @param: 数据返回 10) 其他匹配项 - other
* @author: ljw
**/
export function fileSuffixTypeUtil(fileName){
// 后缀获取
var suffix = "";
// 获取类型结果
var result = "";
try {
var flieArr = fileName.split(".");
suffix = flieArr[flieArr.length - 1];
} catch (err) {
suffix = "";
}
// fileName无后缀返回 false
if (!suffix) {
result = false;
return result;
}
// 图片格式
var imglist = ["png", "jpg", "jpeg", "bmp", "gif"];
// 进行图片匹配
result = imglist.some(function (item) {
return item == suffix;
});
if (result) {
result = "image";
return result;
}
// 匹配txt
var txtlist = ["txt"];
result = txtlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "txt";
return result;
}
// 匹配 excel
var excelist = ["xls", "xlsx"];
result = excelist.some(function (item) {
return item == suffix;
});
if (result) {
result = "excel";
return result;
}
// 匹配 word
var wordlist = ["doc", "docx"];
result = wordlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "word";
return result;
}
// 匹配 pdf
var pdflist = ["pdf"];
result = pdflist.some(function (item) {
return item == suffix;
});
if (result) {
result = "pdf";
return result;
}
// 匹配 ppt
var pptlist = ["ppt"];
result = pptlist.some(function (item) {
return item == suffix;
});
if (result) {
result = "ppt";
return result;
}
// 匹配 视频
var videolist = ["mp4", "m2v", "mkv"];
result = videolist.some(function (item) {
return item == suffix;
});
if (result) {
result = "video";
return result;
}
// 匹配 音频
var radiolist = ["mp3", "wav", "wmv"];
result = radiolist.some(function (item) {
return item == suffix;
});
if (result) {
result = "radio";
return result;
}
// 其他 文件类型
result = "other";
return result;
};
文件下载
/**
* @param: fileType - 文件类型
* @param: fileName - 文件名称
* @param: data - 数据流文件
**/
export function download(fileType, bucketName,data) {
let downType = "";
let downName = "";
if (fileType == "image") {
downType = "image/png";
downName = fileName + ".png";
} else if (fileType == "word") {
downType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
downName = fileName + ".docx";
} else if (fileType == "video") {
downType = "video/mpeg4";
downName = fileName + ".mp4";
} else if (fileType == "radio") {
downType = "audio/mpeg";
downName = fileName + ".mp3";
} else if (fileType == "pdf") {
downType = "application/pdf";
downName = fileName + ".pdf";
}
let blob = new Blob([data], { type: downType });
let downloadElement = document.createElement("a");
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
document.body.appendChild(downloadElement);
//downloadElement.setAttribute("download", downName);//设置下载名称
downloadElement.download = downName; //设置下载文件名称
downloadElement.click();
document.body.removeChild(downloadElement); //移除元素;防止连续点击创建多个a标签
window.URL.revokeObjectURL(href);
}
使用a标签下载本地静态资源文件
- 1、public目录下存放要下载的静态资源
- 2、a 标签下载
<a href="/demo.rar" download="demo.rar">点击下载</a>
下载文件操作
```
<div style="width: 100%" v-if="upload.isConsume">
消耗导入的文件必须是xls或xlsx格式,<span
style="color: #409eff"
@click="excelTemplatesConsume"
>
点击下载
</span>
<a ref="downloadTemplateConsume" style="display: none" href="/excelTemplate/templatesConsume.xlsx" target="_blank"> </a>
示例以帮助您顺利地完成导入
</div>
```
js代码
```
excelTemplatesConsume() {
this.$refs.downloadTemplateConsume.dispatchEvent(new MouseEvent('click'))
},
复制代码
```
如果是链接的操作方法
```
window.open(file[index].url, '_blank')
window.open(
"http://xxxxx.myqcloud.com/1650676904624.xlsx"
);
或 window.location.href = www.mxleather.com:6103/template/能源… ```
判断是否是ie浏览器
// 判断是否是ie浏览器
if (!!window.ActiveXObject || 'ActiveXObject' in window) {
if (!sessionStorage.getItem('ieBrowser')) {
this.ieBrowser = true
}
} else {
this.ieBrowser = false
移动端返回顶部方法
返回顶部是一个比较常见的功能,但是在 PC web 端和 Wap 端实现方式并不一致。PC 端通过 window/ document 的 scroll 方法实现。而移动端则需要指定当前页的最外层 dom 节点(滚动条对应的容器节点)。
scrollTop() {
const outermostWrapper = document.getElementsByClassName('content')[0]
// obj.scrollTop = 0
outermostWrapper.scroll({
top: 0,
left: 0,
behavior: 'smooth'
})
}
outermostWrapper为滚动条对应的容器节点。
Vue实现导航栏鼠标上滑显示下滑隐藏
思路:首先我们要在DOM加载完毕之后获取滚轮事件,把滚轮位置赋值给data中的top变量,用watch监听top的newValue和oldValue值,当新值比旧值大的时候证明滚轮在向下滚动,触发相对应事件,反之亦然。
// dom节点
<template>
<div id = "nav-bar" :class = "navShow ? 'navOn' : 'navOff'">
// nav内容
</div>
</template>
// 获取top值
data() {
return {
top:''
}
},
// 获取浏览器滚轮
mounted() {
window.addEventListener('scroll', () => {
this.top = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset
})
},
// 监听top值的变化
watch:{
top(newValue,oldValue){
// 等新值大于100的时候再做变化(优化一下)
if(newValue > 100){
if(newValue > oldValue){
this.navShow = false
console.log('向下滚动')
}else{
this.navShow = true
console.log('向上滚动')
}
}
}
}
// css样式
.navOn{
position: fixed;
top: 0;
left: 0;
right: 0;
transition: all 0.2s ease-in-out 0.2s;
transform: translateZ(0);
}
.navOff{
position: fixed;
top: 0;
left: 0;
right: 0;
transition: all 0.2s ease-in-out 0.2s;
transform: translate3d(0,-100%,0);
}
解决字母不换行
1. word-break:break-all;只对英文起作用,以字母作为换行依据
2. word-wrap:break-word; 只对英文起作用,以单词作为换行依据
文字分段
<p v-html="changeLine(company.introductionDesc)" class="animate__animated animate__slideInLeft"></p>
changeLine(str) {
// console.log('str',str)
if (str != null) {
return str.replace(/\n/gim, "<p>");
}
},
图片360度旋转
完整代码如下
.transformed {
width:250px;height:250px;
animation: spin 30s linear infinite;
}
@keyframes spin {
from {
//调整x轴到合适角度
transform:rotateX(75deg) rotateZ(0) ;
}
to {
transform:rotateX(75deg) rotateZ(360deg) ;
}
}
修改滚动条样式
/* 在全局CSS里引用: */
/* 滚动条宽度 */
::-webkit-scrollbar {
width: 7px;
height: 10px;
}
/* 滚动条的滑块 */
::-webkit-scrollbar-thumb {
background-color: #a1a3a9;
border-radius: 3px;
}
滚动到制定位置添加动画
- 给元素绑定ref:
-
data() { return { currentScroll: 0, testShow: false, testShow2: false, }; }, mounted() { window.addEventListener("scroll", this.handleScroll, true); }, methods: { handleScroll() { this.currentScroll = window.pageYOffset; if (this.currentScroll >= this.$refs.ttref.offsetTop-880) { this.testShow = true; } if (this.currentScroll >= this.$refs.tfref.offsetTop - 870) { this.testShow2 = true; } }, }, beforeDestroy() { window.removeEventListener("scroll", this.handleScroll,true); }, 样式: //开始位置在下侧20%位置 .boxTop{ transform: translateY(30%); opacity: 0; } //开始位置在右侧120%位置 .boxtranslateXright { transform: translateX(120%); opacity: 0; } //滚动到指定位置时回到原来的位置 .boxenterY { transform: translateY(0%) !important; opacity: 1 !important; transition: all 1s ease; }
唯一获取v-for循环出来的某个dom元素
<ul>
<li v-for="(value,index) in list" :key="index" :class="`list-item-${index}`" @click="clickItem(index)">
{{value}}
</li>
</ul>
clickItem(i) {
const node = document.querySelector(`.list-item-${i}`);
console.log(node)
}
后端返回的数据中含有\n,前端做换行的格式转换
univDtlInfo.highSchoolDesc为后台返回数据
replace(/\n/g, '<br/>')处理换行
<p class="univ-details-text" v-html="univDtlInfo.highSchoolDesc.replace(/\n/g, '<br/>')"></p>
changeLine(str){
// console.log('str',str)
if(str !=null){
return str.replace(/\\n/gmi, '<br>')
}
},
css层级总结
标准流盒子<低于浮动的盒子<浮动的盒子<定位的盒子。
定位中:absolute是不占位置的,relative是占位置的,fixed是不占位置的。
而层级的高低,与占不占位置没有关系。
用法:
1、必须有定位。(除去static之外)。
2、给定z-index的值为层级的值。(不给默认为0)
特点
1、层级为0的盒子,也比标准流和浮动高。
2、层级为负数的盒子,比标准流和浮动低。
3、层级不取小数
4、层级一样,后面的盒子比前面的层级高。
5、浮动或者标准流的盒子,后面的盒子比前面的层级高。
vue中滚动页面,改变样式&&导航栏滚动时,样式透明度修改
<div class="commonHeader" v-bind:class="{ 'navActive': scrollFlag }">
<img src="@/images/home/icon_jdjr.png" v-bind:class="{ 'scrollFlag': scrollFlag }">
data
scrollFlag:false,
mounted
window.addEventListener('scroll', this.handleScroll)
methods
handleScroll () {
let _this=this;
var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
// console.log(scrollTop)
if(scrollTop){
_this.scrollFlag=true
}else{
_this.scrollFlag=false
}
}
vue 跳转外部链接(百度等)
window.location.href = 'http://www.baidu.com';
window.open(url , '_blank')新开一个页面
vue路由跳转打开新窗口
使用 this.$router.resolve
const openNewUrl=(url) => {
let routeData = this.$router.resolve({path: url})
window.open(routeData.href, '_blank')
}
解决vue中v-html元素中标签样式失效问题
加上[scope](https://so.csdn.net/so/search?q=scope "scope")d会导致 v-html 下绑定的标签样式不生效
解决方案:
定义两个style标签,一个加上scoped属性,一个不加上scoped属性
遇到的项目问题:导出文件,在请求接口时,设置responseType: 'blob',返回Blob对象后进行处理下载。 但当返回失败的时候, 还是导出了,但是导出的是一个不正确的文件,导致接收不到返回的json对象。
解决办法:需要根据response.data里的type去判断,当返回的类型为application/json时,需要将blob转换成json,获取到相应的报错信息。
短信验证码功能
let time = 60
let timer = setInterval(() => {
this.codeText = time + 's后重试'
this.disabled = true
time--
if (time == 0) {
clearInterval(timer)
this.codeText = '获取验证码'
this.disabled = false
}
}, 1000)
获取当前页面url参数
/**
* 获取当前页面URL参数
* @param 传入参数key返回value
*/
export function getUrlKey(name) {
return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [""])[1].replace(/+/g, '%20')) || null
}
获取链接URL参数
我们都知道 window.location.search 可以获取 url 中 ““?” 问号后面的参数:
window.location.search
然后我们可以再通过 new URLSearchParams(location.search).get('type') 方法获取具体某一个参数的值
let type = new URLSearchParams(location.search).get('type');
比如:
解析URL参数
export const getSearchParams = () => {
const searchPar = new URLSearchParams(window.location.search)
const paramsObj = {}
for (const [key, value] of searchPar.entries()) {
paramsObj[key] = value
}
return paramsObj
}
复制代码
示例:
// 假设目前位于 https://****com/index?id=154513&age=18;
getSearchParams(); // {id: "154513", age: "18"}
复制代码
url参数序列化
将对象序列化成url参数传递
function stringifyUrl(search = {}) {
return Object.entries(search).reduce(
(t, v) => `${t}${v[0]}=${encodeURIComponent(v[1])}&`,
Object.keys(search).length ? "?" : ""
).replace(/&$/, "");
}
console.log(stringifyUrl({ age: 27, name: "YZW" })); // "?age=27&name=YZW"
url参数反序列化
一般会通过location.search拿到路由传递的参数,并进行反序列化得到对象
function parseUrlSearch() {
const search = '?age=25&name=TYJ'
return search.replace(/(^?)|(&$)/g, "").split("&").reduce((t, v) => {
const [key, val] = v.split("=");
t[key] = decodeURIComponent(val);
return t;
}, {});
}
console.log(parseUrlSearch()); // { age: "25", name: "TYJ" }
推荐switch case
推荐使用 switch case 而不是 if...else if...
if (1 == month) {days = 31;}
else if (2 == month) {days = IsLeapYear(year) ? 29 : 28;}
else if (3 == month) {days = 31;}
else if (4 == month) {days = 30;}
else if (5 == month) {days = 31;}
else if (6 == month) {days = 30;}
else if (7 == month) {days = 31;}
else if (8 == month) {days = 31;}
else if (9 == month) {days = 30;}
else if (10 == month) {days = 31;}
else if (11 == month) {days = 30;}
else if (12 == month) {days = 31;}
switch(month) {
case 1: days = 31; break;
case 2: days = IsLeapYear(year) ? 29 : 28; break;
case 3: days = 31; break;
case 4: days = 30; break;
case 5: days = 31; break;
case 6: days = 30; break;
case 7: days = 31; break;
case 8: days = 31; break;
case 9: days = 30; break;
case 10: days = 31; break;
case 11: days = 30; break;
case 12: days = 31; break;
default: break;
}
也更推荐用 map 的方法:
let getDays={
1:31,
2:IsLeapYear(year) ? 29 : 28,
3:31,
...
}
getDays[month]
避免直接使用字符串作为条件
function convertToHex(color) {
if (typeof color === 'string') {
if (color === 'slate') {
return '#64748b'
} else if (color === 'gray') {
return '#6b7280'
} else if (color === 'red') {
return '#ef4444'
} else if (color === 'orange') {
return '#f97316'
} else if (color === 'yellow') {
return '#eab308'
} else if (color === 'green') {
return '#22c55e'
} else {
return '#ffffff'
}
} else {
return '#ffffff'
}
}
//改进
const Colors = {
SLATE: '#64748b',
GRAY: '#6b7280',
// ...
}
function convertToHex(color) {
if (color in Colors) {
return Colors[color]
} else {
return '#ffffff'
}
}
convertToHex(Colors.SLATE)
不符合预期,提前 return
const Colors = {
SLATE: '#64748b',
GRAY: '#6b7280',
// ...
}
function convertToHex(color) {
if (!color in Colors) {
return '#ffffff'
}
return Colors[color]
}
convertToHex(Colors.SLATE)
用对象传参
把参数包装成一个对象再传,否则谁能读懂这种没头没尾的且要求顺序的参数的意义?
function getItem(price, quantity, name, description) {}
getItem(15, undefined, 'bananas', 'fruit')
function getItem(args) {
const {price, quantity, name, description} = args
}
getItem({
name: 'bananas',
price: 10,
quantity: 1,
description: 'fruit'
})
Watch immediate
当 watch 一个变量的时候,初始化时并不会执行,如下面的例子,你需要在created的时候手动调用一次。
// bad
created() {
this.getsearchText();
},
watch: {
searchText: 'getSearchText',
}
你可以添加immediate属性,这样初始化的时候也会触发,代码也就可以简化成这样
// good
watch: {
searchText: {
handler: 'getSearchText',
immediate: true,
}
}
检测元素外部(或内部)的单击
例如我们检测一个id为 target 的 div 目标元素
let el= document.querySelector('#target')
window.addEventListener('mousedown', e => {
// 获取被点击的元素
const clickedEl = e.target;
if (el.contains(clickedEl)) {
//在 "el "里面点击了
} else {
//在 "el "外点击了
}
});
iframe框架内页面控制父框架页面跳转到某地址
const { href } = this.$router.resolve({ path: "/index", query: { key: key } });
// iframe 控制父页面跳转
window.parent.window.location.href = href
hookEvent
组件内使用
开发中用到定时器时我们一般这样
// bad
mounted() {
// 创建一个定时器
this.timer = setInterval(() => {
// ......
}, 500);
},
// 销毁这个定时器。
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
而借助 hook,可以更方便维护
// good
mounted() {
let timer = setInterval(() => {
// ......
}, 500);
this.$once("hook:beforeDestroy", function() {
if (timer) {
clearInterval(timer);
timer = null;
}
});
}
监听子组件生命周期函数
原本
//父组件
<child
:value="value"
@childMounted="onChildMounted"
/>
method () {
onChildMounted() {
// do something...
}
},
// 子组件
mounted () {
this.$emit('childMounted')
},
hooks:
//父组件
<child
:value="value"
@hook:mounted="onChildMounted"
/>
method () {
onChildMounted() {
// do something...
}
},
在Vue组件中,可以用过$on,$once去监听所有的生命周期钩子函数,如监听组件的 updated 钩子函数可以写成 this.$on('hook:updated', () => {})
外部监听生命周期函数
我们有时会遇到这样的情况,用了一个第三方组件,当需要监听第三方组件数据的变化,但是组件又没有提供change事件时。我们可以利用Vue 提供的@hook:updated 来监听组件的 updated 生命钩子函数
<template>
<!--通过@hook:updated监听组件的updated生命钩子函数-->
<!--组件的所有生命周期钩子都可以通过@hook:钩子函数名 来监听触发-->
<custom-select @hook:updated="onSelectUpdated" />
</template>
<script>
import CustomSelect from './components/custom-select'
export default {
components: {
CustomSelect
},
methods: {
onSelectUpdated() {
console.log('custom-select组件的updated钩子函数被触发')
}
}
}
</script>
vue跳转相同路径报错
在vue的router的js中添加下面代码,new VueRouter 前
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
// push
VueRouter.prototype.push = function push(location, onResolve, onReject) {
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
// replace
VueRouter.prototype.replace = function push(location, onResolve, onReject) {
if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject)
return originalReplace.call(this, location).catch(err => err)
}
Vue-cli3 打包后报错 Failed to load resource: net::ERR_FILE_NOT_FOUND
根目录下新建文件 vue.config.js
// vue.config.js
module.exports = {
publicPath: './'
}
默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,例如 www.my-app.com/。如果应用被部署在一个… www.my-app.com/my-app/,则设置 publicPath 为 /my-app/。
这个值也可以被设置为空字符串 ('') 或是相对路径 ('./'),这样所有的资源都会被链接为相对路径,这样打出来的包可以被部署在任意路径。
css解决fixed布局不会出现滚动条的问题
如果我们布局的是fixed并且想要高度为100%的时候,我们一般会这样设置:
div {
display:fixed;
height:100%;
overflow:scroll;
}
但是这样的话不会出现滚动条,设置
div {
top: 0;
bottom:0;
position:fixed;
overflow-y:scroll;
overflow-x:hidden;
}
require.context() 自动注册
require.context():
你可以通过 require.context() 函数来创建自己的 context。
可以给这个函数传入三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。
webpack 会在构建中解析代码中的 require.context() 。
// 利用require.context()自动引入 除 index.js 外其他 js 文件
const routerContext = require.context('./', true, /.js$/)
routerContext.keys().forEach(route => {
// 如果是根目录的 index.js 、不处理
if (route.startsWith('./index')) {
return
}
const routerModule = routerContext(route)
/**
* 兼容 import export 和 require module.export 两种规范
*/
routes = routes.concat(routerModule.default || routerModule)
})
生产环境去除 console.log
vue.config.js 中配置
configureWebpack: (config) => {
if (process.env.NODE_ENV === "production") {
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true;
config.optimization.minimizer[0].options.terserOptions.compress.pure_funcs = [
"console.log",
];
}
}
chrome表单自动填充导致input文本框背景失效
我们在开发登录页的时候经常遇到,登陆页的表单自动填充导致input文本框背景失效的问题。
// 自动填充样式覆盖
input:-internal-autofill-previewed,
input:-internal-autofill-selected {
-webkit-text-fill-color: #fff;
transition: background-color 5000s ease-out 0.5s;
}
将一个 prop 限制在一个类型的列表中
我们在使用 prop 时,可能会有时候需要判断该 prop 是否在我们规定的范围内(或者说规定的值内),这个时候我们可以使用 prop 定义中的 validator 选项,将一个 prop 类型限制在一组特定的值里。
// 只能选择一个
props: {
type: String,
validator(value) {
return ['A', 'B', 'C'].indexOf(value) > -1
}
}
validator 函数接收一个prop值,如果 prop 有效或无效,则返回 true 或 false。
Vue在子组件中判断父组件是否传来事件
在做二次封装时,我们经常用到,v-bind="$attrs" 和 v-on="$listeners"进行多层组件监听,那么我们还可以利用 $listeners在子组件中判断父组件是否传来事件
例如我们封装一个搜索组件,里面有重置按钮,当我们点击重置按钮时,默认操作是清空搜索栏的值并且刷新列表,而如果父组件传来事件,则以自定义事件为准,即我们想点击重置按钮做一些其他的自定义操作。
resetFields() {
//...
if (this.$listeners.resetFields) {
// 自定义事件
this.$emit('resetFields')
} else {
// 默认刷新列表事件
this.loadList()
}
}
Vue中的method赋值为高阶函数防抖节流
<script>
import { debounce } from "lodash";
export default {
methods: {
search: debounce(async function (keyword) {
// ... 请求逻辑
}, 500),
},
};
</script>
给 slot 插槽绑定事件
- 1、作用域插槽 slot-scope 传方法
<!-- 伪代码:下拉框组件 -->
<template>
<slot change-display="changeDisplay"></slot>
<div v-show="visiable">*下拉框代码省略*<div>
<template>
<script>
export default {
data(){
return {
visiable: false
}
}
methods:{
changeDisplay(){
this.visiable = !this.visiable
}
}
}
</script>
使用:
<!--使用下拉弹窗组件-->
<dropdown v-model="value" :list="list">
<button slot-scope="{changeDisplay}"
@click="changeDisplay">{{value}}</button>
</dropdown>
- 2、vnode 中对应的页面元素
<!-- 伪代码:下拉框组件 -->
<template>
<slot></slot>
<div v-show="visiable">*下拉框代码省略*<div>
<template>
<script>
export default {
data(){
return {
visiable: false
reference: undefined
}
}
methods:{
changeDisplay(){
this.visiable = !this.visiable
}
}
mounted() {
if (this.$slots.default) {
this.reference = this.$slots.default[0].elm
}
if (this.reference) {
this.reference.addEventListener('click', this.changeVisiable, false)
// hook
this.$once('hook:beforeDestroy', () => {
this.reference.removeEventListener('click', this.changeVisiable)
})
}
}
}
</script>
二次封装作用域插槽
在二次封装组件时,我们知道可以通过判断 $slots.xxx 是否存在来判断我们在使用这个组件时是否传递了插槽内容。从而更好的定制默认的插槽内容。
那么在二次封装一个原本具有作用域插槽的组件时,我们可以通过 $scopedSlots.xxx 来进行判断
子组件
<template>
<Tree class="tree" v-if="items.length" :data="items" :options="options" :filter='search' ref="tree" v-model="treeModel">
<!-- 作用域插槽 -->
<template slot-scope="{node}">
<span v-if="!$scopedSlots.default">{{node.text}}</span>
<slot v-else :node="node"></slot>
</template>
</Tree>
</template>
使用
```vue
<template>
<custom-tree ref="tree" checkbox :data='data' :props="{children:'children',text: 'text'}">
<template slot-scope="{node}">
<span class="tree-text">
<!-- 自定义插槽内容 -->
<template v-if="!node.children">
<van-icon name="user-o" size="18" class="icon" />
{{ node.text }}
</template>
<template v-else>
{{ node.text }}
</template>
</span>
</template>
</custom-tree>
</template>
优雅更新props
更新 prop 在业务中是很常见的需求,但在子组件中不允许直接修改 prop,因为这种做法不符合单向数据流的原则,在开发模式下还会报出警告。因此大多数人会通过 $emit 触发自定义事件,在父组件中接收该事件的传值来更新 prop。
// 子组件.vue
export defalut {
props: {
title: String
},
methods: {
changeTitle(){
this.$emit('change-title', 'hello')
}
}
}
// 父组件.vue:
<child :title="title" @change-title="changeTitle"></child>
export default {
data(){
return {
title: 'title'
}
},
methods: {
changeTitle(title){
this.title = title
}
}
}
这种做法没有问题,我也常用这种手段来更新 prop。但如果你只是想单纯的更新 prop,没有其他的操作。那么 sync 修饰符能够让这一切都变得特别简单。
// 父组件
<child :title.sync="title"></child>
// 子组件
export defalut {
props: {
title: String
},
methods: {
changeTitle(){
this.$emit('update:title', 'hello')
}
}
}
只需要在绑定属性上添加 .sync,在子组件内部就可以触发 update:属性名 来更新 prop。可以看到这种手段确实简洁且优雅,这让父组件的代码中减少一个“没必要的函数”。
provide/inject
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
简单来说,一个组件将自己的属性通过 provide 暴露出去,其下面的子孙组件 inject 即可接收到暴露的属性。
// App.vue
export default {
provide() {
return {
app: this
}
}
}
// 子组件
export default {
inject: ['app'],
created() {
console.log(this.app) // App.vue实例
}
}
// 在 2.5.0+ 版本可以通过设置默认值使其变成可选项:
export default {
inject: {
app: {
default: () => ({})
}
},
created() {
console.log(this.app)
}
}
如果你想为 inject 的属性变更名称,可以使用 from 来表示其来源:
export default {
inject: {
myApp: {
// from的值和provide的属性名保持一致
from: 'app',
default: () => ({})
}
},
created() {
console.log(this.myApp)
}
}
需要注意的是 provide 和 inject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。但是某些时候,或许它能帮助到我们。
小型状态管理器
大型项目中的数据状态会比较复杂,一般都会使用 vuex 来管理。但在一些小型项目或状态简单的项目中,为了管理几个状态而引入一个库,显得有些笨重。
在 2.6.0+ 版本中,新增的 Vue.observable 可以帮助我们解决这个尴尬的问题,它能让一个对象变成响应式数据:
// store.js
import Vue from 'vue'
export const state = Vue.observable({
count: 0
})
// 使用
<div @click="setCount">{{ count }}</div>
import {state} from '../store.js'
export default {
computed: {
count() {
return state.count
}
},
methods: {
setCount() {
state.count++
}
}
}
当然你也可以自定义 mutation 来复用更改状态的方法:
import Vue from 'vue'
export const state = Vue.observable({
count: 0
})
export const mutations = {
SET_COUNT(payload) {
if (payload > 0) {
state.count = payload
}
}
}
// 使用
import {state, mutations} from '../store.js'
export default {
computed: {
count() {
return state.count
}
},
methods: {
setCount() {
mutations.SET_COUNT(100)
}
}
}
卸载watch观察
通常定义数据观察,会使用选项的方式在 watch 中配置:
export default {
data() {
return {
count: 1
}
},
watch: {
count(newVal) {
console.log('count 新值:'+newVal)
}
}
}
// 除此之外,数据观察还有另一种函数式定义的方式:
export default {
data() {
return {
count: 1
}
},
created() {
this.$watch('count', function(){
console.log('count 新值:'+newVal)
})
}
}
它和前者的作用一样,但这种方式使定义数据观察更灵活,而且 $watch 会返回一个取消观察函数,用来停止触发回调:
let unwatchFn = this.$watch('count', function(){
console.log('count 新值:'+newVal)
})
this.count = 2 // log: count 新值:2
unwatchFn()
this.count = 3 // 什么都没有发生...
// $watch 第三个参数接收一个配置选项:
this.$watch('count', function(){
console.log('count 新值:'+newVal)
}, {
immediate: true // 立即执行watch
})
巧用template
相信 v-if 在开发中是用得最多的指令,那么你一定遇到过这样的场景,多个元素需要切换,而且切换条件都一样,一般都会使用一个元素包裹起来,在这个元素上做切换。
<div v-if="status==='ok'">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</div>
如果像上面的 div 只是为了切换条件而存在,还导致元素层级嵌套多一层,那么它没有“存在的意义”。
我们都知道在声明页面模板时,所有元素需要放在 <template> 元素内。除此之外,它还能在模板内使用,<template> 元素作为不可见的包裹元素,只是在运行时做处理,最终的渲染结果并不包含它。
<template>
<div>
<template v-if="status==='ok'">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
</div>
</template>
同样的,我们也可以在 <template> 上使用 v-for 指令,这种方式还能解决 v-for 和 v-if 同时使用报出的警告问题。
<template v-for="item in 10">
<div v-if="item % 2 == 0" :key="item">{{item}}</div>
</template>
过滤器复用
<div>{{ text | capitalize }}</div>
export default {
data() {
return {
text: 'hello'
}
},
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
}
试想一个场景,不仅模板内用到这个函数,在 method 里也需要同样功能的函数。但过滤器无法通过 this 直接引用,难道要在 methods 再定义一个同样的函数吗?
要知道,选项配置都会被存储在实例的 $options 中,所以只需要获取 this.$options.filters 就可以拿到实例中的过滤器。
export default {
methods: {
getDetail() {
this.$api.getDetail({
id: this.id
}).then(res => {
let capitalize = this.$options.filters.capitalize
this.title = capitalize(res.data.title)
})
}
}
}
除了能获取到实例的过滤器外,还能获取到全局的过滤器,因为 this.$options.filters 会顺着 __proto__ 向上查找,全局过滤器就存在原型中。
字数限制省略号
//和methods同级
// <a>{{ record.remark | ellipsisTitle }}</a>
filters: {
ellipsisTitle(value) {
if (!value) return ''
if (value.length > 15) {
// 截取长度
return value.slice(0, 15) + '...'
}
return value
},
},
自定义指令获取实例
有的情况下,当需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。像是项目中常用的权限指令,它能精确到某个模块节点。大概思路为获取权限列表,如果当前绑定权限不在列表中,则删除该节点元素。
Vue.directive('role', {
inserted: function (el, binding, vnode) {
let role = binding.value
if(role){
const applist = sessionStorage.getItem("applist")
const hasPermission = role.some(item => applist.includes(item))
// 是否拥有权限
if(!hasPermission){
el.remove() //没有权限则删除模块节点
}
}
}
})
复制代码
自定义指令钩子函数共接收3个参数,包括 el (绑定指令的真实dom)、binding (指令相关信息)、vnode (节点的虚拟dom)。
假设现在业务发生变化,applist 存储在 vuex 里, 但指令内想要使用实例上的属性,或者是原型上的 $store。我们是没有办法获取到的,因为钩子函数内并没有直接提供实例访问。vnode 作为当前的虚拟dom,它里面可是绑定到实例上下文的,这时候访问 vnode.context 就可以轻松解决问题。
Vue.directive('role', {
inserted: function (el, binding, vnode) {
let role = binding.value
if(role){
// vnode.context 为当前实例
const applist = vnode.context.$store.state.applist
const hasPermission = role.some(item => applist.includes(item))
if(!hasPermission){
el.remove()
}
}
}
})
复制代码
优雅注册插件
插件通常用来为 Vue 添加全局功能。像常用的 vue-router、vuex 在使用时都是通过 Vue.use 来注册的。Vue.use 内部会自动寻找 install 方法进行调用,接受的第一个参数是 Vue 构造函数。
一般在使用组件库时,为了减小包体积,都是采用按需加载的方式。如果在入口文件内逐个引入组件会让 main.js 越来越庞大,基于模块化开发的思想,最好是单独封装到一个配置文件中。配合上 Vue.use,在入口文件使用能让人一目了然。
vant.config.js:
import {
Toast,
Button
} from 'vant'
const components = {
Toast,
Button
}
const componentsHandler = {
install(Vue){
Object.keys(components).forEach(key => Vue.use(components[key]))
}
}
export default componentsHandler
复制代码
main.js
import Vue from 'vue'
import vantCompoents from '@/config/vant.config'
Vue.config.productionTip = false
Vue.use(vantCompoents)
new Vue({
render: h => h(App)
}).$mount('#app')
复制代码
自动化引入模块
在开发中大型项目时,会将一个大功能拆分成一个个小功能,除了能便于模块的复用,也让模块条理清晰,后期项目更好维护。
像 api 文件一般按功能划分模块,在组合时可以使用 require.context 一次引入文件夹所有的模块文件,而不需要逐个模块文件去引入。每当新增模块文件时,就只需要关注逻辑的编写和模块暴露,require.context 会帮助我们自动引入。
需要注意 require.context 并不是天生的,而是由 webpack 提供。在构建时,webpack 在代码中解析它。
let importAll = require.context('./modules', false, /.js$/)
class Api extends Request{
constructor(){
super()
//importAll.keys()为模块路径数组
importAll.keys().map(path =>{
//兼容处理:.default获取ES6规范暴露的内容; 后者获取commonJS规范暴露的内容
let api = importAll(path).default || importAll(path)
Object.keys(api).forEach(key => this[key] = api[key])
})
}
}
export default new Api()
复制代码
require.context 参数:
- 文件夹路径
- 是否递归查找子文件夹下的模块
- 模块匹配规则,一般匹配文件后缀名
只要是需要批量引入的场景,都可以使用这种方法。包括一些公用的全局组件,只需往文件夹内新增组件即可使用,不需要再去注册。如果还没用上的小伙伴,一定要了解下,简单实用又能提高效率。
路由懒加载(动态chunkName)
路由懒加载作为性能优化的一种手段,它能让路由组件延迟加载。通常我们还会为延迟加载的路由添加“魔法注释”(webpackChunkName)来自定义包名,在打包时,该路由组件会被单独打包出来。
let router = new Router({
routes: [
{
path:'/login',
name:'login',
component: import(/* webpackChunkName: "login" */ `@/views/login.vue`)
},
{
path:'/index',
name:'index',
component: import(/* webpackChunkName: "index" */ `@/views/index.vue`)
},
{
path:'/detail',
name:'detail',
component: import(/* webpackChunkName: "detail" */ `@/views/detail.vue`)
}
]
})
复制代码
上面这种写法没问题,但仔细一看它们结构都是相似的,作为一名出色的开发者,我们可以使用 map 循环来解决这种重复性的工作。
const routeOptions = [
{
path:'/login',
name:'login',
},
{
path:'/index',
name:'index',
},
{
path:'/detail',
name:'detail',
},
]
const routes = routeOptions.map(route => {
if (!route.component) {
route = {
...route,
component: () => import(`@/views/${route.name}.vue`)
}
}
return route
})
let router = new Router({
routes
})
复制代码
在书写更少代码的同时,我们也把“魔法注释”给牺牲掉了。总所周知,代码中没办法编写动态注释。这个问题很尴尬,难道就没有两全其美的办法了吗?
强大的 webpack 来救场了,从 webpack 2.6.0 开始,占位符 [index] 和 [request] 被支持为递增的数字或实际解析的文件名。我们可以这样使用“魔法注释”:
const routes = routeOptions.map(route => {
if (!route.component) {
route = {
...route,
component: () => import(/* webpackChunkName: "[request]" */ `@/views/${route.name}.vue`)
}
}
return route
})
复制代码
使用Map代替switch或多个if判断,该方式也是可避免代码复杂度过高的有效方法之一
function getStatusText(status) {
switch (status) {
case 1:
return '待发货';
case 2:
return '已发货';
case 3:
return '已完成';
default:
return '';
}
}
// 使用Map替代
const statusMap = new Map()
.set(1, '待发货')
.set(2, '已发货')
.set(3, '已完成');
// 或
const statusMap = new Map([
[1, '待发货'],
[2, '已发货'],
[3, '已完成'],
]); // 这种写法的内部执行的算法实际上也是循环执行set,与上面自己写set其实是一样的
const statusText = statusMap.get(status);
// 其实还有更简单的,直接用数组下标映射
const statusText = ['待发货', '已发货', '已完成'][status - 1];
复制代码
此处不推荐使用对象字面量存储数据,阮一峰老师的建议很有道理。
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
尤其公司有代码复杂度要求的同学,一定要学会以上两种方法。
更可靠的判断数据类型
typeof 检测一些基本的数据类型,正则、{}、[]、null输出结果为object
console.log(typeof /\d/); //object
console.log(typeof {}); //object
console.log(typeof []); //object
console.log(typeof (null)); //object
console.log(typeof 123); //number
console.log(typeof true); //boolean
console.log(typeof function () {}); //function
console.log(typeof (undefined)); //undefined
复制代码
A instanceof B 判断a的构造器是否为b
function b(){}
let a = new b;
console.log(a instanceof b); //true
console.log(b instanceof Object); //true
let arr = [1,2,3,4];
console.log(arr instanceof Array); //true
复制代码
以上两种方法有有其局限性,推荐使用更可靠的判断数据类型方法
function judgeDataType(val, type) {
const dataType = Object.prototype.toString.call(val).replace(/[object (\w+)]/, "$1").toLowerCase();
return type ? dataType === type : dataType;
}
console.log(judgeDataType("young")); // "string"
console.log(judgeDataType(20190214)); // "number"
console.log(judgeDataType(true)); // "boolean"
console.log(judgeDataType([], "array")); // true
console.log(judgeDataType({}, "array")); // false
复制代码
使用Proxy在对象执行某些操作前拦截做一些事情
常用于更改原生API的行为,如数组方法或框架提供的方法等:
// 如拦截微信小程序框架的showToast方法
const toastProxy = new Proxy(wx.showToast, {
apply: (target, ctx, argArr) => {
// 在方法被调用时更改传入参数
const newArgArr = argArr;
newArgArr[0].title = `温馨提示:${newArgArr[0].title}`;
return Reflect.apply(target, ctx, newArgArr); // 执行目标对象原始行为
},
});
toastProxy({ title: '您输入的名称过长' });
复制代码
这只是一个小应用,实际上它可以做很多大事,例如埋点上报等,用法大家自己扩展,要了解这个思路,遇到某些情况时能够想起来用Proxy解决问题。
可以拦截的操作:
const proxy = new Proxy(target, handler);
handler.get // 拦截取值
handler.set // 拦截赋值
handler.has // 拦截in运算符 如'nickname' in user
handler.apply // 拦截方法被调用
handler.construct // 拦截 Proxy 实例作为构造函数调用的操作,比如:new proxy(...args)
handler.defineProperty // 拦截
// Object.defineProperty(proxy, propKey, propDesc)
// Object.defineProperties(proxy, propDescs)
handler.deleteProperty // 拦截删除属性 delete proxy[propKey]
handler.ownKeys // 拦截
// Object.getOwnPropertyNames(proxy)
// Object.getOwnPropertySymbols(proxy)
// Object.keys(proxy)
// for...in 循环
handler.isExtensible // 拦截 Object.isExtensible(proxy),
handler.preventExtensions // 拦截 Object.preventExtensions(proxy)
handler.getPrototypeOf // 拦截 Object.getPrototypeOf(proxy)
handler.setPrototypeOf // 拦截 Object.setPrototypeOf(proxy, proto)
handler.getOwnPropertyDescriptor // 拦截 Object.getOwnPropertyDescriptor(proxy, propKey)
复制代码
格式化时间(加强版)
/**
* 格式化时间对象
* @param {Date|Number} date Date对象
* @param {String} fmt 目标格式,如:yyyy年MM月dd日,MM/dd/yyyy,yyyyMMdd,yyyy-MM-dd hh:mm:ss等
* @returns {String} 格式化结果;异常情况下返回空串
*/
const formatDateTime = (date, fmt) => {
const dateObj = date instanceof Date ? date : new Date(date);
if (isNaN(dateObj.getTime())) return '';
let dateStr = fmt || 'yyyy-MM-dd hh:mm:ss';
// 处理年
if (/(y+)/.test(dateStr)) {
// RegExp.$1代表括号里匹配到的字符串
// '2022'.substr(2) -> '22'
// 如yyyy年MM月dd日处理为2022年,yy年处理为22年
dateStr = dateStr.replace(RegExp.$1, (`${dateObj.getFullYear()}`).substr(4 - RegExp.$1.length));
}
// 处理月份、日、小时、分、秒、毫秒
const obj = {
'M+': dateObj.getMonth() + 1, // 月份
'd+': dateObj.getDate(), // 日
'h+': dateObj.getHours(), // 小时
'm+': dateObj.getMinutes(), // 分
's+': dateObj.getSeconds(), // 秒
S: dateObj.getMilliseconds(), // 毫秒
};
for (const [key, value] of Object.entries(obj)) {
if (new RegExp(`(${key})`).test(dateStr)) {
// RegExp.$1 -> 'M'或'MM' 如果是M的话直接返回月份值如5
// 如果是MM要将5前面补0,变成'05',其他同理
dateStr = dateStr.replace(RegExp.$1, (RegExp.$1.length === 1) ? (value) : (`${value}`.padStart(2, '0')));
}
}
return dateStr;
};
复制代码
序列(range)生成器
const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step));
range(0, 4, 1);
// [0, 1, 2, 3, 4]
// 范围1..10,步进2
range(1, 10, 2);
// [1, 3, 5, 7, 9]
复制代码
大家日常开发中使用类似上面的一些小技巧方法可以大大提高开发效率,使你的代码更优雅。
推荐使用ES6+语法,新语法带来的便利性不再过多赘述。
不推荐使用lodash等现成的工具库,或者说可以用,但是不要只会用不知道是如何实现的,这样对你的提升没有什么帮助,在项目的公共模块下写一遍也没啥不好的。
Vue中通过v-for指令动态生成多级表头时,如果以都以index作为key,极有可能导致自定义列显示隐藏操作时出现表格错乱的问题
<el-table-column
v-for="(col,index) in tableCols"
:key="index"
:label="col.label"
:prop="col.prop">
<template v-if="col.children">
<el-table-column
v-for="(child,cindex) in col.children"
:key="cindex"
:label="child.label"
:prop="child.prop">
</el-table-column>
</template>
</el-table-column>
复制代码
分析问题:如上代码中,一级和二级表头在动态渲染是分别以index和cindex作为key,我们知道,这里的index和cindex其实都是指循环时的下标0123...,也就是说,这样渲染结束之后,一级表头的与二级表头会产生重复的key,虽然初次渲染时不会有问题,但是一旦涉及到表格列配置或动态显示隐藏导致的表头数据变更,就会触发到vue中的核心--diff算法。众所周知,key是diff算法中判断页面内容是否需要重绘的关键指标,同一个表格中出现重复的key会对diff算法产生干扰,进而导致表格渲染结果错乱。
解决问题:
<el-table-column
v-for="(col,index) in tableCols"
:key="col.prop"
:label="col.label"
:prop="col.prop">
<template v-if="col.children">
<el-table-column
v-for="(child,cindex) in col.children"
:key="child.prop"
:label="child.label"
:prop="child.prop">
</el-table-column>
</template>
</el-table-column>
复制代码
在一个绘图项目中,发现给某个元素绑定了@click事件但是无论如何都没办法触发
分析问题:因为项目中绘图的定制性较高,无法使用echarts或者其他开源库,所以使用原生html与canvas绘制,为了实现画布、图例等布局效果使用了绝对定位,相当于对画布和图例进行了叠加,但由于先使用div绘制了图例,后绘制的canvas,即canvas是覆盖于div图例之上的,每次点击图例元素的位置相当于都是点击在了canvas上,导致为图例绑定的@click事件总是无法触发
解决问题:为图例元素设置z-index,保证其图层位于canvas上方
title中含空格导致显示不全
let value="2022-06-10 09:21"
let html=`<span title=${value}>${value"}</span>`
复制代码
显示效果为:title中只有2022-06-10,后面的时间丢失
解决方案:
let value="2022-06-10 09:21"
let html=`<span title="${value}">${value"}</span>`
项目从零到一
项目中使用sass时由于版本过高导致的报错
1.先删掉整个node_moudules文件夹
2.删除package.json中sass和sass-loader相关配置项
3.按照下面的要求重新安装sass和loader
node-v 16.11.1及以上版本: npm install sass-loader@10.2.0 node-sass@6.0.1 --save-dev
node-v 16.11.1 以下版本: npm install sass-loader@7.3.1 node-sass@4.14.1 --save-dev
node版本与sass版本对应关系:
3.大屏项目自适应使用transform(scale)
data(){
return {
designUI:{
width:1920,
height:1080
}
}
},
created(){
this.setScale();
window.onresize=()=>this.setScale();
},
methods: {
setScale(){
let ratioX = window.innerWidth / this.designUI.width;
let ratioY = window.innerHeight / this.designUI.height;
let style = document.body.style;
style.transform="scale(" + ratioX + "," + ratioY + ")";
style.transformOrigin="left top";
style.backgroundSize="100% 100%";
style.overflow='hidden';
}
}
后续:由于对body进行了缩放,导致elment-ui中与tooltip相关的提示信息都出现了位置偏移,究其原因,在tooltip出现时,是在body内部末尾追加元素,其定位方式其实是相对于body的宽高的,想要使其能够正常显示,就要使body保持正常缩放比例,因此可以将原本加在body上的缩放样式转移到其下一层根元素document.getElementById("app")中,同样达到了自适应的效果,但又不会影响到tooltip气泡位置。注意,此时要将生命周期created改成mounted。
使用液晶字体
注意:引入字体文件后如果启动项目报错提示.TTF文件解析失败,则需要安装url-loader
如果安装url-loader后重启项目依然失败,可以检查下是build/webpack.base.conf.js文件中module-rules中是否只匹配解析了ttf,我们只需要把TTF加上就行
使用天地图
参考文档:zhuanlan.zhihu.com/p/121701936
注意:请一定保证在页面dom元素挂在完成后(即mounted生命周期中)进行地图初始化,如果非要在created中初始化,请配合$nectTick使用
一堆index.vue文件导致断点总是进入错误的位置
参考文档:www.freesion.com/article/446…
环境变量
开发环境:config/dev.env.js
生产环境:config/prod.env.js
前端配置跨域代理
按照以往的开发经验,前端处理跨域一般是在vue.config.js中通过代理服务器proxy实现,我如法炮制之后发现请求接口总是404(前提:postman测试接口是正常的),把百度翻烂之后终于发现问题所在,老项目是用vue-cli2创建的,配置代理可以在vue.config.js中,但是我的新项目是vue-cli3创建的,要配置跨域代理必须在根路径下的config/index.js中实现,配置项和vue-cli2的方式是一样的,相当于换了个地方而已,也就是说,vue.config.js已经完全没用啦。
vue-cli2老项目,在vue.config.js中配置代理解决跨域:
vue-cli3新项目,在config/index.js中配置代理解决跨域:
参考文档:t.zoukankan.com/EnSnail-p-8…
8.js中动态引入并加载图片
你如果天真的以为
<img :src="imgSrc"> this.imgSrc='../images/logo.png' 这样的代码能让你正常渲染图片的话就大错特错了,必须使用require才行 this.imgSrc=require('../images/logo.png')
新项目托管到git
参考文档:Git新建仓库首次提交
添加闪烁动画
.span{
-webkit-animation: twinkling 1s infinite ease-in-out;
animation: twinkling 1s infinite ease-in-out;
}
@-webkit-keyframes twinkling{
0%{
opacity: 0.5;
}
100%{
opacity: 1;
}
}
@keyframes twinkling{
0%{
opacity: 0.5;
}
100%{
opacity: 1;
}
}
复制代码
11.vscode设置保存时自动格式化代码,自动缩进对齐
1.文件–首选项–设置 或者vscode左下角齿轮–设置
2.搜索框搜索emmet.include ,点击在settings.json中编辑
3.在打开的settings.json中添加这两行代码
"editor.formatOnType": true,
"editor.formatOnSave": true,
复制代码
参考文档:blog.csdn.net/qq_43657442…
好用的vscode扩展程序
将后台返回的文件流格式的图片进行转化并显示
getImg() {
axios
.get(url, {
responseType: "arraybuffer"
})
.then(res => {
return (
"data:image/png;base64," +
btoa(
new Uint8Array(res.data).reduce(
(data, byte) => data + String.fromCharCode(byte),
""
)
)
);
})
.then(data => {
this.imgSrc = data;
})
.catch(ex => {
console.error(ex);
});
}
复制代码
下载后台返回的文件流格式Excel
export function exportExcel() {
return request({
url: `/dna/watersampledata/export`,
method: "get",
responseType: "blob"
})
}
复制代码
clickExport() {
exportExcel().then(res => {
const blob = res.data;
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = e => {
const a = document.createElement("a");
a.download = `数据.xls`;
a.href = e.target.result;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
});
}
复制代码
记录一次犯蠢
平时自己用ES6对象简写用的很爽,但是当用到一些组件库时看到对象简写经常会反应不过来,以为是什么不可随意改变的高级用法,这不,使用天地图创建地图标注是乍一看就被唬到了,小小犯蠢
当时想要创建多个自定义标注图片,类似icon icon1 icon2这种的,然后在创建Marker时根据条件使用不同图片,就发现只有叫icon的能被渲染,其他都渲染失败,当时还在纳闷,明明是自己定义的变量,为啥改个名字就不能用了呢?后来检查代码才突然灵光乍现,麻蛋,识别不了除icon之外的名字因为天地图内置方法new Marker中规定了必须以icon为key,{icon}这种方式其实是触发了ES6的对象简写,完整写法应该是{icon:icon},即{icon:icon1},而后面的icon1才是我们在前文自定义的变量名字,随便怎么改都行
总结:当时用框架或者库时,看到一些貌似高级的语法千万不要被唬住了
关于Content-Type
Content-Type分类
application/x-www-form-urlencoded HTTP会将请求参数用key1=val1&key2=val2的方式进行组织,并放到请求实体里面,注意如果是中文或特殊字符如"/"、","、“:" 等会自动进行URL转码。不支持文件,一般用于表单提交(传参显示在Form Data中)。
multipart/form-data与application/x-www-form-urlencoded不同,这是一个多部分多媒体类型。首先生成了一个 boundary 用于分割不同的字段,在请求实体里每个参数以------boundary开始,然后是附加信息和参数名,然后是空行,最后是参数内容。多个参数将会有多个boundary块。如果参数是文件会有特别的文件域。最后以------boundary–为结束标识。multipart/form-data支持文件上传的格式,一般需要上传文件的表单则用该类型(传参显示在payload中)。
application/jsonJSON 是一种轻量级的数据格式,以“键-值”对的方式组织的数据。这个使用这个类型,需要参数本身就是json格式的数据,参数会被直接放到请求实体里,不进行任何处理。服务端/客户端会按json格式解析数据(传参显示在payload中)。 application/xml 和 text/xml与application/json类似,这里用的是xml格式的数据,text/xml的话,将忽略xml数据里的编码格式
Content-Type使用
request 的Content-Type
一般我们在开发的过程中需要注意客户端发送请求(Request)时的Content-Type设置,特别是使用ajax的时候,如果设置得不准确,很有可能导致请求失败。比如在spring中,如果接口使用了@RequestBody,spring强大的自动解析功能,会将请求实体的内容自动转换为Bean,但前提是请求的Content-Type必须设置为application/json,否正就会返回415错误。(注:415 错误是 Unsupported media type,即不支持的媒体类型。)
建议:如果是一个restful接口(json格式),一般将Content-Type设置为application/json; charset=UTF-8; 如果是文件上传,一般Content-Type设置为multipart/form-data 如果普通表单提交,一般Content-Type设置为application/x-www-form-urlencoded
response的Content-Type
服务端响应(Response)的Content-Type最好也保持准确,虽然一般web开发中,前端解析响应的数据不会根据Content-Type,并且服务端一般能自动设置准确的Content-Type,但是如果乱设置某些情况下可能会有问题,比如导出文件,打开图片等。如果在spring项目里使用@ResponseBody,spring会将响应的Content-Type设置为application/json;charset=UTF-8;,可能会导致文件无法导出,需要注意下。
response的Content-Type设置建议:
一般情况下不需要显示设置; 如果是文件导出,Content-Type 设置为 multipart/form-data,并且添加一个Content-Disposition设置为attachment;fileName=文件.后缀。 注:Content-Disposition是Content-Type的扩展,告诉浏览器弹窗下载框,而不是直接在浏览器里展示文件。因为一般浏览器对于它能够处理的文件类型,如txt,pdf 等,它都是直接打开展示,而不是弹窗下载框。
HTTP请求中 request payload 和 formData 区别?
FormData和Payload是浏览器传输给接口的两种格式,这两种方式浏览器是通过Content-Type来进行区分的(了解Content-Type),如果是 application/x-www-form-urlencoded的话,则为formdata方式,如果是application/json或multipart/form-data的话,则为 request payload的方式。 参考:www.cnblogs.com/kaibindirve…
Git不识别文件大小写
git默认配置为忽略大小写,因此无法正确检测大小写的更改
解决方法:运行git config core.ignorecase false,关闭git忽略大小写配置,即可检测到大小写名称更改
闪烁动画
.warning {
background: url("~@/assets/images/home/map_marker_warning.png");
position: relative;
&::before {
background: inherit;
content: "";
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: -2;
animation: twinkling 0.3s ease-in-out infinite; //使用闪烁动画
}
}
//定义闪烁动画
@keyframes twinkling {
0% {
opacity: 0.2;
filter: alpha(opacity=20);
transform: scale(1);
}
50% {
opacity: 1;
filter: alpha(opacity=100);
transform: scale(1.12);
}
100% {
opacity: 0.2;
filter: alpha(opacity=20);
transform: scale(1);
}
}
对document.getElementsByClassName使用forEach报错
获取的元素与报错内容:
报错原因分析:document.getElementsByClassName获取到的内容不是严格意义上的数组,而是一个HTMLColletion集合,并且是只读的
解决方案:使用Array.prototype.forEach.call(els,el=>{})代替els.forEach
截取页面内容并下载为图片
安装依赖:npm install html2canvas
let wrapper = this.$refs.domWrapper;
html2canvas(wrapper,{这里可以写很多配置项,可以缺省}).then(canvas => {
let dataURL = canvas.toDataURL("image/png");
if (dataURL !== "") {
let blob = this.dataURLToBlob(canvas.toDataURL("image/png"));
let a = document.createElement("a");
a.style.display = "none";
a.setAttribute("href", URL.createObjectURL(blob));
a.setAttribute("download", this.dlgInfo.com + ".png");
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(blob);
document.body.removeChild(a);
}
});
复制代码
截取页面内容并下载为PDF
前置工作:
1.npm i html2canvas
2.npm i jspdf
代码:
import html2canvas from "html2canvas";
import JsPDF from 'jspdf';
方法:
getPdf(title, html) {
html2canvas(html, {
allowTaint: false,
taintTest: false,
logging: false,
useCORS: true,
dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍
scale: 4, // 按比例增加分辨率
}).then(canvas => {
var pdf = new JsPDF('p', 'mm', 'a4'); // A4纸,纵向
var ctx = canvas.getContext('2d');
var a4w = 190; var a4h = 277; // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
var imgHeight = Math.floor(a4h * canvas.width / a4w); // 按A4显示比例换算一页图像的像素高度
var renderedHeight = 0;
while (renderedHeight < canvas.height) {
var page = document.createElement("canvas");
page.width = canvas.width;
page.height = Math.min(imgHeight, canvas.height - renderedHeight);// 可能内容不足一页
// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0);
pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width)); // 添加图像到页面,保留10mm边距
renderedHeight += imgHeight;
if (renderedHeight < canvas.height) {
pdf.addPage();// 如果后面还有内容,添加一个空页
}
}
// 保存文件
pdf.save(title + '.pdf');
});
}
调用:
getPdf('我的PDF文件', this.$refs.pdfContainer);
复制代码
二维码水印 防止选中
### 生成二维码:qrcode
### 生成水印:watermark
### 阻止内容被选中:user-select: none
vue性能优化
首屏加载优化
- 1.UI组件按需加载而非一次性全部引入
- 2.路由按需加载(利用箭头函数)
- 3.图片、数据先加载可视区域,剩余部分按需滚动加载
- 4.静态资源使用CDN,用户就近获取内容,提高了资源的访问速度
- 5.适当使用浏览器缓存,减少不必要的请求请求
- 6.JS、CSS压缩合并打包,减少请求数,CSS放在头部加载,JS放在尾部加载,先外链后本页
- 7.避免css表达式、滤镜,优化图片、精灵图
- 8.较少DOM操作,避免页面频繁回流、重绘
- 9.恰当使用防抖节流
- 10.移除生产环境的控制台打印,在开发基本完成后去掉无用的console(vscode中的turbo console就蛮好的)
vue项目的理解
侧边栏跨组件传值
1、通过头部组件来控制侧边栏组件的展开折叠,用到了vuex来进行组件通信。在state中定义iscollapse判断是否折叠,在mutation中定义一个方法collapseMenu来控制state里的值
2、是由iscollapse这个值来判断侧边栏的,所以在侧边栏组件定义一个方法,然后绑定这个值,监听iscollapse的变化
3、因为是通过头部组件的按钮来控制侧边栏展开折叠的,所以给按钮绑定一个点击事件来改变侧边栏组件拿到的state状态,用$store.commit 调用mutation的collapseMenu。
动画有卡顿延迟问题,可以通过设置collapse-transition=false来解决
导航守卫登录校验
第一次登录调用接口发送给后端用户名和密码,后端收到后会进行验证,成功后返回一个token,前端拿到token后存储在cookie和vuex中。每次跳转路由,就判断cookie中有没有token,设置一个导航守卫判断用户是否登录,没有就跳转到登录页面,有的话就跳转到对应路由页面
权限渲染
初始化时只挂载一些不需要权限的路由如注册登录,等用户登录后返回当前用户的权限表,前端根据这个权限表遍历前端路由表,动态生成用户权限路由,用vue-router的add router将权限路由表动态添加到路由实例中
组件跳转方法
router-link:类似a标签跳转
this.$router.push/replace push:跳转后会像history栈添加记录,点击后退会返回上一个页面
replace:不会添加记录,上一个记录是不存在的
封装echarts
创建echarts组件来加载并初始化echarts容器,根据配置的变化更新图表,在里面新建一个props属性接收父组件传过来的配置。然后创建图表组件(如饼图),引入配置文件,将数据传给子组件echarts组件。哪个页面要用到就引入
图片懒加载
通过观察窗口和图片是否有交叉来判断是否将img写入src
雪碧图
小图标整合在一张背景透明的大图上,通过设置位置来显示不同的图标,可以减少对图片的请求。
用公共css设置背景为这张大图,这样每个元素都会以这张图片为背景,页面只用加载这一张图片
安装插件
需要安装babel-plugin-transform-remove-console插件
yarn add babel-plugin-transform-remove-console -D
复制代码
生产环境删除console.log
打开babel.config.js文件,然后在文件内添加
// 所有生产环境
const prodPlugin = []
if (process.env.NODE_ENV === 'production') {
// 如果是生产环境,则自动清理掉打印的日志,但保留error 与 warn
prodPlugin.push([
'transform-remove-console',
{
// 保留 console.error 与 console.warn
exclude: ['error', 'warn']
}
])
}
module.exports = {
plugins: [
...prodPlugin
]
}
页面元素不固定实现
参考链接:www.cnblogs.com/xiaohuasan/…
主要代码是这里
.chy-test:last-of-type { //这个才是取同类最后一个 https://www.csdn.net/tags/NtjaYgxsMjAyNTMtYmxvZwO0O0OO0O0O.html
margin-right: calc(26% + 22% / 2);
}
.chy-test:nth-child(3n) { //第3个和3n元素的处理
margin-right: 0;
}
复制代码
子组件刷新父组件 和父组件调用子组件方法
子组件刷新父组件可以使用多层$parent,从而达到刷新或关闭或改变父级或顶级窗体相关属性
当在子组件里更改了某些信息且关闭子组件后,需要父组件更新修改后的内容,该如何操作
1、$emit触发
父组件 @add=“add(val)”
子组件 this.$emit('add', newVal)
2、使用$parent触发
父组件 更新数据是由某个方法触发,如getList()
子组件 this.$parent.getList()
```
this.$refs.commonForm.$children[0].saveToDb()//保存子组件数据
复制代码
```
组件传值自定义字段是数组或对象的注意的事项
是数组设置
```
props: {
cluesList: {
type: Array,
default: ()=>[],
},
},
复制代码
```
是对象的设置
```
props: {
goalObject: {
//每个目标信息
type: Object,
default: () => {},
},
},
复制代码
```
css 背景图片
```
<div :class="showBg(state)">
</div>
复制代码
```
```
<style scoped>
.chy-bg-reject {
background: url("~@/assets/chyimages/reject.png") no-repeat;
background-position: 100% 0%;
background-size: 200px 200px;
}
.chy-bg-revoke {
background: url("~@/assets/chyimages/revoke.png") no-repeat;
background-position: 100% 0%;
background-size: 200px 200px;
}
.chy-bg-approved {
background: url("~@/assets/chyimages/approved.png") no-repeat;
background-position: 100% 0%;
background-size: 200px 200px;
}
</style>
复制代码
```
```
showBg(state) {
let bgClass = '';
if (state == '3') {
bgClass = 'chy-bg-approved'
}
else if (state == '4') {
bgClass = 'chy-bg-reject'
}
else if (state == '5') {
bgClass = 'chy-bg-revoke'
}
else {
bgClass = '';
}
return bgClass;
}
复制代码
```
打包后白屏
vue项目完成后,输入npm run build命令,会生成dist文件,本地打开dist/index.html,一般页面为空,或者报错找不到css和js文件,则需要更改一些路径参数,改成相对当前目录,然后再次运行npm run build 就可以在本地打开index.html。 在vue.config解决打包白屏,使用相对路径
publicPath: './',
引入多个组件require.context()
1.场景:如页面需要导入多个组件,原始写法:
import titleCom from '@/components/home/titleCom'
import bannerCom from '@/components/home/bannerCom'
import cellCom from '@/components/home/cellCom'
components:{titleCom,bannerCom,cellCom}
2.这样就写了大量重复的代码,利用 require.context 可以写成
const path = require('path')
const files = require.context('@/components/home', false, /.vue$/)
const modules = {}
files.keys().forEach(key => {
const name = path.basename(key, '.vue')
modules[name] = files(key).default || files(key)
})
components:modules
复制代码
这样不管页面引入多少组件,都可以使用这个方法
3.API 方法
实际上是 webpack 的方法,vue 工程一般基于 webpack,所以可以使用
require.context(directory,useSubdirectories,regExp)
接收三个参数:
directory:说明需要检索的目录
useSubdirectories:是否检索子目录
regExp: 匹配文件的正则表达式,一般是文件名
复制代码
动态组件is
场景:做一个 tab 切换时就会涉及到组件动态加载
<component v-bind:is="currentTabComponent"></component>
复制代码
但是这样每次组件都会重新加载,会消耗大量性能,所以 就起到了作用
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
复制代码
这样切换效果没有动画效果,这个也不用着急,可以利用内置的
<transition>
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
</transition>
v-cloak 解决数据闪烁
场景:在网速慢的情况下,在使用vue绑定数据的时候,渲染页面时会出现变量闪烁 用法:这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕
// template 中
<div class="#app" v-cloak>
<p>{{value.name}}</p>
</div>
// css 中
[v-cloak] {
display: none;
}
复制代码
Object.freeze
场景:一个长列表数据,一般不会更改,但是vue会做getter和setter的转换 用法:是ES5新增的特性,可以冻结一个对象,防止对象被修改 支持:vue 1.0.18+对其提供了支持,对于data或vuex里使用freeze冻结了的对象,vue不会做getter和setter的转换 注意:冻结只是冻结里面的单个属性,引用地址还是可以更改
new Vue({
data: {
// vue不会对list里的object做getter、setter绑定
list: Object.freeze([
{ value: 1 },
{ value: 2 }
])
},
mounted () {
// 界面不会有响应,因为单个属性被冻结
this.list[0].value = 100;
// 下面两种做法,界面都会响应
this.list = [
{ value: 100 },
{ value: 200 }
];
this.list = Object.freeze([
{ value: 100 },
{ value: 200 }
]);
}
})
img 加载失败
场景:有些时候后台返回图片地址不一定能打开,所以这个时候应该加一张默认图片
// page 代码
<img :src="imgUrl" @error="handleError" alt="">
<script>
export default{
data(){
return{
imgUrl:''
}
},
methods:{
handleError(e){
e.target.src=reqiure('图片路径') //当然如果项目配置了transformToRequire,参考上面 33.2
}
}
}
</script>
经典Flex布局
如今Flex布局不管是移动端还是PC端的应用已经非常广泛了,下面我列举几个平时项目中非常常见的几个需求。以下例子我们都以Vue项目为例~
flex布局均匀分布后换行问题
需求一:ul下有多个li,每三个li排一列,多余的换行显示。
很显然,绝大部分的小伙伴都会使用Flex布局,很显然会出现一个问题就是如果li是3的倍数的话就能正常显示,若不是的话,布局就不是产品经理满意的结果。
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
复制代码
解决方案:
我们在ul的底部新增li,个数为数组总长度%3的余数即可。
<li class="item" style="border: none;" v-for="(ite, idx) in list.length%3" :key="idx"></li>
复制代码
两栏布局
两栏布局:
左右两栏,左边固定,右边自适应
效果图
-
第一种方式 ---
浮动HTML部分:
<div class="outer outer1"> <div class="left">1-left</div> <div class="right">1-right</div> </div> 复制代码CSS部分:
.outer1 .left { width: 200px; float: left; } .outer1 .right { width: auto; margin-left: 200px; } 复制代码 -
第二种方式 ---
flexHTML部分:
<div class="outer outer2"> <div class="left">2-left</div> <div class="right">2-right</div> </div> 复制代码CSS部分:
.outer2 { display: flex; } .outer2 .left { flex: 0 0 200px; /* flex-grow: 0; flex-shrink:0; flex-basis:200px; */ } .outer2 .right { flex: auto; } 注意:flex: 0 0 200px是flex: flex-grow flex-shrink flex-basis的简写 复制代码 -
第三种方式 ---
positionHTML部分:
<div class="outer outer3"> <div class="left">3-left</div> <div class="right">3-right</div> </div> 复制代码CSS部分:
.outer3 { position: relative; } .outer3 .left { position: absolute; width: 200px; } .outer3 .right { margin-left: 200px; } 复制代码 -
第四种方式 ---
position againHTML部分:
<div class="outer outer4"> <div class="left">4-left</div> <div class="right">4-right</div> </div> 复制代码CSS部分:
.outer4 { position: relative; } .outer4 .left { width: 200px; } .outer4 .right { position: absolute; top: 0; left: 200px; right: 0; } 复制代码
三栏布局
三栏布局:
中间列自适应宽度,旁边两侧固定宽度
效果图
-
第一种方式 ---
定位HTML部分:
<div class="outer outer1"> <div class="left">1-left</div> <div class="middle">1-middle</div> <div class="right">1-right</div> </div> 复制代码CSS部分:
.outer1 { position: relative; } .outer1 .left { position: absolute; width: 100px; } .outer1 .middle { margin: 0 200px 0 100px; } .outer1 .right { position: absolute; width: 200px; top: 0; right: 0; } 注意:左右分别使用绝对定位,中间设置外边距 复制代码 -
第二种方式 ---
flex布局HTML部分:
<div class="outer outer2"> <div class="left">2-left</div> <div class="middle">2-middle</div> <div class="right">2-right</div> </div> 复制代码CSS部分:
.outer2 { display: flex; } .outer2 .left { flex: 0 0 100px; } .outer2 .middle { flex: auto; } .outer2 .right { flex: 0 0 200px; } 复制代码 -
第三种方式 ---
浮动原理HTML部分:
<div class="outer outer3"> <div class="left">3-left</div> <div class="right">3-right</div> <div class="middle">3-middle</div> </div> 复制代码CSS部分:
.outer3 .left{ float: left; width: 100px; } .outer3 .right { float: right; width: 200px; } .outer3 .middle { margin: 0 200px 0 100px; } 复制代码
圣杯布局
圣杯布局:
中间的优先渲染,独立的左中右结构
具体实现圣杯布局的步骤:
- 让左右浮动在一行显示,相对定位
- 让中间模块的middle宽度为100%
- 让左边的色块移动到middle前面,margin-left:-100%
- 让右边的色块移动到middle的后面,margin-left:-宽度
- 给三个小块的父元素加一个内填充的属性padding,为的是填充挤到中间
- 给左边的块移动到左边left:-200px, 给右边的块移动到右边right:-200px
效果图
HTML部分:
<header>header</header>
<div class="container">
<div class="middle">midlle</div>
<div class="left">left</div>
<div class="right">right</div>
</div>
<footer>footer</footer>
复制代码
CSS部分:
header, footer {
height: 100px;
width: 100%;
background-color: antiquewhite;
}
.container {
height: 200px;
padding-left: 200px;
padding-right: 300px;
}
.container > div {
float: left;
position: relative;
height: 100%;
}
.left {
width: 200px;
height: 200px;
background-color: burlywood;
margin-left: -100%;
left: -200px;
}
.right {
width: 300px;
height: 200px;
background-color: burlywood;
margin-left: -300px;
right: -300px;
}
.middle {
width: 100%;
height: 200px;
background-color: #b0f9c2;
}
复制代码
双飞翼布局
双飞翼布局
具体实现双飞翼布局的步骤:
- 给左,中,右 加浮动,在一行显示
- 给middle宽度为100%
- 让左边的模块移动middle的左边 margin-left:-100%
- 让右边的模块移动middle的右边 margin-left:-自己宽度
- 给middle里面的容器添加外间距 margin: 左右
效果:
html部分
<div class="main">
<div class="middle">
<div class="middle-inner">中间</div>
</div>
<div class="left">左边</div>
<div class="right">右边</div>
</div>
复制代码
css部分
.main>div {
float:left;
position: relative;
height: 300px;
}
.middle {
width: 100%;
background-color: lightgreen
}
.left {
width:200px;
margin-left:-100%;
background-color:#b0f9c2
}
.right {
width: 200px;
margin-left:-200px;
background-color:pink
}
.middle-inner{
margin:0 200px;
background-color: burlywood;
height:300px;
}
如何修改chrome记住密码后自动填充表单的黄色背景 ?
input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill {
background-color: rgb(250, 255, 189); /* #FAFFBD; */
background-image: none;
color: rgb(0, 0, 0);
}
让页面里的字体变清晰,变细用CSS怎么做?
-webkit-font-smoothing: antialiased;
复制代码
让overflow:scroll平滑滚动?
-webkit-overflow-scrolling: touch;
复制代码
手机上的多行省略
.overflow-hidden{
display: box !important;
display: -webkit-box !important;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;/*第几行出现省略号*/
/*text-align:justify;不能和溢出隐藏的代码一起写,会有bug*/
}
复制代码
长时间按住页面闪退
.element {
-webkit-touch-callout: none;
}
复制代码
改变输入框内提示文字颜色
::-webkit-input-placeholder { /* WebKit browsers */
color: #999; }
:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
color: #999; }
::-moz-placeholder { /* Mozilla Firefox 19+ */
color: #999; }
:-ms-input-placeholder { /* Internet Explorer 10+ */
color: #999; }
input:focus::-webkit-input-placeholder{ color:#999; }
复制代码
watch监听多个变量
watch本身无法监听多个变量。但我们可以将需要监听的多个变量通过计算属性返回对象,再监听这个对象来实现“监听多个变量”
export default {
data() {
return {
msg1: 'apple',
msg2: 'banana'
}
},
compouted: {
msgObj() {
const { msg1, msg2 } = this
return {
msg1,
msg2
}
}
},
watch: {
msgObj: {
handler(newVal, oldVal) {
if (newVal.msg1 != oldVal.msg1) {
console.log('msg1 is change')
}
if (newVal.msg2 != oldVal.msg2) {
console.log('msg2 is change')
}
},
deep: true
}
}
}
定时器销毁
data() {
return {
timer: null // 定时器名称
}
},
const timer = setInterval(() =>{
// 某些定时器操作
}, 500);
// 通过$once来监听定时器,在beforeDestroy钩子可以被清除。
this.$once('hook:beforeDestroy', () => {
clearInterval(timer);
})复制代码
常用的倒计时功能
首先我们先来屡一下逻辑
把时间差转换为天数、小时数、分钟数和秒数来显示。主要是以%取模来运算。得到剩余结束时间的毫秒数(剩余毫秒数),
除以 1000 得到剩余秒数,再除以 60 得到剩余分钟数,再除以 60 得到剩余小时数。除以 24 得到剩余天数。
剩余秒数 time/1000 % 60 得到秒数,剩余分钟数 time/(1000 * 60) %60 得到分钟数,
剩余小时数time/(1000 * 60 * 60) %24 得到小时数。
后台会传一个倒计时结束时间给你;那么只需要那这个参数传进我写下面写的方法里面去了
HTML部分
<body>
<div id="countdown"></div>
</body>
<script>
let timer
let timeEvent = document.getElementById("countdown");
function countdown() {
let nowTime = new Date(); //当前的时间
let endTime = new Date("2021/11/02"); //结束的时间
//上面只是我写死的日期,后台传的结束时间数据是时间戳可以省略上面一步
let timeLog = endTime.getTime() - nowTime.getTime(); //时间差
//一步得出的时间戳是毫秒的;顺便普及一下知识:10位数的时间戳是秒,13位的时间戳是毫秒;
let day = Math.floor(timeLog / (1000 * 60 * 60 * 24)); //获取天数
let hour = Math.floor((timeLog / (1000 * 60 * 60)) % 24); //获取小时
let min = Math.floor((timeLog / (1000 * 60)) % 60); //获取分钟
let sec = Math.floor((timeLog / 1000) % 60); // 获取秒数
//所以显示数据为0时清除定时器
if (day==0 && hour==0 && min==0 && sec == 0) {
clearInterval(timer);
countdown.style.display = "none";
}
if (day == 0) {
return (hour<10?'0'+hour:hour) + ":" + (min<10?'0'+min:min) + ":" + (sec<10?'0'+sec:sec);
} else {
return (day<10?'0'+day:day) + "天" + " " + (hour<10?'0'+hour:hour) + ":" + (min<10?'0'+min:min) + ":" + (sec<10?'0'+sec:sec);
}
}
//设置一个倒计时,每秒执行一次
timer = setInterval(() => {
timeEvent.innerHTML = countdown();
}, 1000);
</script>
//第二种
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>倒计时</title>
</head>
<body>
<div id="app">
<div>抢购开始时间:{{count}}</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: '', //倒计时
seconds: 864000 // 10天的秒数
}
},
mounted() {
this.Time() //调用定时器
},
methods: { // 天 时 分 秒 格式化函数
countDown() {
let d = parseInt(this.seconds / (24 * 60 * 60))
d = d < 10 ? "0" + d : d
let h = parseInt(this.seconds / (60 * 60) % 24);
h = h < 10 ? "0" + h : h
let m = parseInt(this.seconds / 60 % 60);
m = m < 10 ? "0" + m : m
let s = parseInt(this.seconds % 60);
s = s < 10 ? "0" + s : s
this.count = d + '天' + h + '时' + m + '分' + s + '秒'
}, //定时器没过1秒参数减1
Time() {
setInterval(() => {
this.seconds -= 1
this.countDown()
}, 1000)
},
}
})
</script>
</html>
封装倒计时组件
- utils/countDown
<template>
<div class="Date">
<span>{{ `${d || 0}天${h || 0}小时${m || 0}分${s || 0}秒` }}</span>
</div>
</template>
<script>
export default {
props: {
stopTime: {
type: String,
default: ''
},
},
data() {
return {
d: '',
h: '',
m: '',
s: '',
timer: null,
}
},
mounted() {
clearTimeout(this.timer);
this.countTime();
},
beforeDestroy() {
clearTimeout(this.timer);
},
methods: {
countTime: function () {
//获取当前时间
let date = new Date()
let now = date.getTime()
//设置截止时间
let endDate = new Date(this.stopTime);
let end = endDate.getTime()
//时间差
let leftTime = end - now;
//定义变量 d,h,m,s保存倒计时的时间
if (leftTime >= 0) {
this.d = Math.floor(leftTime / 1000 / 60 / 60 / 24)//天数我没用到,暂且写上
this.h = Math.floor((leftTime / 1000 / 60 / 60) % 24)
this.m = Math.floor((leftTime / 1000 / 60) % 60)
this.s = Math.floor((leftTime / 1000) % 60)
// console.log(this.s)
//递归每秒调用countTime方法,显示动态时间效果
this.timer = setTimeout(this.countTime, 1000)
}
},
},
}
</script>
- 使用
传的格式 2022-02-25 23:59:59
<!-- 倒计时 -->
<div class="stop-time">
报价剩余时间:
<count-down
v-if="routeData.quotationDeadline"
:stopTime="routeData.quotationDeadline"
></count-down>
<span v-else>0天0小时0分0秒</span>
</div>
秒数转化为天时分秒
formatSeconds(value) {
let theTime = parseInt(value)
let theTime1 = 0 // 分
let theTime2 = 0 // 小时
let theTime3 = 0 // 天
if (theTime > 60) {
theTime1 = parseInt(theTime / 60)
theTime = parseInt(theTime % 60)
if (theTime1 > 60) {
theTime2 = parseInt(theTime1 / 60)
theTime1 = parseInt(theTime1 % 60)
if (theTime2 > 24) {
//大于24小时
theTime3 = parseInt(theTime2 / 24)
theTime2 = parseInt(theTime2 % 24)
}
}
}
let result = ''
if (theTime > 0) {
result = '' + parseInt(theTime) + '秒'
} else if (theTime == 0) {
result = '0天0小时0分0秒'
}
if (theTime1 > 0) {
result = '' + parseInt(theTime1) + '分' + result
}
if (theTime2 > 0) {
result = '' + parseInt(theTime2) + '小时' + result
}
if (theTime3 > 0) {
result = '' + parseInt(theTime3) + '天' + result
}
return result
},
日期和时间
- js获取
let date = new Date(),
seperator1 = "-", //格式分隔符
year = date.getFullYear(), //获取完整的年份(4位)
month = date.getMonth() + 1, //获取当前月份(0-11,0代表1月)
strDate = date.getDate(); // 获取当前日(1-31)
let strDate_ = date.getDate(); // 获取当前日(1-31)+1
if (month >= 1 && month <= 9) month = "0" + month; // 如果月份是个位数,在前面补0
if (strDate >= 0 && strDate <= 9) strDate = "0" + strDate; // 如果日是个位数,在前面补0
if (strDate_ >= 0 && strDate_ <= 9) strDate_ = "0" + strDate_; // 如果日是个位数,在前面补0
//根据需要拼接
let currentdate = year + seperator1 + month + seperator1 + strDate + " 00:00:00";
- moment插件
npm install moment --save # npm
yarn add moment # Yarn
import moment from "moment";
console.log(moment().format("YYYY-MM-DD hh:00:00"));//2022-09-29 12:00:00
// 年月日
export const shortTime = function (value) {
return moment(value).format("YYYY-MM-DD");
};
// 年月日-时分秒
export const time = function (value) {
return moment(value).format("YYYY-MM-DD HH:mm:ss");
};
// 年月
export const monthTime = function (value) {
return moment(value).format("YYYY-MM");
};
// 年
export const yearTime = function (value) {
return moment(value).format("YYYY");
};
// 月
export const getMonth = function (value) {
return moment(value).format("MM");
};
// 时分秒
export const secondsTime = function (value) {
return moment(value).format("HH:mm:ss");
};
//获取当前日期的周六
export const saturdayTime = function (value) {
return moment(value).endOf("week").format("YYYY-MM-DD");
};
//获取当前日期的周日
export const sundayTime = function (value) {
return moment(value).day(7).format("YYYY-MM-DD");
};
//计算日期之间的天数
export const DateDiff = function (sDate1, sDate2) {
var aDate, oDate1, oDate2, iDays;
aDate = sDate1.split("-");
oDate1 = new Date(aDate[1] + "-" + aDate[2] + "-" + aDate[0]); //转换为02-05-2018格式
aDate = sDate2.split("-");
oDate2 = new Date(aDate[1] + "-" + aDate[2] + "-" + aDate[0]);
iDays = parseInt(Math.abs(oDate1 - oDate2) / 1000 / 60 / 60 / 24) + 1; //把相差的毫秒数转换为天数
return iDays;
};
/** addDate: 获取几天之后的时间
* date:起始时间对象
* days: Number 当前时间'2019/12/24',传入6,则返回'2019/12/30'
* key => years: 'y' quarters: 'Q' months: 'M' weeks: 'w' days: 'd' hours: 'h'
*
*/
export const addDate = function (
date,
days,
key = "d",
forchart = "YYYY-MM-DD"
) {
return moment(date).add(days, key).format(forchart);
};
/** 通过减去时间来改变原始的 moment */
export const subDate = function (
date,
days,
key = "d",
forchart = "YYYY-MM-DD"
) {
return moment(date).subtract(days, key).format(forchart);
};
String.trimStart && String.trimEnd
咱们都知道JavaScript有个trim方法,可以清除字符串首尾的空格
const str = ' 林三心 '
console.log(str.trim()) // '林三心'
复制代码
去除空格
trimStart和trimEnd用来单独去除字符串的首和尾的空格
const str = ' 林三心 '
// 去除首部空格
console.log(str.trimStart()) // '林三心 '
// 去除尾部空格
console.log(str.trimEnd()) // ' 林三心'
复制代码
js根据指定日期来获取周数
getWeekStart(date) {
// date = formatTimebytype(date, 'yyyy-MM-dd');//将日期转换成yyyy-mm-dd格式
var dates = new Date(date)
var date2 = new Date(dates.getFullYear(), 0, 1)
var day1 = dates.getDay()
if (day1 == 0) day1 = 7
var day2 = date2.getDay()
if (day2 == 0) day2 = 7
let d = Math.round((dates.getTime() - date2.getTime() + (day2 - day1) * (24 * 60 * 60 * 1000)) / 86400000)
if (dates.getDay() == 6 || dates.getDay() == 0) {
// 当周数大于52则为下一年的第一周
if (dates.getFullYear() == '2022') {
console.log(dates.getFullYear() + '-' + Math.ceil(d / 7 + 1) + '周')
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7 + 1) + '周'
} else if (dates.getFullYear() == '2023') {
console.log(date)
if (date == '2023-01-01') {
this.params.weekStart = dates.getFullYear() - 1 + '-' + '53周'
} else {
console.log(dates.getFullYear() + '-' + Math.ceil(d / 7 + 1) + '周')
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7) + '周'
}
}
} else {
if (dates.getFullYear() == '2022') {
console.log(dates.getFullYear() + '-' + Math.ceil(d / 7) + '周')
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7) + '周'
} else if (dates.getFullYear() == '2023') {
if (
date == '2023-01-02' ||
date == '2023-01-03' ||
date == '2023-01-04' ||
date == '2023-01-05' ||
date == '2023-01-06'
) {
this.params.weekStart = dates.getFullYear() - 1 + '-' + '53周'
} else {
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7 - 1) + '周'
}
}
}
},
//修改为特定日期
getWeekStart(date) {
// date = formatTimebytype(date, 'yyyy-MM-dd');//将日期转换成yyyy-mm-dd格式
var dates = new Date(date)
var date2 = new Date(dates.getFullYear(), 0, 1)
var day1 = dates.getDay()
if (day1 == 0) day1 = 7
var day2 = date2.getDay()
if (day2 == 0) day2 = 7
// console.log(dates, '777777777777', date2)
let d = Math.round((dates.getTime() - date2.getTime() + (day2 - day1) * (24 * 60 * 60 * 1000)) / 86400000)
if (dates.getDay() == 6 || dates.getDay() == 0) {
// 当周数大于52则为下一年的第一周
if (dates.getFullYear() == '2022') {
console.log(dates.getFullYear() + '-' + Math.ceil(d / 7 + 1) + '周')
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7 + 1) + '周'
} else if (dates.getFullYear() == '2023') {
// console.log(date)
if (date == '2023-01-01') {
this.params.weekStart = dates.getFullYear() - 1 + '-' + '53周'
} else {
// console.log(dates.getFullYear() + '-' + Math.ceil(dt / 7 + 1) + '周')
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7) + '周'
}
}
} else {
if (dates.getFullYear() == '2022') {
console.log(dates.getFullYear() + '-' + Math.ceil(d / 7) + '周')
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7) + '周'
} else if (dates.getFullYear() == '2023') {
if (date == '2023-01-02' || date == '2023-01-03') {
this.params.weekStart = dates.getFullYear() - 1 + '-' + '53周'
} else if (dates.getDay() == 1 || dates.getDay() == 2) {
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7 - 1) + '周'
} else {
this.params.weekStart = dates.getFullYear() + '-' + Math.ceil(d / 7) + '周'
}
}
}
},