从零到一构建 Umi+React 企业级项目实战指南(附 3000 字踩坑全记录)

380 阅读4分钟

一、环境准备与项目初始化

1.1 脚手架安装与项目创建

bash

复制

# 安装 Umi 脚手架
yarn global add umi

# 创建项目(推荐使用 pnpm 避免幽灵依赖)
pnpm create @umijs/umi-app agent-manage

# 进入项目目录
cd agent-manage

# 安装基础依赖
pnpm install

1.2 初始目录结构解析

bash

复制

├── config
│   └── config.ts    # Umi 核心配置文件
├── mock             # Mock 数据目录
├── src
│   ├── assets       # 静态资源
│   ├── components   # 公共组件
│   ├── layouts      # 全局布局
│   ├── models       # Dva 模型
│   ├── pages        # 页面组件
│   └── utils        # 工具类
└── .umirc.ts        # Umi 运行时配置

1.3 首坑预警:Node 版本兼容问题

现象:安装依赖时出现 gyp ERR 错误
原因:Umi 4.x 需要 Node 16+ 环境
解决方案

bash

复制

# 使用 nvm 管理 Node 版本
nvm install 16.14.0
nvm use 16.14.0

二、工程化配置实战

2.1 编辑器规范统一

.vscode/settings.json 配置:

json

复制

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "prettier.configPath": ".prettierrc"
}

2.2 代理配置与跨域处理

config/config.ts 核心配置:

typescript

复制

export default {
  proxy: {
    '/api': {
      target: 'http://backend-service.com',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },
      // 解决 Cookie 丢失问题
      onProxyRes: function (proxyRes) {
        const cookies = proxyRes.headers['set-cookie'];
        if (cookies) {
          proxyRes.headers['set-cookie'] = cookies.map(cookie => 
            cookie.replace(/; secure/gi, '').replace(/; SameSite=None/gi, '')
          );
        }
      }
    }
  }
}

2.3 路径别名配置

tsconfig.json 配置:

json

复制

{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

config/config.ts 同步配置:

typescript

复制

import { defineConfig } from 'umi';

export default defineConfig({
  alias: {
    '@': path.resolve(__dirname, 'src'),
    '@components': path.resolve(__dirname, 'src/components'),
    '@utils': path.resolve(__dirname, 'src/utils')
  }
});

三、移动端适配方案

3.1 高清方案配置

安装 PostCSS 插件:

bash

复制

pnpm add postcss-pxtorem postcss-flexbugs-fixes -D

config/config.ts 配置:

typescript

复制

export default {
  extraPostCSSPlugins: [
    require('postcss-flexbugs-fixes'),
    require('postcss-pxtorem')({
      rootValue: 75, // 750 设计稿对应 75
      propList: ['*'],
      selectorBlackList: ['am-'] // 排除 antd-mobile 组件
    })
  ]
}

3.2 动态 REM 方案

src/utils/rem.js

javascript

复制

const baseSize = 75; // 与 postcss 配置一致
function setRem() {
  const scale = document.documentElement.clientWidth / 750;
  document.documentElement.style.fontSize = 
    baseSize * Math.min(scale, 2) + 'px';
}

window.addEventListener('resize', setRem);
setRem();

四、请求层封装实战

4.1 umi-request 二次封装

src/utils/request.ts

typescript

复制

import { extend } from 'umi-request';

const request = extend({
  prefix: '/api',
  timeout: 30000,
  errorHandler: (error) => {
    console.error('Request Error:', error);
    throw error;
  }
});

// 请求拦截器
request.interceptors.request.use((url, options) => {
  const token = localStorage.getItem('ACCESS_TOKEN');
  return {
    url,
    options: {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json'
      }
    }
  };
});

// 响应拦截器
request.interceptors.response.use(async (response) => {
  const data = await response.clone().json();
  if (data.code !== 200) {
    throw new Error(data.message || 'Request Error');
  }
  return response;
});

export default request;

4.2 业务接口组织

src/services/agent.ts

typescript

复制

import request from '@/utils/request';

export async function fetchAgentList(params: API.ListParams) {
  return request<API.Response<API.AgentItem[]>>('/agent/list', {
    method: 'POST',
    data: params
  });
}

export async function deleteAgent(id: string) {
  return request(`/agent/${id}`, { method: 'DELETE' });
}

五、复杂列表页开发

5.1 Class 组件实现

tsx

复制

import { PullToRefresh, List, InfiniteScroll } from 'antd-mobile';

class AgentList extends React.Component {
  state = {
    data: [],
    page: 1,
    hasMore: true
  };

  async componentDidMount() {
    await this.loadData();
  }

  loadData = async () => {
    const { page, data } = this.state;
    const res = await fetchAgentList({ page });
    this.setState({
      data: data.concat(res.data.list),
      hasMore: res.data.hasMore
    });
  };

  render() {
    return (
      <PullToRefresh onRefresh={this.handleRefresh}>
        <List>
          {this.state.data.map(item => (
            <List.Item key={item.id}>{item.name}</List.Item>
          ))}
        </List>
        <InfiniteScroll
          loadMore={this.loadData}
          hasMore={this.state.hasMore}
        />
      </PullToRefresh>
    );
  }
}

5.2 Hooks 重构方案

tsx

复制

