日常工作中,使用 vue 进行项目开发居多,因此把一些 vue 项目常见问题进行总结,在此记录,避免以后开发中做过多的重复工作。
而掌握一些有用的技巧,使用一些更高级的技术点,也能让我们成为更好的 Vue 开发者
# Watch immediate
当 watch 一个变量的时候,初始化时并不会执行,如下面的例子,你需要在 created 的时候手动调用一次。
// bad | |
created() { | |
this.getsearchText(); | |
}, | |
watch: { | |
searchText: 'getSearchText', | |
} |
你可以添加 immediate 属性,这样初始化的时候也会触发,代码也就可以简化成这样
// good | |
watch: { | |
searchText: { | |
handler: 'getSearchText', | |
immediate: true, | |
} | |
} |
# vue 路由跳转打开新窗口
使用 this.$router.resolve
const openNewUrl=(url) => { | |
let routeData = this.$router.resolve({path: url}) | |
window.open(routeData.href, '_blank') | |
} |
# el-input 限制输入框只能输入数字
<el-input v-model.number="num" onkeyup="this.value = this.value.replace(/[^\d.]/g,'');"></el-input> |
# el-input 过滤特殊字符或身份证脱敏
v-model 拆分为:value 和 @input
<el-input :value="input" @input='e => input = idCardValid (e)' placeholder="请输入内容"></el-input> | |
methods:{ | |
idCardValid(val){ | |
const idCard= val.replace(/^(\d{6})\d+(\d{4})$/, "$1******$2") | |
console.log(idCard) | |
return idCard | |
} | |
}, |
# 使用 a 标签下载本地静态资源文件
- 1、public 目录下存放要下载的静态资源
- 2、a 标签下载
<a href="/demo.rar" download="demo.rar">点击下载</a> |
# 检测元素外部 (或内部) 的单击
例如我们检测一个 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", | |
]; | |
} | |
} |
# vue+elementUI 在输入框中按回车键会刷新页面
当一个 form 元素中只有一个输入框时,在该输入框中按下回车应提交该表单。如果希望阻止这一默认行为,可以在 <el-form>
标签上添加 @submit.native.prevent。
<templat> | |
<el-form @submit.native.prevent >< /el-form > | |
</templat> |
# el-select 下拉框样式修改
使用样式穿透修改下拉框样式,你会发现打死都不生效,那是因为下拉框是默认挂载在 body 下面。解决办法:设置 :popper-append-to-body="false"
, 然后再用样式穿透
# element-ui select 组件 change 事件传递多个参数的方法
- 方法一
@change="onChange($event,customParam)" |
- 方法二
@change="((val)=>{changeEvent(val,args)})" |
其他组件的的默认事件同样的方法传递
<el-dropdown trigger="click" @command="((val)=>{handleCommand(val,scope.row)})"> | |
<span class="el-dropdown-link"> | |
<i class="el-icon-more el-icon--right"></i> | |
</span> | |
<el-dropdown-menu slot="dropdown"> | |
<el-dropdown-item command="volumes">新增</el-dropdown-item> | |
<el-dropdown-item command="log">查看</el-dropdown-item> | |
<el-dropdown-item command="shell">更新</el-dropdown-item> | |
<el-dropdown-item command="container">删除</el-dropdown-item> | |
</el-dropdown-menu> | |
</el-dropdown> |
# el-input type=number 去除聚焦时的上下箭头
解决
<el-input class="clear-number-input" type="number"></el-input> | |
<style scoped> | |
.clear-number-input ::v-deep input[type="number"]::-webkit-outer-spin-button, | |
.clear-number-input ::v-deep input[type="number"]::-webkit-inner-spin-button { | |
-webkit-appearance: none !important; | |
} | |
</style> |
# chrome 表单自动填充导致 input 文本框背景失效
我们在开发登录页的时候经常遇到,登陆页的表单自动填充导致 input 文本框背景失效的问题。
// 自动填充样式覆盖 | |
input:-internal-autofill-previewed, | |
input:-internal-autofill-selected { | |
-webkit-text-fill-color: #fff; | |
transition: background-color 5000s ease-out 0.5s; | |
} |
# 巧用 $options
$options 是一个记录当前 Vue 组件的初始化属性选项,当我们想把 data 里的某个值重置为初始值时,非常有用
例如:
this.value = this.$options.data().value; |
# dialog 里重置表单
利用上面介绍的 $options 特性
我们经常的业务场景是这样:一个 el-dialog 中有一个 el-form,而且我们通常是新增和编辑复用同一个组件,现在我们要求每次打开 el-dialog 时都要重置 el-form 里的数据,并且清除校验状态。
// 弹框打开时 | |
initForm(){ | |
this.$refs['form'] && this.$refs['form'].resetFields() | |
this.form = this.$options.data.call(this).form; | |
} |
# 将一个 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() | |
} | |
} |
# 同一组件上存在多个 table 进行 tabs 和 v-if/v-show 切换时,多表格的数据会相互混淆,串在一起,引发 bug
为每个 table 指定对应且唯一的 key 属性。
其他一些类似的问题也可以尝试为其添加 key 属性来解决
# vue element 多个 Form 表单同时验证
<template> | |
<el-form ref="form1"></el-form> | |
<el-form ref="form2"></el-form> | |
<el-form ref="form3"></el-form> | |
</template> | |
<script> | |
export default{ | |
methods: { | |
onValidate() { // 保存操作 | |
const formArr =['form1', 'form2','form3']// 三个 form 表单的 ref | |
const resultArr = [] // 用来接受返回结果的数组 | |
let _self = this | |
function checkForm(formName) { // 封装验证表单的函数 | |
let result = new Promise(function (resolve, reject) { | |
_self.$refs[formName].validate((valid) => { | |
if (valid) { | |
resolve(); | |
} else { reject() } | |
}) | |
}) | |
resultArr.push(result) // 得到 promise 的结果 | |
} | |
formArr.forEach(item => { // 根据表单的 ref 校验 | |
checkForm(item) | |
}) | |
Promise.all(resultArr).then(values => { | |
// 此时必填完成,做保存后的业务操作 | |
// ... | |
console.log('success'); | |
}).catch(_ => { | |
console.log('err') | |
}) | |
}, | |
} | |
} | |
</script> |
# 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"></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"></span> | |
<slot v-else :node="node"></slot> | |
</template> | |
</Tree> | |
</template> |
使用
<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" /> | |
</template> | |
<template v-else> | |
</template> | |
</span> | |
</template> | |
</custom-tree> | |
</template> |