Vue2高级用法
1、mixin复用【vue不会用了,了解一下】
mixin实现复用的同时带来了很多问题,例如:命名污染、依赖不透明
Vue3 用 Composition API替代
1.1 基础使用
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
例子:
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
1.2 选项合并
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
- 值为对象的选项:合并成一个对象,同名的话,组件的值会覆盖混入对象
- 钩子函数:合并成一个数组,都执行,混入对象的钩子先执行,组件后执行(组件的函数覆盖混入的)
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}
new Vue({
mixins: [mixin],
data: function () {
return {
message: 'goodbye',
bar: 'def'
}
},
created: function () {
console.log(this.$data)
// => { message: "goodbye", foo: "abc", bar: "def" }
}
})
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
var mixin = {
created: function () {
console.log('混入对象的钩子被调用')
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log('组件钩子被调用')
}
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"
值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
var mixin = {
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}
var vm = new Vue({
mixins: [mixin],
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
注意:Vue.extend() 也使用同样的策略进行合并。
1.3 全局混入
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。使用恰当时,这可以用来为自定义选项注入处理逻辑。
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
// => "hello!"
1.4 细数 mixin 存在的问题
- 命名冲突:vue组件的值会覆盖mixin选项,相同类型的生命周期钩子添加到同一个数组里依次执行
- 依赖不透明:mixin 和使用它的组件之间没有层次关系。组件可以用mixin的数据,mixin可以用设定在vue里的数据(如上文的this.$options.myOption)。这样要是修改mixin,就比较复杂。
2、vue.js 动画特效& 常见组件库介绍
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:
- 在 CSS 过渡和动画中自动应用 class
- 可以配合使用第三方 CSS 动画库,如 Animate.css
- 在过渡钩子函数中使用 JavaScript 直接操作 DOM
- 可以配合使用第三方 JavaScript 动画库,如 Velocity.js;
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
- 组件根节点
2.1 进入/离开基础使用示例
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
export default {
data() {
show: true
}
}
2.2 进入/离开自定义过度类名
<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">
<div id="example-3">
<button @click="show = !show">
Toggle render
</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>
2.3 进入/离开动画钩子
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:enter-cancelled="enterCancelled"
v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
>
<!-- ... -->
</transition>
2.4 多组件过渡与列表过渡
<div id="list-demo" class="demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
{
data: {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10
},
methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.items.length)
},
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++)
},
remove: function () {
this.items.splice(this.randomIndex(), 1)
},
}
}
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
2.5 状态过渡
通过状态去驱动视图更新从而实现动画过渡
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>
<div id="animated-number-demo">
<input v-model.number="number" type="number" step="20">
<p>{{ animatedNumber }}</p>
</div>
{
data: {
number: 0,
tweenedNumber: 0
},
computed: {
animatedNumber: function() {
return this.tweenedNumber.toFixed(0);
}
},
watch: {
number: function(newValue) {
gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue });
//gsap js动画库, gsap.to('要变化的对象,数据对象或者dom对象',{对象里变化的参数},)
}
}
}
2.6 常用动画相关库
- gsap
- animated.css
- tween.js
3、 插槽
插槽 slot 是写在子组件的代码中,供父组件使用的占位符
3.1 插槽的三种使用方法
- 默认插槽:没有名字,普通的
- 具名插槽:带名字,父组件可以根据名字插入子组件的对应的位置(多个,区分占位符位置)<slot name='‘header’>(区分位置)
- 作用域插槽:父组件可以使用子组件插槽传过来的games数据(传值)
ps: vue 2.6.0版本之后的slot插槽: 用v-slot:default=‘ctx’ 替代slot=‘’。
默认和具名案例:
<template>
<div class="parent">
我是父组件
<Child></Child>
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
name: 'app',
components: {
Child
}
}
</script>
<style>
.parent {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
padding:10px;
background-color:#eee;
}
</style>
<template>
<div class="hello">
我是子组件
<!-- 如果父组件没有填充内容,就显示slot里的默认内容,
如果父组件填充内容,显示父组件的内容 -->
<div>
<!-- 具名插槽 -->
<slot name="header">把头放这里</slot>
</div>
<div>
<!-- 默认插槽 -->
<slot>这里是默认插槽</slot>
</div>
<div>
<!-- 具名插槽 -->
<slot name="footer">把尾巴放这里</slot>
</div>
</div>
</template>
<script>
export default {
name: 'Child',
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.hello{
background-color: #097493;
color: #fff;
padding:20px;
}
.hello>div{
padding:10px;
border: 1px solid red;
margin: 5px;
}
</style>
当父组件什么都没传时,子组件按自己默认的显示
当父组件填入对应内容,将会替换子组件的默认占位(子组件也可以不设置默认内容)
父组件改为
<template>
<div class="parent">
我是父组件
<Child>
<div slot="header">
替换头部内容
</div>
<div slot="header">
替换头部内容2
</div>
<div slot="footer">
替换底部内容
</div>
<div>
没有定义slot='',默认放默认插槽
</div>
<div>
如果再来一个默认插槽呢,都会进入默认插槽里
</div>
</Child>
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
name: 'app',
components: {
Child
}
}
</script>
<style>
.parent {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
padding:10px;
background-color:#eee;
}
</style>
显示:
作用域插槽:子组件定义数据,传出去,在父组件用slot-scope接收并使用,就是作用域插槽的功能
<template>
<div class="parent">
我是父组件
<Child>
<div slot="header" >
替换头部内容
</div>
<div slot="footer" slot-scope="user">
替换底部内容
{{user.games}}
</div>
<div slot-scope="{games}">
没有定义slot='',默认放默认插槽
{{games}}
</div>
</Child>
</div>
</template>
<script>
import Child from './components/Child.vue'
export default {
name: 'app',
components: {
Child
}
}
</script>
<style>
.parent {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
padding:10px;
background-color:#eee;
}
</style>
<template>
<div class="hello">
我是子组件
<!-- 如果父组件没有填充内容,就显示slot里的默认内容,
如果父组件填充内容,显示父组件的内容 -->
<div>
<!-- 具名插槽 -->
<slot :games="games" name="header">把头放这里</slot>
</div>
<div>
<!-- 默认插槽 -->
<slot :games="games">这里是默认插槽</slot>
</div>
<div>
<!-- 具名插槽 -->
<slot :games="games" name="footer">把尾巴放这里</slot>
</div>
</div>
</template>
<script>
export default {
name: 'Child',
data() {
return {
games:['王者荣耀','吃鸡','斗地主']
}
},
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.hello{
background-color: #097493;
color: #fff;
padding:20px;
}
.hello>div{
padding:10px;
border: 1px solid red;
margin: 5px;
}
</style>
显示:
4、插件
插件可以是对象,或者是一个函数。如果是对象,那么对象中需要提供 install 函数,如果是函数,形态需要跟前面提到的 install 函数保持一致。
install 是组件安装的一个方法,跟 npm install 完全不一样,npm install 是一个命令
4.1 定义插件
const MyPlugin = {
install(Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
};
4.2 使用插件
Vue.use(MyPlugin);
{{ $myMethod }}
4.3 插件化机制原理
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 获取已经安装的插件
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 看看插件是否已经安装,如果安装了直接返回
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// toArray(arguments, 1)实现的功能就是,获取Vue.use(plugin,xx,xx)中的其他参数。
// 比如 Vue.use(plugin,{size:'mini', theme:'black'}),就会回去到plugin意外的参数
const args = toArray(arguments, 1)
// 在参数中第一位插入Vue,从而保证第一个参数是Vue实例
args.unshift(this)
// 插件要么是一个函数,要么是一个对象(对象包含install方法)
if (typeof plugin.install === 'function') {
// 调用插件的install方法,并传入Vue实例
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
// 在已经安装的插件数组中,放进去
installedPlugins.push(plugin)
return this
}
}
4.4 具体实践
Vue-Router
for Vue2
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
for Vue3
install(app: App) {
const router = this
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})
// this initial navigation is only necessary on client, on server it doesn't
// make sense because it will create an extra unnecessary navigation and could
// lead to problems
if (
isBrowser &&
// used for the initial navigation client side to avoid pushing
// multiple times when the router is used in multiple apps
!started &&
currentRoute.value === START_LOCATION_NORMALIZED
) {
// see above
started = true
push(routerHistory.location).catch(err => {
if (__DEV__) warn('Unexpected error when starting the router:', err)
})
}
const reactiveRoute = {} as {
[k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
RouteLocationNormalizedLoaded[k]
>
}
for (const key in START_LOCATION_NORMALIZED) {
// @ts-expect-error: the key matches
reactiveRoute[key] = computed(() => currentRoute.value[key])
}
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
const unmountApp = app.unmount
installedApps.add(app)
app.unmount = function () {
installedApps.delete(app)
// the router is not attached to an app anymore
if (installedApps.size < 1) {
// invalidate the current navigation
pendingLocation = START_LOCATION_NORMALIZED
removeHistoryListener && removeHistoryListener()
removeHistoryListener = null
currentRoute.value = START_LOCATION_NORMALIZED
started = false
ready = false
}
unmountApp()
}
// TODO: this probably needs to be updated so it can be used by vue-termui
if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
addDevtools(app, router, matcher)
}
},
}
5、过滤器
Vue.js允许我们自定义过滤器,对数据进行格式化。过滤器应该放在JS表达式的尾部,由管道符号连接。过滤器可以用在两个地方:双花括号差值和v-bind表达式。
5.1 使用过滤器
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="message | capitalize"></div>
5.2 定义过滤器
组件中定义过滤器:
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
全局中定义过滤器:
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
new Vue({
// ...
})
5.3 串联过滤器
我们可以同时使用多个过滤器,过滤器函数总接收表达式的值 (上一个过滤器的结果) 作为第一个参数。
{{ message | filterA | filterB }}
过滤器是 JavaScript 函数,因此可以接收参数:
ps:没有传参时候,默认传入当前值filterA
等价于filterA(message)
{{ message | filterA('arg1', 'arg2') }}