Jansiel Notes

react自定义应用封装

基于create-react-app搭建自定义应用

本文是基于 create-react-app搭建自定义应用,配置路由、less、axios请求封装和变量环境等...。
供自己以后查漏补缺,也欢迎同道朋友交流学习。

环境准备

  • node版本:v18.12.0
  • yarn版本:v1.22.17

react相关应用版本

  • "react":"^18.3.1"
  • "react-scripts": "5.0.1"
  • "react-router-dom": "^6.23.1"
  • "@craco/craco": "^7.1.0"
  • "antd": "^5.17.4"
  • "mobx": "^6.12.3"
  • "mobx-react": "^9.1.1"

基础搭建

使用yarn进行基础构建

1yarn create react-app my-app
2
3# 运行项目, 默认生成一个3000端口的页面
4cd my-app
5yarn start
6

得到一个官方标准的文件结构:

 1.
 2├── README.md
 3├── package.json
 4├── public
 5├── src
 6│   ├── App.css // css可以清空
 7│   ├── App.js
 8│   ├── App.test.js // 没有单元测试可以删除
 9│   ├── index.css
10│   ├── index.js
11│   ├── logo.svg
12│   ├── reportWebVitals.js // 不用也可以删除
13│   └── setupTests.js // 不用也可以删除
14└── yarn.lock
15

官网案例的缺陷

这个文件目录可以看出来,官方案例只能做一个简单的demo,不能作为一个企业标准应用,缺失很多:

  1. 没有路由模块
  2. 没有less(或Sass)样式支持
  3. 没有UI组件库
  4. 没有封装请求库
  5. 没有状态共享库
  6. 配置环境变量

增加路由模块

安装路由

1yarn add react-router-dom
2

修改APP.js,配置路由组件:

 1import { BrowserRouter } from "react-router-dom";
 2import AppRoutes from "./routes";
 3function App() {
 4  return (
 5    <BrowserRouter>
 6      <AppRoutes />
 7    </BrowserRouter>
 8  );
 9}
10export default App;
11

创建routes目录存储路由:

1# 创建目录
2mkdir routes && cd routes
3# 新建index.js文件
4touch index.js
5

路由配置列表和详情页:

 1import { Routes, Route, Navigate } from 'react-router-dom';
 2import List from "../pages/List";
 3import Detail from "../pages/Detail";
 4
 5const AppRoutes = () => {
 6  return (
 7    <Routes>
 8      <Route path="/" exact element={<Navigate to="/list" replace />} />
 9      <Route path="/list" exact Component={List} />
10      <Route path="/detail/:id" exact Component={Detail} />
11    </Routes>
12  );
13};
14
15export default AppRoutes;
16

src下新增page目录存储页面如下:

 1# 创建页面根目录
 2mkdir pages && cd pages
 3# 创建列表和详情页
 4mkdir List Detail
 5# 创建页面和样式
 6cd List
 7touch index.jsx
 8cd Detail
 9touch index.jsx
10
11# 目录结构
12pages
13├── Detail
14│   ├── index.jsx
15└── List
16  ├── index.jsx
17

列表页:

 1import { useState, useEffect } from "react";
 2import { useNavigate } from 'react-router-dom';
 3
 4const List = () => {
 5  const navigate = useNavigate();
 6  const [listData, setListData] = useState([]);
 7
 8  useEffect(() => {
 9    setListData([
10      { id: 1, title: "新闻标题11111", date: "2024-05-30 11:01:00" },
11      { id: 2, title: "新闻标题22222", date: "2024-05-30 12:02:00" },
12      { id: 3, title: "新闻标题33333", date: "2024-05-30 13:03:00" },
13    ]);
14  }, []);
15
16  const goToDetail = (id) => {
17    navigate(`/detail/${id}`)
18  };
19
20  return (
21    <div>
22      {listData.map((item) => (
23        <div key={item.id}>
24          <div style={{ color: "blue" }} onClick={() => goToDetail(item.id)}>
25            {item.title}
26          </div>
27          <div>{item.date}</div>
28        </div>
29      ))}
30    </div>
31  );
32};
33
34export default List;
35

详情页:

 1import { useState, useEffect } from "react";
 2import { useParams } from "react-router-dom";
 3
 4const detailData = {
 5  1: {
 6    id: 1,
 7    title: "新闻标题11111",
 8    date: "2024-05-30 11:01:00",
 9    content: "新闻内容11111",
10  },
11  2: {
12    id: 2,
13    title: "新闻标题22222",
14    date: "2024-05-30 12:02:00",
15    content: "新闻内容22222",
16  },
17  3: {
18    id: 3,
19    title: "新闻标题33333",
20    date: "2024-05-30 13:03:00",
21    content: "新闻内容33333",
22  },
23};
24
25const Detail = () => {
26  const { id } = useParams();
27  const [detail, setDetail] = useState(null);
28
29  useEffect(() => {
30    setDetail(detailData[id] || null);
31  }, [id]);
32
33  return (
34    <div>
35      {detail ? (
36        <div>
37          <div>{detail.title}</div>
38          <div>{detail.date}</div>
39          <div>{detail.content}</div>
40        </div>
41      ) : (
42        <div>新闻不存在</div>
43      )}
44    </div>
45  );
46};
47
48export default Detail;
49

