从零开始一个完整的全栈项目(8) - 搭建前端框架

194 阅读6分钟

完成了用户注册的后端功能之后,我们也就完成了:

  • 数据库:数据库搭建
  • 后端:后端项目整体搭建
  • 后端功能测试:增加用户注册功能
    等功能。

现在开始完成如下功能:

  • 前端项目搭建
  • 添加一个测试用的页面(用户注册页面)

这篇文章主要介绍前端React项目的框架搭建


1. 用 create-react-app 创建React项目。

首先,还是要用到React官方推荐的项目生成工具,生成项目大体框架。具体指令为:

npx create-react-app . --template typescript

然后安装一些必要的依赖:

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material react-router-dom axios @reduxjs/toolkit react-redux

创建必要的目录(也可自己手动创建):

mkdir -p src/{components,pages,services,store,utils,types,layouts,assets}

2. 创建一些基本的文件

首先放上最终的文件结构图:

image.png

1. 创建一个基本的布局组件(MainLayout.tsx

import React from 'react';
import { Box, Drawer, AppBar, Toolbar, Typography, List, ListItem, ListItemIcon, ListItemText, IconButton } from '@mui/material';
import { 
  Menu as MenuIcon,
  Dashboard, 
  Inventory, 
  People, 
  LocalShipping, 
  Settings,
  ShoppingCart,
  AddBox,
  Person,
  Assignment
} from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';

const drawerWidth = 240;

interface MainLayoutProps {
  children: React.ReactNode;
}

const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
  const navigate = useNavigate();
  const [mobileOpen, setMobileOpen] = React.useState(false);

  const menuItems = [
    { text: 'Dashboard', icon: <Dashboard />, path: '/' },
    { text: 'Products', icon: <Inventory />, path: '/products' },
    { text: 'Orders', icon: <ShoppingCart />, path: '/orders' },
    { text: 'Inbound', icon: <AddBox />, path: '/inbound' },
    { text: 'Customers', icon: <Person />, path: '/customers' },
    { text: 'Delivery', icon: <LocalShipping />, path: '/delivery' },
    { text: 'Users', icon: <People />, path: '/users' },
    { text: 'Reports', icon: <Assignment />, path: '/reports' },
    { text: 'Settings', icon: <Settings />, path: '/settings' },
  ];

  const drawer = (
    <div>
      <Toolbar>
        <Typography variant="h6" noWrap component="div">
          QuickStore
        </Typography>
      </Toolbar>
      <List>
        {menuItems.map((item) => (
          <ListItem 
            key={item.text} 
            onClick={() => navigate(item.path)}
            sx={{ cursor: 'pointer' }}
          >
            <ListItemIcon>{item.icon}</ListItemIcon>
            <ListItemText primary={item.text} />
          </ListItem>
        ))}
      </List>
    </div>
  );

  return (
    <Box sx={{ display: 'flex' }}>
      <AppBar
        position="fixed"
        sx={{
          width: { sm: `calc(100% - ${drawerWidth}px)` },
          ml: { sm: `${drawerWidth}px` },
        }}
      >
        <Toolbar>
          <IconButton
            color="inherit"
            edge="start"
            onClick={() => setMobileOpen(!mobileOpen)}
            sx={{ mr: 2, display: { sm: 'none' } }}
          >
            <MenuIcon />
          </IconButton>
          <Typography variant="h6" noWrap component="div">
            Aluminum Warehouse Management
          </Typography>
        </Toolbar>
      </AppBar>
      <Box
        component="nav"
        sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
      >
        <Drawer
          variant="temporary"
          open={mobileOpen}
          onClose={() => setMobileOpen(false)}
          ModalProps={{
            keepMounted: true,
          }}
          sx={{
            display: { xs: 'block', sm: 'none' },
            '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
          }}
        >
          {drawer}
        </Drawer>
        <Drawer
          variant="permanent"
          sx={{
            display: { xs: 'none', sm: 'block' },
            '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
          }}
          open
        >
          {drawer}
        </Drawer>
      </Box>
      <Box
        component="main"
        sx={{
          flexGrow: 1,
          p: 3,
          width: { sm: `calc(100% - ${drawerWidth}px)` },
          mt: '64px',
        }}
      >
        {children}
      </Box>
    </Box>
  );
};

