在 Vue.js 中使用作用域插槽抽象功能

Avatar of Mateusz Rybczonek
Mateusz Rybczonek

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

让我们从 Vue.js 插槽 概念的简要介绍开始。当您希望在组件的特定位置注入内容时,插槽非常有用。您可以定义的这些特定位置称为插槽。

例如,您想创建一个以特定方式设置样式的包装器组件,但希望能够传递任何内容在该包装器内部呈现(它可能是字符串、计算值,甚至可能是另一个组件)。

插槽有三种类型

  • 默认/未命名插槽当组件中只有一个插槽时使用。我们通过在模板中添加<slot>来创建它们,我们希望能够在其中注入我们的内容。此<slot>标签将替换为传递给组件模板的任何内容。
  • 命名插槽当组件中有多个插槽并且我们希望在不同位置(插槽)注入不同内容时使用。我们通过添加带有name属性的<slot>来创建这些插槽(例如<slot name="header"></slot>)。然后,当我们渲染组件时,我们通过添加带有插槽名称的slot属性为每个命名插槽提供插槽内容。
<base-layout>
  <template slot="header">
    <h1>My awsome header</h1>
  </template>
  <template slot="footer">
    <p>My awsome footer</p>
  </template>
</base-layout>

通过这样做,组件中的<slot>标签将被替换为传递给组件的内容。

  • 作用域插槽当您希望插槽内的模板访问渲染插槽内容的子组件中的数据时使用。当您需要自由创建使用子组件数据属性的自定义模板时,这尤其有用。
scoped slots diagram

真实案例:创建 Google 地图加载器组件

想象一个配置和准备外部 API 以在另一个组件中使用的组件,但没有与任何特定模板紧密耦合。然后,可以在多个位置重用此类组件,渲染不同的模板,但使用具有特定 API 的相同基本对象。

我创建了一个组件(GoogleMapLoader.vue),它

  1. 初始化 Google Maps API
  2. 创建googlemap对象
  3. 将这些对象公开给使用GoogleMapLoader的父组件

下面是如何实现此目的的示例。我们将在下一节中逐段分析代码并了解实际发生的情况。

首先让我们建立我们的GoogleMapLoader.vue模板

<template>
  <div>
    <div class="google-map" data-google-map></div>
    <template v-if="Boolean(this.google) && Boolean(this.map)">
      <slot :google="google" :map="map" />
    </template>
  </div>
</template>

现在,我们的脚本需要向组件传递一些 props,这使我们能够设置 Google Maps APIMap 对象

import GoogleMapsApiLoader from "google-maps-api-loader";

export default {
  props: {
    mapConfig: Object,
    apiKey: String
  },
  data() {
    return {
      google: null,
      map: null
    };
  },
  async mounted() {
    const googleMapApi = await GoogleMapsApiLoader({
      apiKey: this.apiKey
    });
    this.google = googleMapApi;
    this.initializeMap();
  },
  methods: {
    initializeMap() {
      const mapContainer = this.$el.querySelector("[data-google-map]");
      this.map = new this.google.maps.Map(mapContainer, this.mapConfig);
    }
  }
};

这只是工作示例的一部分。您可以在 此示例 中深入了解。

好的,现在我们已经设置了用例,让我们继续分解该代码以探索它在做什么。

1. 创建一个初始化我们地图的组件

在模板中,我们为地图创建一个容器,该容器将用于挂载从 Google Maps API 中提取的 Map 对象。

// GoogleMapLoader.vue
<template>
  <div>
    <div class="google-map" data-google-map></div>
  </div>
</template>

接下来,我们的脚本需要从父组件接收 props,这将使我们能够设置 Google 地图。这些 props 包括

  • mapConfig:Google 地图配置对象
  • apiKey:Google 地图所需的我们的个人 api 密钥
// GoogleMapLoader.vue
import GoogleMapsApiLoader from "google-maps-api-loader";

