《Vue》踩坑笔记
# 《Vue》踩坑笔记
博主使用 Vue 开发项目中亲身经历,自己踩的坑,防止再次踩坑!!!博主使用 Vue 开发项目中亲身经历,自己踩的坑,防止再次踩坑!!!博主使用 Vue 开发项目中亲身经历,自己踩的坑,防止再次踩坑!!!
若对本文内容有所异议欢迎指正讨论,若感兴趣,欢迎点赞收藏。
提示
本章内容是博主自身在使用 Vue 开发项目过程中遇到的坑,并持续更新中,若有不足,欢迎指正。
# 1. 计算属性(computed)和监视(watch)的使用场景区分
computed: 是计算属性,依赖其它属性值,通过属性计算而得来的属性,并且 computed 的值在getter执行后是会缓存的,只有它依赖的属性值再次发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;computed中的函数必须要用return返回最终的结果;computed 是一个新的属性,并且挂载到Vue实例上。computed不支持异步。
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;watch的回调里面会传入监听属性的新旧值,通过这两个值可以做一些特定的操作,不是必须要用return返回结果。watch中的函数是不需要调用的。watch 是监听一个已经存在并且已经挂载到Vue实例上的属性,所以watch同样可以监听computed(计算属性)的变化,其他还有data,props。watch支持异步。
使用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;computed适用于一个属性被多个属性影响。
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。watch 会生成一个watcher对象,在监视的属性每次变动时都会触发回调,watcher 更像是一个 data 的数据监听回调,当依赖的 data 的数据变化,执行回调,在方法中会传入 newVal 和 oldVal。可以提供输入值无效,提供中间值 特场景。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。如果你需要在某个数据变化时做一些事情,使用watch。watch适用于一个属性影响多个属性。
2
3
4
5
6
7
8
9
10
# 2. 切换页码后将滚动条重新设置到顶部
# 2.1 < el-main> 标签切换页码后将滚动条重置为顶部
在切换页码的方法中添加如下代码
this.$refs.webTabMain.$el.scrollTop = 0 //主要代码
<el-main ref="webTabMain" class="web-dialog-graphic">
<el-pagination
class="pagination"
background
:current-page.sync="pageNum"
@current-change="handleCurrentChange"
:page-sizes="[10]"
:page-size="pageSize"
layout="prev, pager, next, jumper"
:total="totalCount"
>
</el-pagination>
</el-main>
// 改变弹出框中页码的方法
handleCurrentChange(pageNum) {
this.$refs.webTabMain.$el.scrollTop = 0 //webTabMain为当前表格或其他显示的数据的标签中ref值。
//再重新获取当前列表
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2.2 < el-table>标签切换页码后将滚动条重置为顶部
在切换页码的方法中添加如下代码
this.$refs.staffTable.bodyWrapper.scrollTop = 0 //主要代码
<el-table ref="staffTable">
<el-pagination
class="pagination"
background
:current-page.sync="pageNum"
@current-change="handleCurrentChange"
:page-sizes="[10]"
:page-size="pageSize"
layout="prev, pager, next, jumper"
:total="totalCount"
>
</el-pagination>
</el-table>
// 改变弹出框中页码的方法
handleCurrentChange(pageNum) {
this.$refs.staffTable.bodyWrapper.scrollTop = 0 // staffTable为当前表格或其他显示的数据的标签中ref值。
//再重新获取当前列表
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3. el-form 表单中输入框回车事件导致刷新整个页面
问题描述:当我们使用 elementUI 的 el-form 表单时,如果只有一个el-form-item
标签,且这个 item 标签中是一个 input 输入框时,此时使用@keyup.enter.native
为 input 输入框绑定回车事件,此时会出现一个问题:点击回车键后,浏览器会刷新整个页面,导致表单提交失败,不论这个回车事件绑定函数还是直接操作数据。
原因:由于当表单只有一个文本框时,按下回车将会触发表单的提交事件, 导致页面的刷新。
解决方案一:在 el-from 标签中 加上 @submit.native.prevent
<el-form
:model="groupForm"
:rules="groupRules"
ref="addGroupForm"
class="demo-ruleForm"
@submit.native.prevent
>
<el-form-item label="" prop="name">
<el-input
type="text"
placeholder="请输入名称"
v-model="groupForm.name"
maxlength="4"
show-word-limit
@keyup.enter.native="saveGroupName('addGroupForm')">
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveGroupName('addGroupForm')">
确定
</el-button>
<el-button @click="closeDialog('addGroupForm')">取消</el-button>
</el-form-item>
</el-form>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
解决方案二:加一个隐藏的文本框,即表单不只有一个文本框
<el-form
:model="groupForm"
:rules="groupRules"
ref="addGroupForm"
class="demo-ruleForm"
@submit.native.prevent
>
<el-form-item label="" prop="name">
<el-input
type="text"
placeholder="请输入名称"
v-model="groupForm.name"
maxlength="4"
show-word-limit
@keyup.enter.native="saveGroupName('addGroupForm')">
</el-input>
</el-form-item>
<!-- 添加一个不显示的文本框 -->
<el-form-item style="margin-bottom:0;display:none;">
<el-input></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveGroupName('addGroupForm')">
确定
</el-button>
<el-button @click="closeDialog('addGroupForm')">取消</el-button>
</el-form-item>
</el-form>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 4. v-if 和 v-show 的区别
v-if
是"真正"的条件渲染,确保在切换过程中条件块内的事件监听器和子组件适当地被创建和销毁,即每次使用到都会重新创建,条件不满足的时候就会销毁,是根据判断条件动态的进行 DOM 元素的增加和删除。也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。v-show
只是简单的控制元素的display
属性,不管初始条件是什么,元素总是被渲染,只是简单的控制了元素的显示与否,DOM 元素创建后在不满足条件时会被隐藏,而不是销毁。v-show 只是动态的更改样式而不需要增删 DOM 元素。- v-if 可以与 v-else 连用,v-show 则不能与 v-else 连用。
- v-if 可以搭配 template 使用,而 v-show 不行
# 5. elementUI 中切换标签页之前阻止切换事件
<el-tabs
type="card"
v-model="activeType"
:before-leave="cutoverTab"
>
</el-tabs>
2
3
4
5
6
7
methods: {
cutoverTab(activeName ,oldActiveName ) {
console.log('切换标签页')
console.log('activeName', activeName) //为跳转到的标签页
console.log('oldActiveName', oldActiveName) //为跳转之前的标签页
if (this.dragDisabled === false) {
return this.$confirm(
'页面未保存,离开将不保留数据,是否离开?',
'提示',
{
confirmButtonText: '离开',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(() => {
console.log('离开')
//可以在这里对跳转到的新的标签页进行处理
})
.catch(() => {
console.log('不离开')
throw new Error('阻止切换')
})
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 6. 通过calc
设置宽度或高度时出现失效的问题
//失效的情况
.test{
width:calc(100% -70px) //此种情况下设置的宽度会失效,原因是因为-70px之间没有空格
}
2
3
4
//正确的情况
.test{
width:calc(100% - 70px)//这种情况下设置的宽度才会生效,以后使用需要注意。
}
2
3
4
# 7. element-ui 的下拉框有值却无法选中的问题(实际上已经选中了,只是数据没有及时刷新,显示的还是老的数据,刷新之后的数据就是新的数据)
问题描述:在使用 Vue 框架和 element-ui 开发时,下拉框遇见一个问题,在函数中改变了页面中的某个值,在函数中查看是修改成功了,但在页面中没有及时刷新改变后的值,也就是下拉框值无法选中。
<el-select
class="select"
size="small"
v-model="updateVRForm.class"
placeholder="请选择类型"
@change="onChange"
>
<el-option
v-for="item in classList"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</el-select>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
解决方式:出现这个问题好像是因为下拉框数据是循环掉别的接口得来的,因为数据层次太多,render 函数没有自动更新,需手动强制刷新所以我直接强制刷新了值,而 forceUpdate 就是重新 render。在 select 的 change 事件中调用此方法,运用 this.$forceUpdate()强制刷新,页面正常选值。
methods: {
onChange() {
this.$forceUpdate()
},
}
2
3
4
5
# 8. 当页面缩小时没有出现滚动条,需要出现滚动条的设置(只需要在需要出现滚动条的 css 样式中添加 min-width 属性为一个定值即可)
<template>
<div class="container">
xxxxxx
</div>
</template>
<style lang="scss" scoped>
.container {
margin: 0;
padding: 0;
min-width: 1300px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
# 9. 页面之间通过路由传值(vue-router)
# params 传值
(params 传值时,不能使用 path 进行跳转,params 只能用 name 来引入路由,如果只提供了 path,params 会自动被忽略,接收参数页面会是 undefined!)
注意: params 传值时,刷新页面后会传的值会消失,params 传值时传的值不会在页面的地址栏中显示,相当于 post 请求。
//传值
this.$router.push({
name: 'vrList',
params: { type: vr.type }
})
//取值
this.$route.params.type
2
3
4
5
6
7
8
# query 传值
query 传值使用 path 和 name 均可以进行路由跳转的,相当于 get 请求,页面请求时会在地址栏中看到请求参数。刷新页面后传的值不会消失。
//传值
this.$router.push({
path: '/vr/update',
query: { type: this.activeType }
})
//取值
this.$route.query.type
2
3
4
5
6
7
8
参考链接:https://segmentfault.com/a/1190000012735168 (opens new window)
# 10. API 接口中通过 params 传参和 data 传参
通过 axios 来请求后台接口时传参的形式有 params 和 data 两种形式
# prams 传参
//通过prams传参,params是添加到url的请求字符串中的,用于get请求
//此时axios会将请求的url自动转化为/xxx/deleteById?id=123466
export const delVrData = id =>
http({ url: `${prefix}/deleteById`,
params: { id },
method: 'DELETE'
})
//此时的vue页面中调用API接口为
vrApi.delVrData(item.id).then(() => {
this.$message({
type: 'success',
message: '删除成功!'
})
})
//此时axios会将请求的url自动转化为/xxx/selectLists?type=xxxxxxx
export const getVrList = params =>
http({ url: `${prefix}/selectLists`, params })
//vue中调用API接口
vrApi.getVrList({ type: type }).then(res => { console.log(res) })
//两者的不同之处在于API接口的js文件中传值时是否是json对象,第一个API接口中params是一个json对象,所以调用的时候直接传递参数即可,第二个API接口中params只是一个参数,所以在调用第二个接口的使用需要传递的参数就是一个json对象。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# data 传参
// 通过data传参,data是添加到请求体(body)中的, 用于post请求。
// 如果我们将get方法中的params修改为data,显然是不能请求成功的,因为get请求中不存在data这个选项。
// 此时axios会将请求的url自动转化为/xxx/update
export const putUpdateVr = data =>
http({ url: `${prefix}/xxx/update`, method: 'PUT', data })
//此时的vue页面中调用API接口为
//此时的请求后端接口时传递的参数vr为一个json对象
vrApi.putUpdateVr(vr).then(() => {
this.$message({
message: '修改成功',
type: 'success'
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 11. 输入框前后去空格
输入框中可以随意进行输入,包括空格和特殊字符等,只是在鼠标的离焦事件中将空格去除,并将去除空格后的数据重新赋值给输入框绑定的数据,然后在鼠标离焦事件中对输入框数据进行校验。
<el-input
class="input"
v-model="updateVRForm.url"
type="url"
inputmode="url"
@blur="onBlurUrl(updateVRForm.url)">
</el-input>
<script>
// 链接输入框的离焦事件中去掉首尾的空格
onBlurUrl(val) {
// 去掉首尾的空格
val = val.replace(/^\s+|\s+$/g, '')
// 去掉所有的空格
// value = val.replace(/\s+/g, '')
this.updateVRForm.url = val
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 12. 修改 elementui 的样式(使用 ::v-deep 修饰符 )
// 修改elementUI中标签页的表头的字体的大小
::v-deep .el-tabs__item {
font-size: 16px;
}
// 修改elementUI中的图片的圆角样式
::v-deep .el-image {
border-radius: 5px;
}
//修改elementUI中的tab标签页中内容区的最左侧的卡片不显示边框的样式
::v-deep .el-tabs__content {
overflow: visible;
}
2
3
4
5
6
7
8
9
10
11
12
# 13. 当前页面数据未保存时点击侧边导航栏(切换路由页面)后给出提示信息
beforeRouteLeave(to, from, next) {
if (this.submited) {
next()
} else {
this.$confirm('页面未保存,离开将不保留数据,是否离开?', '提示', {
confirmButtonText: '离开',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
this.submited = true
})
.catch(() => {})
.finally(() => {
if (this.submited) {
next()
} else {
next(false)
}
})
}
},
//注意,此操作具有局限性,会导致在当前页面中切换所有的路由信息均会执行此方法,(若添加页面的保存方法中需要修改路由页面,则也会执行此方法,所以在弹出提示信息框之前需要先判断submited这个属性是否为true),此方法是需要通过elementUI的组件来进行提示信息才进行修改的,更合理的方法是通过window来调用执行,具体如下方代码。
beforeRouteLeave(to, from, next) {
if (this.submited || !this.editing) {
next()
} else {
const answer = window.confirm('您有信息未保存,确定返回吗?')
if (answer) {
next()
} else {
next(false)
}
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 14. 让一定宽度中的文字溢出时显示省略号(css 实现)
# 14.1 让溢出的文字在一行显示,超出的部分显示省略号
<div title="指定文字出现省略号时鼠标移入显示完整的文字(可以使用v-bind指令动态指定需要显示的内容)">哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈</div>
/*这四个缺一不可 ,注意,此时只能是超出一行的文字显示省略号 */
.test{
/* display: block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden; */
}
2
3
4
5
6
7
8
9
# 14.2 让溢出的文字在显示指定行后再显示省略号
<div :title="指定文字出现省略号时鼠标移入显示完整的文字(可以使用v-bind指令动态指定需要显示的内容)">哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈</div>
/* 让两行以后溢出的文字显示省略号 */
/* -webkit-line-clamp 是一个 不规范的属性(unsupported WebKit property),
它没有出现在 CSS 规范草案中。限制在一个块元素显示的文本的行数。 为了实现该效果,它需要组合其他外来的WebKit属性。
display: -webkit-box; 必须结合的属性 ,将对象作为弹性伸缩盒子模型显示 。
-webkit-box-orient 必须结合的属性 ,设置或检索伸缩盒对象的子元素的排列方式 。
text-overflow,可以用来多行文本的情况下,用省略号“...”隐藏超出范围的文本 。
*/
.test{
display: -webkit-box;
/* 注意,此时不能设置display: block; ,设置为块级元素会导致下面的配置失效 */
text-overflow: ellipsis;
overflow: hidden;
/* -webkit-line-clamp: 行数; */
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 15. 定位相关知识
相对定位参照与元素本来的位置来进行定位。
绝对定位参照与元素的包含块来定位的。
固定定位参照与浏览器的视口来进行定位的。
没有定位就是参照的包含块进行排列的。
初始包含块是一个视口大小的矩形,但不等于视口。
定位的默认值为auto,即`left:auto;right:auto;top:auto;bottom:auto;,不可被继承`,
width和height的默认值也是auto,不可被继承,margin和padding的默认值为0,
border-width的默认值为中等大小,如果不存在border-style,则没有border-width这个说法。
width的百分比参照与包含块的width,height参照与包含块的height,left参照与包含块的width,top参照与包含块的height
margin和padding中的百分比参照的是当前元素的包含块的width的值。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 16. 去除元素中不希望出现的黑色边框
//设置元素的outline属性为0
<style>
* {
margin: 0;
padding: 0;
}
.test{
outline:none
}
</style>
<html>
<p>哈哈哈哈</p>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
参考链接:https://developer.mozilla.org/zh-CN/docs/Web/CSS/outline (opens new window)
# 17. 判断字符串中是否包含字符 ' 无' ,若包含,则替换为 ''
// 方式一:使用indexOf来判断字符串中是否包含'无'这个字符,若包含,则使用replace来替换为''
if (res.data[0].list[i].name.indexOf('无') !== -1) {
res.data[0].list[i].name = res.data[0].list[i].name.replace(/无/, '')
} else if (res.data[0].list[i].name.indexOf('无') == -1) {
//若不存在无这个字符,则将字符串中通过、分割的最后的字符与之前的字符之间加上空格
//例如 字符串 (三室、两厅、一卫、复式) 替换后的结果为 (三室两厅一卫 复式)
let nameTemp = res.data[0].list[i].name.split('、')
nameTemp.splice(nameTemp.length - 1, 0, ' ')
nameTemp = nameTemp.toString()
res.data[0].list[i].name = nameTemp.replace(/,/g, '')
}
//方式二: 通过Unicode编码匹配汉字 无 ,若符合则替换为''
res.data[0].list[i].name = res.data[0].list[i].name.replace(/\u65e0/g, '')
2
3
4
5
6
7
8
9
10
11
12
13
14
# 18. 当鼠标移动图片上方时,显示指定内容
//当鼠标移动到图片上方时,显示编辑和删除按钮,鼠标不移动到图片上方时,默认只展示图片,不展示编辑和删除按钮
<templeate>
<div class="image">
<el-image :src="vr.image"> </el-image>
<div v-show="dragDisabled" class="btngroup">
<el-button
type="primary"
:disabled="!dragDisabled"
size="mini"
@click="onEdit(vr)"
>编辑</el-button
>
<el-button
type="danger"
:disabled="!dragDisabled"
size="mini"
@click="onDel(vr, index)"
>删除</el-button
>
</div>
</div>
</templeate>
/*html中将el-image和两个按钮所在的div放在同一个div(.image)的内部,即el-image和两个按钮是兄弟元素,在css样式中设置 .image:hover{ .btngroup {}},即当image元素hover时改变两个按钮的样式,让两个按钮由最初的默认不显示,改为通过定位显示在指定位置,若想根据最外层的div的位置来进行定位,则需要将最外层的div设置成相对定位,在鼠标hover图片的时候,设置按钮为参照最外层div的绝对定位,然后根据自己的需要,调整left、top等值即可。
*/
<style lang="scss" scoped>
.image {
width: 250px;
height: 188px;
cursor: pointer;
position: relative;
}
.image:hover {
.btngroup {
display: inline-block;
position: absolute;
right: 0;
top: 0;
}
}
.btngroup {
display: none;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 19. 使用 vuedraggable 组件时,会将所有的元素整体拖走的解决方案
<templeate>
<draggable
v-model="vrThreeList"
class="list"
chosen-class="chosen"
:scroll="true"
force-fallback="true"
animation="100"
:disabled="dragDisabled"
>
<div v-for="(vr, index) in vrThreeList" :key="index" class="item">
<div class="image">
<el-image :src="vr.image"> </el-image>
</div>
</div>
</draggable>
</templeate>
// draggable标签内应该紧跟着需要遍历元素的div,需要被遍历的元素不能再被div包裹,draggable组件默认会将其标签内部的第一个div当做一个整体进行拖拽。,如果需要给整体写样式,可以在draggable标签上写样式。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 20. element-ui 表单报错 Cannot read property ‘clearValidate’ of undefined
原因:因为 页面的 Dom 元素没有加载完成,导致获取不到 refs 元素,报错 Cannot read property ‘clearValidate’ of undefined。
this.$nextTick(() => {
this.$refs.staffForm.clearValidate()
})
2
3
# 21. 查看、添加、编辑使用同一个组件不想在查看页面中看到 form 表单的必填项前面的红色的 * 号
当查看和添加、编辑等使用同一个组件时,若查看页面不想出现必填项前面的红色的 * 号,可以先定义一个空的校验规则,在添加和编辑页面中再将需要的校验规则赋值给原先初始化时定义的那个空的校验规则。
<el-dialog
:title="dialogTitle"
:visible.sync="staffDialogVisible"
width="550px"
:append-to-body="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
@open="checkTitle"
@close="closeDialog"
>
<el-form :model="staffForm" :rules="staffRules" ref="staffForm"></el-form>
</el-dialog>
data(){
return{
staffRules: {}, // 表单校验规则
// 新增或编辑时使用的校验规则,用于解决查看的时候也显示必填项的那个*号的问题
staffRulesEdit: {
nickname: [
{
transform(value) {
return value.toString().trim()
},
trigger: 'change'
},
{
required: true,
message: '请输入员工姓名',
trigger: 'blur'
},
{
min: 2,
max: 15,
message: '请输入2-15个汉字、字母或数字',
trigger: 'change'
},
{
pattern: /^[\u4e00-\u9fa5a-z0-9]+$/gim,
message: '请输入正确的员工姓名',
trigger: 'change'
}
]
}
}
},
methods:{
// 打开弹窗事件
checkTitle() {
if (this.isAdd) {
this.dialogTitle = '添加员工信息'
this.staffRules = this.staffRulesEdit
this.$nextTick(() => {
this.$refs.staffForm.clearValidate()
})
} else if (this.isEdit) {
this.dialogTitle = '编辑员工信息'
this.staffRules = this.staffRulesEdit
this.$nextTick(() => {
this.$refs.staffForm.clearValidate()
})
} else {
this.dialogTitle = '查看员工信息'
this.staffRules = {}
}
},
closeDialog() {
this.$nextTick(() => {
this.$refs.staffForm.resetFields()
})
this.staffRules = {}
this.isAdd = false
this.isEdit = false
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# 22. < el-input>标签 修改 placeholder 字体的颜色和大小
注意此时会导致全局生效,不能使用 scoped (使用 scope 会导致样式修改不成功)
<style lang="scss">
.el-input__inner,
.el-textarea__inner {
&::placeholder {
color: red;
}
&::-webkit-input-placeholder {
/* WebKit browsers 适配谷歌 */
color: red;
}
&:-moz-placeholder {
/* Mozilla Firefox 4 to 18 适配火狐 */
color: red;
}
&::-moz-placeholder {
/* Mozilla Firefox 19+ 适配火狐 */
color: red;
}
&:-ms-input-placeholder {
/* Internet Explorer 10+ 适配ie*/
color: red;
}
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 23. axios 下载文件
JS 跳转页面常用的方式主要依赖于 window 对象的属性和方法,我们可以使用其进行文件下载,但会存在一些小问题,在某些情况下(不同源)还是可以使用的,下载文件推荐使用利用 JS 创建 <a></a>
标签的方式。
window.location 是 window 对象的属性,可用于获取当前页面地址(URL)并把浏览器重定向到新页面。
- window.location.href 是用新的域名替换当前页, 也就是重新定位当前页,不会被窗口拦截
- window.location.replace 用新的文档替换当前文档,没有返回功能
- window.location.assign 加载新文档
window.open()是 window 对象的方法,打开一个新的浏览器窗口或查找一个已命名的窗口,可能会被浏览器拦截。
下载文件时使用 window.location.href
可能会导致一个文件下载两次且可能出现浏览器直接打开下载页面,显示一个空白页面等情况。
此时就可以使用 axios
来向后端发送请求获取后端返回的二进制文件
// 后端返回的类型为 application/octet-stream;charset=UTF-8
axios({
url: '/goods/exportGoodList',
method: 'GET',
// 注意,此处需要设置下载的文件的返回类型为二进制,即 blob
responseType: 'blob'
}).then(res => {
// console.log('请求类型为', res.headers['content-type'])
const contentTypeJson = res.headers['content-type'].indexOf(
'application/json'
)
// 若 contentTypeJson 大于 1 ,则表明后端返回的是类型为json,则表明出错了,需要将请求时的二进制对象转化为json对象,然后显示后端返回的错误信息。
if (contentTypeJson > -1) {
// console.log('后台返回的是json对象,表明后台出现错误了')
// 因为执行的是文件操作,是异步操作,所以需要使用promise解决异步请求问题
let judgeType = new Promise((reslove, reject) => {
//文件下载出错, 将上述转化为blob对象的数据转化为json对象,然后提示用户
let reader = new FileReader()
reader.addEventListener('loadend', function() {
res = JSON.parse(reader.result)
if (res.code && res.code != 1000) {
// 如果后台返回的是json对象,在将二进制对象转化完成后会得到一个json对象,包括{code,data,msg}等属性
reslove(res)
}
if (
!res.code &&
res.data.constructor &&
res.data.constructor.name == Blob
) {
reject('后台返回的是blob对象,此种情况不可能出现')
}
})
reader.readAsText(res.data, 'utf-8')
})
judgeType
.then(resData => {
this.$message.error(resData.msg)
})
.catch(err => {
console.log(err)
})
return
}
let blobUrl = window.URL.createObjectURL(res.data)
// 从请求头中获取文件名(文件名在请求头的content-disposition属性的filename属性中。)
// 如果在请求头中获取不到 content-disposition 内容,需要后端在response的header中设置: Access-Control-Expose-Headers: Content-Disposition
if (res.headers['content-disposition']) {
const disposition = res.headers['content-disposition']
// 使用indexOf并从 filename= 之后在加9是因为 filename= 占了九位字符
let fileName = disposition.substring(
disposition.indexOf('filename=') + 9,
disposition.length
)
// 将iso8859-1的字符转化为中文字符
fileName = decodeURI(escape(fileName))
let link = document.createElement('a')
link.href = blobUrl
link.style.display = 'none'
link.download = fileName
document.body.appendChild(link)
link.click()
window.URL.revokeObjectURL(blobUrl)
document.body.removeChild(link)
} else {
this.$message.error('抱歉,服务器获取组织名称异常,请稍后重试')
console.log('没有找到文件名称')
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 24. < el-tree>标签中 树节点的文字过长,需要显示省略号并在鼠标滑过时显示全部文字
el-tree 标签源码中有自定义节点的内容,只是源码中是使用点击事件实现的插入和删除节点,我们可以仿照着其使用鼠标悬浮事件实现我们的需求
<el-tree ref="tree">
<span class="span-ellipsis" slot-scope="{ node }">
<span :title="node.label">{{ node.label }}</span>
</span>
</el-tree>
2
3
4
5
6
css 样式如下:(span-ellipsis 为 el-tree 标签中 span 标签的样式)
.span-ellipsis {
display: block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
2
3
4
5
6
# 25. JS 向一个数组的末尾中添加新的数组
方式一:
let arr1 = [0,1,2,3]
let arr2 = [4,5,6]
const newArr = arr1.concat(arr2)
concat()方法返回的是新数组,不会改变原数组
2
3
4
5
6
7
方式二:
let arr1 = [0,1,2,3]
let arr2 = [4,5,6]
arr1.push(...arr2)
const newArr = arr1
push()方法是向一个数组的末尾添加元素,所以可以使用es6中的扩展运算符将数组展开,然后再添加;
push()方法返回的数组的新长度,该方法会改变原来的数组
2
3
4
5
6
7
8
9
10
11
12
# 26. JS 去除数组中存在的空值
方式一:(es6 的过滤器 filter 方法)
array.filter()方法从给定数组中创建一个新数组,该数组由所提供数组中满足参数函数条件的元素组成。filter()方法会改变原数组,它返回过滤后的新数组,如果没有元素复合条件则会返回空数组。
array.filter( callback (cValue, index, arr), tValue )
filter()方法接受下面列出的两个参数
callback :它包含三个参数
cValue:此参数是必需的,它是当前元素的值。
index:该参数是可选的,它是当前元素的数组索引。
arr:此参数是可选的,它是当前元素所属的数组对象。
tValue:此参数是可选的,该值将会被作为 callback 函数 被调用时的 “this” 值。如果为空,则 “this” 值在非严格模式下将是全局对象,严格模式下为 undefined。
filter
为数组中的每个元素调用一次 callback
函数,并利用所有使得 callback
返回 true
或等价于 true
的值的元素创建一个新数组。callback
只会在已经赋值的索引上被调用,对于那些已经被删除或者从未被赋值的索引不会被调用。那些没有通过 callback 测试的元素会被跳过,不会被包含在新数组中。
filter
遍历的元素范围在第一次调用 callback
之前就已经确定了。在调用 filter()
之后被添加到数组中的元素不会被 filter
遍历到。如果已经存在的元素被改变了,则他们传入 callback
的值是 filter
遍历到它们那一刻的值。被删除或从来未被赋值的元素不会被遍历到。
示例:
let arr = [1,2,'asdf','',4,5,'',6,'',qwer,'',1,,undefined,null,10]
const newArr = arr.filter( n => n)
2
3
方式二:(for 循环遍历结合 splice 方法)
let arr = [1,2,'asdf','',4,5,'',6,'',qwer,'',1,,undefined,null,10]
function removeEmpty(array){
for(let i = 0; i < array.length; i++) {
if(array[i] == "" || typeof(array[i]) == "undefined") {
array.splice(i,1)
i = i - 1 // i - 1 ,因为空元素之前在数组存在位置,删除空值之后,后面的元素要向前补位
}
}
return array
}
removeEmpty(arr)
2
3
4
5
6
7
8
9
10
11
12
13
# 27. JS 数组实现数组去重
方式一:利用数组的 reduce()方法,去除对象数组中某个属性相同的元素
// 数组去重,包括对象数组
clearDuplicate(arr, key) {
if (!Array.isArray(arr)) {
return
}
// 纯数组
if (key === undefined) return [...new Set(arr)]
// 对象数组
let newObj = {}
let newArr = arr.reduce(function(item, curr) {
newObj[curr[key]] ? '' : (newObj[curr[key]] = true && item.push(curr))
return item
}, [])
return newArr
}
// 验证
// 去除对象数组中 name属性相同的元素
clearDuplicate(dataPool,'name')
// 去除纯数组中重复元素
clearDuplicate([1,2,1,4,5,6,2,5,8])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
方式二:(es6 的 set 实现,但无法去除{}对象)
向 Set 加入值的时候,不会发生类型转换,所以 3 和 "3" 是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是 NaN 等于自身
,而精确相等运算符认为 NaN 不等于自身。在 Set 内部,两个NaN是相等,两个对象总是不相等的
。
let arr = [1,2,1,2,1,5,6,3,6,4,5,"3","acd"]
let unique = [...new Set(arr)]
2
3
方式三:(for 循环遍历结合 indexOf 实现)
该方法首先定义一个空数组 temp,然后调用 indexOf()方法对原来的数组进行遍历判断,如果元素不在 temp 中,则将其 push 进 temp 中,最后将 temp 返回即可获得去重的数组。
let arr = [1,2,1,2,1,5,6,3,6,4,5,"3","acd"]
function uniqe(arr){
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
let temp = []
for(let i=0;i<arr.length;i++){
//利用indexOf()检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素
if(arr.indexOf(arr[i]) == i){
temp.push(arr[i])
}
}
return temp
}
uniqe(arr)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
方式四:(双重 for 循环遍历实现)
先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是 O(n^2),如果数组长度很大,那么将会非常耗费内存
let arr = [1,2,1,2,1,5,6,3,6,4,5,"3","acd"]
function unique(arr){
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
for(let i=0; i<arr.length; i++){
for(let j=i+1; j<arr.length; j++){
if(arr[i]==arr[j]){ //第一个等同于第二个,splice方法删除第二个
arr.splice(j,1)
j--
}
}
}
return arr;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
数组去重的 12 种方式参考链接 (opens new window)
数组去重的参考链接 2 (opens new window)
# 28. padStart()方法,padEnd()方法,用于字符串补全长度
目前(2021-05-07),微信 PC 端内置内核不支持(ES2017(es8)内容)padStart()方法,如有相关需求可采用原生的判断字符长度之后再为其添加相关内容。
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
padStart()和 padStart()一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串,若省略则默认使用空格补全长度。
如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串,即补全后的字符串长度可以大于第一个参数指定的最小长度。
如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
'abc'.padStart(10, '0123456789') // '0123456abc'
'abc'.padEnd(10, '0123456789') // 'abc0123456'
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
'123456'.padStart(10, '0') // "0000123456"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 29. 浏览器强制使用 Chromium 内核
在开发过程中需要考虑到兼容多种浏览器(某些浏览器低版本或非 webkit 内核模式下不兼容 css3 和 HTML5 的部分语法),
解决方案一(推荐):配置 babel,兼容低版本浏览器,
解决方案二(不一定生效):在一些简单的情况下也可以在项目的 index.html 中添加如下的 meta 标签来强制浏览器使用 chromium 内核来解决问题(注意,此种方式不一定生效,因为浏览器是用户选择的模式的优先级更高)
// 强制Chromium内核,作用于360浏览器、QQ浏览器等国产双核浏览器,在不是新版本的浏览器即没有chromium内核时先使用ie兼容内核(ie6-7版本,使用Trident内核,兼容性问题较少,但不支持HTML5及CSS3),如果还是没有再使用ie标准内核(IE9 / IE10 / IE11版本,兼容HTML5及CSS3)
<meta name="renderer" content="webkit|ie-comp|ie-stand"/>
// 强制Chromium内核,作用于其他双核浏览器。
<meta name="force-rendering" content="webkit|ie-comp|ie-stand"/>
// 如果有安装 Google Chrome Frame 插件则强制为Chromium内核,否则强制本机支持的最高版本IE内核,作用于IE浏览器。IE=Edge:Edge 模式告诉 IE 以最高级模式渲染文档,也就是任何 IE 版本都以当前版本所支持的最高级标准模式渲染,避免版本升级造成的影响。chrome=1:强制 IE 使用 Chrome Frame 渲染
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"/>
2
3
4
5
6
# 30. el-image 组件中使用图片的相对路径后加载图片失败
使用 elementui 中的组件 el-image 引入图片的相对路径后加载图片失败的主要原因是:在 assets 目录下的文件在使用 webpack 打包后图片的路径及文件名发生了变化(只有 public 目录下的文件是直接打包不会被改变的)。
如果在使用中需要使用相对路径可以采用以下几种方式:
方式一:使用 import 导入方式(具体使用方式参照下方代码)
<template>
<el-image :src="testImageUrl">
</el-image>
</template>
<script>
import testImage from '@/assets/test.png' //引入文件并取别名,在下文data中赋值使用
export default {
data(){
return {
testImageUrl: testImage
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
方式二:使用 require 引入方式(具体使用方式参照下方代码)
<el-image :src="require('@/assets/test.png')"></el-image>
//或使用下面方式
<template>
<el-image :src="testImageUrl">
</el-image>
</template>
<script>
export default {
data(){
return {
testImageUrl: require('@/assets/test.png')
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 31. el-form 组件在表单出现时就立刻触发校验的问题
出现此问题是因为设置的表单的校验的触发方式为 'change',可能是因为在表单出现时为其赋值触发了表单的验证,若想解决此问题,只需将校验的触发方式改为 'blur'即可,若依旧想实现和触发方式为 'change'同样的效果,可以为表单中某个元素设置 change 事件,在表单中某个元素的值被改变时重新执行校验(调用 el-form 的 validateField 方法)即可。
示例:
<template>
<el-form
:model="memberForm"
:rules="rules"
ref="memberForm"
label-width="100px"
>
<el-form-item label="添加成员:" prop="ids">
<el-select
filterable
multiple
:multiple-limit="10"
:filter-method="filterMethod"
:loading="selectLoading"
v-model="memberForm.ids"
placeholder="请选择添加成员"
@focus="filterMethod('')"
@change="onChangeMember"
>
<el-option
v-for="item in memberOptions"
:key="item.id"
:label="item.label"
:value="item.id"
>
</el-option>
</el-select>
<el-input
clearable
minlength="11"
maxlength="11"
v-model="memberForm.phone"
placeholder="请输入员工手机号"
@input="onPhoneChange(memberForm.phone)"
></el-input>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
selectLoading: false,
memberForm: {}, //表单
memberOptions: [], //成员列表下拉选择框
rules: {
ids: [
{
required: true,
message: '请选择添加成员',
trigger: 'blur'
}
]
}
}
},
methods:{
//在表单中元素被改变时对部分表单字段重新进行校验
onChangeMember() {
this.$refs.memberForm.validateField('ids')
},
onPhoneChange(){
this.$refs['memberForm'].validateField('phone')
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 32. 在使用 element-ui 时,如果 el-dialog 中嵌套了 el-tabs,当关闭 dialog 时,页面就会被卡死的解决方案
原因:el-dialog 中的 :destroy-on-close="true" 与 el-tabs 冲突,陷入死循环。
解决方案:
- 删去:destroy-on-close="true"或改为:destroy-on-close="false"。
- 优点:不会有隐藏的其他问题。
- 缺点:无法销毁该组件。
- 将:destroy-on-close="true"改为 v-if="dialogVisible"。
- 优点:既可以销毁该组件,又可以解决错误。
- 缺点:如果 dialog 为自定义的组件,而一个页面出现多个 dialog,在关闭一个后打开其他 dialog 会无法打开。
- 在使用 ref ="xxx" , 通过 $refs.xxx 来控制组件中的 dialog 打开或关闭的变量值。
- 缺点: 在 dialog 组件为子组件时适用,当 dialog 和其他 html 在同一个页面时不适用。
示例:(注意,在 Vue3 中 v-bind 指令上的.sync 修饰符已被弃用,如有类似需求,请使用 v-model)
<el-dialog
v-if="dialogVisible"
:visible="true"
width="500px"
:show-close="false"
:append-to-body="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
>
<el-tabs v-model="activeName">
<el-tab-pane label="第一个" name="first">
第一个标签页内容
</el-tab-pane>
<el-tab-pane label="第二个" name="second">
第二个标签页内容2222
</el-tab-pane>
</el-tabs>
</el-dialog>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 33. Vue 中全局引入某些依赖,在组件中不需要再重复引入,可以直接引用
通过 Vue-cli 创建的项目可以在 vue.config.js 中的 configureWebpack 的 plugins 中使用 ProvidePlugin 插件来全局引入某些依赖,注意,eslint 会报错,需要在 eslintrc.js 的 globals 中配置引入的依赖为 readonly。
vue.config.js
文件配置参考示例
configureWebpack: {
plugins: [
new webpack.ProvidePlugin({
// $: 'jquery',
// jQuery: 'jquery',
// 'windows.jQuery': 'jquery'
echarts: 'echarts',
elementResizeDetectorMaker: 'element-resize-detector'
})
]
}
2
3
4
5
6
7
8
9
10
11
eslintrc.js
配置参考示例
module.exports = {
root: true,
env: {
node: true
},
extends: ['plugin:vue/essential', 'eslint:recommended'],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
strict: 2,
'newline-per-chained-call': 0,
semi: ['error', 'never'],
'comma-dangle': ['error', 'never'],
'array-bracket-spacing': ['error', 'never'],
'space-before-function-paren': ['error', 'never']
},
globals: {
// $: 'readonly',
// jQuery: 'readonly',
// 'windows.jQuery': 'readonly',
echarts: 'readonly',
elementResizeDetectorMaker: 'readonly'
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 34. Vue 项目配置 gzip 压缩
通过 Vue-cli 创建的项目可以在 vue.config.js 中的 configureWebpack 的 plugins 中使用 CompressionWebpackPlugin 插件来压缩项目文件,同时需要在服务器端对 nginx 配置,支持 gzip 压缩。
vue.config.js
配置示例
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const productionGzipExtensions = ['js', 'css']
module.exports = {
configureWebpack: {
plugins: [
// 配置compression-webpack-plugin压缩
new CompressionWebpackPlugin({
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8
})
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nginx.conf
文件配置示例
http {
# 开启gzip
gzip on;
# # 禁用IE 6 gzip
gzip_disable "MSIE [1-6]\.";
# # 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
# # gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
gzip_comp_level 6;
# # 设置压缩所需要的缓冲区大小
gzip_buffers 32 4k;
# # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
gzip_min_length 32;
# # 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到。
gzip_types application/atom+xml application/geo+json application/javascript application/x-javascript application/json application/ld+json application/manifest+json application/rdf+xml application/rss+xml application/xhtml+xml application/xml font/eot font/otf font/ttf image/svg+xml text/css text/javascript text/plain text/xml;
server{
location / {
root html;
index index.html index.htm;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 35. Element-ui@2.15.9 版本中关于日期选择器(el-date-picker)存在 bug(使用后直接会在控制台出现错误信息)
Element-ui@2.15.9 版本中使用日期选择器会直接在控制台出现错误信息,截止目前(2022-08-08)仍未修复。
目前的解决方案是对 Element-ui 版本降级,可降级为 2.15.8 及以下版本。
注意:不能直接在
package.json
文件中直接修改版本号降级,需要先在控制台执行命令npm uninstall element-ui
移除 element-ui 依赖,然后执行命令npm i element-ui@2.15.8
重新安装 2.15.8 版本的 element-ui。
# 36. Element UI 中表格翻页后序号连续
<el-table
:data="pageList"
highlight-current-row
v-loading="loading"
>
<el-table-column
label="序号"
width="60"
type="index"
:index="indexMethod"
>
</el-table>
<script>
export default {
data(){
return {
pageList: [], //表格数据
pageNum: 1, //页码
pageSize: 10, //每页条数
totalCount: 0, //总记录数
}
},
method:{
// 表格翻页后序号连续
indexMethod(index){
return (this.pageNum - 1) * this.pageSize + index + 1
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31