export default MainLayout; 

2. 创建一个基本的页面组件(Dashboard.tsx

import React from 'react';
import { Grid, Paper, Typography, Box } from '@mui/material';
import { 
  Inventory, 
  ShoppingCart, 
  LocalShipping, 
  TrendingUp,
  Person,
  Assignment
} from '@mui/icons-material';

const Dashboard: React.FC = () => {
  const stats = [
    { 
      title: 'Total Products', 
      value: '156', 
      icon: <Inventory />, 
      color: '#1976d2',
      description: 'Different types of aluminum products'
    },
    { 
      title: 'Pending Orders', 
      value: '23', 
      icon: <ShoppingCart />, 
      color: '#2e7d32',
      description: 'Orders waiting for processing'
    },
    { 
      title: 'Delivery Tasks', 
      value: '8', 
      icon: <LocalShipping />, 
      color: '#ed6c02',
      description: 'Pending deliveries'
    },
    { 
      title: 'Active Customers', 
      value: '45', 
      icon: <Person />, 
      color: '#9c27b0',
      description: 'Regular customers'
    },
    { 
      title: 'Monthly Sales', 
      value: '$45,678', 
      icon: <TrendingUp />, 
      color: '#d32f2f',
      description: 'Total sales this month'
    },
    { 
      title: 'Low Stock Items', 
      value: '12', 
      icon: <Assignment />, 
      color: '#7b1fa2',
      description: 'Products need restocking'
    },
  ];

  return (
    <Box>
      <Typography variant="h4" gutterBottom>
        Dashboard
      </Typography>
      <Grid container spacing={3}>
        {stats.map((stat) => (
          <Grid 
            key={stat.title}
            sx={{
              width: {
                xs: '100%',
                sm: '50%',
                md: '33.33%'
              }
            }}
          >
            <Paper
              sx={{
                p: 2,
                display: 'flex',
                flexDirection: 'column',
                height: 160,
                bgcolor: stat.color,
                color: 'white',
              }}
            >
              <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
                <Typography variant="h6" component="div">
                  {stat.title}
                </Typography>
                {stat.icon}
              </Box>
              <Typography variant="h4" component="div" sx={{ mt: 2 }}>
                {stat.value}
              </Typography>
              <Typography variant="body2" sx={{ mt: 1, opacity: 0.8 }}>
                {stat.description}
              </Typography>
            </Paper>
          </Grid>
        ))}
      </Grid>
    </Box>
  );
};

export default Dashboard; 

3. 更新APP.tsx来设置路由和布局

import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material';
import MainLayout from './layouts/MainLayout';
import Dashboard from './pages/Dashboard';

// 临时占位组件,后续会替换为实际页面组件
const PlaceholderPage = () => <div>Page under construction</div>;

const theme = createTheme({
  palette: {
    primary: {
      main: '#1976d2',
    },
    secondary: {
      main: '#dc004e',
    },
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Router>
        <MainLayout>
          <Routes>
            <Route path="/" element={<Dashboard />} />
            <Route path="/products" element={<PlaceholderPage />} />
            <Route path="/orders" element={<PlaceholderPage />} />
            <Route path="/inbound" element={<PlaceholderPage />} />
            <Route path="/customers" element={<PlaceholderPage />} />
            <Route path="/delivery" element={<PlaceholderPage />} />
            <Route path="/users" element={<PlaceholderPage />} />
            <Route path="/reports" element={<PlaceholderPage />} />
            <Route path="/settings" element={<PlaceholderPage />} />
          </Routes>
        </MainLayout>
      </Router>
    </ThemeProvider>
  );
}

export default App;

4. 更新index.css,设置一些基本的格式

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: #f5f5f5;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

* {
  box-sizing: border-box;
}


至此,我们就搭建好了一个基本的项目框架。它包含了:

  1. 使用Typescript的React项目
  2. Material-UI组件库
  3. React Router用于路由管理
  4. 基本的项目结构如下:
src/
├── assets/          # 静态资源
├── components/      # 可重用组件
├── layouts/         # 布局组件
├── pages/           # 页面组件
├── services/        # API 服务
├── store/           # Redux store
├── types/           # TypeScript 类型定义
├── utils/           # 工具函数
├── App.tsx          # 主应用组件
├── index.css        # 全局样式
├── index.tsx        # 应用入口
└── react-app-env.d.ts  # TypeScript 声明文件

主要功能有:

  1. 响应式侧边栏导航
  2. 仪表盘页面,显示关键指标
  3. 主题定制
  4. 路由系统

因为我们这是一个仓库管理系统,根据前面文章的建表语句,左侧导航栏我们设计如下:

  1. Dashboard(仪表盘)
    • 显示关键业务指标
    • 包括产品总数、待处理订单、配送任务等
  2. Products(产品管理)
    • 铝合金产品的列表
    • 产品详情(规格、单位、价格、库存等)
    • 库存管理功能
  3. Orders(订单管理)
    • 订单列表
    • 订单详情
    • 订单状态管理(pending/fulfilled/canceled)
    • 区分自取和配送订单
  4. Inbound(入库管理)
    • 入库记录
    • 新增入库
    • 入库历史查询
  5. Customers(客户管理)
    • 客户列表
    • 客户详情
    • 客户订单历史
  6. Delivery(配送管理)
    • 配送任务列表
    • 配送状态跟踪
    • 配送记录管理
  7. Users(用户管理)
    • 用户列表(admin/staff/warehouse)
    • 用户权限管理
    • 用户操作记录
  8. Reports(报表)
    • 销售报表
    • 库存报表
    • 配送报表
    • 客户分析
  9. Settings(系统设置)
    • 系统配置
    • 权限设置
    • 其他设置

每个页面都会根据用户角色(admin/staff/warehouse)显示不同的功能和数据。


运行项目

进入项目所在文件夹,输入指令:

npm start

即可启动项目。

如果成功,即可访问:localhost:3000看到页面。

image.png


补充一些关于.gitignore文件的知识。
.gitignore需要忽略的通常为以下文件:

  1. 依赖目录 (node_modules)
  2. 构建输出目录 (build)
  3. 环境变量文件 (.env)
  4. IDE 配置文件
  5. 操作系统生成的文件
  6. 日志文件
  7. 测试覆盖率报告

源码如下:

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.env

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# IDE
.idea/
.vscode/
*.swp
*.swo

# TypeScript
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

.gitignore文件是否应该被上传到github。
需要上传到git服务器。这有助于:

  1. 其他开发者克隆项目时,会立即知道哪些文件不需要被追踪
  2. 确保团队所有成员都使用相同的忽略规则
  3. 防止有人不小心提交了不应该提交的文件

所以其他人clone项目的时候,也应该把.gitignore文件clone下来。


.json文件是否应上传github?
需要上传到git服务器。
前端项目中的几个.json文件为:

  1. package-lock.json

    • 必须上传
    • 作用:锁定所有依赖包的具体版本号
    • 重要性:确保所有开发者使用完全相同的依赖版本,避免"在我机器上能运行"的问题
    • 如果不提交:其他开发者可能安装到不同版本的依赖,导致项目运行不一致
  2. package.json

    • 必须上传
    • 作用:定义项目的基本信息、依赖包、脚本命令等
    • 重要性:其他开发者需要知道项目依赖和可用的命令
    • 如果不提交:其他开发者无法知道项目需要哪些依赖
  3. tsconfig.json

    • 必须上传
    • 作用:TypeScript 的配置文件
    • 重要性:确保所有开发者使用相同的 TypeScript 编译设置
    • 如果不提交:可能导致类型检查结果不一致
  4. .eslintrc.json(如果有)

    • 建议上传
    • 作用:ESLint 的配置文件
    • 重要性:保持代码风格一致
    • 如果不提交:可能导致代码风格不一致
  5. .prettierrc.json(如果有)

    • 建议上传
    • 作用:Prettier 的配置文件
    • 重要性:保持代码格式化规则一致
    • 如果不提交:可能导致代码格式化不一致

不需要上传的 JSON 文件:

  1. 包含敏感信息的配置文件(如包含 API 密钥、密码等)
  2. 本地开发环境的特定配置
  3. 临时生成的 JSON 文件

前端框架搭建完成。
下一篇,编写用户注册页面。