Bootstrap

05.Vue3 中的高级语法: mixin, plugin, render, 自定义指令等

Vue3 高级语法

1. Mixin 基础语法

mixin 是混入的概念。从外部带来一些数据给组件使用。

1.1 使用方法

定义一个 mixin 对象,然后在组件的 mixins 里加入自己定义的 mixin 对象即可。

const myMixin = {
  data() {
    return {
      number: 2,
      count: 222,
    }
  },
  created() {
    console.log("mixin created")
  },
}

const app = Vue.createApp({
  data() {
    return {
      number: 1,
    }
  },
  created() {
    console.log("component created")
  },
  mixins: [myMixin],
  methods: {
    handleClick() {
      console.log("handleClick")
    }
  },
  template: `
      <div>
        <span>{{ number }}</span>
        <button @click="handleClick">增加</button>
      </div>
    `
})

app.mount("#root")

测试一下上面的代码可以得出以下结论:

  1. mixin 和组件结构十分相似,唯一的区别是没有通过 Vue.createApp 方法创造成组件。

  2. 如果组件和 mixin 里的 data 属性、方法等没有发生冲突,则组件导入 mixin 后可以使用 mixin 里所有的相关数据和方法。

  3. 如果发生冲突了,组件本身的 data 和 methods 优先级高于 mixin 里 data 和 methods 优先级,因此 mixin 里的 data 和 methods 因为被覆盖而失效。

  4. 生命周期函数的执行具有叠加效果,先执行 mixin 里面的,再执行组件里的。

说的土一点,就是 mixin 对象被创建组件的对象覆盖后,用 Vue.createApp 创建了一个组合后的组件。

1.2 局部 mixin 和全局 mixin

刚才的用法是根据组件内的 mixins 属性来将 mixin 添加进来的,这就是局部 mixin。局部 mixin 的特性是,只有当前组件能试用 mixin 里的数据和方法,其他组件,就连子组件都不行。

如果要让所有的组件都能用上 mixin 里的数据和方法,需要使用全局 mixin 的方式。全局 mixin 是不推荐的,因为后面的创建的组件都会受到影响,可能会有不可预料到的问题,并且数据定位的难度会加大)。

app.mixin({
  ......
})

1.3 自定义属性合并策略修改

什么是自定义属性?

自定义属性指的是,vue 内置的属性以外的属性直接定义在组件里。vue 内置的属性就是 data,computed,watch, methods 等等。如果要获取自定义属性里的值,需要通过 this.$options.属性名 获取。

组件内自定义属性和 mixin 自定义组件冲突的情况
const myMixin = {
  number: 1,
}

const app = Vue.createApp({
  number: 2,
  mixins: [myMixin],
  template: `
      <div>
        <div>{{ this.$options.number }}</div>
      </div>
    `
})

app.mount("#root")

如果用上面的代码试一试,mixin 和组件内的自定义属性发生冲突的话,仍然是组件内的自定义属性优先级更高的。

但是如果希望自定义属性冲突的时候,mixin 的优先级更高,那应该怎么处理呢?这个时候就需要修改合并策略了。

自定义属性合并策略修改
app.config.optionMergeStrategies.number = (mininVal, appVal) => {
  console.log(mininVal, appVal)
  return mininVal || appVal
}

上面的代码也好理解,用了或操作符。如果 mixin 里存在 number,就直接返回 mixin 里的 number,否则返回组件里的 number。这就成功修改了自定义属性值。

1.4 mixin 局限性

Vue3 推出后,mixin 已经不推荐使用了,因为容易发生冲突问题,并且数据来源不直观不清晰,定位麻烦。使用 vue3 里的 composition API,可维护性会高很多。

2. 实现 Vue 中的自定义指令

之前有许多的默认的内置指令,如 v-model 和 v-show,vue 也支持自定义指令。

使用自定义指令一般都是需要实现一些定制化的 DOM 底层的功能,其他的功能尽量在组件层面解决。

例子:页面加载后输入框自动聚焦

希望输入框在加载的时候就处于 focused 状态。这个功能第一眼的写法是,用 ref 属性获取 input 节点,然后在 mounted 生命周期使得 input 框 focused。

代码如下:

const app = Vue.createApp({
  mounted() {
    this.$refs.input.focus()
  },
  template: `
      <div>
        <input type="text" ref="input" />
      </div>
    `
})

app.mount("#root")

如果自动聚焦的功能想要被复用呢?就可以封装一些自定义指令实现复用了。

3.1 全局自定义指令

全局自定义指令 app.directive

例如,自定义一个 focus 指令,代码如下:

app.directive("focus", {
  mounted(el) {
    el.focus()
  },
})

此时的 input 只需要添加属性 v-focus 即可。

