使用Vue真的太棒了,但是也有可能使你头疼,当你试图使它与jquery插件混用的时候。

问题的原因是jquery与Vue是完全不同的东西,Vue是通过组件与数据绑定来进行渲染的,jquery则主要是用来做简单的单击处理和强大的操纵DOM的能力。

我试图寻找一些东西来帮助自己解决这个问题,但是我发现一些组件所做的工作不是很让我满意,所以让我告诉你怎样使Vue与jquery插件混用。

 

这么做的目的是什么?

大多数情况下你能使用jquery和一些简单的Vue来满足你的需求,Modals, sliders等等和一些Vue组件结合起来是很快的。

所以我们的目标是用强大的jQuery插件与Vue快速结合。

我们将...

  • 用Vue的自定义指令来构建jquery
  • 当元素生成时初始化插件
  • 当元素撤销时销毁插件
  • 发送事件来通知组件
  • 从组件接收事件并将它们传递给插件

 

教程时间

我选择Fengyuan Chen’s的cropper因为它是一个很棒的jQuery插件,如果时间限制在60分钟之内的话你肯定不可能重写这个插件在不使用Vue的情况下。

DEMO:https://vue-jquery-cropper-demo.firebaseapp.com/

我将从最开始来演示,如果你已经完成请跳过这个部分。

 

创建项目

# install vue-cli
$ npm install -g vue-cli
# create a new project using the "webpack" boilerplate
$ vue init webpack vue-cropper

? Project name "vue-cropper"
? Project description "A Vue.js project"
? Author "Christian Gambardella <christian@gambardella.info>"
? Use ESLint to lint your code? "Yes"
? Pick an ESLint preset "Standard"
? Setup unit tests with Karma + Mocha? "No"
? Setup e2e tests with Nightwatch? "No"

$ cd vue-cropper
$ npm install

祝贺你已经有了一个Vue的项目。

安装 jQuery 和 cropper.js

# install jQuery & cropper
$ npm install jquery cropper --save

 

为jquery和Vue自定义指令配置webpack

为webpack配置添加jquery和Vue自定义指令的映射。

通常webpack已经引入了完整的jquery版本,但还是建议再一次引入一下。

您可以看到Vue的webpack模板已经添加到组件的文件夹中。我通常会添加很多其他文件夹像自定义指令,mixin等等。在这个例子中,我们只添加了自定义指令。

这将帮助我们引入依赖关系而无需知道其确切的路径。这也是有益的在你重构你的应用的时候。你也并不需要管理相对路径。

把下面高亮部分添加到build/webpack.base.conf文件中。

resolve: {
  extensions: ['', '.js', '.vue'],
  fallback: [path.join(__dirname, '../node_modules')],
  alias: {
    'src': path.resolve(__dirname, '../src'),
    'assets': path.resolve(__dirname, '../src/assets'),
    'components': path.resolve(__dirname, '../src/components'),
    'jquery': path.resolve(__dirname, '../node_modules/jquery/src/jquery'),
    'directives': path.resolve(__dirname, '../src/directives')
  }
},

不要忘了在末尾添加逗号。

 

准备应用组件

我要开始使用组件,因为我相信这是更容易理解的,漂亮和简单的组件。如果你已经知道如何在组件中使用它们那么就可以立即写这个指令来实现它们。

替换src/App.vue文件的template部分

<template>
  <div id="app">
    <img
      v-cropper="cropOptions"
      src="https://i.imgur.com/WcvkCxl.png"
      alt="jQuery Meme">
  </div>
</template>

替换src/App.vue文件的script部分

<script>
import Cropper from './directives/Cropper'

export default {
  directives: {
    Cropper
  },

  data () {
    return {
      cropOptions: {
        viewMode: 0,
        zoomable: false
      }
    }
  }
}
</script>

一些需要重点注意的地方

  • 我们引入jQuery也不需要处理组件的初始化。
  • 把Cropper的设置选项当做原始绑定数据。
  • Cropper将在初始化时渲染,再销毁时使其消失。
  • 组件的名字可以在template用作标签使用,MyCropper转变为my-cropper。
  • 对它进行初始化时,我们必须添加v-my-cropper元素在组件的模板里。

 

