This commit is contained in:
sebseb7
2025-09-02 08:29:27 +00:00
commit 9cfbe4c0f7
10 changed files with 6981 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

150
README.md Normal file
View File

@@ -0,0 +1,150 @@
# Webpack HMR React Counter App
A React application built with Webpack that features Hot Module Replacement (HMR), WebSocket-based real-time counter syncing, and REST API endpoints. Multiple users can increment a shared counter that syncs across all connected clients.
## Features
- **React 18** with modern hooks
- **Webpack 5** with Hot Module Replacement (HMR)
- **WebSocket** real-time communication for counter syncing
- **Express API** server with REST endpoints
- **Nginx proxy** configuration for production deployment
## Architecture
- **Frontend**: React app served on `127.0.0.1:3000` with Webpack Dev Server
- **Backend API**: Express server on `127.0.0.1:3001` handling REST API and WebSocket
- **WebSocket**: Real-time counter updates broadcast to all connected clients
- **Proxy**: Webpack dev server proxies `/api` requests to the Express server
- **External Access**: Configured for access via `https://dummy7.ddns.net` with dynamic WebSocket URLs
## API Endpoints
- `GET /api/counter` - Get current counter value
- `POST /api/counter/increment` - Increment counter and broadcast update
## WebSocket Events
- **Connection URL**: `ws://localhost:3001/counter-ws` (local) or `wss://dummy7.ddns.net/counter-ws` (external)
- `counter-update` - Broadcast when counter changes with new value
## Local Development
### Prerequisites
- Node.js (v14 or higher)
- npm
### Installation
1. Clone or download the project files
2. Install dependencies:
```bash
npm install
```
### Running the Application
Start both the React dev server and Express API server concurrently:
```bash
npm start
```
This will:
- Start the Express server on `http://127.0.0.1:3001` (internal only)
- Start the Webpack dev server with HMR on `http://127.0.0.1:3000` (internal only)
- Open the app in your default browser
### External Access via Nginx Proxy
The servers listen only on `127.0.0.1` for security. External access is handled through Nginx proxy:
- **Local development**: `http://localhost:3000` or `http://127.0.0.1:3000`
- **External access**: `https://dummy7.ddns.net` (proxied through Nginx)
The WebSocket connections automatically use the correct protocol based on the current domain:
- When accessed via `https://dummy7.ddns.net` → connects to `wss://dummy7.ddns.net`
- Nginx proxies WebSocket connections to the internal `ws://127.0.0.1:3001`
### Manual Commands
Run servers separately if needed:
```bash
# Start only the API/WebSocket server
npm run server
# Start only the React dev server
npm run dev
# Build for production
npm run build
```
## Testing Real-time Syncing
1. Open `http://127.0.0.1:3000` in multiple browser tabs/windows
2. Click the "Increment" button in any tab
3. Observe the counter update in real-time across all tabs
## Production Deployment
### Using Nginx Proxy
Use the provided `nginx.conf` snippet to proxy requests:
1. Copy the server block from `nginx.conf` to your Nginx configuration
2. Update `server_name` to your domain (e.g., `dummy7.ddns.net`)
3. For HTTPS, uncomment and configure SSL certificate paths
4. Adjust VSCode server port if different (default: 8080)
5. Reload Nginx: `sudo nginx -s reload`
### External Access
The app will be accessible at `https://dummy7.ddns.net` (configure SSL as needed).
### Important Notes
- The counter is stored in memory and resets on server restart
- WebSocket connections are handled on the Express server (port 3001)
- The React app connects to WebSocket at `ws://127.0.0.1:3001`
## Project Structure
```
├── src/
│ ├── index.js # React app entry point
│ └── App.js # Main counter component
├── public/
│ └── index.html # HTML template
├── server.js # Express API/WebSocket server
├── webpack.config.js # Webpack configuration with HMR
├── package.json # Dependencies and scripts
├── nginx.conf # Nginx proxy configuration
└── README.md # This file
```
## Technologies Used
- **Frontend**: React 18, Webpack 5, Babel
- **Backend**: Express.js, WebSocket (ws library)
- **Development**: Webpack Dev Server, Hot Module Replacement
- **Deployment**: Nginx proxy
## Troubleshooting
- **Port conflicts**: Ensure ports 3000 and 3001 are available
- **WebSocket connection issues**: Check that the Express server is running on port 3001
- **HMR not working**: Verify Webpack dev server is running and browser console for errors
- **API calls failing**: Confirm the proxy configuration in `webpack.config.js`
- **"Invalid host header" error**: Added `allowedHosts: 'all'` and `client.webSocketURL: 'auto://0.0.0.0:0/ws'` in webpack config to allow proxy access and automatic HMR WebSocket URL detection
- **WebSocket connection failures when accessing externally**: WebSocket URL is now dynamic based on current host (supports both HTTP/WS and HTTPS/WSS)
- **External domain access**: Use the provided Nginx configuration to proxy requests properly - servers listen on 127.0.0.1 only
- **Nginx WebSocket proxy**: Ensure Nginx is configured with proper WebSocket upgrade headers for `/ws` location
## Future Enhancements
- Add user authentication
- Implement persistent storage (database)
- Add multiple counters/rooms
- Include user presence indicators
- Add counter reset functionality

