Code Repository

gitee

Creating a Project

First, ensure Node.js is installed, then create a project using Vite.
vite

1
2
3
npm create vite react-learn
cd react-learn
npm i

Directory Structure

A complete front-end project requires:

  • State Management
    Maintain shared state (data) globally to enable data sharing between page components. We use Pinia.
  • Routing
    Routing allows navigation between pages. We use Vue Router.
  • Styling
    Styling makes the pages more visually appealing. We use TailwindCSS.
  • Network Requests
    The front end needs to interact with the back end via network requests to implement functionality. We use Axios.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
├── dev-dist/               # Development environment build output directory
├── node_modules/ # Node.js dependencies directory
├── public/ # Static resources directory, not processed by build tools
├── src/
├── admin/ # Contains admin pages
├── api/ # API request logic
├── assets/ # Static resources (images, fonts, etc.)
├── components/ # Common components
├── includes/ # Include files, external libraries
├── lib/ # Project-specific common resources
├── locales/ # Internationalization language packs
├── mocks/ # Mock data
├── pages/ # Contains regular page components
├── router/ # Routing configuration
├── store/ # State management (Pinia)
├── styles/ # Global styles or CSS files
├── utils/ # Utility functions
├── App.jsx # Root component
├── main.js # Project entry file
├── middleware.js # Middleware logic (e.g., route guards)
└── settings.js # Project settings or configuration files
├── .gitignore # Git ignore file configuration
├── index.html # Project entry HTML file
├── package.json # Project configuration and dependencies
├── postcss.config.js # PostCSS configuration file
├── README.md # Project documentation
├── tailwind.config.js # Tailwind CSS configuration file
├── vite.config.js # Vite build tool configuration file

Configuration

Path Aliases

Configure path aliases to use @/ to represent the src/ directory.

1
2
# Node type declarations to prevent errors when using Node dependencies
pnpm i @types/node --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
import {defineConfig} from 'vite'
import {join} from 'path';
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
// Path aliases
resolve: {
alias: {
'@': join(__dirname, 'src'),
}
}
})

PWA Configuration

1
2
# v1
pnpm i vite-plugin-pwa --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import {defineConfig} from 'vite'
import {join} from 'path';
import react from '@vitejs/plugin-react'
import {VitePWA} from "vite-plugin-pwa";
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
// Generate manifest file
enabled: true
},
manifest: {
name: "vue-quick-start",
theme_color: '#ff5e3a',
icons: [
{
src: 'assets/logo.png',
size: '192x192',
type: 'image/png'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,png,jpg}']
}
})
],
resolve: {
alias: {
'@': join(__dirname, 'src'),
}
}
})

Project Settings

1
2
3
4
5
6
// settings.js
export default {
routeMode: 'history', // Routing mode
BaseURL: 'http://localhost:4000', // Backend request URL
timeout: 5000, // Request timeout
}

Tailwind

Use Tailwind 3 (if you want to use version 4, there are some installation differences).

1
2
pnpm install -D tailwindcss@3 postcss autoprefixer
pnpm dlx tailwindcss@3 init -p

tailwind.config.js

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

styles/base.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@tailwind base;
@layer base {
h1 {
@apply text-2xl;
}

h2 {
@apply text-xl;
}

h3 {
@apply text-lg;
}

h4 {
@apply text-base;
}

h5 {
@apply text-sm;
}

h6 {
@apply text-xs;
}
}

@tailwind components;
@tailwind utilities;
body {
@apply h-full w-full p-0 m-0;
}

/* Override default max-width limit */
@media (min-width: 1536px) {
.container {
max-width: 100%;
}
}

/* Main content of the page */
.container {
width: 100%;
height: 100%;
background-color: #f5f7fb;
padding: 20px;

.container-wrapper {
width: 100%;
height: 100%;
padding: 15px 30px 0;
background-color: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
}

/* Print control styles */
.plugin-download {
width: 500px !important;

a:hover {
text-decoration: underline;
}
}

Remember to import in src/main.js:

1
2
3
4
5
6
7
8
9
10
import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'
import App from './App.jsx'
import '@/styles/base.css'

createRoot(document.getElementById('root')).render(
<StrictMode>
<App/>
</StrictMode>,
)

Routing (Router)

Use react-router-dom for routing and implement file-based routing, automatically scanning .jsx files in the
directory and registering them as routes. There are two page directories: admin for admin pages and pages for
regular page components.

1
2
# v7
pnpm i react-router-dom@7

File Routing

Native Approach

File routing automatically scans and registers routes based on the directory structure without manual declarations. The
key is:

1
import.meta.glob("xxx")

This is provided by Vite to scan and retrieve files. Webpack has a similar method:

1
require.context()

Since we are using Vite, we can use import.meta.glob. Now, let’s implement the file scanning functionality.

Plugin Approach

Use vite-plugin-pages to achieve file-based route registration.

1
2
npm install -D vite-plugin-pages
npm install react-router react-router-dom

vite.config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
import {VitePWA} from "vite-plugin-pwa";
import {join} from 'path';
import pages from 'vite-plugin-pages'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
///
/*pages({
// Custom configuration
// dir: 'src/pages', // Route component directory
extensions: ['jsx', 'tsx'], // Supported file extensions
// exclude: ['components'], // Exclude component directories
base: process.env.VITE_APP_BASE_URL
}),*/
pages({
// Register multiple directories with different route prefixes
dirs: [
// Basic
{dir: 'src/pages', baseRoute: ''},
// With custom file pattern
{dir: 'src/admin/', baseRoute: 'admin'},
],
}),
],
resolve: {
alias: {
'@': join(__dirname, 'src'),
}
},
})

