发布于 2025-12-10 3 阅读
0

使用 React、Socket.io 和 Leaflet 构建实时位置共享应用程序

使用 React、Socket.io 和 Leaflet 构建实时位置共享应用程序

有很多关于使用 React 和 Socket IO 构建聊天应用的教程。虽然构建聊天应用是探索实时应用的一种很酷的方式,但我想更有创意一些。于是,我想到了一个创建位置共享应用的想法。另外,我需要一个项目来充实我空白的简历,弥补我经验的不足。

那么,我们开始吧。

LocShare:一款实时位置共享应用程序,允许您与多个用户共享您的位置。

图片描述

图片描述

当有人加入或离开房间时,您还会收到通知以及当前连接的用户数量。

图片描述

一旦您停止共享位置,房间就会被破坏,并且连接的用户将无法再看到您的位置。

图片描述


这是一款位置共享应用,这意味着为了测试它,你必须步行/跑步/开车好几英里。但愿不会。因为浏览器提供了一种无需离开沙发就能操控坐标的方法。

导航到更多工具并找到传感器选项。从那里,您可以操作当前坐标。

图片描述

图片描述

你可以从这里试用这个应用。后端托管在render的免费层,因此第一次连接需要一些时间,有时甚至几个小时都无法使用。


如果您正在参与编程,可能会觉得有点挑战性。我重点介绍的是代码的关键部分、逻辑流程和应用程序的结构。建议您访问 GitHub 仓库获取完整代码

设置项目

在前端,我使用 Vite、TypeScript 和 Tailwind CSS 配置了我的 React 应用。为了在地图上映射坐标,我们将使用一个著名的开源库 Leaflet。

我最近开始使用 TypeScript,并将其应用于这个项目。不过,需要注意的是,如果你只是跟着别人写代码,TypeScript 是可选的。

以下是前端的依赖关系:

{
  "name": "client",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@types/leaflet": "^1.9.6",
    "@types/react-leaflet": "^3.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-icons": "^4.11.0",
    "react-leaflet": "^4.2.1",
    "react-router-dom": "^6.16.0",
    "react-toastify": "^9.1.3",
    "socket.io-client": "^4.7.2"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "autoprefixer": "^10.4.15",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "postcss": "^8.4.29",
    "tailwindcss": "^3.3.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  }
}
Enter fullscreen mode Exit fullscreen mode

对于后端,我们有一个具有以下依赖项的基本 node-express 服务器。

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\""
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "socket.io": "^4.7.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.17",
    "concurrently": "^8.2.0",
    "nodemon": "^3.0.1",
    "typescript": "^5.2.2"
  }
}
Enter fullscreen mode Exit fullscreen mode

文件夹结构

React 应用:

📁 src
├── 📁 components
│   ├── 📁 Element
│   │   ├── 📄 Header
│   │   ├── 📄 Map
│   │   ├── 📄 Status
│   │   ├── 📄 StatusPanel
│   ├── 📁 Layout
├── 📁 context
│   ├── 📄 socket.tsx
├── 📁 pages
│   ├── 📄 Home.tsx
│   ├── 📄 Location.tsx
├── 📁 types
├── 📄 App.tsx
├── 📄 main.tsx
├── 📄 index.css
...rest
Enter fullscreen mode Exit fullscreen mode

节点服务器:

📁 src
├── 📄 index.ts
Enter fullscreen mode Exit fullscreen mode

创建套接字上下文

通过将套接字存储在上下文中,我们可以将其全局提供给应用程序的其余部分。connectSocket 函数负责处理连接。如果套接字为 null(不存在连接),则建立一个新的套接字连接。如果套接字不为 null(断开连接),则连接到套接字。

// SocketProvider.js
import {useState, createContext, useContext, JSX} from 'react'
import {io, Socket} from 'socket.io-client'
import { SOCKET_URL } from '../config'

type SocketContextType = {
  socket: Socket | null;
  connectSocket: () => void;
}

type SocketProviderProps = {
  children: JSX.Element
}

export const SocketContext = createContext<SocketContextType | null>(null)

