首页 > 解决方案 > 生产构建 - 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,
    }
});

标签: node.jsreactjstypescriptexpresswebpack

解决方案


推荐阅读