const app = Vue.createApp({
  template: `
      <div>
        <input type="text" v-focus />
      </div>
    `
})

当然,上面的指令通过 app 注册,因此也是全局自定义指令,也可以定义局部指令

3.2 局部自定义指令

局部的自定义指令,就和 mixin 和 component 一样,需要在组件内进行注册,其他的正常使用即可。

const directives = {
  focus: {
    mounted(el) {
      el.focus()
    }
  }
}

const app = Vue.createApp({
  directives: directives,
  template: `
      <div>
        <input type="text" v-focus />
      </div>
    `
})

3.3 自定义指令内的钩子函数

在上文,使用了自定义指令的 mounted 钩子函数,还有更多的钩子函数,例如 created,beforeMount,beforeUpdate,updated,beforeUnmount,unmounted,执行的时期和组件的生命周期钩子函数保持一致。

3.4 自定义指令动态参数

要用自定义指令实现一个功能,使用 v-pin:top=“200”,使得该元素离顶部 200px,v-pin:left=“300”,则离左边 300px。

这时,钩子函数里需要第二个参数 binding,代表该自定义指令绑定的内容。参数为 binding.arg,参数值为 binding.value。

const app = Vue.createApp({
  data() {
    return {
      hello: true,
    }
  },
  template: `
      <div v-pin:left="300">
        <input type="text" />
      </div>
    `
})

app.directive("pin", {
  mounted(el, binding) {
    // 使元素变为绝对定位
    el.style.position = "absolute"
    // 元素的位置进行修改
    el.style[binding.arg] = binding.value + "px"
  },
  // 每次数据更新会进行重置
  updated(el, binding) {
    // 元素的位置进行修改
    el.style[binding.arg] = binding.value + "px"
  },
})

app.mount("#root")

简写形式

如果自定义指令里只有 mounted 和 updated,且需要实现的功能相同,那么可以用下面的简写形式:

app.directive("pin", (el, binding) => {
  el.style.top = binding.value + "px"
})

3. Teleport 传送门功能

实现一个功能,点击按钮,整个页面出现蒙层,把整个屏幕蒙上。

相关样式:

.area {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 200px;
  height: 300px;
  background-color: green;
}

.mask {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;background-color: #000;
  opacity: 0.5;
}

相关 js 代码:

const app = Vue.createApp({
  data() {
    return {
      show: false,
    }
  },
  methods: {
    handleBtnClick() {
      this.show = !this.show
    }
  },
  template: `
    <div class="area">
      <button @click="handleBtnClick">click</button>
      <div class="mask" v-show="show"></div>
    </div>
  `
})

app.mount("#root")

现在已经有蒙层的效果了,但是问题是,仅局限在 .area 这个 div 里,因此 position 都是相对于父元素。

正确的做法应该是,蒙层应该放在 body 的下一层。但是这需要操作 dom,比较麻烦。vue 的 teleport 传送门可以方便地把蒙层的 div 传送到 body 标签那边。

使用方法:

在蒙层的 div 外边包裹 teleport 标签,里面的属性名 to 指向该元素传送到哪个节点下。to="body" 就是将该 div 传送到 body 下,成为 body 标签的子集。

  template: `
    <div class="area">
      <button @click="handleBtnClick">click</button>
      <teleport to="body">
        <div class="mask" v-show="show"></div>
      </teleport>
    </div>
  `
  ......

4. render 函数

render 函数是比 template 更底层的模板函数。

实现一个功能:通过父组件穿过来的属性值来修改标签。比如,传过来的是 1,标签名就是 h1,传过来的是 h2,标签名就是 h2,以此类推。

如果用 template 来渲染,将会十分十分麻烦。

const app = Vue.createApp({
  template: `
    <my-title :level="1">
      hello  
    </my-title>
  `
})

app.component("my-title", {
  props: ["level"],
  template: `
    <h1 v-if="level === 1">
      <slot />
    </h1>
    <h2 v-if="level === 2">
      <slot />
    </h2>
    下面的重复代码省略......
  `
})

app.mount("#root")

现在引入 render 的使用方法:

// 引入 h 函数
const { h } = Vue

const app = Vue.createApp({
  template: `
    <my-title :level="1">
      hello  
    </my-title>
  `
})

