我试图在 React 中实现外部 API,并希望能够使用 Google 地图的 API 在子组件中显示地图。理想情况下,我想了解如何在没有任何外部库的情况下执行此操作,以便在使用 Axios 之类的东西之前对该过程有一个基本的了解。
我的问题是:如何在 React 中使用 Google 文档中的以下代码片段?
<script async defer
src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDZfVO29Iytspv4xz7S68doIoiztiRLhbk&callback=initMap'>
</script>
我尝试在我的索引.html文件中使用它,但是当我在 React 的子组件中引用 google 对象时,出现错误:
./src/Main.js 第 114 行:"google"未定义 no-undef
即使这不是首选或最优雅的方法,也非常感谢有关如何在没有任何外部库的情况下实现 API 的一些基本理解。谢谢!
编辑:
我的应用.js:
import React, { Component } from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import Main from './Main';
import logo from './logo.svg';
import './App.css';
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
class App extends Component {
render() {
return (
<MuiThemeProvider>
<Main />
</MuiThemeProvider>
);
}
}
export default App;
我的主要.js:
import React, { Component } from 'react';
import { FlatButton, Dialog, Card, Drawer, Paper, AppBar, Popover, Menu, MenuItem } from 'material-ui';
var items = [
{
id: 0,
name: 'Test 1',
city: 'Toronto',
longitude: 24.42142422,
latitude: 49.24121415,
tags: ['vegan', 'cheap', 'low-calorie'],
reviews: [
{
rating: 5,
reviewText: 'This was an amazing restaurant. Incredibly fast service, a large variety of options, and delicious food. I'll be here often',
author: 'Mohammad Sheikh',
date: new Date(),
helpfulCount: 5,
notHelpfulCount: 4
},
{
rating: 2,
reviewText: 'Absolutely horrible. Please stop making food.',
author: 'Dissatisfied Customer',
date: new Date(),
helpCount: 2,
notHelpfulCount: 3
},
],
foods:
[
{
id: 0,
name: 'Salad',
img: 'http://www.images.google.com/',
tags: ['vegan', 'low-calorie', 'cheap'],
nutrition:
{
calories: 300,
fat: 5,
carbs: 40,
protein: 24
},
reviews:
{
rating: 4,
reviewText: 'Decent salad. Would recommend.',
author: 'Vegan Bro',
date: new Date(),
helpCount: 4,
notHelpfulCount: 1
}
},
{
id: 1,
name: 'Pasta',
img: 'http://www.images.google.com/',
tags: ['vegetarian', 'dinner'],
nutrition:
{
calories: 800,
fat: 40,
carbs: 80,
protein: 20
},
reviews:
{
rating: 5,
reviewText: 'Absolutely amazing',
author: 'Food Fan',
date: new Date(),
helpCount: 8,
notHelpfulCount: 4
}
},
],
},
];
const paperStyle = {
height: 100,
width: 100,
margin: 20,
textAlign: 'center',
display: 'table',
position: 'relative',
clear: 'both',
float: 'right',
zIndex: 6
};
const paperContent = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}
class RestaurantDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false
}
}
render() {
return (
<Dialog>
</Dialog>
)
}
}
class RestaurantButton extends React.Component {
constructor(props) {
super(props);
}
handleClick = () => {
}
render() {
return (
<FlatButton style={{width: '100%', height: '64px'}} onClick>
{this.props.item.city}
<RestaurantDialog restaurant={this.props.item.name} />
</FlatButton>
)
}
}
class MapComponent extends React.Component {
constructor(props) {
super(props);
this.googleChecker = this.googleChecker.bind(this);
this.renderMap = this.renderMap.bind(this);
}
googleChecker() {
if (!window.google.maps) {
setTimeout(this.googleChecker, 100);
}
else {
this.renderMap();
}
}
renderMap() {
var map = google.maps.Map(document.getElementById('map'), {
zoom: 4,
center: {lat: 0, lng: 0}
});
}
componentDidMount() {
this.googleChecker();
}
render() {
const selections = this.props.currentSelections;
const buttons = items.filter((item) => {
for (let i = 0; i < selections.length; i++) {
if (selections.map((selection) => {return selection.toLowerCase()}).indexOf(item.tags[i].toLowerCase()) > -1) {
return true;
}
}}).map((item) => {
return (
<RestaurantButton style={{zIndex: '5'}} item={item} />
)
});
return (
<Paper id='map' zDepth={3} style={{height: '300px', width: '100%', backgroundColor: 'white', position: 'absolute'}}>
{ buttons }
</Paper>
)
}
}
class SelectionIcon extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Paper circle={true} zDepth={5} style={this.props.style} key={this.props.index} onClick={this.props.close} >
<div style={paperContent}>{this.props.item}</div>
</Paper>
)
}
}
class SelectionIcons extends React.Component {
constructor(props) {
super(props);
}
handleSelectionClose = (e) => {
e.currentTarget.open = false;
}
render() {
let currentSelections = this.props.currentSelections.slice();
let list = currentSelections.map((item, i) => {
return (
<Paper circle={true} zDepth={5} style={paperStyle} key={i} onClick={this.handleSelectionClose}>
<div style={paperContent}>{item}</div>
</Paper>
)
});
return (
<div>
{list}
</div>
)
}
}
class Main extends React.Component {
constructor(props){
super(props);
this.state = {
navMenuOpen: false,
currentSelections: []
}
}
handleMenuButtonTouch = (e) => {
this.setState({
anchorEl: e.currentTarget.parentNode,
navMenuOpen: !this.state.navMenuOpen
})
}
handleRequestChange = (change) => {
this.setState({
navMenuOpen: change.open
})
console.log(document.getElementById('test').style);
}
handleMenuClick = (e) => {
let currentSelections = this.state.currentSelections.slice();
if (currentSelections.indexOf(e) > -1) {
currentSelections.splice(currentSelections.indexOf(e), 1);
}
else {
currentSelections.push(e);
}
console.log(currentSelections)
this.setState({ currentSelections });
}
render() {
return (
<div>
<AppBar title='The App' id='test' zDepth={1} onLeftIconButtonTouchTap={this.handleMenuButtonTouch} style={{zIndex: 4}}>
</AppBar>
<Drawer
id='test2'
open={this.state.navMenuOpen}
onRequestChange={() => {this.handleRequestChange;}}
containerStyle={{zIndex: 3, marginTop: '64px'}}>
<Menu>
<MenuItem primaryText='High Protein' onClick={() => this.handleMenuClick('High Protein')} />
<MenuItem primaryText='Vegetarian' onClick={() => this.handleMenuClick('Vegetarian')} />
<MenuItem primaryText='Vegan' onClick={() => this.handleMenuClick('Vegan')} />
</Menu>
</Drawer>
<MapComponent items={items} currentSelections={this.state.currentSelections} />
<SelectionIcons currentSelections={this.state.currentSelections} />
</div>
)
}
}
export default Main;
我的索引.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<script async defer
src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDZfVO29Iytspv4xz7S68doIoiztiRLhbk'>
</script>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
这个问题与使用谷歌地图API时异步和延迟的工作方式有关。
基本上,当您的代码达到必须渲染地图的点时,Google API 尚未加载。请看一下这篇文章,了解它是如何工作的:
https://stackoverflow.com/a/36909530/2456879
有两种解决方案。
解决方案一不要在脚本标记中使用异步和延迟,以便在执行应用之前下载脚本:
<script src='https://maps.googleapis.com/maps/api/js?key=AIzaSyDZfVO29Iytspv4xz7S68doIoiztiRLhbk&callback=initMap'>
</script>
解决方案二创建某种递归检查器,以查看Google API是否已加载,以便在Google Maps API可用后继续执行您的应用:
class MyMap extends Component{
constructor(props){
super(props);
this.googleChecker = this.googleChecker.bind(this);
this.renderMap = this.renderMap.bind(this);
}
googleChecker() {
// check for maps in case you're using other google api
if(!window.google.maps) {
setTimeout(googleChecker, 100);
console.log("not there yet");
} else {
console.log("we're good to go!!");
// the google maps api is ready to use, render the map
this.renderMap();
}
}
renderMap(){
const coords = { lat: 41.375885, lng: 2.177813 };
// create map instance
new google.maps.Map(this.refs.mapContainer, {
zoom: 16,
center: {
lat: coords.lat,
lng: coords.lng
}
});
}
componentDidMount(){
this.googleChecker();
}
render(){
return(
<div className="card map-holder">
<div className="card-block" ref="mapContainer" />
</div>
);
}
}
您也可以使用 promise 并在 checker 方法或类似方法中解决它。您还可以将该代码放在父组件中,在该状态下存储布尔值并将其传递给子组件,以便在 API 可用后开始渲染映射。这种方法也可以与 redux 和 redux thunk 一起使用,以解决承诺。如您所见,根据您的方法,有几种选择。
下面是使用超时检查器的实时示例:
https://jsbin.com/tejutihoka/edit?js,output
...仅使用钩子。 这个解决方案使用库,但它是谷歌自己的加载器:https://developers.google.com/maps/documentation/javascript/overview#js_api_loader_package
// https://developers.google.com/maps/documentation/javascript/overview#js_api_loader_package
import { useState, useEffect, useRef } from "react";
import { Loader } from "@googlemaps/js-api-loader";
export default function Map({
apiKey = "",
label = "",
zoom = 16,
coords = {
lat: 0,
lng: 0,
},
}) {
const [gmapWin, setGmapWin] = useState(false);
const [gmapObj, setGmapObj] = useState();
const mapBox = useRef();
const props = useRef({ apiKey, label, coords, zoom });
// load map
useEffect(() => {
const loader = new Loader({
apiKey: props.current.apiKey,
version: "weekly",
});
// https://stackoverflow.com/a/61980156
const abortController = new AbortController();
(async function () {
loader.load().then(() => {
if (
!abortController.signal.aborted &&
window.google?.maps &&
!gmapWin
) {
setGmapWin(true);
}
if (gmapWin) {
setGmapObj(
new window.google.maps.Map(mapBox.current, {
center: props.current.coords,
zoom: props.current.zoom,
})
);
}
});
})();
return () => {
abortController.abort();
};
}, [gmapWin]);
// add marker
useEffect(() => {
if (gmapObj) {
new window.google.maps.Marker({
position: props.current.coords,
map: gmapObj,
label: props.current.label,
});
}
}, [gmapObj]);
return <div className="map" ref={mapBox} />;
};