export default {
  props: {
    mapConfig: Object,
    apiKey: String
  },

然后,我们将googlemap的初始值设置为null

data() {
  return {
    google: null,
    map: null
  };
},

在挂载钩子中,我们创建了googleMapApimap对象的实例。我们还需要将googlemap的值设置为创建的实例

async mounted() {
  const googleMapApi = await GoogleMapsApiLoader({
    apiKey: this.apiKey
  });
  this.google = googleMapApi;
  this.initializeMap();
},
methods: {
  initializeMap() {
    const mapContainer = this.$el.querySelector("[data-google-map]");
    this.map = new this.google.maps.Map(mapContainer, this.mapConfig);
  }
}
};

到目前为止,一切顺利。完成所有这些操作后,我们可以继续向地图添加其他对象(标记、折线等),并将其用作普通地图组件。

但是,我们希望仅将GoogleMapLoader组件用作准备地图的加载器——我们不想在其上呈现任何内容。

为了实现这一点,我们需要允许将使用我们的GoogleMapLoader的父组件访问在GoogleMapLoader组件内部设置的this.googlethis.map。这就是 作用域插槽 真正闪耀的地方。作用域插槽允许我们将子组件中设置的属性公开给父组件。这听起来可能像是盗梦空间,但请再坚持一分钟,我们将进一步分解它。

2. 创建使用我们的初始化组件的组件

在模板中,我们渲染GoogleMapLoader组件并传递初始化地图所需的 props。

// TravelMap.vue
<template>
  <GoogleMapLoader
    :mapConfig="mapConfig"
    apiKey="yourApiKey"
  />
</template>

我们的脚本标签应如下所示

import GoogleMapLoader from "./GoogleMapLoader";
import { mapSettings } from "@/constants/mapSettings";

export default {
  components: {
    GoogleMapLoader,
  },
  computed: {
    mapConfig() {
      return {
        ...mapSettings,
        center: { lat: 0, lng: 0 }
      };
    },
  }
};

仍然没有作用域插槽,所以让我们添加一个。

3. 通过添加作用域插槽将 google 和 map 属性公开给父组件

最后,我们可以添加一个作用域插槽来完成这项工作,并允许我们在父组件中访问子组件 props。我们通过在子组件中添加<slot>标签并传递我们想要公开的 props(使用v-bind指令或:propName简写)来实现。它与向下传递 props 到子组件没有什么区别,但在<slot>标签中执行此操作将反转数据流的方向。

// GoogleMapLoader.vue
<template>
  <div>
    <div class="google-map" data-google-map></div>
    <template v-if="Boolean(this.google) && Boolean(this.map)">
      <slot
        :google="google"
        :map="map"
      />
    </template>
  </div>
</template>

现在,当我们在子组件中拥有插槽时,我们需要在父组件中接收和使用公开的 props。

4. 使用 slot-scope 属性在父组件中接收公开的 props

要在父组件中接收 props,我们声明一个模板元素并使用slot-scope属性。此属性可以访问承载从子组件公开的所有 props 的对象。我们可以获取整个对象,也可以 解构该对象 并仅获取我们需要的内容。

让我们解构这个东西以获取我们需要的内容。

// TravelMap.vue
<template>
  <GoogleMapLoader
    :mapConfig="mapConfig"
    apiKey="yourApiKey"
  >
    <template slot-scope="{ google, map }">
      {{ map }}
      {{ google }}
    </template>
  </GoogleMapLoader>
</template>

即使googlemap props 不存在于TravelMap作用域中,组件也可以访问它们,并且我们可以在模板中使用它们。

是的,好的,但是我为什么要做这样的事情?所有这些有什么用?

很高兴您提问!作用域插槽允许我们向插槽传递模板而不是呈现的元素。它被称为作用域插槽,因为它即使在父组件作用域中呈现模板,也可以访问某些子组件数据。这使我们能够自由地用来自父组件的自定义内容填充模板。

5. 为标记和折线创建工厂组件

现在,当我们的地图准备就绪时,我们将创建两个工厂组件,这些组件将用于向TravelMap添加元素。

