3

Vue的异构

组件化是至上而下的,一旦一个页面从某个Dom节点开始逐渐话之后,相当于从这个Dom节点开始所有的子辈节点都被组件化框架所接管了,这是因为Vue或React这类组件化框架都有一个重要的特性:那就是 数据=》视图 的一一对应关系,所以在被组件化框架所接管的区域内,所以的Dom操作都应该有组件化框架来完成,如果中间有其他的Dom操作,比如jQuery修改Dom节点元素,会破坏 数据=》视图 这个一一对应关系。

但是事实上,我们有大量的网页系统和框架并不是基于组件化框架所搭建的,所以很多时候我们必须拿出异构的方案。这里的异构指的是 组件化框架和其他框架共存的情况。

我们主要介绍Vue异构方法,实际上Vue提供的computed、watch 、directive等对异构提供了很大的帮助。所以Vue的异构相对其他组件化框架来说更加容易理解和操作。

组件化异构的核心思想就是在不破坏 数据=》视图 的前提下,将非组件化的代码封装成类组件化的代码。

不属于异构的情况

在列举Vue的异构方法之前,我们要分清楚什么情况才称之为异构,并不是引入了任意js库就叫异构。只有引入其他js库,并且我们使用该库去操作Vue所接管的Dom节点时才称之为异构。

一个不是异构的例子

我们从github上下载Vue的源代码,里面有一些实用的例子在example文件夹里,其中有一个《elastic-header》的例子,我们能看到这个例子里引用了dynamic.js这个js库来实现平滑的动画效果。那这里面算不算异构能,需不需要我们采取什么特殊处理呢?会不会破坏 数据=》视图 的映射关系呢?其实是不会的!
Alt text
我们看这里面使用dynamics.js的方法,dynamics被当成一个工具库来使用,并没有直接操作Dom,而是改变Vue里面对象的数值,最终是由Vue去操作Dom,所以并不会破坏 数据=》视图 的映射关系,所以我们并不需要担心引入这类的库与Vue混用会产生问题。

下面我们对Vue的异构方法做一些分类,不同的异构需求关注的重心也不太一样。

通过封装成Vue组件的方式实现异构

在Vue的example库里就有一个Vue异构的例子,在example/select2这个目录下。这个例子是Vue与jQuery的插件select2进行异构的例子 ,并且是通过将select2封装成Vue组件的方式实现异构的。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue.js wrapper component example (jquery plugin: select2)</title>
    <!-- Delete ".min" for console warnings in development -->
    <script src="../../dist/vue.min.js"></script>
    <script src="https://unpkg.com/jquery"></script>
    <script src="https://unpkg.com/select2@4.0.3"></script>
    <link href="https://unpkg.com/select2@4.0.3/dist/css/select2.min.css" rel="stylesheet">
    <style>
      html, body {
        font: 13px/18px sans-serif;
      }
      select {
        min-width: 300px;
      }
    </style>
  </head>
  <body>

    <div id="el">
    </div>

    <!-- using string template here to work around HTML <option> placement restriction -->
    <script type="text/x-template" id="demo-template">
      <div>
        <p>Selected: {{ selected }}</p>
        <select2 :options="options" v-model="selected">
          <option disabled value="0">Select one</option>
        </select2>
      </div>
    </script>

    <script type="text/x-template" id="select2-template">
      <select>
        <slot></slot>
      </select>
    </script>

    <script>
    Vue.component('select2', {
      props: ['options', 'value'],
      template: '#select2-template',
      mounted: function () {
        var vm = this
        $(this.$el)
          .val(this.value)
          // init select2
          .select2({ data: this.options })
          // emit event on change.
          .on('change', function () {
            vm.$emit('input', this.value)
          })
      },
      watch: {
        value: function (value) {
          // update value
          $(this.$el).val(value).trigger('change')
        },
        options: function (options) {
          // update options
          $(this.$el).select2({ data: options })
        }
      },
      destroyed: function () {
        $(this.$el).off().select2('destroy')
      }
    })

    var vm = new Vue({
      el: '#el',
      template: '#demo-template',
      data: {
        selected: 1,
        options: [
          { id: 1, text: 'Hello' },
          { id: 2, text: 'World' }
        ]
      }
    })
    </script>
  </body>
</html>

这里面最关键的代码就是 组件的mounted方法和watch方法,异构最关键的事情有三件

  • Vue要初始化其他库(组件化异构的时候一般在mounted方法里,因为这个时候能拿到Dom元素)
  • 当Vue数据发生变化的时候要调用其他库的方法更新页面
  • 当其他库的数据方法发生变化的时候要对Vue中的数据重新赋值,保证 数据=》视图这个对应关系不变
