【问题标题】:Is there a nice way to wrap a JQuery based widget into a module that can be easily used in Vue.js?有没有一种很好的方法可以将基于 JQuery 的小部件包装到可以在 Vue.js 中轻松使用的模块中?
【发布时间】:2021-03-06 19:23:54
【问题描述】:

我的一些同事使用 Vue.js 开始了一个相当复杂的 Web 应用程序。他们希望能够使用我过去使用 JQuery 从头开始​​制作的一些小部件,因为重新实现它们需要大量的精力/时间。

我知道如果你小心的话,可以安全地将 JQuery 与 Vue.js 一起使用,但我能够找到的信息似乎被归为相当模糊的博客文章,我的同事已经通知我他们正在努力弄清楚怎么做。所以我正在考虑是否可以找到一种方法,将我的小部件很好地包装到一个可移植的跨框架库中(对于可以在 Vue.js 中使用的初学者)。例如,类似于人们创建提供跨语言 API 的绑定的方式。理想情况下,它应该让人们很容易将它与 Vue.js 一起使用,并且应该消除潜在陷阱的危险。这样做有什么问题吗?是否有任何现有的工作可以利用,或者人们这样做的惯用方式?

对于添加的上下文,目前,小部件有一个接口,其中包括一个构造函数(您可以在其中传递将要附加到的父 DOM 元素的 id)、一个配置函数,并且它还发出几个信号/事件当它发生变化时(尽管可以用定期检查其状态的函数来替换)。

【问题讨论】:

  • @FSDford - 阅读第一句话 - jquery IS javascript :p
  • @mvtc - 您是在使用 jquery 还是自定义 jquery-ui 组件?如果你的组件只使用了 jquery,那么你可以在 vue 中使用它。
  • @StefanWang 它在内部使用 jquery-ui 可拖动,我想我可以只用 jquery 重做那部分。
  • @MVTC,我同意你的看法。在我的 exp 中,使用 jquery 只能在 vue 中正常工作,但如果组件应该与 vue 交互,则不能使用 jquery-ui。

标签: javascript jquery vue.js


【解决方案1】:

就创建可移植和跨框架库而言,我认为 jQuery 只是一个依赖项,它允许您创建某些元素并执行某些任务,您可以根据目标框架的拦截和/或修改要求。因此,您实际上是在围绕它创建一个包装器组件,因为当今排名前三的 JavaScript 框架(React、Vue、Angular)都是基于组件的。

主要区别之一(简单地说)是:反应性系统与 DOM 操作。

现在,谈论将 jQuery 插件移植到 Vue — 我不是这两个库的专家,但我自己来自 jQuery,我想说它可以像在 Vue 上保留对小部件/插件实例的引用一样简单组件内部 data 和/或 props 并使其可选公开相应的方法。方法公开部分是可选的原因与一个库与另一个库的区别是相同的——Vue 在库和框架之间进行扩展时更加通用。

在 jQuery 中,您将创建一个对象的实例并将其传递给它的公共方法使用;而在 Vue 中,您不会显式创建除根实例之外的实例(您 could,但您通常不必这样做)——因为组件本身 (内部构造的)实例。维护其状态和数据是组件的责任;兄弟组件和/或父组件通常不会直接访问它们。

Vue 和 jQuery 的相似之处在于它们都支持状态/数据同步。使用 jQuery,很明显,因为所有引用都在全局范围内;对于 Vue,可以使用 v-model.sync 修饰符(在 Vue 3 中替换为 v-model 上的参数)。此外,他们还有事件订阅方式略有不同。

让我们使用 jQuery Autocomplete widget 并为其添加一些 Vue 支持。我们将专注于 3 件事(选项、事件和方法),并以它们各自的 3 个项目作为示例和比较。我不能在这里涵盖所有内容,但这应该会给你一些基本的想法。

设置:jQuery

​​>

为了符合您的规范,我们假设这个小部件/插件是new-able 类在window 范围内。

在 jQuery 中,您将编写以下内容(在 document 准备好或包装在 IIFE 中,然后关闭 <body> 标记):

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,我将假设 Macross(最新稳定版本:2.6.12,ATTOW)带有 ES 模块;否则,请尝试 ES 模块 compatible build

对于 Vue 中的这个特殊用例,我们希望在 mounted 钩子中实例化这个插件,因为这是我们的目标元素将被创建并且可用于构建的地方。在图表here 中了解有关生命周期挂钩的更多信息。

创建组件: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>

我们还没有到那里!我故意省略了上述示例的"two-way binding" 部分,以便我们现在仔细查看。但是,此步骤是可选的,仅当您需要在组件(父 ↔ 子)之间同步数据/状态时才应执行,例如:您在组件上有一些逻辑,当某些值获取时将输入的边框颜色设置为红色进入。现在,由于您正在修改作为道具绑定到此组件的父状态(例如 invaliderror),因此您需要通过 $emit-ting 新值通知他们其更改。

所以,让我们进行以下更改(在同一个 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);
    }
  }
}

我们基本上是watch-ing“急切地”进行数据更改。如果愿意,您可以使用 $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 更新,它可能正在处理一些直到 next event loop "tick" 才会生效的内容,请参阅 Async Update Queue 了解更多信息。
  • 由于 JavaScript 的限制,有些类型的更改是 Vue 无法检测到的。但是,有 ways to circumvent them 来保持反应性。
  • this 对象为 undefinednull 或在嵌套方法或外部函数中引用时出现意外实例。转到the docs 并搜索“箭头函数”以获取完整说明以及如何避免遇到此问题。

我们已经为自己创建了一个 Vue 移植版本的 jQuery Autocomplete!再说一遍,这些只是帮助您入门的一些基本想法。

现场演示

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 规则将对已访问但未在同一文件中定义的变量发出警告。解决方法见this post

P.P.S.如果您需要将它们作为插件发布,请参阅:Writing a Plugin(不用担心,不需要太多额外的工作)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-26
    • 1970-01-01
    • 2011-02-16
    相关资源
    最近更新 更多