javascript - 如何使一个组件中的按钮 onClick 调用另一个组件中的函数 React
问题描述
我有一个反应应用程序,它有一个导航栏和一个网格系统。这个 NavBar 显示一些信息并具有运行应用程序的按钮,网格系统包含功能并可视化应用程序。我想让 NavBar 按钮单击触发一个函数来调用网格系统组件,特别是 animateAlgorithm 函数。如您所见,我已经有一个提供程序在这两个组件之间共享一些状态信息。我不明白的是如何让它调用函数。
https://github.com/austinedger0811/path-finding-visualization
应用程序.js
import GridContainer from './Components/GridContainer'
import NavBar from './Components/NavBar'
import {OptionsProvider} from './Context/OptionsContext'
import './App.css';
function App() {
return (
<OptionsProvider>
<div className="App">
<NavBar />
<GridContainer rows={40} colums={40} />
</div>
</OptionsProvider>
);
}
export default App;
导航栏.js
import React, { useState, useContext } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import MenuIcon from '@material-ui/icons/Menu'
import Button from '@material-ui/core/Button'
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'
import { OptionsContext } from '../Context/OptionsContext'
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
button: {
marginRight: theme.spacing(2),
},
title: {
marginRight: theme.spacing(12)
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
listItemSecondaryText: {
color: 'lightgray',
},
}));
const algorithmOptions = [
'Breadth First Search',
'Depth First Search',
'Dijkstra\'s',
'Other',
];
const wallOptions = [
'None',
'Random Wall Generation',
'Randomized Depth First Search',
'Recursive Division'
];
function NavBar() {
const classes = useStyles();
const [anchorElAlgorithm, setAnchorElAlgorithm] = React.useState(null);
const [anchorElWall, setAnchorElWall] = React.useState(null);
const { algorithmIndex, setAlgorithmIndex, wallIndex, setWallIndex } = useContext(OptionsContext);
const handleClickAlgorithmListItem = (event) => {
setAnchorElAlgorithm(event.currentTarget);
};
const handleClickWallListItem = (event) => {
setAnchorElWall(event.currentTarget);
};
const handleAlgorithmItemClick = (event, index) => {
setAlgorithmIndex(index);
setAnchorElAlgorithm(null);
};
const handleWallItemClick = (event, index) => {
setWallIndex(index);
setAnchorElWall(null);
}
const handleClose = () => {
setAnchorElAlgorithm(null);
setAnchorElWall(null);
};
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} edge="start" color="inherit" aria-label="menu">
<MenuIcon />
</IconButton>
<Typography className={classes.title} variant="h6">Path Finding Visualizer</Typography>
<List component="nav" aria-label="algorithm selector">
<ListItem
button
aria-aria-haspopup="true"
aria-controls="search-algorithm"
aria-label="Search Algorithm"
onClick={handleClickAlgorithmListItem}
>
<ListItemText primary="Search Algorithm" secondary={algorithmOptions[algorithmIndex]} classes={{ secondary: classes.listItemSecondaryText }} />
</ListItem>
</List>
<Menu
id="search-algorithm"
anchorEl={anchorElAlgorithm}
keepMounted
open={Boolean(anchorElAlgorithm)}
onClose={handleClose}
>
{algorithmOptions.map((option, index) => (
<MenuItem
key={algorithmOptions}
selected={index === algorithmIndex}
onClick={(event) => handleAlgorithmItemClick(event, index)}
>
{option}
</MenuItem>
))}
</Menu>
<List component="nav" aria-label="add walls">
<ListItem
button
aria-aria-haspopup="true"
aria-controls="add-walls"
aria-label="Add Walls"
onClick={handleClickWallListItem}
>
<ListItemText primary="Wall Generation" secondary={wallOptions[wallIndex]} classes={{ secondary: classes.listItemSecondaryText }} />
</ListItem>
</List>
<Menu
id="add-walls"
anchorEl={anchorElWall}
keepMounted
open={Boolean(anchorElWall)}
onClose={handleClose}
>
{wallOptions.map((option, index) => (
<MenuItem
key={wallOptions}
selected={index === wallIndex}
onClick={(event) => handleWallItemClick(event, index)}
>
{option}
</MenuItem>
))}
</Menu>
<Button variant="contained" color="secondary" className={classes.button} disableElevation>Run Visualization</Button>
<Button variant="contained" color="secondary" className={classes.button} disableElevation>Reset</Button>
</Toolbar>
</AppBar>
</div>
)
}
export default NavBar
GridContainer.js
import React, {useState, useEffect, useContext } from 'react'
import makeStyles from '@material-ui/core/styles/makeStyles'
import Button from '@material-ui/core/Button'
import Node from './Node'
import { OptionsContext } from '../Context/OptionsContext'
import './Grid.css'
const useStyles = makeStyles({
root: {
display: 'flex',
justifyContent: 'center'
},
grid: props => ({
display: 'grid',
gridTemplateColumns: `repeat(${props.colums}, 1fr)`,
gridTemplateRows: `repeat(${props.rows}, 1fr)`,
alignSelf: 'flex-start',
width: props.colums * 20,
height: props.rows * 20,
justifyContent: 'center',
}),
});
function GridContainer(props) {
let classes = useStyles(props);
const { rows, colums } = props;
const { algorithmIndex, wallIndex } = useContext(OptionsContext);
var visited = [];
var path = [];
const [Grid, setGrid] = useState([]);
useEffect(() => {
initGrid();
}, []);
var start = [4, rows / 2];
var end = [colums - 5, colums / 2];
const initGrid = () => {
var grid = [];
for (let row = 0; row < rows; row++) {
grid.push([])
for (let col = 0; col < colums; col++) {
grid[row].push(createNode(row, col));
}
}
setGrid([...grid]);
}
const createNode = (row, col) => {
return {
row,
col,
isStart: row === start[0] && col === start[1],
isEnd: row === end[0] && col === end[1],
isVisited: false,
isWall: false,
isPath: false,
distance: Infinity,
prevNode: null,
};
};
const bfs = () => {
var location = {
row: start[0],
col: start[1],
};
var grid = Grid;
var queue = [];
queue.push(location);
while (queue.length) {
var currentLocation = queue.shift();
var row = currentLocation.row;
var col = currentLocation.col;
if (row === end[0] && col === end[1]) {
setGrid(...[grid]);
getPath(grid);
return true;
}
if (grid[row][col].isVisited === false) {
grid[row][col].isVisited = true;
visited.push(grid[row][col]);
}else {
continue;
}
var neighbors = getNeighbors(grid, row, col);
for (let neighbor of neighbors) {
if (grid[neighbor.row][neighbor.col].isVisited !== true) {
queue.push(neighbor);
grid[neighbor.row][neighbor.col].prevNode = currentLocation;
}
}
}
return false;
};
const getNeighbors = (grid, row, col) => {
let neighbors = [];
if (validNode(grid, row, col - 1)) {
neighbors.push({
row: row,
col: col - 1,
});
}
if (validNode(grid, row, col + 1)) {
neighbors.push({
row: row,
col: col + 1,
});
}
if (validNode(grid, row - 1, col)) {
neighbors.push({
row: row - 1,
col: col,
});
}
if (validNode(grid, row + 1, col)) {
neighbors.push({
row: row + 1,
col: col,
});
}
return neighbors;
};
const validNode = (grid, row, col) => {
var rowLength = grid.length;
var colLength = grid[0].length;
if (row < 0 || row >= rowLength || col < 0 || col >= colLength) {
return false;
}
if (grid[row][col].isWall) {
return false;
}
return true;
};
const getPath = (grid) => {
var currentNodeCord = {
row: end[0],
col: end[1],
}
path.push(currentNodeCord);
var curRow = currentNodeCord.row;
var curCol = currentNodeCord.col;
var prevNodeCord = grid[curRow][curCol].prevNode;
while (prevNodeCord !== null) {
currentNodeCord = prevNodeCord;
path.push(currentNodeCord);
curRow = currentNodeCord.row;
curCol = currentNodeCord.col;
var curNode = grid[curRow][curCol];
prevNodeCord = curNode.prevNode;
}
path.reverse();
};
const animateAlgorithm = () => {
bfs();
console.log(`Visited length: ${visited.length}`)
for (let i = 1; i <= visited.length; i++) {
if (i === visited.length) {
setTimeout(() => {
drawPath();
}, 7 * i);
} else {
let nodeCord = visited[i];
let row = nodeCord.row;
let col = nodeCord.col;
setTimeout(() => {
markVisited(row, col);
}, 6 * i);
}
}
};
const drawPath = () => {
for (let i = 1; i < path.length - 1; i++) {
let nodeCord = path[i];
let row = nodeCord.row;
let col = nodeCord.col;
setTimeout(() => {
markPath(row, col);
}, 40 * i);
}
};
const markPath = (row, col) => {
document.getElementById(`node-${row}-${col}`).className = 'node path';
};
const markVisited = (row, col) => {
document.getElementById(`node-${row}-${col}`).className = 'node visited';
};
const addRandomWalls = (threshold) => {
let grid = [...Grid];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < colums; col++) {
if (!grid[row][col].isStart && !grid[row][col].isEnd) {
grid[row][col].isWall = (Math.floor(Math.random() * 10) > threshold);
}
}
}
setGrid(grid);
};
const resetGridColors = () => {
for (let row = 0; row < rows; row++) {
for (let col = 0; col < colums; col++) {
if (Grid[row][col].isStart !== true && Grid[row][col].isEnd !== true){
document.getElementById(`node-${row}-${col}`).className = 'node';
}
}
}
};
const logStates = () => {
console.log(Grid)
console.log(visited)
console.log(path)
};
const reset = () => {
visited = [];
path = [];
resetGridColors();
initGrid();
};
var GridMap = Grid.map((row, rowIndex) => {
return (
<div key={rowIndex}>
{row.map((node, nodeIndex) => {
const { row, col, isStart, isEnd, isWall, isPath, isVisited } = node;
return (
<Node
key={`${row}${col}`}
width={20}
height={20}
row={row}
col={col}
isStart={isStart}
isEnd={isEnd}
isWall={isWall}
isPath={isPath}
isVisited={isVisited}
/>
);
})}
</div>
);
})
return (
<>
<div className={classes.root}>
<div className={classes.grid}>
{GridMap}
</div>
</div>
<Button variant="contained" color="primary" onClick={ () => animateAlgorithm() }>Animate Algorithm</Button>
<Button variant="contained" color="primary" onClick={ () => addRandomWalls(6) }>Add Random Walls</Button>
<Button variant="contained" color="primary" onClick={ () => reset() }>Reset</Button>
<Button variant="contained" color="secondary" onClick={ () => logStates() }>Log Data</Button>
</>
)
}
export default GridContainer
OptionsContext.js
import React, { useState, createContext, useMemo } from 'react'
export const OptionsContext = createContext();
export const OptionsProvider = (props) => {
const [algorithmIndex, setAlgorithmIndex] = useState(0);
const [wallIndex, setWallIndex] = useState(0);
const menuValue= useMemo(() => ({
algorithmIndex, setAlgorithmIndex,
wallIndex, setWallIndex,
}), [algorithmIndex, wallIndex]);
return (
<OptionsContext.Provider value={menuValue}>
{props.children}
</OptionsContext.Provider>
);
};
任何帮助,将不胜感激!
解决方案
您需要做的是使用useImperativeHandle
,因此您可以从组件外部访问该功能。
首先,调用useRef
创建引用并将其传递给您的GridContainer
as prop,稍后我们将ref
在组件内部使用fowardRef
. 然后,您需要将animateAlgorithm
处理函数包装在内部并将其用作道具,NavBar
以便在单击按钮时调用它。
应用程序.js
import React, {useRef} from 'react';
import GridContainer from './Components/GridContainer'
import NavBar from './Components/NavBar'
import {OptionsProvider} from './Context/OptionsContext'
import './App.css';
function App() {
const containerRef = useRef(null);
const handleClick = () => {
if (containerRef?.current) containerRef.current.animateAlgorithm();
};
return (
<OptionsProvider>
<div className="App">
<NavBar handleClick={handleClick} />
<GridContainer ref={containerRef} rows={40} colums={40} />
</div>
</OptionsProvider>
);
}
export default App;
现在我们需要正确使用handleClick
定义在NavBar
. 因此,使用道具设置onClick
按钮的NavBar
handleClick
道具。您从未指定要单击哪个按钮,所以我假设带有Run Visualization
内容的按钮。如果不是这个,您可以移动onClick
到您希望的另一个按钮。
导航栏.js
import React, { useState, useContext } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import MenuIcon from '@material-ui/icons/Menu'
import Button from '@material-ui/core/Button'
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'
import { OptionsContext } from '../Context/OptionsContext'
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
button: {
marginRight: theme.spacing(2),
},
title: {
marginRight: theme.spacing(12)
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
listItemSecondaryText: {
color: 'lightgray',
},
}));
const algorithmOptions = [
'Breadth First Search',
'Depth First Search',
'Dijkstra\'s',
'Other',
];
const wallOptions = [
'None',
'Random Wall Generation',
'Randomized Depth First Search',
'Recursive Division'
];
function NavBar(props) {
const {handleClick} = props;
const classes = useStyles();
const [anchorElAlgorithm, setAnchorElAlgorithm] = React.useState(null);
const [anchorElWall, setAnchorElWall] = React.useState(null);
const { algorithmIndex, setAlgorithmIndex, wallIndex, setWallIndex } = useContext(OptionsContext);
const handleClickAlgorithmListItem = (event) => {
setAnchorElAlgorithm(event.currentTarget);
};
const handleClickWallListItem = (event) => {
setAnchorElWall(event.currentTarget);
};
const handleAlgorithmItemClick = (event, index) => {
setAlgorithmIndex(index);
setAnchorElAlgorithm(null);
};
const handleWallItemClick = (event, index) => {
setWallIndex(index);
setAnchorElWall(null);
}
const handleClose = () => {
setAnchorElAlgorithm(null);
setAnchorElWall(null);
};
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<IconButton className={classes.menuButton} edge="start" color="inherit" aria-label="menu">
<MenuIcon />
</IconButton>
<Typography className={classes.title} variant="h6">Path Finding Visualizer</Typography>
<List component="nav" aria-label="algorithm selector">
<ListItem
button
aria-aria-haspopup="true"
aria-controls="search-algorithm"
aria-label="Search Algorithm"
onClick={handleClickAlgorithmListItem}
>
<ListItemText primary="Search Algorithm" secondary={algorithmOptions[algorithmIndex]} classes={{ secondary: classes.listItemSecondaryText }} />
</ListItem>
</List>
<Menu
id="search-algorithm"
anchorEl={anchorElAlgorithm}
keepMounted
open={Boolean(anchorElAlgorithm)}
onClose={handleClose}
>
{algorithmOptions.map((option, index) => (
<MenuItem
key={algorithmOptions}
selected={index === algorithmIndex}
onClick={(event) => handleAlgorithmItemClick(event, index)}
>
{option}
</MenuItem>
))}
</Menu>
<List component="nav" aria-label="add walls">
<ListItem
button
aria-aria-haspopup="true"
aria-controls="add-walls"
aria-label="Add Walls"
onClick={handleClickWallListItem}
>
<ListItemText primary="Wall Generation" secondary={wallOptions[wallIndex]} classes={{ secondary: classes.listItemSecondaryText }} />
</ListItem>
</List>
<Menu
id="add-walls"
anchorEl={anchorElWall}
keepMounted
open={Boolean(anchorElWall)}
onClose={handleClose}
>
{wallOptions.map((option, index) => (
<MenuItem
key={wallOptions}
selected={index === wallIndex}
onClick={(event) => handleWallItemClick(event, index)}
>
{option}
</MenuItem>
))}
</Menu>
<Button variant="contained" color="secondary" className={classes.button} onClick={handleClick} disableElevation>Run Visualization</Button>
<Button variant="contained" color="secondary" className={classes.button} disableElevation>Reset</Button>
</Toolbar>
</AppBar>
</div>
)
}
export default NavBar
最后,我们需要处理ref
里面的GridContainer
. 这样我们就可以通过 成功暴露 了animateAlgorithm
,ref
因此父组件可以直接调用它。
GridContainer.js
import React, {useState, useEffect, useContext, useImperativeHandle} from 'react'
import makeStyles from '@material-ui/core/styles/makeStyles'
import Button from '@material-ui/core/Button'
import Node from './Node'
import { OptionsContext } from '../Context/OptionsContext'
import './Grid.css'
const useStyles = makeStyles({
root: {
display: 'flex',
justifyContent: 'center'
},
grid: props => ({
display: 'grid',
gridTemplateColumns: `repeat(${props.colums}, 1fr)`,
gridTemplateRows: `repeat(${props.rows}, 1fr)`,
alignSelf: 'flex-start',
width: props.colums * 20,
height: props.rows * 20,
justifyContent: 'center',
}),
});
const GridContainer = React.forwardRef((props, ref) => {
let classes = useStyles(props);
const { rows, colums } = props;
const { algorithmIndex, wallIndex } = useContext(OptionsContext);
var visited = [];
var path = [];
const [Grid, setGrid] = useState([]);
useEffect(() => {
initGrid();
}, []);
var start = [4, rows / 2];
var end = [colums - 5, colums / 2];
const initGrid = () => {
var grid = [];
for (let row = 0; row < rows; row++) {
grid.push([])
for (let col = 0; col < colums; col++) {
grid[row].push(createNode(row, col));
}
}
setGrid([...grid]);
}
const createNode = (row, col) => {
return {
row,
col,
isStart: row === start[0] && col === start[1],
isEnd: row === end[0] && col === end[1],
isVisited: false,
isWall: false,
isPath: false,
distance: Infinity,
prevNode: null,
};
};
const bfs = () => {
var location = {
row: start[0],
col: start[1],
};
var grid = Grid;
var queue = [];
queue.push(location);
while (queue.length) {
var currentLocation = queue.shift();
var row = currentLocation.row;
var col = currentLocation.col;
if (row === end[0] && col === end[1]) {
setGrid(...[grid]);
getPath(grid);
return true;
}
if (grid[row][col].isVisited === false) {
grid[row][col].isVisited = true;
visited.push(grid[row][col]);
}else {
continue;
}
var neighbors = getNeighbors(grid, row, col);
for (let neighbor of neighbors) {
if (grid[neighbor.row][neighbor.col].isVisited !== true) {
queue.push(neighbor);
grid[neighbor.row][neighbor.col].prevNode = currentLocation;
}
}
}
return false;
};
const getNeighbors = (grid, row, col) => {
let neighbors = [];
if (validNode(grid, row, col - 1)) {
neighbors.push({
row: row,
col: col - 1,
});
}
if (validNode(grid, row, col + 1)) {
neighbors.push({
row: row,
col: col + 1,
});
}
if (validNode(grid, row - 1, col)) {
neighbors.push({
row: row - 1,
col: col,
});
}
if (validNode(grid, row + 1, col)) {
neighbors.push({
row: row + 1,
col: col,
});
}
return neighbors;
};
const validNode = (grid, row, col) => {
var rowLength = grid.length;
var colLength = grid[0].length;
if (row < 0 || row >= rowLength || col < 0 || col >= colLength) {
return false;
}
if (grid[row][col].isWall) {
return false;
}
return true;
};
const getPath = (grid) => {
var currentNodeCord = {
row: end[0],
col: end[1],
}
path.push(currentNodeCord);
var curRow = currentNodeCord.row;
var curCol = currentNodeCord.col;
var prevNodeCord = grid[curRow][curCol].prevNode;
while (prevNodeCord !== null) {
currentNodeCord = prevNodeCord;
path.push(currentNodeCord);
curRow = currentNodeCord.row;
curCol = currentNodeCord.col;
var curNode = grid[curRow][curCol];
prevNodeCord = curNode.prevNode;
}
path.reverse();
};
const animateAlgorithm = () => {
bfs();
console.log(`Visited length: ${visited.length}`)
for (let i = 1; i <= visited.length; i++) {
if (i === visited.length) {
setTimeout(() => {
drawPath();
}, 7 * i);
} else {
let nodeCord = visited[i];
let row = nodeCord.row;
let col = nodeCord.col;
setTimeout(() => {
markVisited(row, col);
}, 6 * i);
}
}
};
useImperativeHandle(ref, () => ({
animateAlgorithm
});
const drawPath = () => {
for (let i = 1; i < path.length - 1; i++) {
let nodeCord = path[i];
let row = nodeCord.row;
let col = nodeCord.col;
setTimeout(() => {
markPath(row, col);
}, 40 * i);
}
};
const markPath = (row, col) => {
document.getElementById(`node-${row}-${col}`).className = 'node path';
};
const markVisited = (row, col) => {
document.getElementById(`node-${row}-${col}`).className = 'node visited';
};
const addRandomWalls = (threshold) => {
let grid = [...Grid];
for (let row = 0; row < rows; row++) {
for (let col = 0; col < colums; col++) {
if (!grid[row][col].isStart && !grid[row][col].isEnd) {
grid[row][col].isWall = (Math.floor(Math.random() * 10) > threshold);
}
}
}
setGrid(grid);
};
const resetGridColors = () => {
for (let row = 0; row < rows; row++) {
for (let col = 0; col < colums; col++) {
if (Grid[row][col].isStart !== true && Grid[row][col].isEnd !== true){
document.getElementById(`node-${row}-${col}`).className = 'node';
}
}
}
};
const logStates = () => {
console.log(Grid)
console.log(visited)
console.log(path)
};
const reset = () => {
visited = [];
path = [];
resetGridColors();
initGrid();
};
var GridMap = Grid.map((row, rowIndex) => {
return (
<div key={rowIndex}>
{row.map((node, nodeIndex) => {
const { row, col, isStart, isEnd, isWall, isPath, isVisited } = node;
return (
<Node
key={`${row}${col}`}
width={20}
height={20}
row={row}
col={col}
isStart={isStart}
isEnd={isEnd}
isWall={isWall}
isPath={isPath}
isVisited={isVisited}
/>
);
})}
</div>
);
})
return (
<>
<div className={classes.root}>
<div className={classes.grid}>
{GridMap}
</div>
</div>
<Button variant="contained" color="primary" onClick={ () => animateAlgorithm() }>Animate Algorithm</Button>
<Button variant="contained" color="primary" onClick={ () => addRandomWalls(6) }>Add Random Walls</Button>
<Button variant="contained" color="primary" onClick={ () => reset() }>Reset</Button>
<Button variant="contained" color="secondary" onClick={ () => logStates() }>Log Data</Button>
</>
)
});
export default GridContainer
推荐阅读
- java - 没有最后一个索引值的字符串数组到字符串
- mysql - 有没有办法在 mysql npm 中设置“max_allowed_packet”?
- jquery - Ajax 调用从我的 div html 中删除内联 css
- php - 如何进行 URL 路径(?)和 .php 与 .html
- html - 如何使用光标在横向模式下显示 div 的行为类似于横向模式
- android - 应用已在 Play 商店发布,但未编入下载索引
- c# - Xamarin Android 异步崩溃(没有任何日志)
- git - Xcode 10.1 使用 ssh 密钥推送到 github
- mysql - Ruby on Rails 5.2 如何修复 root 用户的 mysql 访问被拒绝问题?
- angular - 如何在 Angular 4 中构建和部署选择性模块