// GoogleMapMarker.vue
import { POINT_MARKER_ICON_CONFIG } from "@/constants/mapSettings";

export default {
  props: {
    google: {
      type: Object,
      required: true
    },
    map: {
      type: Object,
      required: true
    },
    marker: {
      type: Object,
      required: true
    }
  },
  mounted() {
    new this.google.maps.Marker({
      position: this.marker.position,
      marker: this.marker,
      map: this.map,
      icon: POINT_MARKER_ICON_CONFIG
    });
  },
};
// GoogleMapLine.vue
import { LINE_PATH_CONFIG } from "@/constants/mapSettings";

export default {
  props: {
    google: {
      type: Object,
      required: true
    },
    map: {
      type: Object,
      required: true
    },
    path: {
      type: Array,
      required: true
    }
  },
  mounted() {
    new this.google.maps.Polyline({
      path: this.path,
      map: this.map,
      ...LINE_PATH_CONFIG
    });
  },
};

这两个组件都接收我们用来提取所需对象(标记或折线)的google以及提供我们想要放置元素的地图引用的map

每个组件还期望一个额外的 prop 来创建相应的元素。在这种情况下,我们分别有markerpath

在挂载的钩子上,我们创建一个元素(标记/折线),并通过将map属性传递给对象构造函数将其附加到我们的地图上。

还有一步要走……

6. 将元素添加到地图

让我们使用我们的工厂组件将元素添加到我们的地图中。我们必须渲染工厂组件并传递googlemap对象,以便数据流向正确的位置。

我们还需要提供元素本身所需的数据。在我们的例子中,那是带有标记位置的marker对象和带有折线坐标的path对象。

就是这样,将数据点直接集成到模板中

// TravelMap.vue
<template>
  <GoogleMapLoader
    :mapConfig="mapConfig"
    apiKey="yourApiKey"
  >
    <template slot-scope="{ google, map }">
      <GoogleMapMarker
        v-for="marker in markers"
        :key="marker.id"
        :marker="marker"
        :google="google"
        :map="map"
      />
      <GoogleMapLine
        v-for="line in lines"
        :key="line.id"
        :path.sync="line.path"
        :google="google"
        :map="map"
      />
    </template>
  </GoogleMapLoader>
</template>

我们需要在我们的脚本中导入所需的工厂组件,并设置将传递给标记和线条的数据

import { mapSettings } from "@/constants/mapSettings";

export default {
  components: {
    GoogleMapLoader,
    GoogleMapMarker,
    GoogleMapLine
  },
  data() {
    return {
      markers: [
        { id: "a", position: { lat: 3, lng: 101 } },
        { id: "b", position: { lat: 5, lng: 99 } },
        { id: "c", position: { lat: 6, lng: 97 } }
      ],
      lines: [
        { id: "1", path: [{ lat: 3, lng: 101 }, { lat: 5, lng: 99 }] },
        { id: "2", path: [{ lat: 5, lng: 99 }, { lat: 6, lng: 97 }] }
      ]
    };
  },
  computed: {
    mapConfig() {
      return {
        ...mapSettings,
        center: this.mapCenter
      };
    },
    mapCenter() {
      return this.markers[1].position;
    }
  }
};

搞定了!

完成所有这些零零碎碎的部分后,我们现在可以将GoogleMapLoader组件作为所有地图的基础进行重新使用,方法是为每个地图传递不同的模板。想象一下,您需要创建另一个具有不同标记或仅具有标记而没有折线的标记的地图。通过使用作用域插槽模式,这变得非常容易,因为我们现在只需要将不同的内容传递给GoogleMapLoader组件即可。

此模式并非严格与 Google Maps 相关联;它可以与任何库一起使用,以设置基本组件并公开库的 API,然后可以在调用基本组件的组件中使用该 API。

创建更复杂或更强大的解决方案可能会很诱人,但这为我们提供了所需的抽象,并且它成为我们代码库的独立部分。如果我们达到那个点,那么可能值得考虑提取到一个附加组件中。