src/components/router-guard.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import {matchPath, Route, Routes, useLocation, useNavigate, useRoutes} from "react-router-dom";
import {Suspense, useEffect} from "react";
import routes from "~react-pages";
import Login from "../pages/login.jsx";
import NotFount from "../pages/not-fount.jsx";

const isAuthenticated = () => {
return localStorage.getItem("token") !== null; // Example logic
};

// Global route component
function RouterGuard() {
const navigate = useNavigate();
const location = useLocation();
// Add requiresAuth property
const toNeedAuth = (r) => {
const route = {...r, requiresAuth: true};
if (route.children) {
route.children = route.children.map(c => toNeedAuth(c));
}
return route;
}
const authRoutes = routes.map(toNeedAuth)
useEffect(() => {
/*
// Cannot match nested routes
const currentRoute = authRoutes.find(route =>
matchPath({ path: route.path, end: true }, location.pathname)
);
console.log(currentRoute)
*/
console.log(location.pathname)
// Global navigation guard logic
/*
if (currentRoute?.requiresAuth && !isAuthenticated()) {
navigate("/login", { replace: true });
}
*/
// Add more pages that do not require authentication later
if (location.pathname !== '/login' && !isAuthenticated()) {
navigate("/login", {replace: true});
}
}, [location, navigate]); // Listen for route changes
return (
<Suspense fallback={<p>Loading...</p>}>
{useRoutes(
[
...authRoutes,
{
path: 'login',
element: <Login/>
},
{
path: "*",
element: <NotFount/>
}
]
)}
</Suspense>
);
}

export default RouterGuard;

app.jsx

1
2
3
4
5
6
7
8
9
import RouterGuard from "./components/router-guard.jsx";

function App() {
return (
<RouterGuard/>
)
}

export default App

main.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'
import App from './App.jsx'
import '@/styles/base.css'
import {BrowserRouter} from "react-router-dom";

createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</StrictMode>,
)

State Management (Store)

When learning React, you may find that passing values between components, especially siblings or across levels, can be
cumbersome. We can use a global state management solution to address this. Here, we use Zustand.

1
pnpm i zustand

src/store/count/index.js

1
2
3
4
5
6
7
import {create} from 'zustand'

export const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({count: state.count + 1})),
reset: () => set({count: 0}),
}))

Test in /pages/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from 'react';
import {useCountStore} from "../store/count/index.js";

const Index = () => {
const {count, increment, reset} = useCountStore()
localStorage.setItem("token", "123")
return (
<div className={`flex items-center flex-col`}>
<div className={``}>
{count}
</div>
<div className={`flex gap-x-4`}>
<button
className={`bg-blue-400 py-1 px-2 rounded-lg`}
onClick={() => increment()}>+1
</button>
<button
className={`bg-blue-400 py-1 px-2 rounded-lg`}
onClick={() => reset()}>reset
</button>
</div>
</div>
);
};
export default Index;

Network Requests (API)

Use Axios for network requests. Typically, front-end and back-end development is done by different teams. The front-end
request URLs are provided by the back-end. If your back-end is not ready yet, you can use mock data or json-server for
testing.

1
2
# v1
pnpm i axios

Wrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// request.js
import axios from 'axios'
import settings from "../settings.js";

const request = axios.create({
baseURL: settings.BaseURL, // Set base URL
timeout: settings.timeout, // Request timeout
})
// Request interceptor
request.interceptors.request.use(
(config) => {
// Do something before sending the request, e.g., add a token
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
(error) => {
// Do something with request error
return Promise.reject(error)
}
)
// Response interceptor
request.interceptors.response.use(
(response) => {
// Process response data
return response.data
},
(error) => {
// Process response error
return Promise.reject(error)
}
)
export default request

Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
* @Author Malred · Wang
* @Date 2025-06-23 16:42:29
* @Description
* @Path src/pages/login.jsx
*/
import React, {useState} from 'react';
import auth from '@/api/auth/index.js'
import {useNavigate} from "react-router-dom";

const Login = () => {
const navigate = useNavigate();
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
return (
<div className={`flex items-center flex-col`}>
<h1>login</h1>
<form
onSubmit={async (e) => {
e.preventDefault(); // Prevent default submission behavior
const res = await auth.login({username, password})
localStorage.setItem('token', res.token)
navigate('/')
}}
className={`flex flex-col gap-y-2`}
>
Account
<input className={`px-2 py-1 border rounded-md`} type="text" value={username}
onChange={(e) => setUsername(e.target.value)}/>
Password
<input className={`px-2 py-1 border rounded-md`} type="password" value={password}
onChange={(e) => setPassword(e.target.value)}/>
<button
type={"submit"}
className={`text-white bg-blue-400 py-1 px-2 rounded-lg`}
>
Login
</button>
</form>
</div>
);
};
export default Login;

You can refer to my previous backend web monolithic teaching code templates:
rust-web-starter
go-web-starter

Next Steps

  • Simulate interfaces with json-server
  • Code generator, batch generate repetitive pages and code

Community

You can contact me on these platforms:


本站总访问量