使反应叶与nextjs一起使用
#javascript #react #nextjs #leaflet

我遇到了一些问题,该问题在PlaceKit上为管理面板实施了NextJ。因此,让我们将我的发现收集到一篇文章中,希望它可以节省一些时间。

由于NextJS具有SSR(服务器端渲染)层,因此导入第三方前端库有时会导致头痛。

大多数情况下,您只需要用koude0包装前端组件即可使其懒惰,使SSR Pass只需忽略它:

// MyComponent.jsx
import frontLib from '<your-front-end-library>';
const MyComponent = (props) => {
  // do something with frontLib
};
export default MyLazyComponent;


// MyPage.jsx
import dynamic from 'next/dynamic';

const MyComponent = dynamic(
  () => import('./MyComponent'),
  {
    ssr: false,
    loading: () => (<div>loading...</div>),
  }
);

const MyPage = (props) => (
  <MyComponent />
);

但对于React Leaflet,您可能需要做更多的努力。

通过ref

如果您只是将ref分配给负载的<MapContainer>,您将获得来自dynamic的无法使用的代理参考:

// Map.jsx
import dynamic from 'next/dynamic';
import { useEffect, useRef } from 'react';

const MapContainer = dynamic(
  () => import('react-leaflet').then((m) => m.MapContainer),
  { ssr: false }
);

const Map = (props) => {
  const mapRef = useRef(null);
  useEffect(
   () => console.log(mapRef.current), // { retry: fn, ... }
   [mapRef.current]
  );
  return (
    <MapContainer ref={mapRef} ?>
  );
};

export default Map;

诀窍有点笨重,但是您必须将其包装在另一个组件下,然后将ref作为标准属性(在此处mapRef)转发,然后您懒惰地加载了那个:

// MapLazyComponents.jsx
import {
  MapContainer as LMapContainer,
} from 'react-leaflet';

export const MapContainer = ({ mapRef, ...props }) => (
  <LMapContainer {...props} ref={mapRef} />
);

// Map.jsx
import dynamic from 'next/dynamic';
import { forwardRef, useEffect, useRef } from 'react';

const LazyMapContainer = dynamic(
  () => import('./MapLazyComponents').then((m) => m.MapContainer),
  { ssr: false }
);

const MapContainer = forwardRef((props, ref) => (
  <LazyMapContainer {...props} mapRef={ref} />
));

const Map = (props) => {
  const mapRef = useRef(null);
  useEffect(
   () => console.log(mapRef.current), // this works!
   [mapRef.current]
  );
  return (
    <MapContainer ref={mapRef} />
  );
};

export default Map;

组织组件

因为我们将在以下示例中准备一些其他React传单组件,所以让我们将其重新组织为3个文件:

  • Map.jsx:您的最终组件或页面显示地图。
  • MapComponents.jsx:将lazy-Load React Flaylet的组件。这些将准备好进口。
  • MapLazyComponents.jsx:that that that ref或使用前端特定功能的包装器,由MapComponents.jsx懒惰。

我们还添加<TileLayer><ZoomControl>,因为除了加载dynamic之外,我们不需要任何特定的更改。

所以在这一点上您得到了:

// MapLazyComponents.jsx
import {
  MapContainer as LMapContainer,
} from 'react-leaflet';

export const MapContainer = ({ mapRef, ...props }) => (
  <LMapContainer {...props} ref={mapRef} />
);


// MapComponents.jsx
import dynamic from 'next/dynamic';
import { forwardRef } from 'react';

export const LazyMapContainer = dynamic(
  () => import('./MapLazyComponents').then((m) => m.MapContainer),
  {
    ssr: false,
    loading: () => (<div style={{ height: '400px' }} />),
  }
);
export const MapContainer = forwardRef((props, ref) => (
  <LazyMapContainer {...props} mapRef={ref} />
));

// direct import from 'react-leaflet'
export const TileLayer = dynamic(
  () => import('react-leaflet').then((m) => m.TileLayer),
  { ssr: false }
);
export const ZoomControl = dynamic(
  () => import('react-leaflet').then((m) => m.ZoomControl),
  { ssr: false }
);


// Map.jsx
import { useEffect, useRef } from 'react';

// import and use components as usual
import { MapContainer, TileLayer, ZoomControl } from './MapComponents.jsx';

const Map = (props) => {
  const mapRef = useRef(null);
  return (
    <MapContainer
      ref={mapRef}
      touchZoom={false}
      zoomControl={false}
      style={{ height: '400px', zIndex: '0!important' }}
    >
      <TileLayer url="..." attribution="..." style={{ zIndex: '0!important' }} />
      <ZoomControl position="topright" style={{ zIndex: '10!important' }} />
    </MapContainer>
  );
};

export default Map;

使用自定义标记图标