24
nginx.conf Normal file
View File

@@ -0,0 +1,24 @@
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# WebSocket proxy to Express server on port 3001
location /counter-ws {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

6588
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "webpack-hmr-react-app",
"version": "1.0.0",
"description": "React app with Webpack HMR, WebSocket syncing, and API",
"main": "server.js",
"scripts": {
"start": "concurrently \"npm run server\" \"npm run dev\"",
"dev": "webpack serve --mode development --open",
"build": "webpack --mode production",
"server": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"ws": "^8.14.2"
},
"devDependencies": {
"@babel/core": "^7.23.0",
"@babel/preset-env": "^7.22.20",
"@babel/preset-react": "^7.22.15",
"babel-loader": "^9.1.3",
"concurrently": "^8.2.1",
"html-webpack-plugin": "^5.5.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}
}

11
public/index.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Webpack HMR React Counter App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

51
server.js Normal file
View File

@@ -0,0 +1,51 @@
const express = require('express');
const WebSocket = require('ws');
const http = require('http');
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server, path: '/counter-ws' });
let counter = 0; // In-memory counter (resets on server restart)
app.use(express.json());
// API endpoint to get current counter value
app.get('/api/counter', (req, res) => {
res.json({ value: counter });
});
// API endpoint to increment counter
app.post('/api/counter/increment', (req, res) => {
counter += 1;
res.json({ value: counter });
// Broadcast update to all connected WebSocket clients
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ type: 'counter-update', value: counter }));
}
});
});
// WebSocket connection handling
wss.on('connection', (ws) => {
console.log('Client connected');
ws.on('message', (message) => {
console.log('Received:', Buffer.from(message).toString());
});
ws.on('error', (error) => {
console.log('WebSocket error:', error);
});
ws.on('close', (code, reason) => {
console.log('Client disconnected', 'Code:', code, 'Reason:', reason.toString());
});
});
const PORT = 3001; // API and WebSocket server port
server.listen(PORT, '127.0.0.1', () => {
console.log(`Server running on http://127.0.0.1:${PORT}`);
});

70
src/App.js Normal file
View File

@@ -0,0 +1,70 @@
import React, { useState, useEffect } from 'react';
function App() {
const [counter, setCounter] = useState(0);
const [ws, setWs] = useState(null);
useEffect(() => {
// Fetch initial counter value
fetch('/api/counter')
.then(response => response.json())
.then(data => setCounter(data.value))
.catch(error => console.error('Error fetching counter:', error));
// Connect to WebSocket - use dynamic URL based on current location
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const websocket = new WebSocket(`${protocol}//${window.location.host}/counter-ws`);
setWs(websocket);
websocket.onopen = () => {
console.log('WebSocket connected');
};
websocket.onerror = (error) => {
console.log('WebSocket error:', error);
};
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'counter-update') {
setCounter(data.value);
}
};
websocket.onclose = (event) => {
console.log('WebSocket disconnected', 'Code:', event.code, 'Reason:', event.reason);
};
return () => {
websocket.close();
};
}, []);
const incrementCounter = () => {
fetch('/api/counter/increment', {
method: 'POST',
})
.then(response => response.json())
.then(data => {
setCounter(data.value);
// Broadcast update via WebSocket
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'counter-update', value: data.value }));
}
})
.catch(error => console.error('Error incrementing counter:', error));
};
return (
<div style={{ textAlign: 'center', marginTop: '50px' }}>
<h1>Shared Counter Demo</h1>
<h2>Counter: {counter}</h2>
<button onClick={incrementCounter} style={{ fontSize: '20px', padding: '10px 20px' }}>
Increment
</button>
<p>Open this page in multiple tabs to see real-time syncing!</p>
</div>
);
}
export default App;

6
src/index.js Normal file
View File

@@ -0,0 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

51
webpack.config.js Normal file
View File

@@ -0,0 +1,51 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
resolve: {
extensions: ['.js', '.jsx'],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
})
],
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
compress: true,
port: 3000,
host: '127.0.0.1', // Listen only on localhost, external access via Nginx proxy
hot: true,
historyApiFallback: true,
allowedHosts: 'all', // Allow all hosts to prevent "invalid host header" error
client: {
webSocketURL: 'auto://0.0.0.0:0/ws'
},
proxy: {
'/api': 'http://127.0.0.1:3001', // Proxy API calls to Express server
},
},
};