export const SocketProvider = ({children}: SocketProviderProps) => {
  const [socket, setSocket] = useState<Socket | null>(null)

  const connectSocket = () => {
    if(!socket) {
      const newSocket: Socket = io(SOCKET_URL)
      setSocket(newSocket)
      return
    }
    socket.connect()
  }

  return (
    <SocketContext.Provider value={{socket, connectSocket}}>
      {children}
    </SocketContext.Provider>
  )
}

export const useSocket = () => {
  const context = useContext(SocketContext)
  if(!context) {
    throw new Error('Something went wrong!')
  }
  return context
}

Enter fullscreen mode Exit fullscreen mode

在这里,我添加了一个名为 useSocket 的自定义钩子,它负责处理访问 SocketContext 和 SocketProvider 的逻辑,从而简化了代码。这种方法简化了代码,使得在组件中使用上下文值时更易于阅读和理解。

使用套接字上下文包装应用程序

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { SocketProvider } from './context/socket.tsx'
import {ToastContainer} from 'react-toastify'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <SocketProvider>
      <>
        <App />
        <ToastContainer newestOnTop/>
      </>
    </SocketProvider>
  </React.StrictMode>,
)
Enter fullscreen mode Exit fullscreen mode

应用路线

这里有两个页面:主页和位置。在主页上,我们可以创建房间并引导我们的位置。当 URL 与模式 /location/1242 匹配时,会显示位置页面,这表明 1242 是特定的房间 ID 或参数。

这些页面被包裹在布局组件中。

import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Home from './pages/Home'
import Location from './pages/Location'