好吧,现在我们开始拥有地图,让我们添加一个标记。但是大多数时候,您想使用自定义图标。

自定义标记图标需要使用leaflet本身的L.Icon(),这是库德17中的库实例化内容,因此在下一步导入时会破坏​​SSR。但是,它不能加载dynamic(),甚至可以用React.lazy()加载。

所以,让我们将我们的<Marker>组件包装在MapLazyComponents.jsx中,因为它将取决于前端独家功能:

// MapLazyComponents.jsx
import { useEffect, useState } from 'react';
import {
  MapContainer as LMapContainer,
  Marker as LMarker,
} from 'react-leaflet';

// ...
export const Marker = ({ markerRef, icon: iconProps, ...props }) => {
  const [icon, setIcon] = useState();

  useEffect(
    () => {
      // loading 'leaflet' dynamically when the component mounts
      const loadIcon = async () => {
        const L = await import('leaflet');
        setIcon(L.icon(iconProps));
      }
      loadIcon();
    },
    [iconProps]
  );

  // waiting for icon to be loaded before rendering
  return (!!iconProps && !icon) ? null : (
    <LMarker
      {...props}
      icon={icon}
      ref={markerRef}
    />
  );
};

// MapComponents.jsx
// ...
const LazyMarker = dynamic(() => import('./MapLazyComponents').then((m) => m.Marker), { ssr: false });
export const Marker = forwardRef((props, ref) => (
  <LazyMarker {...props} forwardedRef={ref} />
));

// Map.jsx
// ...
import { MapContainer, TileLayer, ZoomControl, Marker } from './MapComponents.jsx';

import CustomIcon from '../public/custom-icon.svg';

const Map = (props) => {
  const mapRef = useRef(null);
  const markerRef = useRef(null);
  return (
    <MapContainer
      ref={mapRef}
      touchZoom={false}
      zoomControl={false}
      style={{ height: '400px', zIndex: '0!important' }}
    >
      <TileLayer url="..." attribution="..." style={{ zIndex: '0!important' }} />
      <ZoomControl position="topright" style={{ zIndex: '10!important' }} />
      <Marker
        ref={markerRef}
        icon={{
          iconUrl: CustomIcon.src,
          iconAnchor: [16,32],
          iconSize: [32,32]
        }}
        style={{ zIndex: '1!important' }}
      />
    </MapContainer>
  );
};
//...

处理地图事件

对于标记事件,您已经可以通过eventHandlers属性,并且可以使用。但是要处理地图事件,无法在<MapContainer>组件上完成,您需要使用儿童组件中的react feaflet的useMapEvents()钩子。

在这里,我们需要包装它,然后将其在自定义的<MapConsumer>元素中进行简化:

// MapLazyComponents.jsx
//...
import { useMapEvents } from 'react-leaflet/hooks';
export const MapConsumer = ({ eventsHandler }) => {
  useMapEvents(eventsHandler);
  return null;
};

// MapComponents.jsx
//...
export const MapConsumer = dynamic(
  () => import('./MapLazyComponents').then((m) => m.MapConsumer),
  { ssr: false }
);

所以在您的Map.jsx文件中,您现在可以在<MapContainer>中添加<MapConsumer>

// Map.jsx
//...
const Map = (props) => {
  const mapRef = useRef(null);
  const markerRef = useRef(null);

  const mapHandlers = useMemo(
    () => ({
      click(e) {
        // center view on the coordinates of the click
        // `this` is the Leaflet map object
        this.setView([e.latlng.lat, e.latlng.lng]);
      },
    }),
    []
  );

  return (
    <MapContainer
      ref={mapRef}
      touchZoom={false}
      zoomControl={false}
      style={{ height: '400px', zIndex: '0!important' }}
    >
      <TileLayer url="..." attribution="..." style={{ zIndex: '0!important' }} />
      <ZoomControl position="topright" style={{ zIndex: '10!important' }} />
      <MapConsumer
        eventsHandler={mapHandlers}
      />
      <Marker
        ref={markerRef}
        icon={{
          iconUrl: CustomIcon.src,
          iconAnchor: [16,32],
          iconSize: [32,32]
        }}
        style={{ zIndex: '1!important' }}
      />
    </MapContainer>
  );
};

几个状态和CSS之后,这是我的结果:

Image description


所以我们已经看到了:

  1. 带有next/dynamic的懒负载组件,
  2. 使ref与懒惰的组件一起使用,
  3. 动态加载leaflet以访问其方法,例如L.Icon
  4. 包裹react-leaflet自定义挂钩处理事件。

适应这些技巧应涵盖您的大多数边缘案例。我希望分解这些特定用例将帮助您在NextJ上更好地与React Leflet一起工作!

当然,如果您需要反向地理编码API来从地址获得坐标,请查看PlaceKit.io:)!