genesis
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
150
README.md
Normal file
150
README.md
Normal 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
24
nginx.conf
Normal 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
6588
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal 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
11
public/index.html
Normal 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
51
server.js
Normal 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
70
src/App.js
Normal 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
6
src/index.js
Normal 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
51
webpack.config.js
Normal 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
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user