创建Cropper组件

这是本教程的核心。我们要创建一个Cropper指令来处理低层代码的DOM操作。在这种情况下我们要初始化Cropper。

自定义指令是用来提供一种机制来映射数据并任意操作DOM的行为。

创建src/directives/Cropper.js

import jQuery from 'jquery'
import 'cropper'

export default {
  deep: true,

  bind () {},

  update (options) {
    // Destroy in case it has been initialized already.
    jQuery(this.el).cropper('destroy')
    // Initializing directly after destroying
    // didn't work. Wrapping it in a setTimeout
    // seems to do the trick.
    setTimeout(() => {
      jQuery(this.el).cropper(options)
    }, 0)
  },

  unbind () {
    jQuery(this.el).cropper('destroy')
  }
}

每一个Vue自定义组件的最核心部分是其提供可以用来绑定,更新,解绑相应方法。

  • bind:当元素在文档中生成时只触发一次。
  • update:当bind方法触发后和绑定数据改变时触发,在这种情况下cropOptions只是一个对象。Vue.js自定义指令需要知道如果它的相关属性也是一个对象。这就是为什么我们需要添加deep: true在这种情况下。
  • unbind:当DOM元素被移除时触发,在这个方法里可以销毁组件并且移除监听的事件。

在Vue 2中会有些许变化。

vm实例被作为函数参数传递,不会被设置为this。

指令的update方法只能在数据改变后触发而不会在bind方法之后触发。

 

第一次运行

这是我们所能做的最小限度,让我们运行起来看看如何。

这是很快做出来的,所以我们并没有添加cropper的样式,让我们添加并再一次运行。

先添加高亮部分在src/main.js文件中

import '../node_modules/cropper/dist/cropper.css'
import Vue from 'vue'
import App from './App'

/* eslint-disable no-new */
new Vue({
  el: 'body',
  components: { App }
})

 

启动服务

$ npm run dev

当所有事情都做好后,你应该看到一个图片和一个cropbox并且可以使其移动。

切换 on/off

从现在起一切都会很简单。我们将通过这个案件。使每件事都会简单起来。

我们用一个按钮来启用和禁用cropper。页面开始用户选择一个文件切换为cropper的打开状态。证明了cropper可以在任何时候被初始化。

编辑src/App.vue的script部分

import Cropper from './directives/Cropper'

export default {
  directives: {
    Cropper
  },

  data () {
    return {
      cropOptions: {
        viewMode: 0,
        zoomable: false
      },
      showCropper: false
    }
  },

  methods: {
    toggleCropper () {
      this.showCropper = !this.showCropper
    }
  }
}
  • data中添加showCropper: false
  • 添加一个切换方法toggleCropper

编辑src/App.vue的template部分

<div id="app">
  <button id="toggle" @click="toggleCropper">Toggle Cropper</button>
  <img
    v-if="showCropper"
    v-cropper="cropOptions"
    src="https://i.imgur.com/WcvkCxl.png"
    alt="Mustache">
</div>
  • 添加一个按钮绑定@click
  • 在img标签上添加v-if指令

每一次图像的显示都会调用自定义cropper指令的bind和update方法。

当showCropper设置为false时,cropper的unbind将被调用。

这里不会使我们陷入困境。

cropper将被加载在用户需要时和卸载后。

 

改变选项

我们已经拥有需要更新cropper的选项。如果我们有一个现有jQuery插件,并且会接收新选项,我们将不需要销毁当前实例来重新创建插件实例。不幸的是cropper不允许这样。

让我们添加一个开关来控制图像缩放。

编辑src/App.vue的template部分

<div id="app">
  <button id="toggle" @click="toggleCropper">Toggle Cropper</button>
  <img
    v-if="showCropper"
    v-cropper="cropOptions"
    src="https://i.imgur.com/WcvkCxl.png"
    alt="Mustache">
  <input id="zoomable" type="checkbox" v-model="cropOptions.zoomable">
  <label for="zoomable">Zoomable?</label>