app.component("my-title", {
  props: ["level"],
  render() {
    // h 函数有三个参数:
    // 第一个参数是标签名称
    // 第二个参数是属性
    // 第三个参数是一个数组,存放子元素,如果只有单个元素,不写成数组也行
    // 调用默认插槽的内容:this.$slots.default()
    return h("h" + this.level, {}, this.$slots.default())
})

完全实现上面 template 实现的功能,且代码更简短。

template 与 render 的关系

template 在底层被编译之后会生成 render 函数。h 函数实行之后返回了虚拟 DOM。虚拟 DOM 是 DOM 节点的 JS 对象的表述。

template -> render -> h -> 虚拟 DOM (JS 对象)-> 真实 DOM -> 展示到页面上

虚拟 DOM 使得性能更快的同时,还使得框架具有跨平台的能力,不仅可以写网页,还可以写移动端的东西。

知名前端框架 React 和 Vue 等都用到了虚拟 DOM。

一个简单虚拟 DOM 差不多长这样:

{
    tag: "div",
    attrs: {
        class: "a",
    }
    text: "hello",
    children: []
}

5. 插件的定义和使用

plugin 插件,用于把通用性的功能封装起来。可以实现比 mixin 效果更好的封装。比较常见的效果,例如轮播图,就是通过插件来实现的。

install 方法是插件执行时会走的逻辑,里边接收两个参数 app 和 options。第一个参数 app 是 vue 实例,第二个参数 options 指的是插件使用时传递过来的参数。

接下来使用插件,调用 app.use(插件, options) 即可。

既然插件可以拿到 vue 实例,那么就可以实现大部分功能。

例如,使用一个插件,安装该插件的 vue 实例可以在全局范围提供数据,同时提供 v-focus 自定义指令(功能和上文相同)。

const myPlugin = {
  install(app, options) {
    // 提供 name 全局属性,name 的值取决于插件可选配置
    app.provide("name", options.name)
    // 提供 v-focus 自定义指令
    app.directive("focus", {
      mounted(el) {
        el.focus()
      },
    })
  }
}

const app = Vue.createApp({
  template: `
    <my-title />
  `
})

app.use(myPlugin, {
  name: "sjh"
})

app.component("my-title", {
  inject: ["name"],
  template: `
    <div>
      hello {{ name }}
      <input type="text" v-focus />
    </div>
  `
})

app.mount("#root")

6. 数据校验插件开发实例

先写一下代码:

const app = Vue.createApp({
  data() {
    return {
      name: "sjh",
      age: 20,
    }
  },
  rules: {
    name: {
      validate: name => name.length > 4,
      message: "名字太短"
    },
    age: {
      validate: age => age > 25,
      message: "太年轻了"
    },
  },
  template: `
    <div>
      <div>
        name: <input type="text" v-model="name" />
      </div>
      <div>
        age: <input type="text" v-model="age" />
      </div>
    </div>
  `
})

看上面的代码可以了解到,这上面想要实现一个功能,校验 age 是否过于年轻或者名字是否太短。如果太年轻或者名字太短的话,提供相关的提示信息。

但是可想而知,vue 里并没有 rules 这个默认属性。

6.1 使用 mixin 实现

组件组合 mixin 后,mixin 可以通过 this.$options 自定义属性来获取到 rules。然后在里边对 rules 提供的数据进行监听,每次的数据修改都会触发校验。

实现代码:

const myMixin = {
  created() {
    const rules = this.$options.rules
    for (let key in rules) {
      const { validate, message } = rules[key]
      // 每次数据更改,对实例做监控
      this.$watch(key, (value) => {
        // 判断更新后的值是否通过验证
        const result = validate(value)
        // 无法通过验证则输出信息
        if (!result) console.log(message)
      })
    }
  },
}

const app = Vue.createApp({
  data() {
    return {
      name: "sjh",
      age: 20,
    }
  },
  mixins: [myMixin],
  rules: {
    name: {
      validate: name => name.length > 4,
      message: "名字太短"
    },
    age: {
      validate: age => age > 25,
      message: "太年轻了"
    },
  },
  template: `
    <div>
      <div>
        name: <input type="text" v-model="name" />
      </div>
      <div>
        age: <input type="text" v-model="age" />
      </div>
    </div>
  `
})

app.mount("#root")

6.2 使用 plugin 实现

把之前 mixin 实现的功能放在 plugin 里即可。

const validatorPlugin = (app, options) => {
  app.mixin({
    created() {
      const rules = this.$options.rules
      for (let key in rules) {
        const { validate, message } = rules[key]
        this.$watch(key, (value) => {
          const result = validate(value)
          if (!result) console.log(message)
        })
      }
    },
  })
}

const app = Vue.createApp({
  data() {
    return {
      name: "sjh",
      age: 20,
    }
  },
  rules: {
    name: {
      validate: name => name.length > 4,
      message: "名字太短"
    },
    age: {
      validate: age => age > 25,
      message: "太年轻了"
    },
  },
  template: `
    <div>
      <div>
        name: <input type="text" v-model="name" />
      </div>
      <div>
        age: <input type="text" v-model="age" />
      </div>
    </div>
  `
})

app.use(validatorPlugin)
app.mount("#root")
;