增加样式支持

这里使用less来做样式支持,也支持css modules

1# 使用craco来修改webpack的配置:首先,你需要安装 @craco/craco 包
2yarn add @craco/craco
3
4# 安装 craco-less 插件以支持 Less
5yarn add craco-less less less-loader --dev
6

根目录配置 craco.config.js 文件以支持 craco-less,同时也配置css modules

 1const CracoLessPlugin = require('craco-less');
 2
 3module.exports = {
 4  plugins: [
 5    {
 6      plugin: CracoLessPlugin,
 7      options: {
 8        lessLoaderOptions: {
 9          lessOptions: {
10            modifyVars: { '@primary-color': '#1DA57A' }, // 自定义Ant Design主题变量,可选
11            javascriptEnabled: true,
12          },
13        },
14      },
15    },
16  ],
17  style: {
18    modules: true,
19    // 其他样式配置,比如postcss等...
20  }
21};
22

安装UI组件库

这里引入antd5和使用刚配置好的less美化页面UI:

1# 引入antd
2yarn add antd
3# 引入craco-alias起别名
4yarn add craco-alias --dev
5

修改 craco.config.js 增加起别名配置

 1const CracoAliasPlugin = require('craco-alias');
 2
 3module.exports = {
 4  plugins: [
 5    {
 6      plugin: CracoAliasPlugin,
 7      options: {
 8        aliases: {
 9          '@': 'src'
10        }
11      },
12    },
13    // ...
14  ],
15  // ...
16};
17

修改 package.json 更新 scripts 部分,使用 craco 替换 react-scripts

1{
2    "scripts": {
3       "start": "craco start",
4       "build": "craco build",
5       "test": "craco test",
6       // ...其他脚本保持不变
7    },
8},
9

列表页美化
修改 List/index.jsx:

 1import { Card, Button } from "antd";
 2import styles from "./index.module.less";
 3// ...引入其他组件
 4
 5const List = () => {
 6  // ...js部分
 7  return (
 8    <div className={styles.wrapper}>
 9      {listData.map((item) => (
10        <Card
11          key={item.id}
12          title={item.title}
13          extra={<div>{item.date}</div>}
14          style={{ marginBottom: 20 }}
15        >
16          {item.desc}...
17          <Button type="link" onClick={() => goToDetail(item.id)}>
18            More
19          </Button>
20        </Card>
21      ))}
22    </div>
23  );
24};
25
26export default List;
27

新建 List/index.module.less:

.wrapper {
  width: 600px;
  margin: 100px auto;
}

详情页美化
修改 Detail/index.jsx:

 1import styles from "./index.module.less";
 2// ...引入其他组件
 3const Detail = () => {
 4  // ...js部分
 5  return (
 6    <div>
 7      {detail ? (
 8        <div className={styles.container}>
 9          <div className={styles.title}>{detail.title}</div>
10          <div className={styles.date}>{detail.date}</div>
11          <div className={styles.content}>{detail.content}</div>
12        </div>
13      ) : (
14        <div>新闻不存在</div>
15      )}
16    </div>
17  );
18};
19
20export default Detail;
21

新建 Detail/index.module.less:

.container {
  width: 800px;
  margin: 100px auto;
  .title {
    font-size: 20px;
    font-weight: 600;
    padding: 5px 0;
    border-bottom: 1px solid #666;
    margin-bottom: 20px;
  }
  .date {
    font-size: 12px;
    color: #999;
    margin-bottom: 20px;
  }
}

封装请求库

安装axios

1yarn add axios
2

src下新建 http.js

 1import axios from "axios";
 2import { message } from "antd";
 3
 4const baseURL = "xxx";
 5// 设置基础URL
 6axios.defaults.baseURL = baseURL;
 7
 8// 创建一个 Axios 实例
 9const instance = axios.create({
10  // 可以在这里设置默认的 headers、超时时间等
11  timeout: 10000,
12});
13
14// 请求拦截器,可以用来添加token等操作
15instance.interceptors.request.use(
16  (config) => {
17    // 在发送请求之前做些什么,比如添加认证信息
18    // config.headers.Authorization = `Bearer ${yourToken}`;
19    return config;
20  },
21  (error) => {
22    // 对请求错误做些什么
23    return Promise.reject(error);
24  }
25);
26
27// 响应拦截器,统一处理错误
28instance.interceptors.response.use(
29  async (response) => {
30    // 非200的错误处理
31    if (response?.data?.code !== "200") {
32      message.error(response?.data?.msg);
33      return response.data;
34    }
35
36    return response?.data?.result || null;
37  },
38  (error) => {
39    // 处理错误响应,比如弹出错误提示、跳转登录页等
40    const msg = error.response ? error.response.data : error.message;
41    console.error("Axios error:", msg);
42    message.error(msg);
43    return Promise.reject(error);
44  }
45);
46
47// 封装 GET 请求
48export const get = (url, params) => instance.get(url, { params });
49
50// 封装 POST 请求
51export const post = (url, data, config) => instance.post(url, data, config);
52