</div>

很简单,对吗?

 

通过指令获取数据

cropper有一个回调方法,当我们触发一个事件时将会调用这个方法。

编辑src/directives/Cropper.js

import jQuery from 'jquery'
import 'cropper'

export default {
  deep: true,

  bind () {},

  update (options) {
    options.crop = (event) => {
      this.vm.$emit('crop', event)
    }
    // Destroy in case it has been initialized already.
    jQuery(this.el).cropper('destroy')
    // Initializing directly after destroying
    // didn't work. Wrapping it in a setTimeout
    // seems to do the trick.
    setTimeout(() => {
      jQuery(this.el).cropper(options)
    }, 0)
  },

  unbind () {
    jQuery(this.el).cropper('destroy')
  }
}

编辑App组件的script部分来处理crop事件

import Cropper from './directives/Cropper'

export default {
  directives: {
    Cropper
  },

  data () {
    return {
      cropData: {},
      cropOptions: {
        viewMode: 0,
        zoomable: false
      },
      showCropper: false
    }
  },

  events: {
    crop (event) {
      this.cropData = {
        x: event.x,
        y: event.y,
        width: event.width,
        height: event.height
      }
    }
  },

  methods: {
    toggleCropper () {
      this.showCropper = !this.showCropper
    }
  }
}

编辑App组件的template部分来展示crop数据

<template>
  <div id="app">
    <button id="toggle" @click="toggleCropper">Toggle Cropper</button>
    <img
      v-if="showCropper"
      v-cropper="cropOptions"
      src="https://i.imgur.com/WcvkCxl.png"
      alt="Mustache">
    <input id="zoomable" type="checkbox" v-model="cropOptions.zoomable">
    <label for="zoomable">Zoomable?</label>
    <pre id="output">
x: {{ cropData.x }}
y: {{ cropData.y }}
width: {{ cropData.width }}
height: {{ cropData.height }}
    </pre>
  </div>
</template>

正如您所看到的我选择从指令来发出事件来监听App组件。问问自己为什么选择这么做。我也可以选择在App组件上面创建一个方法,将其绑定到cropper的监听器中。 

反模式。options.crop=this.vm.handleCrop

 

操作其他action

最后使事件从一个组件流向一个指令来改变显示状态。我们要用它来调用cropper的action。让我告诉你我将如何添加一个按钮来控制图片旋转。

编辑App组件的temlpate部分并添加一个控制图片旋转的按钮

<template>
  <div id="app">
    <button id="toggle" @click="toggleCropper">Toggle Cropper</button>
    <img
      v-if="showCropper"
      v-cropper="cropOptions"
      src="https://i.imgur.com/WcvkCxl.png"
      alt="Mustache">
    <input id="zoomable" type="checkbox" v-model="cropOptions.zoomable">
    <label for="zoomable">Zoomable?</label>
    <button @click="$emit('rotate', 90)">Rotate 90°</button>
    <pre id="output">
x: {{ cropData.x }}
y: {{ cropData.y }}
width: {{ cropData.width }}
height: {{ cropData.height }}
    </pre>
  </div>
</template>

编辑src/directives/Cropper.js添加用来监听和解绑的事件

import jQuery from 'jquery'
import 'cropper'

export default {
  deep: true,

  bind () {
    this.vm.$on('rotate', (deg) => {
      jQuery(this.el).cropper('rotate', deg)
    })
  },

  update (options) {
    options.crop = (event) => {
      this.vm.$emit('crop', event)
    }
    // Destroy in case it has been initialized already.
    jQuery(this.el).cropper('destroy')
    // Initializing directly after destroying
    // didn't work. Wrapping it in a setTimeout
    // seems to do the trick.
    setTimeout(() => {
      jQuery(this.el).cropper(options)
    }, 0)
  },

  unbind () {
    jQuery(this.el).cropper('destroy')
    this.vm.$off('rotate')
  }
}

 

摘要

Et voilà you’re done!

这样做的美妙之处在于,指令是用来解耦组件并在多个组件中使用,它会自动初始化并附加到DOM元素中,在DOM被移除之前销毁。