$(this.$el)
          .val(this.value)
          // 在mounted方法里初始化 select2库
          .select2({ data: this.options })
          // 当其他库的数据方法发生变化的时候要对Vue中的数据重新赋值,保证 数据=》视图这个对应关系不变
          .on('change', function () {
            vm.$emit('input', this.value)
          })
watch: {
        // 当Vue数据发生变化的时候要调用其他库的方法更新页面
        value: function (value) {
          // update value
          $(this.$el).val(value).trigger('change')
        },
        options: function (options) {
          // update options
          $(this.$el).select2({ data: options })
        }
      },

通过directive的方式实现异构

下面我们将官方提供的例子修改一下,改成由 directive的方式来实现异构。因为指令在Vue2里能力已经被极大的削弱,但也依然具备能够实现异构的能力

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Vue.js wrapper component example (jquery plugin: select2)</title>
    <!-- Delete ".min" for console warnings in development -->
    <script src="../../dist/vue.min.js"></script>
    <script src="https://unpkg.com/jquery"></script>
    <script src="https://unpkg.com/select2@4.0.3"></script>
    <link href="https://unpkg.com/select2@4.0.3/dist/css/select2.min.css"
          rel="stylesheet">
    <style>
        html, oubody {
            font: 13px/18px sans-serif;
        }

        select {
            min-width: 300px;
        }
    </style>
</head>
<body>

<div id="el"></div>

<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template"
        id="demo-template">
    <div>
        <p>Selected: {{ selected }}</p>
        // 使用v-select指令
        <select v-select="{options:options,onChange:onChange,value:selected}"></select>
    </div>
</script>

<script>
    Vue.directive('select', {
        // 当绑定元素插入到 DOM 中。
        bind: function (el, value) {
            console.log('bind ' + arguments);
        },
        inserted: function (el, binding) {
            var value = binding.value
            // 聚焦元素
            $(el)
                    .val(value.value)
                    // init select2
                    .select2({data: value.options})
                    // emit event on change.
                    .on('select2:select', function (evt) {
                        var id = evt.params.data.id
                        value.onChange(id)
                    })
            console.log('inserted ' + arguments);
        },
        update: function (el, binding) {
            var val = $(el).val()
            var value = binding.value.value
            if (val !== value) {
                $(el).val(value).trigger('change')
            }
        },
        componentUpdated: function () {
            console.log('componentUpdated ' + arguments);
        },
        unbind: function () {
            console.log('unbind ' + arguments);
        }
    })

    var vm = new Vue({
        el: '#el',
        template: '#demo-template',
        data: {
            selected: 1,
            options: [
                {
                    id: 1,
                    text: 'Hello'
                },
                {
                    id: 2,
                    text: 'World'
                }
            ]
        },
        methods: {
            onChange: function (val) {
                debugger;
                this.selected = val
            }
        }
    })
</script>
</body>
</html>

因为指令无法直接获取到vm,所以我们通过binding里传入onChange方法来更改组件的值。

这里面最关键的代码就是指令的inserted方法和update方法

  • 指令要初始化其他库(在inserted方法里,因为这时候能获取到对应的Dom节点)
  • 当Vue数据发生变化的时候要调用其他库的方法更新页面 (在update方法里调用select2库的更新方法)
  • 当其他库的数据方法发生变化的时候要对Vue中的数据重新赋值,保证 数据=》视图这个对应关系不变(注册事件监听,调用指令的binding.value值里的onChange方法)

循环嵌套Vue组件

想象中很复杂的异构其实只要把数据到视图的逻辑都理清楚,其实实现起来非常容易。

我们在想象一个应用场景,如果这时候需要在select2这个jQuery插件库中再插入 Vue组件应该怎么处理呢?

首先在由其他库中生成的Dom节点中插入Vue组件,只能通过 new Vue()方法去重新初始化Vue组件。但是需要注意一点的是组件是一个可以多实例的概念,所以我们不能简单的给嵌套Vue组件一个Id,因为当多实例的情况下通过这个Id能取到不只一个Dom节点,所以我们最佳方式是通过Dom节点树向下寻找,直到找到具体拿来初始化的对应元素,可以通过 jQuery 的 find方法,也能通过生成 uuid的方式来生成唯一的Id,通过这些方式来保证组件在多实例的情况下运行起来也不会出现问题。


kukuv2
1.1k 声望54 粉丝