node.js - 生产构建 - React Client App 和 Express Api 错误无法在模块外使用 import 语句
问题描述
我是使用 React/Typescript、Express 和 Webpack 开发的新手,如果这个问题已经得到解答,请原谅我。我按照本教程
的所有三个部分使用 Webpack 创建了一个 React 客户端应用程序,并创建了一个单独的 Express Api/Server 来将 JSON 数据发送到我的组件。在开发中,客户端使用 WebpackDevServer 在 localhost:3001 上运行,而 express 服务器在 localhost:3000 上运行。对于生产,我希望它们在同一个端口上运行,因此我尝试按照这篇博文在 express 服务器上提供为生产而构建的静态文件。当我尝试运行我的生产脚本时,我收到错误“无法在模块外使用导入语句”。我尝试了这个堆栈溢出问题的答案
并将 "type": "module" 添加到我的 package.json 中,但这只是使我的 webpack 配置文件中的 "require()" 未知。我不确定我的问题是来自我的 webpack 配置文件还是来自我的 package.json 脚本或其他东西,但我希望能在获得工作生产构建方面提供一些帮助。我在下面添加了我的代码的最小版本。谢谢你。
我的项目结构
公共/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Surfer Visualization</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
src/client/components/header.tsx
import * as React from 'react'
import Navbar from 'react-bootstrap/Navbar';
import Nav from 'react-bootstrap/Nav';
import {LinkContainer} from 'react-router-bootstrap';
class Header extends React.Component {
render() {
return (
<Navbar bg="dark" variant="dark" fixed="top">
<LinkContainer to="/">
<Navbar.Brand>Test UI</Navbar.Brand>
</LinkContainer>
<Nav className="mr-auto">
<LinkContainer to="/">
<Nav.Link>Maps</Nav.Link>
</LinkContainer>
</Nav>
</Navbar>
);
}
}
export default Header;
src/client/components/maps.tsx
import * as React from 'react';
import Jumbotron from 'react-bootstrap/Jumbotron';
import Container from 'react-bootstrap/Container';
import Accordion from 'react-bootstrap/Accordion';
import Card from 'react-bootstrap/Card';
import ListGroup from 'react-bootstrap/ListGroup';
import axios from 'axios';
import "../styles/default_layout.css";
type MyAccord = {
first_name: string,
last_name: string,
email: string,
city: string
}
function tagToClass(tag: string) {
let tagClass = 'accord-tag-';
tagClass += tag;
return tagClass;
}
function Accord(props: MyAccord) {
return (
<Accordion className={tagToClass(props.last_name)}>
<Card>
<Accordion.Toggle as={Card.Header} eventKey="0">
Surfer: {props.first_name}
</Accordion.Toggle>
<Accordion.Collapse eventKey="0">
<Card.Body>
<ListGroup variant="flush">
<ListGroup.Item>Surfer Name: {props.first_name} {props.last_name}</ListGroup.Item>
<ListGroup.Item>Surfer Email: {props.email}</ListGroup.Item>
<ListGroup.Item>Surfer City: {props.city}</ListGroup.Item>
</ListGroup>
</Card.Body>
</Accordion.Collapse>
</Card>
</Accordion>
)
}
const accordCreate = (item: MyAccord) => {
return <Accord
key={item.first_name}
first_name={item.first_name}
last_name={item.last_name}
email={item.email}
city={item.city} />;
}
class Maps extends React.Component<any, any> {
constructor(props: any) {
super(props);
this.state = {
isLoading: true,
maps: []
}
}
componentDidMount() {
fetch('/api/maps')
.then( response => response.json())
.then(data => {
this.setState({
isLoading: false,
maps: data.surfers
});
});
}
render() {
return (
<div className="default-page">
<Container className="p-3">
<div className="default-jumbotron">
<Jumbotron>
<h1 className="imaps-jumbo-title">Test Surfers</h1>
</Jumbotron>
</div>
<div className="imaps-section">
<section>
<h2 className="default-title-accord">Current Surfers</h2>
<div className="imaps-accord">
{
this.state.isLoading ? 'loading...' : (
<div>
{this.state.maps.map(accordCreate)}
</div>
)
}
</div>
</section>
</div>
</Container>
</div>
);
}
}
export default Maps;
src/client/styles/default_layout.css
.default-page {
margin: 75px 20px 20px 20px;
padding: 10px;
}
.default-jumbotron {
text-align: center;
}
.default-title-accord {
text-align: center;
}
src/client/app.tsx
import * as React from 'react';
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import Header from './components/header';
import Maps from './components/maps';
class App extends React.Component {
render() {
return (
<Router>
<Header />
<Switch>
<Route exact path="/" component={Maps}/>
</Switch>
</Router>
);
}
}
export default App;
src/client/index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './app';
import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.render(
<App />,
document.getElementById('root')
);
src/server/index.ts
import express from "express";
import fs from "fs";
import path from "path";
import cors from "cors";
let corsOptions: object = {
origin: 'http://localhost:3001'
}
const server = express();
server.use(cors(corsOptions));
server.use("/", express.static(path.join(__dirname, "../../build")));
const filePath = path.join(__dirname, "testData.json");
server.get("/api/maps", (req, res) => {
fs.readFile(filePath, 'utf-8', function(error, content) {
var data = JSON.parse(content);
res.send(data);
});
});
server.listen(3000, () => {
console.log(`Server running on http://localhost:3000`);
});
src/server/testData.json
{
"surfers": [
{
"first_name": "Adel",
"last_name": "Tease",
"email": "atease0@youtu.be",
"city": "Chengxiang"
},
{
"first_name": "Griff",
"last_name": "Kelley",
"email": "gkelley1@seesaa.net",
"city": "Xiaba"
},
{
"first_name": "Arne",
"last_name": "Rolstone",
"email": "arolstone2@histats.com",
"city": "Muhoroni"
},
{
"first_name": "Gale",
"last_name": "Chatten",
"email": "gchatten3@cloudflare.com",
"city": "Wolofeo"
},
{
"first_name": "Alane",
"last_name": "Lent",
"email": "alent4@google.nl",
"city": "Al Ḩarajah"
},
{
"first_name": "Myrta",
"last_name": "Tongs",
"email": "mtongs5@toplist.cz",
"city": "El Guamo"
}
]
}
包.json
{
"name": "test-ui",
"version": "1.0.0",
"description": "Test UI",
"main": "src/server/index.ts",
"scripts": {
"dev:client": "webpack serve --config webpack.dev.js",
"dev:server": "tsnd --respawn --transpile-only src/server/index.ts ",
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"prod:build": "webpack --config webpack.prod.js",
"prod:start": "npm run prod:build && node src/server/index.ts"
},
"devDependencies": {
"@babel/core": "^7.14.6",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.14.5",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"@types/react-router-bootstrap": "^0.24.5",
"@types/react-router-dom": "^5.1.8",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"concurrently": "^6.2.1",
"cors": "^2.8.5",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^3.0.2",
"html-webpack-plugin": "^5.3.2",
"mini-css-extract-plugin": "^2.1.0",
"terser-webpack-plugin": "^5.1.4",
"ts-node": "^10.2.1",
"ts-node-dev": "^1.1.8",
"typescript": "^4.3.5",
"webpack": "^5.43.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"bootstrap": "^4.6.0",
"react": "^17.0.2",
"react-bootstrap": "^1.6.1",
"react-dom": "^17.0.2",
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.2.0"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
webpack.common.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
entry: "./src/client/index.tsx",
output: {
path: __dirname + "/build",
publicPath: "/",
},
optimization: {
minimize: true,
minimizer: [new TerserJSPlugin({}), new CssMinimizerPlugin({})],
},
resolve: {
extensions: [".ts", ".tsx", ".jsx", ".js"],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash].css",
chunkFilename: "static/css/[id].[contenthash].chunk.css",
})
],
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
loader: require.resolve("babel-loader"),
exclude: /node_modules/,
// Options for the plugin
options: {
presets: [
require.resolve("@babel/preset-react"),
require.resolve("@babel/preset-typescript"),
],
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
}
webpack.prod.js
const {merge} = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
devtool: false,
output: {
filename: "static/js/[name].[contenthash].js",
chunkFilename: "static/js/[name].[contenthash].chunk.js",
}
});
webpack.dev.js
const {merge} = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'source-map',
output: {
filename: "static/js/bundle.js",
chunkFilename: "static/js/[name].chunk.js",
},
devServer: {
port: "3001",
open: true,
proxy: {
'/api': 'http://localhost:3000'
},
historyApiFallback: true,
}
});
解决方案
推荐阅读
- python - 502错误的网关。在“digitalocean”上实施应用程序后
- javascript - JavaScript 给出错误的输出
- sim900 - 使用 sim800C 发送多个 post 请求
- apache-kafka - Avro Schema Registry 的价值是什么?
- r - 如何将数据读入R中的列联表
- angular - 为什么 RxJS 过滤方法对我不起作用
- reactjs - 在 SVG 中使用外部样式表在 JSX 中不起作用
- laravel - Laravel 电子邮件验证让“user@hotmail”通过
- spring - 如何为全栈 Web 应用程序(Spring Boot JPA、REST、Vue.js)持久化和提供图像文件?
- php - 在 Laravel 中捕获和保存元素,而不是通过 primaryKey