src新建 service 目录,目录里新建 index.js:

 1import { post } from '../http';
 2
 3/**
 4 * 调用某个接口
 5 * @param {*} params
 6 * @returns
 7 */
 8export const getSomeData = (params) => {
 9  let url = 'aaa/bbb';
10  return post(url, params);
11}
12

封装共享状态库

使用 mobx 作为我们的共享状态库是因为mobx基于观察者模式,使用简单,几乎无侵入性。通过自动追踪依赖来更新视图,使得状态管理更加直观和容易。特别适合中型项目或对性能要求较高的应用,比 redux 更轻量。

安装mobx

1yarn add mobx mobx-react mobx-react-lite
2

src下面新建 store 文件目录,并创建一个 counterStore.js 文件:

1cd src && mkdir store
2cd store && touch counterStore.js
3

编辑 counterStore.js 文件:

 1import { makeObservable, observable, action } from "mobx";
 2
 3class CounterStore {
 4  count = 0;
 5
 6  constructor() {
 7    makeObservable(this, {
 8      count: observable,
 9      increment: action,
10      decrement: action,
11    });
12  }
13
14  increment() {
15    this.count++;
16  }
17
18  decrement() {
19    this.count--;
20  }
21}
22
23const counterStore = new CounterStore();
24
25export default counterStore;
26

List/index.jsx 页面使用

 1import { Card, Button } from "antd";
 2import { observer } from "mobx-react-lite";
 3import counterStore from "@/store/counterStore";
 4// ...其他引入
 5
 6const List = () => {
 7  // ...其他逻辑
 8  return (
 9    <div className={styles.wrapper}>
10      // 其他UI
11      <div>
12        mobx 案例 Count: {counterStore.count}
13        <Button
14          style={{ marginLeft: 5, marginRight: 5 }}
15          type="primary"
16          onClick={() => counterStore.increment()}
17        >
18          +
19        </Button>
20        <Button type="primary" onClick={() => counterStore.decrement()}>
21          -
22        </Button>
23      </div>
24    </div>
25  );
26};
27
28// 这里要注意用observer包一下
29export default observer(List);
30

配置变量环境

我们在实际开发环境一般分为测试、UAT、准生产、生产等多个环境,调用不同的服务地址。通过配置环境变量可以自动打包到对应环境

安装依赖

1yarn add dotenv-webpack --dev
2

修改 craco.config.js

 1const Dotenv = require('dotenv-webpack');
 2
 3module.exports = {
 4  plugins: [
 5    {
 6      plugin: Dotenv,
 7      // 动态加载 .env 文件
 8      options: {
 9        path: `.env.${process.env.REACT_APP_ENV}`, // 根据 process.env.NODE_ENV 动态加载 .env 文件
10      },
11    },
12    // ...
13  ],
14};
15

配置.env文件,根目录新建 .env.development.env.test.env.uat.env.production:

 1// .env.development文件里配置
 2REACT_APP_ENV=development
 3
 4// .env.test文件里配置
 5REACT_APP_ENV=test
 6
 7// .env.uat文件里配置
 8REACT_APP_ENV=uat
 9
10// .env.production文件里配置
11REACT_APP_ENV=production
12

修改 package.json:

 1{
 2    "scripts": {
 3        "build:test": "REACT_APP_ENV=test craco build",
 4        "build:uat": "REACT_APP_ENV=uat craco build",
 5        "build:prod": "REACT_APP_ENV=production craco build",
 6        // ...其他命令
 7    },
 8    // ...
 9}
10

src下新建 config.js 做baseURL的配置:

 1const apiMap = {
 2  development: "https://xxx-test.xxx.com",
 3  test: "https://xxx-test.xxx.com",
 4  uat: "https://xxx-uat.xxx.com",
 5  production: "https://xxx-uat.prod.com",
 6};
 7
 8console.log("@@@ process.env.REACT_APP_ENV", process.env.REACT_APP_ENV);
 9
10export const env = process.env.REACT_APP_ENV || "test";
11export const baseURL = apiMap[env];
12

修改 http.js 替换掉之前写死的baseURL:

1import { baseURL } from   "@/config";
2
3// 设置基础URL
4axios.defaults.baseURL = baseURL;
5
6// ...
7

案例地址

地址:gitee.com/cangnaiwen/…