function App() {

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="location/:roomId" element={<Location />}/>
        </Route>
      </Routes>
    </BrowserRouter>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

布局组件

import { Outlet } from 'react-router-dom'
import Header from '../Elements/Header'

function index() {
  return (
    <div className='flex justify-center px-3 py-2'>
      <div className='flex flex-col w-full md:min-w-full xl:min-w-[1100px] xl:max-w-[1200px] mb-4'>
        <Header />
        <main>
          <Outlet />
        </main>
      </div>
    </div>
  )
}

export default index
Enter fullscreen mode Exit fullscreen mode

访问用户位置

要获取用户的当前位置,我们可以使用浏览器提供的地理位置接口来获取当前位置。这将打开一个弹窗,询问用户是否需要位置权限。

// get the current position
navigator.geolocation.getCurrentPosition(success, error, options)
Enter fullscreen mode Exit fullscreen mode

success、error(可选)、options(可选)是需要传递的回调函数,通过它们我们可以获取坐标、错误信息,或者传递其他参数。更多详情,请参阅Mozilla 文档。

我们不仅需要当前位置;我们还想跟踪用户从 A 点到 B 点的移动

我们可以创建一个 setInterval 函数来重复调用 getCurrentPosition()。这里不需要这样做,因为地理位置接口提供了另一个名为 watchPosition() 的方法。每次位置发生变化时都会调用此方法。

// watch the current position
let id = navigator.geolocation.watchPosition(success, error, options)

// clear the resource
navigator.geolocation.clearWatch(id)
Enter fullscreen mode Exit fullscreen mode

watchPosition 方法类似于浏览器的 setTimeout 或 setInterval 功能,它在后台异步运行并监控用户当前位置。通过将其存储在变量中,我们可以保留一个引用。因此,当我们以后不再需要跟踪用户位置时,可以清除该变量或将其设置为 null。如果不这样做,它将继续在后台运行,这可能会导致内存问题和不必要的资源消耗。

type GeolocationPosition = {
  lat: number
  lng: number
}
type LocationStatus = 'accessed' | 'denied' | 'unknown' | 'error'

export default function Home() {
  const [locationStatus, setLocationStatus] = useState<LocationStatus>('unknown')
  const [position, setPosition] = useState<GeolocationPosition | null>(null)

  useEffect(() => {
    let watchId: number | null = null
        // check for geolocation support in browser
    if('geolocation' in navigator) {
      watchId = navigator.geolocation.watchPosition((position) => {
      setPosition({
        lat: position.coords.latitude,
        lng: position.coords.longitude
      })
      setLocationStatus('accessed')
      }, (error) => {
        switch (error.code) {
          case error.PERMISSION_DENIED:
            setLocationStatus('denied')
            break
          case error.POSITION_UNAVAILABLE:
            setLocationStatus('unknown')
            break
          case error.TIMEOUT:
            setLocationStatus('error')
            break
          default:
            setLocationStatus('error')
            break
        }
      })
      return () => {
        if(watchId) {
          navigator.geolocation.clearWatch(watchId)
        }
      }
    }
  }, [])
...
...
Enter fullscreen mode Exit fullscreen mode

页面加载时,会出现位置提示并请求位置权限。它会根据该提示判断用户是否允许位置权限。一旦允许位置权限,我们就可以访问包含用户坐标的position对象。

通过地理位置提供的错误代码,我们可以很好地处理错误并确切知道用​​户无法访问位置的原因。

在地图上定位坐标

一旦我们获得了用户的坐标,我们就可以将它们传递到我们的地图组件中,并且位置将显示在地图上。

我们希望地图标记能够自动移动到传入的坐标。在 Location Marker 组件中,我们将 leaflet 实例提供的 map.flyTo 方法放入 useEffect hook 中,并以位置作为依赖项。每当位置发生变化时,标记就会飞到那里。

import { useState, useEffect } from 'react'
import { MapContainer, TileLayer, useMapEvents, Marker, Popup } from 'react-leaflet'
import { GeolocationPosition } from '../../../types' 
import 'leaflet/dist/leaflet.css'

function Map({ location }: { location: GeolocationPosition }) {
  if (!location) return 'No location found'

  return (
    <div className='w-full bg-gray-100 h-[600px] md:h-[550px]'>
      <MapContainer center={[location.lat, location.lng]} zoom={30} scrollWheelZoom={true} className='h-screen'>
        <TileLayer
          url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' // OpenStreetMap tile layer URL
        />
        <LocationMarker location={location} />
      </MapContainer>
    </div>
  )
}

function LocationMarker({ location }: { location: GeolocationPosition }) {

  const map = useMapEvents({})  // Use map events to access the Leaflet map instance
  const [position, setPosition] = useState({
    lat: location.lat,
    lng: location.lng
  })

  // Effect to update marker position and fly to the new location when location data changes
  useEffect(() => {
    setPosition({
      lat: location.lat,
      lng: location.lng
    })
    map.flyTo([location.lat, location.lng]) // Fly to the new location on the map
  }, [location])

  return position === null ? null : (
    <Marker position={position}>
      <Popup>User is here!</Popup>
    </Marker>
  )
}

export default Map
Enter fullscreen mode Exit fullscreen mode

现在,我们可以在我们的主页中使用这个组件。

export default function Home() {
  const [locationStatus, setLocationStatus] = useState<LocationStatus>('unknown')
  const [position, setPosition] = useState<GeolocationPosition | null>(null)

    // ... rest

return(
<>  
{/** ...rest **/}
                {
            position && (<Map location={position}/>)
        }
</>
)
Enter fullscreen mode Exit fullscreen mode

设置 node-express 套接字服务器

在这段代码中,我们使用 Node.js 和 Express 搭建了一个简单的 HTTP 服务器。然后,我们创建了一个 Socket.io 服务器实例 (io),并将我们的 Express 服务器传递给它。尽管 HTTP 和 Web Socket 是不同的通信协议,但 Socket.io 允许 HTTP 服务器和 WebSocket 服务器共享同一个服务器实例,从而允许它们通过同一个网络端口进行通信。

import express, { Express, Request, Response } from 'express'
import {Socket, Server} from 'socket.io'
import cors from 'cors'
import dotenv from 'dotenv'

dotenv.config()

const app: Express = express()
const port = process.env.PORT || 5000

app.use(cors())
app.use(express.json())

app.get('/', (req: Request, res: Response) => {
  res.send('Welcome to LocShare!')
})


const server = app.listen(port, () => {
  console.log(`Server is running`)
})

const io: Server = new Server(server, {
  cors: {
    origin: '*',
  },
})

io.on('connection', (socket: Socket) => {
  console.log(`User connected: ${socket.id}`)
})
Enter fullscreen mode Exit fullscreen mode

当用户分享位置时,我们实际上要创建房间。

房间  是一个任意的通道,套接字可以 join ……连接 leave。它可以用来向一部分客户端广播事件。阅读更多

这个房间会有一个唯一的 ID。房间创建完成后,我们会将 roomId 发送给用户。

流程如下

  1. 用户共享位置:发出创建房间事件
  2. 服务器接收事件
    1. 生成房间ID
    2. 加入房间
    3. 将房间 ID 附加到当前套接字客户端
    4. 发出房间创建事件和 roomid
    5. 储藏室创建者

💡我正在扩展我的 Socket 接口,并为它添加一个名为 roomId 的属性。稍后我们将在用户离开房间时使用它。

// Define a custom interface extending the Socket interface
interface CustomSocket extends Socket {
  roomId?: string
}
const roomCreator = new Map<string, string>() // roomid => socketid

io.on('connection', (socket: CustomSocket) => {
  console.log(`User connected: ${socket.id}`)

  socket.on('createRoom', (data) => {
    const roomId = Math.random().toString(36).substring(2, 7)
    socket.join(roomId) // joining room in sockets
        socket.roomId = roomId //  attach roomId to socket
    const totalRoomUsers = io.sockets.adapter.rooms.get(roomId)
    socket.emit('roomCreated', { 
      roomId,
      position: data.position,
      totalConnectedUsers: Array.from(totalRoomUsers || []),
    })
    roomCreator.set(roomId, socket.id) // map roomid with socket
  })
})
Enter fullscreen mode Exit fullscreen mode

加入房间

为了加入房间,我们的客户端将发出一个带有 roomId 的事件“joinRoom”。

  1. 用户使用 roomId 发出“joinRoom”
  2. 检查房间是否存在:
    • 存在
      1. 加入房间
      2. 将 roomid 附加到套接字
      3. 获取房间创建者并通知他
      4. 向房间连接者(插座)发送消息
    • !存在
      1. 通知套接字
...
...
socket.on('joinRoom', (data: {roomId: string}) => {

    // check if room exists
    const roomExists = io.sockets.adapter.rooms.has(data.roomId)
    if (roomExists) {
      socket.join(data.roomId)
      socket.roomId = data.roomId //  attach roomId to socket

      // Notify the room creator about the new user     
      const creatorSocketID = roomCreator.get(data.roomId)
      if (creatorSocketID) {
        const creatorSocket = io.sockets.sockets.get(creatorSocketID) // get socket instance of creator
        if (creatorSocket) {
          const totalRoomUsers = io.sockets.adapter.rooms.get(data.roomId)
          creatorSocket.emit('userJoinedRoom', {
            userId: socket.id,
            totalConnectedUsers: Array.from(totalRoomUsers || [])
          })
        }
      }
      // msg to joiner
      io.to(`${socket.id}`).emit('roomJoined', {
        status: 'OK',
      })

    } else {
      io.to(`${socket.id}`).emit('roomJoined', {
        status: 'ERROR'
      })
    }
  })
Enter fullscreen mode Exit fullscreen mode

用于位置更新

socket.on('updateLocation', (data) => {
    io.emit('updateLocationResponse', data)
})
Enter fullscreen mode Exit fullscreen mode

主页:连接插座和创建房间

现在,我们可以从主页连接到套接字服务器。要共享位置,用户必须先共享其位置。获取坐标后,我们就可以连接到服务器。当用户成功建立连接后,我们将自动发出 createRoom 事件。

我选择这种方法是为了防止用户访问页面时自动连接到服务器。在我们的应用中,将来可能会有其他页面,例如登录或注册页面。为了避免不必要的连接,我这样设计是为了确保仅在明确需要时才建立连接。

type SocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error'
type RoomInfo = {
  roomId: string
  position: GeolocationPosition
  totalConnectedUsers: string[]
}

export default function Home() {
    // ...location related states

  const {socket, connectSocket} = useSocket()
  const [socketStatus, setSocketStatus] = useState<SocketStatus>('disconnected')
    const [roomLink, setRoomLink] = useState<string>('')
    const [roomInfo, setRoomInfo] = useState<RoomInfo | null>(null)

  function connectToSocketServer() {
    connectSocket()
    setSocketStatus('connecting')
  }

  useEffect(() => {
    let watchId: number | null = null
    // ...geolocation logic
  }, [])

  useEffect(() => {
    if(socket) {
      socket.on('connect', () => {
        setSocketStatus('connected')
        socket.emit('createRoom', {
          position
        })
      })

      socket.on('roomCreated', (data: RoomInfo) => {
        toast.success('You are live!', {
          autoClose: 2000,
          })
        setRoomInfo(data)
      })

      socket.on('userJoinedRoom', (data: {userId: string, totalConnectedUsers: string[]}) => {
        setRoomInfo((prev) => {
          if(prev) {
            return {
              ...prev,
              totalConnectedUsers: data.totalConnectedUsers
            }
          }
          return null
        })

        toast.info(`${data.userId} joined the room`, {
          autoClose: 2000,
        })

        position && socket.emit('updateLocation', {
          position
        })
      })

      socket.on('disconnect', () => {
        setSocketStatus('disconnected')
      })
    }
  }, [socket])

  useEffect(() => {
    if(socket) {
      socket.emit('updateLocation', {
        position
      })
    }
  }, [position])


  return (
    <>
            {/* ...rest */}
            {
              socketStatus === 'disconnected' && (
                <div className='flex flex-col gap-6 items-start w-full'>
                  <button 
                    className={`${locationStatus === 'accessed' ? 'bg-purple-800' : 'bg-gray-600 cursor-not-allowed'}`}
                    onClick={() => {
                      if(locationStatus === 'accessed') {
                        connectToSocketServer()
                      } else {
                        toast.error('Please allow location access', {
                          autoClose: 2000,
                        })
                      }
                    }}
                    disabled={locationStatus !== 'accessed'}
                    >Share Location</button>
                  {/*  ...rest */}
              </div>
              )
Enter fullscreen mode Exit fullscreen mode

位置页面:获取roomId并加入房间

页面加载时,我们从 URL 中提取房间 ID。随后,我们与套接字建立连接。连接成功后,触发“加入房间”事件,表示用户已加入指定房间。

import React, {useState, useEffect} from 'react'
import { useParams } from 'react-router-dom'
import {useSocket} from '../context/socket'

type RoomStatus = 'unknown' | 'joined' | 'not-exist'

function Location() {
  const { roomId } = useParams()
  const { socket, connectSocket } = useSocket()
  const [socketStatus, setSocketStatus] = useState<SocketStatus>('disconnected')
  const [roomStatus, setRoomStatus] = useState<RoomStatus>('unknown')
  const [position, setPosition] = useState<GeolocationPosition | null>(null)

  useEffect(() => {
    connectSocket()
    setSocketStatus('connecting')
    return () => {
      if(socket) {
        socket.disconnect()
        setSocketStatus('disconnected')
      }
    }
  }, [])

  useEffect(() => {

    if(socket){      

      socket.on('connect', () => {
        setSocketStatus('connected')
        socket.emit('joinRoom', {
          roomId
        })
      })

      socket.on('roomJoined', ({status}: {status: string}) => {
        if(status === 'OK') {
          setRoomStatus('joined')
        } else if (status === 'ERROR') {
          setRoomStatus('not-exist')
        } else {
          setRoomStatus('unknown')
        }
      })

      socket.on('updateLocationResponse', ({position}:{position: GeolocationPosition}) => {
        if(position) {
          setPosition(position)
        }
      })

      socket.on('disconnect', () => {
        setSocketStatus('disconnected')
      })
    }

  }, [socket])

// ...rest
Enter fullscreen mode Exit fullscreen mode

服务员:离开房间

这里,用户离开房间时的逻辑:

用户离开房间:

  1. 对于房间创建者:
    • 如果离开的用户是创建者:
      1. 摧毁房间:
      2. 通知其他用户
  2. 对于房间连接器:
    • 如果离开的用户是参与者:
      1. 通知创建者:
        • 通知房间创建者离开事宜。
      2. 离开房间:
        • 确保离开的用户已从房间的参与者列表中删除。
io.on('connection', (socket: CustomSocket) => {
  console.log(`User connected: ${socket.id}`)

// ...rest code

socket.on('disconnect', () => {
    console.log(`User disconnected: ${socket.id}`)

    const roomId = socket.roomId
    if(roomId){
      // if disconnected user is creator, destroy room
      if(roomCreator.get(roomId) === socket.id){
        // notify users in room that room is destroyed
        const roomUsers = io.sockets.adapter.rooms.get(roomId)
        if(roomUsers){
          for (const socketId of roomUsers) {
            io.to(`${socketId}`).emit('roomDestroyed', {
              status: 'OK'
            })
          }
        }
        io.sockets.adapter.rooms.delete(roomId)
        roomCreator.delete(roomId)    
      } else{
        socket.leave(roomId)
        // notify creator that user left room
        const creatorSocketId = roomCreator.get(roomId)
        if(creatorSocketId){
          const creatorSocket = io.sockets.sockets.get(creatorSocketId)
          if(creatorSocket){
            creatorSocket.emit('userLeftRoom', {
              userId: socket.id,
              totalConnectedUsers: Array.from(io.sockets.adapter.rooms.get(roomId) || [])
            })
          }
        }
      }
    }

  })

})
Enter fullscreen mode Exit fullscreen mode

更新主页和位置页面:离开房间

主页.tsx

export default function Home() {

    // ...rest

  useEffect(() => {
    if(socket) {
      socket.on('connect', () => {
        setSocketStatus('connected')
        socket.emit('createRoom', {
          position
        })
      })
            // ...rest

            socket.on('userLeftRoom', (data: {userId: string, totalConnectedUsers: string[]}) => {
        setRoomInfo((prev) => {
          if(prev) {
            return {
                ...prev,
                totalConnectedUsers: data.totalConnectedUsers
              }
            }
            return null
          })
          toast.info(`${data.userId} left the room`, {
            autoClose: 2000,
          })
      })

      socket.on('disconnect', () => {
        setSocketStatus('disconnected')
      })
    }
  }, [socket])
// ...rest
Enter fullscreen mode Exit fullscreen mode

位置.tsx



function Location() {

  // ...rest

  useEffect(() => {

    if(socket){      

      socket.on('connect', () => {
        setSocketStatus('connected')
        socket.emit('joinRoom', {
          roomId
        })
      })

      // ...rest

            socket.on('roomDestroyed', () => {
        setRoomStatus('not-exist')
        socket.disconnect()
      })

      socket.on('disconnect', () => {
        setSocketStatus('disconnected')
      })
    }

  }, [socket])

function stopSharingLocation() {
    if(socket){
      socket.disconnect()
      setSocketStatus('disconnected')
      setRoomInfo(null)
      toast.success('You are no longer live!', {
        autoClose: 2000,
      })
    }
  }

// ...rest
Enter fullscreen mode Exit fullscreen mode

大功告成!👏🏽

我们已经介绍了位置共享应用程序的基本流程和逻辑,重点关注了关键部分。虽然我没有提供 JSX 和 UI 实现代码,但您可以在 GitHub仓库中找到完整的源代码。


最后的话

在这个项目中,我接触到了最基本的框架。在实际应用中,我们会拥有更强大的功能,包括身份验证、数据库等等。最重要的是应用程序的可扩展性,以及它能够处理的用户数量。

希望如果我有足够的时间,我会继续这个系列并尝试构建这些功能。

欢迎您提出请求、提出问题或建议更改。

鏂囩珷鏉ユ簮锛�https://dev.to/sindbad_x/building-a-real-time-location-sharing-app-with-react-socketio-and-leaflet-236l