vue应用技巧集合
链接:https://juejin.im/post/5e5f0ef8518825490b6489a2
来源:掘金
监听组件的生命周期
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,常规的写法可能如下:
// Parent.vue <Child @mounted="doSomething"/> // Child.vue mounted() { this.$emit("mounted"); }
此外,还有一种特别简单的方式,子组件不需要任何处理,只需要在父组件引用的时候通过@hook 来监听即可,代码如下:
<Child @hook:mounted="doSomething" /> <Child @hook:updated="doSomething" />
watch 的初始立即执行
当 watch 一个变量的时候,初始化时并不会执行,如下面的例子,你需要在 created 的时候手动调用一次:
created() { this.getList(); }, watch: { keyWord: 'getList', }
上面这样的做法可以使用,但很麻烦,我们可以添加 immediate 属性,这样初始化的时候就会自动触发(不用再写 created 去调用了),然后上面的代码就能简化为:
watch: { keyWord: { handler: 'getList', immediate: true } }
watch 有三个参数
handler
:其值是一个回调函数。即监听到变化时应该执行的函数deep
:其值是 true 或 false;确认是否深入监听。immediate
:其值是 true 或 false,确认是否以当前的初始值执行 handler 的函数
路由参数变化组件不更新
同一path
的页面跳转时路由参数变化,但是组件没有对应的更新。
原因:主要是因为获取参数写在了created
或者mounted
路由钩子函数中,路由参数变化的时候,这个生命周期不会重新执行。
解决方案1:watch
监听路由
watch: { // 方法1 //监听路由是否变化 '$route' (to, from) { if(to.query.id !== from.query.id){ this.id = to.query.id; this.init();//重新加载数据 } } } //方法 2 设置路径变化时的处理函数 watch: { '$route': { handler: 'init', immediate: true } }
解决方案2 :为了实现这样的效果可以给router-view
添加一个不同的key
,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。
<router-view :key="$route.fullpath"></router-view>
路由懒加载
Vue 项目中实现路由按需加载(路由懒加载)的 3 中方式:
// 1、Vue异步组件技术: { path: '/home', name: 'Home', component: resolve => reqire(['path路径'], resolve) } // 2、es6提案的import() const Home = () => import('path路径') // 3、webpack提供的require.ensure() { path: '/home', name: 'Home', component: r => require.ensure([],() => r(require('path路径')), 'demo') }
require.context()
require.context(directory,useSubdirectories,regExp)
- directory:说明需要检索的目录
- useSubdirectories:是否检索子目录
- regExp: 匹配文件的正则表达式,一般是文件名
场景:如页面需要导入多个组件,原始写法:
import titleCom from '@/components/home/titleCom' import bannerCom from '@/components/home/bannerCom' import cellCom from '@/components/home/cellCom' components: { titleCom, bannerCom, cellCom }
这样就写了大量重复的代码,利用 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
递归组件
- 递归组件: 组件在它的模板内可以递归的调用自己,只要给组件设置 name 组件就可以了。
- 不过需要注意的是,必须给一个条件来限制数量,否则会抛出错误: max stack size exceeded
- 组件递归用来开发一些具体有未知层级关系的独立组件。比如:联级选择器和树形控件
<template> <div v-for="(item,index) in treeArr"> {{index}} <br/> <tree :item="item.arr" v-if="item.flag"></tree> </div> </template> <script> export default { // 必须定义name,组件内部才能递归调用 name: 'tree', data(){ return {} }, // 接收外部传入的值 props: { item: { type:Array, default: ()=>[] } } } </script>
自定义路径别名
我们也可以在基础配置文件中添加自己的路径别名
resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src'), 'assets': resolve('src/assets') } }
然后我们导入组件的时候就可以这样写:
// import YourComponent from '/src/assets/YourComponent' import YourComponent from 'assets/YourComponent'
这样既解决了路径过长的麻烦,又解决了相对路径的烦恼。
动态给修改dom的样式
原因:因为我们在写.vue文件中的样式都会追加scoped。这样针对模板dom中的样式就可以生效,但其生效后的最终样式并不是我们写的样式名,而是编码后的。比如:
<template> <div class="box">dom</div> </template> <style lang="scss" scoped> .box { background: red; } </style>
vue 将代码转译成如下,所以我们在js中拼接上的dom结构样式并不会生效。
.box[data-v-11c6864c]{ background:red; } <template> <div class="box" data-v-11c6864c>dom</div> </template>
解决方法:将要改变的样式写在非scoped样式标签中。
长列表性能优化
我们应该都知道 vue 会通过 object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间。
所以,我们可以通过 object.freeze 方法来冻结一个对象,这个对象一旦被冻结,vue就不会对数据进行劫持了。
export default { data: () => ({ list: [] }), async created() { const list = await axios.get('xxxx') this.list = Object.freeze(list) }, methods: { // 此处做的操作都不能改变list的值 } }
另外需要说明的是,这里只是冻结了 list 的值,引用不会被冻结,当我们需要 reactive 数据的时候,我们可以重新给 list 赋值。
内容分发(slot)
插槽 slot,也是组件的一块 HTML 模板,这一块模板显示不显示、以及怎样显示由父组件来决定。实际上,一个 slot 最核心的两个问题在这里就点出来了,是显示不显示和怎样显示。
默认插槽
又名单个插槽、匿名插槽,这类插槽没有具体名字,一个组件只能有一个该类插槽。
<!-- 父组件 parent.vue --> <template> <div class="parent"> <h1>父容器</h1> <child> <div class="tmpl"> <span>菜单1</span> </div> </child> </div> </template> <!-- 子组件 child.vue --> <template> <div class="child"> <h1>子组件</h1> <slot></slot> </div> </template>
具名插槽
匿名插槽没有 name 属性,所以叫匿名插槽。那么,插槽加了 name 属性,就变成了具名插槽。具名插槽可以在一个组件中出现 N 次,出现在不同的位置,只需要使用不同的 name 属性区分即可。
<!-- 父组件 parent.vue --> <template> <div class="parent"> <h1>父容器</h1> <child> <div class="tmpl" slot="up"> <span>菜单up-1</span> </div> <div class="tmpl" slot="down"> <span>菜单down-1</span> </div> <div class="tmpl"> <span>菜单->1</span> </div> </child> </div> </template> <!-- 子组件 child.vue --> <template> <div class="child"> <!-- 具名插槽 --> <slot name="up"></slot> <h3>这里是子组件</h3> <!-- 具名插槽 --> <slot name="down"></slot> <!-- 匿名插槽 --> <slot></slot> </div> </template>
作用域插槽
作用域插槽可以是默认插槽,也可以是具名插槽,不一样的地方是,作用域插槽可以为 slot 标签绑定数据,让其父组件可以获取到子组件的数据。
<!-- parent.vue --> <template> <div class="parent"> <h1>这是父组件</h1> <child >> <template slot="default" slot-scope="slotProps"> {{ slotProps.user.name }} </template> </child >> </div> </template> <!-- 子组件 child.vue --> <template> <div class="child"> <h1>这是子组件</h1> <slot :user="user"></slot> </div> </template> <script> export default { data() { return { user: { name: '小赵' } } } } </script>
11、事件修饰符
Vue.js
为 v-on
提供了事件修饰符,修饰符是由点开头的指令后缀来表示的。
stop
:阻止事件继续传播prevent
:阻止事件默认行为capture
:添加事件监听器时使用事件捕获模式self
:当前元素触发时才触发事件处理函数once
:事件只触发一次passive
:告诉浏览器你不想阻止事件的默认行为,不能和.prevent 一起使用。
<!-- 阻止单击事件继续传播 --> <a v-on:click.stop="toDo"></a> <!-- 提交事件不再重载页面 --> <form v-on:submit.prevent="toSubmit"></form> <!-- 修饰符可以串联 --> <a v-on:click.stop.prevent="toDo"></a> <!-- 只有修饰符 --> <form v-on:submit.prevent></form> <!-- 添加事件监听器时使用事件捕获模式 --> <!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 --> <div v-on:click.capture="toDo">...</div> <!-- 只当在 event.target 是当前元素自身时触发处理函数 --> <div v-on:click.self="toDo">...</div> <!-- 点击事件将只会触发一次 --> <a v-on:click.once="toDo"></a> <!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --> <div v-on:scroll.passive="onScroll">...</div>
12、表单修饰符
- .lazy 在输入框输入完内容,光标离开时才更新视图
- .trim 过滤首尾空格
- .number 如果先输入数字,那它就会限制你输入的只能是数字;如果先输入字符串,那就相当于没有加.number
13、生命周期函数
beforeCreate
(创建前) vue 实例的挂载元素$el 和数据对象 data 都是 undefined, 还未初始化created
(创建后) 完成了 data 数据初始化, el 还未初始化beforeMount
(载入前) vue 实例的$el 和 data 都初始化了, 相关的 render 函数首次被调用mounted
(载入后) 此过程中进行 ajax 交互beforeUpdate
(更新前)updated
(更新后)beforeDestroy
(销毁前)destroyed
(销毁后)
Vue 的父组件和子组件生命周期钩子执行顺序是什么?
- 渲染过程:父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的 mounted 在子组件 mouted 之后。
- 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
- 子组件更新过程:
- 影响到父组件:父 beforeUpdate -> 子 beforeUpdate->子 updated -> 父 updted
- 不影响父组件:子 beforeUpdate -> 子 updated
- 父组件更新过程:
- 影响到子组件:父 beforeUpdate -> 子 beforeUpdate->子 updated -> 父 updted
- 不影响子组件:父 beforeUpdate -> 父 updated
- 销毁过程:
- 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
14、组件 attrs 和 listeners
attrs
获取子传父中未在 props 定义的值
// 父组件 <home title="这是标题" width="80" height="80" imgUrl="imgUrl"/> // 子组件 mounted() { console.log(this.$attrs) //{title: "这是标题", width: "80", height: "80", imgUrl: "imgUrl"} } // 相对应的如果子组件定义了 props,打印的值就是剔除定义的属性 props: { width: { type: String, default: '' } }, mounted() { console.log(this.$attrs) //{title: "这是标题", height: "80", imgUrl: "imgUrl"} }
listeners
:场景:子组件需要调用父组件的方法。 解决:父组件的方法可以通过 v-on="listeners"
传入内部组件——在创建更高层次的组件时非常有用
// 父组件 <home @change="change"/> // 子组件 mounted() { console.log(this.$listeners) //即可拿到 change 事件 }
15、路由守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
全局前置守卫 常用于判断登录状态和菜单权限校验
router.beforeEach((to, from, next) => { let isLogin = sessionStorage.getItem('isLogin') || '' if (!isLogin && to.meta.auth) { next('/login') } else { next() } })
to
: Route: 即将要进入的目标 路由对象from
: Route: 当前导航正要离开的路由next
: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
组件内的守卫
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
16、路由缓存 keepalive
keep-alive
是 Vue 提供的一个抽象组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在 v 页面渲染完毕后不会被渲染成一个 DOM 元素。
<keep-alive> <router-view></router-view> </keep-alive>
当组件在 keep-alive
内被切换时组件的 activated
、deactivated
这两个生命周期钩子函数会被执行
使用参数include/exclude
- include: 字符串或正则表达式。只有匹配的组件会被缓存。
- exclude: 字符串或正则表达式。任何匹配的组件都不会被缓存。
<keep-alive include="a,b"> <router-view></router-view> </keep-alive> <keep-alive exclude="c"> <router-view></router-view> </keep-alive>
使用$route.meta 的 keepAlive 属性
需要在 router 中设置 router 的元信息 meta
export default new Router({ routes: [ { path: '/', name: 'Hello', component: Hello, meta: { keepAlive: false // 不需要缓存 } }, { path: '/page1', name: 'Page1', component: Page1, meta: { keepAlive: true // 需要被缓存 } } ] })
在 app.vue 进行区别缓存和不用缓存的页面
<div id="app"> <router-view v-if="!$route.meta.keepAlive"></router-view> <keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> </div>
//==================================================/
以下内容出处
17、Vue.use
我们使用的第三方 Vue.js 插件。如果插件是一个对象,必须提供install
方法。如果插件是一个函数,它会被作为install
方法。install
方法调用时,会将Vue
作为参数传入。该方法需要在调用new Vue()
之前被调用。
我们在使用插件或者第三方组件库的时候用到Vue.use
这个方法,比如
import iView from 'iview'
Vue.use(iView)
那么Vue.use
到底做了些什么事情呢?我们先来看一下源码
import { toArray } from '../util/index' export function initUse(Vue: GlobalAPI) { Vue.use = function(plugin: Function | Object) { const installedPlugins = this._installedPlugins || (this._installedPlugins = []) if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } installedPlugins.push(plugin) return this } }
我们由以上可以看出,plugin
参数为函数或者对象类型,首先Vue
会去寻找这个插件在已安装的插件列表里有没有,如果没有,则进行安装插件,如果有则跳出函数,这保证插件只被安装一次。
接着通过toArray
方法将参数变成数组,再判断plugin
的install
属性是否为函数,或者plugin
本身就是函数,最后执行plugin.install
或者plugin
的方法。
举个例子
下面我们来举个实际例子
1、编写两个插件
const Plugin1 = { install(a) { console.log(a) } } function Plugin2(b) { console.log(b) } export { Plugin1, Plugin2 }
2、引入并 use 这两个插件
import Vue from 'vue' import { Plugin1, Plugin2 } from './plugins' Vue.use(Plugin1, '参数1') Vue.use(Plugin2, '参数2')
此时我们运行项目代码就可以用到上面两个插件了。
18、Vue.mixin
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
1、定义一个 mixin.js
export default mixin { data() { return { name: 'mixin' } }, created() { console.log('mixin...', this.name); }, mounted() {}, methods: { //日期转换 formatDate (dateTime, fmt = 'YYYY年MM月DD日 HH:mm:ss') { if (!dateTime) { return '' } moment.locale('zh-CN') dateTime = moment(dateTime).format(fmt) return dateTime } } }
2、在vue文件中使用mixin
import '@/mixin'; // 引入mixin文件 export default { mixins: [mixin], //用法 data() { return { userName: "adimin", time: this.formatDate(new Date()) //这个vue文件的数据源data里面的time就是引用混入进来的方法 } } }
或者在全局中使用在main.js中,所有页面都能使用了
import mixin from './mixin'
Vue.mixin(mixin)
合并选项
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
data
对象在内部会进行递归合并,并在发生冲突时以组件数据优先。- 同名钩子函数将合并为一个数组,因此都将被调用。混入对象的钩子将在组件自身钩子之前调用。
- 值为对象的选项,例如
methods
、components
和directives
,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
19、Vue.extend
Vue.extend
属于 Vue 的全局 API。它使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。如下:
<div id="app"></div> var Profile = Vue.extend({ template: '<p>{{firstName}} {{lastName}}</p>', data: function () { return { firstName: 'Walter', lastName: 'White' } } }) // 创建 Profile 实例,并挂载到一个元素上。 new Profile().$mount('#app')
应用实例
我们常用 Vue.extend
封装一些全局插件,比如 toast
, diolog
等。
下面以封装一个 toast
组件为例。
1、编写组件
- 根据传入的 type 确定弹窗的类型(成功提示,失败提示,警告,加载,纯文字)
- 设置弹窗消失的时间
<template> <div> <transition name="fade"> <div class="little-tip" v-show="showTip"> <img src="/success.png" alt="" width="36" v-if="type=='success'" /> <img src="/fail.png" alt="" width="36" v-if="type=='fail'" /> <img src="/warning.png" alt="" width="36" v-if="type=='warning'" /> <img src="/loading.png" alt="" width="36" v-if="type=='loading'" class="loading" /> <span>{{msg}}</span> </div> </transition> </div> </template> <script> export default { data() { return { showTip: true, msg: '', type: '' } }, mounted() { setTimeout(() => { this.showTip = false }, 1500) } } </script> <style lang="less" scoped> /* 样式略 */ </style>
2、利用 Vue.extend
构造器把 toast
组件挂载到 vue
实例下
import Vue from 'vue' import Main from './toast.vue' let Toast = Vue.extend(Main) let instance const toast = function(options) { options = options || {} instance = new Toast({ data: options }) instance.vm = instance.$mount() document.body.appendChild(instance.vm.$el) return instance.vm } export default toast
3、在 main.js
引入 toast
组价并挂载在 vue
原型上
import Vue from 'vue' import toast from './components/toast' Vue.prototype.$toast = toast
4、在项目中调用
this.$toast({ msg: '手机号码不能为空' }) this.$toast({ msg: '成功提示', type: 'success' })
Vue.extend 和 Vue.component 的区别
component
是需要先进行组件注册后,然后在template
中使用注册的标签名来实现组件的使用。Vue.extend
则是编程式的写法。- 控制
component
的显示与否,需要在父组件中传入一个状态来控制或者在组件外部用v-if/v-show
来实现控制,而Vue.extend
的显示与否是手动的去做组件的挂载和销毁。
20、Vue.directive
注册或获取全局指令。指令定义函数提供了几个钩子函数(可选):
- bind: 只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作。
- inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
- update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值。
- componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
- unbind: 只调用一次, 指令与元素解绑时调用。
应用实例
下面封装一个复制粘贴文本的例子。
1、编写指令 copy.js
const vCopy = { bind (el, { value }) { el.$value = value // 用一个全局属性来存传进来的值 el.handler = () => { if (!el.$value) { alert('无复制内容') return } // 动态创建 textarea 标签 const textarea = document.createElement('textarea') // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域 textarea.readOnly = 'readonly' textarea.style.position = 'absolute' textarea.style.left = '-9999px' // 将要 copy 的值赋给 textarea 标签的 value 属性 textarea.value = el.$value // 将 textarea 插入到 body 中 document.body.appendChild(textarea) // 选中值并复制 textarea.select() // textarea.setSelectionRange(0, textarea.value.length); const result = document.execCommand('Copy') if (result) { alert('复制成功') } document.body.removeChild(textarea) } // 绑定点击事件,就是所谓的一键 copy 啦 el.addEventListener('click', el.handler) }, // 当传进来的值更新的时候触发 componentUpdated (el, { value }) { el.$value = value }, // 指令与元素解绑的时候,移除事件绑定 unbind (el) { el.removeEventListener('click', el.handler) } } export default vCopy
2、注册指令
import copy from './copy' // 自定义指令 const directives = { copy } // 这种写法可以批量注册指令 export default { install (Vue) { Object.keys(directives).forEach((key) => { Vue.directive(key, directives[key]) }) } }
3、在 main.js
引入并 use
import Vue from 'vue' import Directives from './JS/directives' Vue.use(Directives)
这样就可以在项目直接用 vCopy
指令了。