Nuxt + Algolia - Google 地图不更新即时搜索的新结果
Nuxt + Algolia - Google Maps not updating the new results from instantsearch
我正在使用 nuxt 开发即时搜索。 Googlemaps 在页面加载时收到第一组结果并毫无问题地呈现标记 - 但是在搜索新结果时不会在地图上更新?有没有一种方法可以将新的结果集推送到地图以呈现标记?
我假设这将是 beforeMount
挂钩并将结果传递给 updateMap
?
pages/index.vue
<template>
<div class="root">
<div class="container">
<ais-instant-search-ssr
:search-client="searchClient"
index-name="dive_buddy"
>
<ais-configure :hits-per-page.camel="7">
<ais-search-box placeholder="Search here…" class="searchbox" />
<ais-stats />
<div class="h-screen">
<ais-hits>
<template #default="{ items }">
<div class="grid grid-cols-2 gap-2">
<div v-if="items.length">
<div
v-for="item in items"
:key="item.objectID"
class="my-2 p-2 border"
>
<SiteRow
:site="item"
@mouseover.native="highlightMarker(item.objectID, true)"
@mouseout.native="highlightMarker(item.objectID, false)"
/>
</div>
</div>
<div v-else>No results found</div>
<Map
:items="items"
:lat="items[0].lat"
:lng="items[0].lng"
/>
</div>
</template>
</ais-hits>
<ais-pagination />
</div>
</ais-configure>
</ais-instant-search-ssr>
</div>
</div>
</template>
<script>
import {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'
function renderToString(app) {
return new Promise((resolve, reject) => {
_renderToString(app, (err, res) => {
if (err) reject(err)
resolve(res)
})
})
}
const searchClient = algoliasearch(
'xxx', // AppID
'xxxxxxxx' // APIKey
)
export default {
components: {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
},
mixins: [
createServerRootMixin({
searchClient,
indexName: 'dive_buddy',
}),
],
data() {
return {
searchClient,
}
},
head() {
return {
link: [
{
rel: 'stylesheet',
href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css',
},
],
}
},
serverPrefetch() {
return this.instantsearch
.findResultsState({
component: this,
renderToString,
})
.then((algoliaState) => {
this.$ssrContext.nuxt.algoliaState = algoliaState
})
},
beforeMount() {
const results =
(this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
window.__NUXT__.algoliaState
this.instantsearch.hydrate(results)
// Remove the SSR state so it can't be applied again by mistake
delete this.$nuxt.context.nuxtState.algoliaState
delete window.__NUXT__.algoliaState
},
methods: {
highlightMarker(id, isHighlighted) {
document
.getElementsByClassName(`id-${id}`)[0]
?.classList?.toggle('marker-highlight', isHighlighted)
},
},
}
</script>
components/Map.vue
<template>
<div ref="map" class="h-screen"></div>
</template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
lat: {
type: Number,
required: true,
},
lng: {
type: Number,
required: true,
},
},
mounted() {
this.updateMap()
},
methods: {
// run showMap function in maps.client.js
async updateMap() {
await this.$maps.showMap(
this.$refs.map,
this.lat,
this.lng,
this.getMarkers()
)
},
// pass through markers to googlemaps
getMarkers() {
return this.items.map((item) => {
return {
...item,
}
})
},
},
}
</script>
plugins/maps.client.js
export default function(context, inject){
let isLoaded = false
let waiting = []
addScript()
inject('maps', {
showMap,
})
function addScript(){
const script = document.createElement('script')
script.src = "https://maps.googleapis.com/maps/api/js?key=xxxx&libraries=places&callback=initGoogleMaps"
script.async = true
window.initGoogleMaps = initGoogleMaps
document.head.appendChild(script)
}
function initGoogleMaps(){
isLoaded = true
waiting.forEach((item) => {
if(typeof item.fn === 'function'){
item.fn(...item.arguments)
}
})
waiting = []
}
function showMap(canvas, lat, lng, markers){
if(!isLoaded){
waiting.push({
fn: showMap,
arguments,
})
return
}
const mapOptions = {
zoom: 18,
center: new window.google.maps.LatLng(lat, lng),
disableDefaultUI: true,
zoomControl: true,
styles:[{
featureType: 'poi.business',
elementType: 'labels.icon',
stylers:[{ visibility: 'off' }]
}]
}
const map = new window.google.maps.Map(canvas, mapOptions)
if(!markers){
const position = new window.google.maps.LatLng(lat, lng)
const marker = new window.google.maps.Marker({
position,
clickable: false,
})
marker.setMap(map)
return
}
const bounds = new window.google.maps.LatLngBounds()
let index = 1
markers.forEach((item) => {
const position = new window.google.maps.LatLng(item.lat, item.lng)
const marker = new window.google.maps.Marker({
position,
label: {
text: `${index}`,
color: 'black',
className: `marker id-${item.objectID}`
},
icon: 'https://maps.gstatic.com/mapfiles/transparent.png',
clickable: false,
})
marker.setMap(map)
bounds.extend(position)
index++
})
map.fitBounds(bounds)
}
}
编辑
根据@Bryan 的建议 - 我之前曾尝试使用 vue-googlemaps
插件遵循 algolia 的文档,但这在 Nuxt 上不起作用,因为地图无法加载以及这些错误 -
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'g')
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'h')
虽然这不是上述问题的解决方案 - 我找到了使用不同库的解决方法:vue2-google-maps
因为它适用于 Nuxt (see docs)。
pages/index.vue
<template>
<div class="root">
<div class="container">
<ais-instant-search-ssr
:search-client="searchClient"
index-name="dive_centres"
>
<ais-configure :hits-per-page.camel="7">
<ais-search-box placeholder="Search here…" class="searchbox" />
<ais-stats />
<div class="h-screen">
<ais-hits>
<template #default="{ items }">
<div class="grid grid-cols-2 gap-2">
<div v-if="items.length">
<div
v-for="item in items"
:key="item.objectID"
class="my-2 p-2 border"
>
<SiteRow
:site="item"
@mouseover.native="highlightMarker(item.objectID, true)"
@mouseout.native="highlightMarker(item.objectID, false)"
/>
</div>
</div>
<div v-else>No results found</div>
<Map />
</div>
</template>
</ais-hits>
<ais-pagination />
</div>
</ais-configure>
</ais-instant-search-ssr>
</div>
</div>
</template>
<script>
import {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'
function renderToString(app) {
return new Promise((resolve, reject) => {
_renderToString(app, (err, res) => {
if (err) reject(err)
resolve(res)
})
})
}
const searchClient = algoliasearch(
'xxx', // AppID
'xxxxxxxx' // APIKey
)
export default {
components: {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
},
mixins: [
createServerRootMixin({
searchClient,
indexName: 'dive_sites',
}),
],
head() {
return {
link: [
{
rel: 'stylesheet',
href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css',
},
],
}
},
serverPrefetch() {
return this.instantsearch
.findResultsState({
component: this,
renderToString,
})
.then((algoliaState) => {
this.$ssrContext.nuxt.algoliaState = algoliaState
})
},
beforeMount() {
const results =
(this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
window.__NUXT__.algoliaState
this.instantsearch.hydrate(results)
// Remove the SSR state so it can't be applied again by mistake
delete this.$nuxt.context.nuxtState.algoliaState
delete window.__NUXT__.algoliaState
},
methods: {
highlightMarker(id, isHighlighted) {
console.log(id)
document
.getElementsByClassName(`id-${id}`)[0]
?.classList?.toggle('marker-highlight', isHighlighted)
},
},
}
</script>
components/Map.vue
<template>
<div v-if="state" class="ais-GeoSearch">
<GmapMap
:center="center"
:zoom="zoom"
map-type-id="terrain"
class="h-full w-full"
:options="options"
>
<GmapMarker
v-for="item in state.items"
:key="item.objectID"
:position="item._geoloc"
:clickable="false"
:draggable="true"
:icon="{ url: require('~/static/images/dive-site-small.svg') }"
/>
</GmapMap>
</div>
</template>
<script>
import { createWidgetMixin } from 'vue-instantsearch'
import { connectGeoSearch } from 'instantsearch.js/es/connectors'
export default {
mixins: [createWidgetMixin({ connector: connectGeoSearch })],
data() {
return {
zoom: 4,
options: {
disableDefaultUI: true,
},
}
},
computed: {
center() {
return this.state.items.length !== 0
? this.state.items[0]._geoloc
: { lat: 48.864716, lng: 2.349014 }
},
},
}
</script>
我正在使用 nuxt 开发即时搜索。 Googlemaps 在页面加载时收到第一组结果并毫无问题地呈现标记 - 但是在搜索新结果时不会在地图上更新?有没有一种方法可以将新的结果集推送到地图以呈现标记?
我假设这将是 beforeMount
挂钩并将结果传递给 updateMap
?
pages/index.vue
<template>
<div class="root">
<div class="container">
<ais-instant-search-ssr
:search-client="searchClient"
index-name="dive_buddy"
>
<ais-configure :hits-per-page.camel="7">
<ais-search-box placeholder="Search here…" class="searchbox" />
<ais-stats />
<div class="h-screen">
<ais-hits>
<template #default="{ items }">
<div class="grid grid-cols-2 gap-2">
<div v-if="items.length">
<div
v-for="item in items"
:key="item.objectID"
class="my-2 p-2 border"
>
<SiteRow
:site="item"
@mouseover.native="highlightMarker(item.objectID, true)"
@mouseout.native="highlightMarker(item.objectID, false)"
/>
</div>
</div>
<div v-else>No results found</div>
<Map
:items="items"
:lat="items[0].lat"
:lng="items[0].lng"
/>
</div>
</template>
</ais-hits>
<ais-pagination />
</div>
</ais-configure>
</ais-instant-search-ssr>
</div>
</div>
</template>
<script>
import {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'
function renderToString(app) {
return new Promise((resolve, reject) => {
_renderToString(app, (err, res) => {
if (err) reject(err)
resolve(res)
})
})
}
const searchClient = algoliasearch(
'xxx', // AppID
'xxxxxxxx' // APIKey
)
export default {
components: {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
},
mixins: [
createServerRootMixin({
searchClient,
indexName: 'dive_buddy',
}),
],
data() {
return {
searchClient,
}
},
head() {
return {
link: [
{
rel: 'stylesheet',
href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css',
},
],
}
},
serverPrefetch() {
return this.instantsearch
.findResultsState({
component: this,
renderToString,
})
.then((algoliaState) => {
this.$ssrContext.nuxt.algoliaState = algoliaState
})
},
beforeMount() {
const results =
(this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
window.__NUXT__.algoliaState
this.instantsearch.hydrate(results)
// Remove the SSR state so it can't be applied again by mistake
delete this.$nuxt.context.nuxtState.algoliaState
delete window.__NUXT__.algoliaState
},
methods: {
highlightMarker(id, isHighlighted) {
document
.getElementsByClassName(`id-${id}`)[0]
?.classList?.toggle('marker-highlight', isHighlighted)
},
},
}
</script>
components/Map.vue
<template>
<div ref="map" class="h-screen"></div>
</template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
lat: {
type: Number,
required: true,
},
lng: {
type: Number,
required: true,
},
},
mounted() {
this.updateMap()
},
methods: {
// run showMap function in maps.client.js
async updateMap() {
await this.$maps.showMap(
this.$refs.map,
this.lat,
this.lng,
this.getMarkers()
)
},
// pass through markers to googlemaps
getMarkers() {
return this.items.map((item) => {
return {
...item,
}
})
},
},
}
</script>
plugins/maps.client.js
export default function(context, inject){
let isLoaded = false
let waiting = []
addScript()
inject('maps', {
showMap,
})
function addScript(){
const script = document.createElement('script')
script.src = "https://maps.googleapis.com/maps/api/js?key=xxxx&libraries=places&callback=initGoogleMaps"
script.async = true
window.initGoogleMaps = initGoogleMaps
document.head.appendChild(script)
}
function initGoogleMaps(){
isLoaded = true
waiting.forEach((item) => {
if(typeof item.fn === 'function'){
item.fn(...item.arguments)
}
})
waiting = []
}
function showMap(canvas, lat, lng, markers){
if(!isLoaded){
waiting.push({
fn: showMap,
arguments,
})
return
}
const mapOptions = {
zoom: 18,
center: new window.google.maps.LatLng(lat, lng),
disableDefaultUI: true,
zoomControl: true,
styles:[{
featureType: 'poi.business',
elementType: 'labels.icon',
stylers:[{ visibility: 'off' }]
}]
}
const map = new window.google.maps.Map(canvas, mapOptions)
if(!markers){
const position = new window.google.maps.LatLng(lat, lng)
const marker = new window.google.maps.Marker({
position,
clickable: false,
})
marker.setMap(map)
return
}
const bounds = new window.google.maps.LatLngBounds()
let index = 1
markers.forEach((item) => {
const position = new window.google.maps.LatLng(item.lat, item.lng)
const marker = new window.google.maps.Marker({
position,
label: {
text: `${index}`,
color: 'black',
className: `marker id-${item.objectID}`
},
icon: 'https://maps.gstatic.com/mapfiles/transparent.png',
clickable: false,
})
marker.setMap(map)
bounds.extend(position)
index++
})
map.fitBounds(bounds)
}
}
编辑
根据@Bryan 的建议 - 我之前曾尝试使用 vue-googlemaps
插件遵循 algolia 的文档,但这在 Nuxt 上不起作用,因为地图无法加载以及这些错误 -
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'g')
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'h')
虽然这不是上述问题的解决方案 - 我找到了使用不同库的解决方法:vue2-google-maps
因为它适用于 Nuxt (see docs)。
pages/index.vue
<template>
<div class="root">
<div class="container">
<ais-instant-search-ssr
:search-client="searchClient"
index-name="dive_centres"
>
<ais-configure :hits-per-page.camel="7">
<ais-search-box placeholder="Search here…" class="searchbox" />
<ais-stats />
<div class="h-screen">
<ais-hits>
<template #default="{ items }">
<div class="grid grid-cols-2 gap-2">
<div v-if="items.length">
<div
v-for="item in items"
:key="item.objectID"
class="my-2 p-2 border"
>
<SiteRow
:site="item"
@mouseover.native="highlightMarker(item.objectID, true)"
@mouseout.native="highlightMarker(item.objectID, false)"
/>
</div>
</div>
<div v-else>No results found</div>
<Map />
</div>
</template>
</ais-hits>
<ais-pagination />
</div>
</ais-configure>
</ais-instant-search-ssr>
</div>
</div>
</template>
<script>
import {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
createServerRootMixin,
} from 'vue-instantsearch'
import algoliasearch from 'algoliasearch/lite'
import _renderToString from 'vue-server-renderer/basic'
function renderToString(app) {
return new Promise((resolve, reject) => {
_renderToString(app, (err, res) => {
if (err) reject(err)
resolve(res)
})
})
}
const searchClient = algoliasearch(
'xxx', // AppID
'xxxxxxxx' // APIKey
)
export default {
components: {
AisInstantSearchSsr,
AisConfigure,
AisHits,
AisSearchBox,
AisStats,
AisPagination,
},
mixins: [
createServerRootMixin({
searchClient,
indexName: 'dive_sites',
}),
],
head() {
return {
link: [
{
rel: 'stylesheet',
href: 'https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css',
},
],
}
},
serverPrefetch() {
return this.instantsearch
.findResultsState({
component: this,
renderToString,
})
.then((algoliaState) => {
this.$ssrContext.nuxt.algoliaState = algoliaState
})
},
beforeMount() {
const results =
(this.$nuxt.context && this.$nuxt.context.nuxtState.algoliaState) ||
window.__NUXT__.algoliaState
this.instantsearch.hydrate(results)
// Remove the SSR state so it can't be applied again by mistake
delete this.$nuxt.context.nuxtState.algoliaState
delete window.__NUXT__.algoliaState
},
methods: {
highlightMarker(id, isHighlighted) {
console.log(id)
document
.getElementsByClassName(`id-${id}`)[0]
?.classList?.toggle('marker-highlight', isHighlighted)
},
},
}
</script>
components/Map.vue
<template>
<div v-if="state" class="ais-GeoSearch">
<GmapMap
:center="center"
:zoom="zoom"
map-type-id="terrain"
class="h-full w-full"
:options="options"
>
<GmapMarker
v-for="item in state.items"
:key="item.objectID"
:position="item._geoloc"
:clickable="false"
:draggable="true"
:icon="{ url: require('~/static/images/dive-site-small.svg') }"
/>
</GmapMap>
</div>
</template>
<script>
import { createWidgetMixin } from 'vue-instantsearch'
import { connectGeoSearch } from 'instantsearch.js/es/connectors'
export default {
mixins: [createWidgetMixin({ connector: connectGeoSearch })],
data() {
return {
zoom: 4,
options: {
disableDefaultUI: true,
},
}
},
computed: {
center() {
return this.state.items.length !== 0
? this.state.items[0]._geoloc
: { lat: 48.864716, lng: 2.349014 }
},
},
}
</script>