我的一些同事已经使用Vue.js开始了一个相当复杂的web应用程序。他们希望能够使用我过去使用JQuery从头开始制作的一些小部件,因为重新实现它们需要大量的精力/时间。
我知道,如果你小心的话,可以安全地将JQuery与Vue.js一起使用,但我所能找到的信息似乎只是相当模糊的博客文章,我的同事已经通知我,他们正在努力想办法做到这一点。因此,我正在考虑是否可以找到一种方法,将我的小部件很好地封装到一个可移植的跨框架库中(对于可以在Vue.js中使用的初学者来说)。例如,类似于人们如何创建跨语言API的绑定。理想情况下,它应该让人们很容易将其与Vue.js一起使用,并且应该消除潜在陷阱的危险。这样做有什么问题吗?有没有任何现有的工作可以利用,或者人们这样做的惯用方式?
对于添加的上下文,目前,小部件有一个接口,其中包括一个构造函数(在该构造函数中,您可以传递它将被附加到的父DOM元素的id)、一个配置函数,它还可以在更改时发出几个信号/事件(尽管这些信号/事件可以被定期检查其状态的函数所取代)。
就创建一个可移植的跨框架库而言,我认为jQuery只是一个依赖项,它允许您创建某些元素并执行某些任务,您可以根据目标框架的要求拦截和/或修改这些元素。因此,您实际上是在围绕它创建一个包装器组件,因为目前排名前三的JavaScript框架(React、Vue、Angular)都是基于组件的。
其中一个关键区别(简单地说)是:反应性系统与DOM操作。
现在,谈到将jQuery插件移植到Vue——我不是这两个库的专家,但我自己来自jQuery,我想说,这可能很容易,就像在Vue组件内部data
和/或props
上保留对小部件/插件实例的引用,并使其可选地公开相应的方法一样。方法公开部分是可选的原因与区分一个库和另一个库的原因相同——Vue在库和框架之间进行扩展时更通用。
在jQuery中,您将创建一个对象的实例,并将其传递给公共方法使用;而在Vue中,除了根实例之外,您不会显式创建实例(您可以,但通常不必)——因为组件本身是(内部构建的)实例。组件有责任维护其状态和数据;兄弟组件和/或父组件通常没有直接访问权限。
Vue和jQuery的相似之处在于,它们都支持状态/数据同步。使用jQuery,这是显而易见的,因为所有引用都在全局范围内;对于Vue,可以使用v-model
或.sync
修饰符(用Vue 3中v-model
上的自变量替换)。此外,它们还提供了方法略有不同的事件订阅。
让我们以jQuery Autocomplete小部件为例,并为其添加一些Vue支持。我们将关注3件事(选项、事件和方法),并以它们各自的3项为例进行比较。我不能在这里涵盖所有内容,但这应该会给你一些基本的想法。
设置:jQuery
为了遵守您所讨论的规范,让我们假设这个小部件/插件是window
作用域中的一个new
类。
在jQuery中,您将编写以下内容(在document
就绪或在关闭<body>
标记之前包装在IIFE中):
var autocomplete = new Autocomplete({
source: [
'vue',
'react',
'angular',
'jquery'
],
appendTo: '#autocomplete-container',
disabled: false,
change: function(event, ui) { },
focus: function(event, ui) { },
select: function(event, ui) { }
});
// And then some other place needing manual triggers on this instance
autocomplete.close();
var isDisabled = autocomplete.option('disabled');
autocomplete.search('ue'); // Matches 'vue' and 'jquery' ;)
在父作用域中的某个位置预定义或动态创建目标元素:
<input type="search" class="my-autocomplete" />
移植到Vue
由于您没有提到Vue的任何特定版本,我将假设宏(最新稳定版本:2.6.12,ATTOW)带有ES模块;否则,请尝试ES模块兼容版本。
对于Vue中的这个特定用例,我们希望在mounted
钩子中实例化这个插件,因为这是我们的目标元素将被创建并可用于实际构建的地方。在此处的图表中了解有关生命周期挂钩的更多信息。
创建组件:Autocomplete.vue
<template>
<!--
Notice how this `input` element is added right here rather than we requiring
the parent component to add one, because it's now part of the component. :)
-->
<input type="search" class="my-autocomplete" />
</template>
<script>
export default {
// Basically, this is where you define IMMUTABLE "options", so to speak.
props: {
source: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
}
},
// And this is where to prepare and/or specify the internal options of a component.
data: () => ({
instance: null
}),
mounted() {
// `this` here refers to the local Vue instance
this.instance = new Autocomplete({
source: this.source,
disabled: this.disabled,
appendTo: this.$el // Refers to the `input` element on the template,
change: (event, ui) => {
// You can optionally pass anything in the second argument
this.$emit('change', this.instance);
},
focus: (event, ui) => {
this.$emit('focus', this.instance, event);
},
select: (event, ui) => {
this.$emit('select', this, event, ui);
}
});
},
methods: {
close() {
this.instance.autocomplete('close');
},
getOption(optionName) {
return this.instance.autocomplete('option', optionName);
},
search(keyword) {
this.instance.autocomplete('search', keyword);
}
}
}
</script>
使用组件:Parent.vue
(或其他)
<template>
<div class="parent">
<autocomplete
ref="autocomplete"
:source="items"
:disabled="disabled"
@change="onChange"
@focus="onFocus"
@select="onSelect">
</autocomplete>
</div>
</template>
<script>
import Autocomplete from 'path/to/your-components/Autocomplete.vue';
export default {
data: () => ({
items: [
'vue',
'react',
'angular',
'jquery'
],
disabled: false
}),
methods: {
onChange() {
},
onFocus() {
},
onSelect() {
}
},
mounted() {
// Manually invoke a public method as soon as the component is ready
this.$refs.autocomplete.search('ue');
},
components: {
Autocomplete
}
}
</script>
我们还没有到那里!我故意省略了";双向绑定";上面例子的一部分让我们现在仔细看看。但是,此步骤是可选的,只有在需要同步组件(父级↔child),例如:您在组件上有一些逻辑,可以在输入某些值时将输入的边框颜色设置为红色。现在,由于您正在修改作为道具绑定到此组件的父状态(比如invalid
或error
),因此需要通过$emit
来通知他们其更改。
因此,让我们进行以下更改(在相同的Autocomplete.vue
组件上,为了简洁起见,省略了所有其他内容):
{
model: {
prop: 'source',
event: 'modified' // Custom event name
},
async created() {
// An example of fetching remote data and updating the `source` property.
const newSource = await axios.post('api/fetch-data').then(res => res.data);
// Once fetched, update the jQuery-wrapped autocomplete
this.instance.autocomplete('option', 'source', newSource);
// and tell the parent that it has changed
this.$emit('modified', newSource);
},
watch: {
source(newData, oldData) {
this.instance.autocomplete('option', 'source', newData);
}
}
}
我们基本上是CCD_;急切地";用于数据更改。如果愿意,您可以使用$watch
实例方法来惰性地执行此操作。
父方所需的更改:
<template>
<div class="parent">
<autocomplete
ref="autocomplete"
v-model="items"
:disabled="disabled"
@change="onChange"
@focus="onFocus"
@select="onSelect">
</autocomplete>
</div>
</template>
这将实现上述双向绑定。你可以对你需要的其他道具做同样的事情;反应性";,就像本例中的disabled
道具一样——只是这次您要使用.sync
修饰符;因为在Vue 2中,不支持多个v-model
。(如果你还没有走得太远,我建议你一直选择Vue 3)。
最后,您可能需要注意一些注意事项和常见问题:
- 由于Vue异步执行DOM更新,它可能正在处理一些直到下一个事件循环才会生效的事情;勾号";,阅读有关异步更新队列的详细信息
- 由于JavaScript的限制,有些类型的更改Vue无法检测。然而,有一些方法可以绕过它们来保持反应性
this
对象是undefined
、null
,或者在嵌套方法或外部函数中引用时处于意外实例中。转到文档并搜索";箭头函数";以获得完整的解释以及如何避免遇到此问题
我们已经为自己创建了jQuery Autocomplete的Vue移植版本!再说一遍,这些只是让你开始的一些基本想法。
现场演示
const Autocomplete = Vue.extend({
template: `
<div class="autocomplete-wrapper">
<p>{{label}}</p>
<input type="search" class="my-autocomplete" />
</div>
`,
props: {
source: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
label: {
type: String
}
},
model: {
prop: 'source',
event: 'modified'
},
data: () => ({
instance: null
}),
mounted() {
const el = this.$el.querySelector('input.my-autocomplete');
this.instance = $(el).autocomplete({
source: this.source,
disabled: this.disabled,
change: (event, ui) => {
// You can optionally pass anything in the second argument
this.$emit('change', this.instance);
},
focus: (event, ui) => {
this.$emit('focus', this.instance, event);
},
select: (event, ui) => {
this.$emit('select', this, event, ui);
}
});
},
methods: {
close() {
this.instance.autocomplete('close');
},
getOption(optionName) {
return this.instance.autocomplete('option', optionName);
},
search(keyword) {
this.instance.autocomplete('search', keyword);
},
disable(toState) {
this.instance.autocomplete('option', 'disabled', toState);
}
},
watch: {
source(newData, oldData) {
this.instance.autocomplete('option', 'source', newData);
},
disabled(newState, oldState) {
this.disable(newState);
}
}
});
new Vue({
el: '#app',
data: () => ({
items: [
'vue',
'react',
'angular',
'jquery'
],
disabled: false
}),
computed: {
computedItems: {
get() {
return this.items.join(', ');
},
set(val) {
this.items = val.split(', ')
}
}
},
methods: {
onChange() {
// Do something
},
onFocus() {},
onSelect(instance, event, ui) {
console.log(`You selected: "${ui.item.value}"`);
}
},
components: {
Autocomplete
}
})
#app {
display: flex;
justify-content: space-between;
}
#app > div {
flex: 0 0 50%;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" />
<link rel="stylesheet" href="/resources/demos/style.css" />
<script src="https://vuejs.org/js/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div id="app">
<autocomplete
v-model="items"
:disabled="disabled"
label='Type something (e.g. "ue")'
@change="onChange"
@focus="onFocus"
@select="onSelect">
</autocomplete>
<div>
<p>Edit this comma-separated list of items and see them reflected on the component</p>
<textarea
v-model.lazy="computedItems"
cols="30"
rows="3">
</textarea>
</div>
</div>
附言:如果这些小部件实际上在全局window
范围内并且您正在使用ESLint,则需要确保它们被指定为全局变量;否则,no-undef
规则将对访问但未在同一文件中定义的变量发出警告。有关解决方案,请参阅本文。
附言:如果你需要将它们作为插件发布,请参阅:编写插件(别担心,不需要太多额外的工作)。