import { useState, useEffect } from 'react';

function AgentList() {
  const [data, setData] = useState<API.AgentItem[]>([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

  const loadData = async (isRefresh = false) => {
    const currentPage = isRefresh ? 1 : page;
    try {
      const res = await fetchAgentList({ page: currentPage });
      setData(prev => 
        isRefresh ? res.data.list : [...prev, ...res.data.list]
      );
      setHasMore(res.data.hasMore);
      setPage(currentPage + 1);
    } catch (error) {
      console.error('加载失败:', error);
    }
  };

  useEffect(() => {
    loadData(true);
  }, []);

  return (
    <PullToRefresh onRefresh={() => loadData(true)}>
      <List>
        {data.map(item => (
          <List.Item key={item.id}>{item.name}</List.Item>
        ))}
      </List>
      <InfiniteScroll
        loadMore={() => loadData()}
        hasMore={hasMore}
      />
    </PullToRefresh>
  );
}

5.3 性能优化技巧

tsx

复制

// 使用 React.memo 优化列表项
const MemoListItem = React.memo(({ item }) => (
  <List.Item>
    <Avatar src={item.avatar} />
    <span>{item.name}</span>
  </List.Item>
));

// 虚拟滚动优化(针对长列表)
import { VirtualList } from 'antd-mobile';

function LongList() {
  return (
    <VirtualList
      data={data}
      itemHeight={80}
      renderItem={(item, index) => (
        <MemoListItem item={item} />
      )}
    />
  );
}

六、环境变量与多环境配置

6.1 环境区分方案

.umirc.prod.ts 生产环境配置:

typescript

复制

import { defineConfig } from 'umi';

export default defineConfig({
  define: {
    API_BASE: 'https://prod-api.example.com'
  },
  // 开启压缩
  jsMinifier: 'terser',
  // 关闭 sourcemap
  devtool: false
});

6.2 动态加载配置

src/utils/env.ts

typescript

复制

export const getEnvConfig = () => {
  switch(process.env.NODE_ENV) {
    case 'development':
      return { API_BASE: '/api' };
    case 'production':
      return { API_BASE: 'https://prod-api.example.com' };
    default:
      return { API_BASE: window.location.origin };
  }
};

七、典型踩坑记录

7.1 样式污染问题

现象:antd-mobile 组件样式被全局覆盖
解决方案

typescript

复制

// config/config.ts
export default {
  mobile: {
    // 关闭移动端自适应
    adaptive: false,
    // 开启 CSS Modules
    cssModules: true
  }
}

7.2 页面刷新白屏

现象:路由跳转后刷新页面出现 404
原因:History 路由模式需要服务端支持
解决方案

typescript

复制

export default {
  history: {
    type: 'hash' // 改为 hash 路由
  }
}

7.3 图片加载失败

现象:生产环境图片路径错误
解决方案

typescript

复制

// 使用 require 引入本地资源
<img src={require('@/assets/logo.png')} />

// 动态拼接 CDN 地址
const getImageUrl = (path) => 
  `${process.env.IMAGE_CDN}${path}`;

八、项目优化实践

8.1 构建速度优化

typescript

复制

// config/config.ts
export default {
  mfsu: {
    strategy: 'eager', // 开启 MFSU
  },
  // 按需编译
  extraBabelPresets: [
    ['babel-preset-react-app', { runtime: 'automatic' }]
  ]
}

8.2 首屏加载优化

typescript

复制

// 动态加载组件
import dynamic from 'umi/dynamic';

const Chart = dynamic({
  loader: () => import('@/components/Chart'),
  loading: () => <Loading />
});

// 预加载关键资源
<link rel="preload" href="/fonts/iconfont.woff2" as="font" />

九、项目部署指南

9.1 Docker 部署配置

Dockerfile

dockerfile

复制

FROM node:16-alpine as builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

9.2 Nginx 配置模板

nginx.conf

nginx

复制

server {
  listen 80;
  gzip on;
  
  location / {
    root   /usr/share/nginx/html;
    index  index.html;
    try_files $uri $uri/ /index.html;
  }

  location /api {
    proxy_pass http://backend-service;
    proxy_set_header Host $host;
  }
}

十、扩展功能展望

10.1 状态管理集成

bash

复制

# 安装 Dva
pnpm add @umijs/plugin-dva

src/models/agent.ts

typescript

复制

export default {
  namespace: 'agent',
  state: {
    list: [],
  },
  effects: {
    *fetchList({ payload }, { call, put }) {
      const res = yield call(fetchAgentList, payload);
      yield put({ type: 'save', payload: res.data });
    }
  },
  reducers: {
    save(state, { payload }) {
      return { ...state, list: payload };
    }
  }
};

10.2 微前端集成

typescript

复制

// config/qiankun.ts
export default {
  apps: [
    {
      name: 'subApp',
      entry: '//localhost:7100',
      base: '/sub',
      routes: ['/sub/*']
    }
  ]
}

通过以上 10 个章节的系统讲解,我们完成了从零开始构建 Umi+React 企业级项目的完整流程。本文不仅覆盖了基础配置、核心功能开发,还深入探讨了性能优化、异常处理等高级主题。建议开发者在实践中持续关注 Umi 官方更新动态,结合业务需求灵活调整架构方案。