react项目搭建

本文详细介绍了如何从创建React项目开始,逐步精简项目,配置jsx和sass支持,搭建路由,尤其是深入探讨了Redux的使用,包括安装、创建store、使用react-redux、redux-thunk以及如何处理复杂项目的store分解。此外,还讲解了基于axios封装公用API库的方法。通过这个实战教程,读者将能更好地理解和应用React与Redux。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 创建React-APP

通过官方的create-react-app,找个喜欢的目录,执行:

npx create-react-app app-name

稍等片刻即可完成安装。安装完成后,可以使用npm或者yarn启动项目。或者直接在vscode中打开项目,执行start命令启动项目。项目文件结构如下图:

2 精简项目

2.1 删除不用的文件

删除后目录如下:

2.2 简化代码 

逐个修改以下文件:

src/App.js代码简化如下:

import React from 'react'

function App() {
  return (
    <div className="App">
      <h1>This is React App.</h1>
    </div>
  )
}

export default App

src/index.js代码简化如下:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

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

public/index.html 代码简化如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="./favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React App</title>
  </head>
  <body>
    <!-- <noscript>You need to enable JavaScript to run this app.</noscript> -->
    <div id="root"></div>
  </body>
</html>

3 项目目录结构

项目目录结构可根据项目实际灵活制定。这里分享下我常用的结构,仅供参考。

    ├─ /node_modules
    ├─ package.json
    ├─ /public
    |  ├─ favicon.ico        <-- 网页图标
    |  └─ index.html         <-- HTML页模板
    ├─ README.md
    ├─ /src
    |  ├─ /common            <-- 全局公用目录
    |  |  ├─ /fonts          <-- 字体文件目录
    |  |  ├─ /images         <-- 图片文件目录
    |  |  ├─ /js             <-- 公用js文件目录
    |  |  └─ /style          <-- 公用样式文件目录
    |  |  |  ├─ frame.css    <-- 全部公用样式(import其他css)
    |  |  |  ├─ reset.css    <-- 清零样式
    |  |  |  └─ global.css   <-- 全局公用样式
    |  ├─ /components        <-- 公共模块组件目录
    |  |  ├─ /Header         <-- 头部导航模块
    |  |  |  ├─ index.js     <-- header主文件
    |  |  |  └─ style.module.css   <-- header样式文件
    |  |  └─ ...             <-- 其他模块
    |  ├─ /pages             <-- 页面组件目录
    |  |  ├─ /Home           <-- home页目录
    |  |  |  ├─ index.js     <-- home主文件
    |  |  |  └─ style.module.css      <-- home样式文件
    |  |  ├─ /Login          <-- login页目录
    |  |  |  ├─ index.js     <-- login主文件
    |  |  |  └─ style.module.css     <-- login样式文件
    |  |  └─ ...             <-- 其他页面
    |  ├─ App.js             <-- 项目主模块
    |  └─ index.js           <-- 项目入口文件
    └─ yarn.lock

3.1 支持jsx和sass

本人更喜欢用jsx,会把App.js 和 page , components的组件都改错jsx,直接改后缀名即可。

使用npm 或 yarn安装sass

npm install node-sass

安装好后,直接把项目里的.css 改成 .scss 就可以了。

此时项目目录结构如下:


 

3.2 使用classnames库

看个人习惯或项目要求,可用可不用

安装

npm install classnames

使用

import React from "react";
import styles from './style.module.scss'
import classNames from 'classnames/bind'

const Header = props => { 
    const cx = classNames.bind(styles);

    return (
        <div className={cx("header-test-1")}> header </div>
    )
}

export default Header

4 路由

4.1 页面构建

根据自己的要求创建页面,这里新建了home和login做演示,页面自己创建和编写即可。

4.2 使用react-router-dom

这里是v6的版本,v5版本的这里就不展示了。

安装

npm install react-router-dom

src下新建: /routes/index.js, js内是所有页面,示例代码如下:

import React from 'react';

const Home = React.lazy(() => import('../pages/Home'))
const Login = React.lazy(() => import('../pages/Login'))

const routes = [
  {
    path: "/home",
    component: Home,
  },
  {
    path: "/login",
    component: Login,
  }
]

export default routes

然后在app.jsx中导入路由,直接上代码:

import React, { Suspense } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import Header from './components/Header';
import routes from './routes'
import Home from './pages/Home'

function App() {

  const routerRender = (routes) => {
    return routes.map((item, index) => {
      return (
        <Route
          key={index}
          path={item.path}
          element={ <item.element /> }
        >
          {item.children ? routerRender(item.children) : null} 
        </Route>
      )
    });
  };

  return (
    <div className="App">
      <Header />
      <Suspense fallback={<div></div>}>
      <BrowserRouter>
        <Routes>
          {/* 默认页面 */}
          <Route path={"/"} element={<Home />} />

          {routerRender(routes)}

          {/* 匹配未定义的路由地址 */}
          <Route path={"*"} element={<div>暂无此页面</div>} />
        </Routes>
      </BrowserRouter>
      </Suspense>
    </div>
  );
}

export default App;

4.3  路由跳转

接下来,简单介绍下如果在页面之间进行路由跳转。

在Home页面添加一个用于跳转至Login页的按钮,代码修改如下:

import React, { Fragment, useEffect } from "react";
import styles from './style.module.scss'
import classNames from 'classnames/bind'
import { useNavigate } from 'react-router-dom'

const Home = props => {
  let navigate = useNavigate();
  const cx = classNames.bind(styles);

  return (
    <Fragment>
      <div className={cx("home-test-1")}> Home </div>
  
      <div onClick={() => navigate('login')}>click me go to Login</div>
    </Fragment>
  )
}

export default Home

现在,点击按钮进行页面路由跳转已经实现了。

5 Redux及相关插件

Redux是用来做什么的?简单通俗的解释,Redux是用来管理项目级别的全局变量,而且是可以实时监听变量的变化并改变DOM的。当多个模块都需要动态显示同一个数据,并且这些模块从属于不同的父组件,或者在不同的页面中,如果没有Redux,那实现起来就很麻烦了,问题追踪也很痛苦。因此Redux就是解决这个问题的。

redux涉及的内容较多,把各个依赖组件的官方文档都阅读一遍确实不容易消化。本次分享通过一个简单的Demo,把redux、react-redux、redux-thunk、immutable这些依赖组件的使用方法串起来,非常有利于理解。

5.1 安装redux

npm install redux

仅安装redux也是可以使用的,但是比较麻烦。redux里更新store里的数据,需要手动订阅(subscribe)更新,这里就不展开介绍了。可以借助另一个插件(react-redux)提高开发效率。

5.2 安装react-redux

npm install react-redux

react-redux允许通过connect方法,将store中的数据映射到组件的props,省去了store订阅。原state中读取store的属性改用props读取。

5.3 安装redux-thunk

npm install redux-thunk

redux-thunk允许在actionCreators里传递函数类型的数据。这样可以把业务逻辑(例如接口请求)集中写在actionCreator.js,方便复用的同时,可以使组件的主文件更简洁。

5.4创建store

安装以上各种插件后,可以store用来管理状态数据了。

如果项目比较简单,只有一两个页面,可以只创建一个总store管理整体项目。目录结构参考如下:

以下是各文件的代码:

src/store/index.js:

import { createStore, applyMiddleware, compose } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'

// 这里让项目支持浏览器插件Redux DevTools
const composeEnhancers = typeof window === 'object' &&
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose

const enhancer = composeEnhancers(
  applyMiddleware(thunk)
);

const store = createStore(
  reducer,
  enhancer
)

export default store

以上是store的核心代码,支持了Redux DevTools。同时,利用redux的集成中间件(applyMiddleware)功能将redux-thunk集成进来,最终创建了store。

src/store/constants.js:

export const SET_DATA = 'SET_DATA'

创建这个定义常量的文件,是因为方便被下面的reducer.js和actionCreators.js同时引用,便于统一修改和管理。

src/store/actionCreators.js:

import * as constants from './constants'

export const getData = (data) => ({
  type: constans.SET_DATA,
  data
})

src/store/reducer.js:

import * as constants from './constants'

// 初始默认的state
const defaultState = {
    myData: null
}

const reducer = (state = defaultState, action) => {
    // 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。
    let newState = Object.assign({}, state)
    switch(action.type) {
        case constants.SET_DATA:
            newState.myData = action.data
            return newState
        default:
            return state
    }
}

export default reducer

以上代码,在store设置了一个myData。现在,state修改起来还是有点小麻烦,如何更好地解决这个问题,在5.7章节会提到。

到这里,你可能还是不知道Redux怎么用。实际项目中很少只用一个总store库来管理的。因此,在下面章节的分库内容中具体讲述Redux的使用方法。

5.6 复杂项目store分解

当项目的页面较多,如果数据都集中放在一个store里,维护成本将会变高。接下来分享下如何将store分解到各个组件中。

一般来说,每个组件有自己的store(分库),再由src/store作为总集,集成每个组件的store。以Home和Login两个组件为例,分别创建组件自己的store,文件结构跟store总集一致。

目录结构变动如下:

这里只贴出来Home组件的相关代码,其他组件写法相同。

 src/pages/Home/store/index.js:

import reducer from './reducer'
import * as actionCreators from './actionCreators'
import * as constants from './constants'

export { reducer, actionCreators, constants}

其实就是把当前组件store(分库)下的其他文件集中起来作为统一输出口。

src/pages/Home/store/constants.js:

const ZONE = 'pages/Home'  //ZONE是用来避免与其他组件的constants重名。

export const SET_DATA = ZONE + 'SET_DATA'

src/pages/Home/store/actionCreators.js:

import * as constants from './constants'

export const setData = (data) => ({
  type: constants.SET_DATA,
  data
})

src/pages/Home/store/reducer.js:

import * as constants from './constants'

// 初始默认的state
const defaultState = {
    myHomeData: null
}

const reducer = (state = defaultState, action) => {
    // 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。
    let newState = Object.assign({}, state)
    switch(action.type) {
        case constants.SET_DATA:
            newState.myHomeData = action.data
            return newState
        default:
            return state
    }
}

export default reducer

然后修改项目store总集,删除actionCreators.js和constants.js,index.js不变。

src/store/reducer.js重写如下:

import { combineReducers } from 'redux'
import { reducer as homeReducer } from '../pages/Home/store'
import { reducer as loginReducer } from '../pages/Login/store'

const reducer = combineReducers({
    home: homeReducer,
    login: loginReducer
})

export default reducer

以上代码的作用就是把Home和Login的store引入,然后通过combineReducers合并在一起,并分别加上唯一的对象key值。

这样的好处非常明显:

  1. 避免各组件的store数据互相污染。
  2. 组件独立维护自己的store,减少维护成本。

非常建议使用这种方式维护store。

5.7 安装使用immutable

在5.5章节,提到了store里不能直接修改state,因为state是引用类型,直接修改可能导致监测不到数据变化。

immutable.js从字面上就可以明白,immutable的意思是“不可改变的”。使用immutable创建的数据是不可改变的,对immutable数据的任何修改都会返回一个新的immutable数据,不会改变原始immutable数据。

immutable.js提供了很多方法,非常方便修改对象或数组类型的引用型数据。

安装immutable和redux-immutable,执行:

npm install immutable redux-immutable

然后对代码进行改造:

src/store/reducer.js:

用 import { combineReducers } from 'redux-immutable'

替换 :import { combineReducers } from 'redux'

把combineReducers换成redux-immutable里的。

然后修改src/pages/Home/store/reducer.js:

import * as constants from './constants'
import { fromJS } from 'immutable'

// 初始默认的state
const defaultState = fromJS({
    myHomeData: null
})

const getData = (state, action) => {
    return state.set('myHomeData', action.data)
}

const reducer = (state = defaultState, action) => {
    // 由于state是引用型,不能直接修改,否则是监测不到state发生变化的。因此需要先复制一份进行修改,然后再返回新的state。
    switch(action.type) {
        case constants.SET_DATA:
            return getData(state, action)
        default:
            return state
    }
}

export default reducer

src/pages/Login/store/reducer.js也一样修改即可。

immutable的介入,就是利用fromJS方法,把原始的JS类型转化为immutable类型。

由于state已经是immutable类型了,可以使用immutable的set方法进行数据修改,并返回一个新的state。代码简洁很多,不需要手动通过Object.assign等方法去复制再处理了。

5.8 对接react-redux与store

下面来对接react-redux与store,让全部组件都能方便引用store。

修改src/index.jsx:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './common/style/frame.scss'
import { Provider } from 'react-redux'
import store from './store'


ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
  ,
  document.getElementById('root')
)

以上代码就是用react-redux提供的Provider,把store传给了整个App。

在需要使用store的组件中,要使用react-redux提供的connect方法对组件进行包装。

5.9 设置并实时读取Redux变量

以Home为例,修改src/pages/Home/index.jsx:

import React, { Fragment, useEffect } from "react";
import styles from './style.module.scss'
import classNames from 'classnames/bind'
import { useNavigate } from 'react-router-dom'
import { connect } from 'react-redux'
import * as actionCreators from './store/actionCreators'


const Home = props => {
  const { myHomeData, setData } = props
  let navigate = useNavigate();
  const cx = classNames.bind(styles);

  useEffect(() => {

  }, [])

  const goTo = (link) => {
    navigate(link);
  }

  return (
    <Fragment>
      <div className={cx("home-test-1")}> Home </div>
      <div onClick={() => goTo('login')}>click me go to Login</div>

      <div className="ipt-con">home store: myData = {myHomeData}</div>
      <div className="ipt-con">
        <button onClick={() => { setData('123456') }}>更改home store的myData</button>
      </div>
    </Fragment>
  )
}

// 把store中的数据映射到组件的props
const mapStateToProps = (state) => {
  return {
    // 数组第一个元素的login,对应的是src/store/reducer.js中定义的login分库名称
    myHomeData: state.getIn(['home', 'myHomeData']),
  }
}

// 把store的Dispatch映射到组件的props
const mapDispatchToProps = (dispatch) => ({
  setData(data) {
    const action = actionCreators.setData(data)
    dispatch(action)
  },
})

export default connect(mapStateToProps, mapDispatchToProps)(Home)

关键点说明:

  1. 注意代码最后一行,export的数据被connect方法包装了。

  2. 通过mapStateToProps和mapDispatchToProps方法,把store里的state和dispatch都映射到了组件的props。这样可以直接通过props进行访问了,store中数据的变化会直接改变props从而触发组件的视图更新。

  3. state.getIn()方法是来自于redux-immutable的。

点击按钮后,可以看到页面中显示的myData发生了变化。

5.10 在Login组件实时读取Redux变量

接下来,要实现在Login组件中实时读取在Home页面设置的myHomeData。

修改src/page/Login/index.jsx:

import React, { Fragment } from "react";
import { connect } from 'react-redux'
import styles from './style.module.scss'
import classNames from 'classnames/bind'

const Login = props => {
  const cx = classNames.bind(styles);
  // 接收来自父组件及Redux的数据
  const { myHomeData } = props

  return (
    <Fragment>
      <div className={cx("login-test-1")}> Login </div>

      <div className={cx("login-test-1")}> myHomeData:{myHomeData} </div>
    </Fragment>
  )
}

// 把store中的数据映射到组件的props
const mapStateToProps = (state) => {
  return {
    // 数组第一个元素的login,对应的是src/store/reducer.js中定义的login分库名称
    myHomeData: state.getIn(['home', 'myHomeData']),
  }
}

export default connect(mapStateToProps, null)(Login)

由于在Login中只用到了读取Redux的myLoginData,所以不需要mapDispatchToProps方法了。

这里是通过Redux实时获取的,而非通过父子组件传递方式。因此同样的方式可以在其他页面或者组件中直接使用,无需考虑组件的父子关系。

现在点击“更改home store的myData”,然后点击跳转到login页面后可以发现Login组件可以正常实时获取myHomeData了。

5.11 Redux开发小结

上述Redux相关内容较多,跟着操作一遍好像大概知道了,但又说不清为什么使用这些依赖包。这里做一下小结,便于消化理解。

其实react-redux、redux-thunk、immutable都是围绕如何简化redux开发的。

react-redux是为了简化redux通过订阅方式修改state的繁琐过程。

redux-thunk是为了redux的dispatch能够支持function类型的数据,请回顾8.9章节中login页面代码的mapDispatchToProps。

immutable是为了解决store中的数据不能被直接赋值修改的问题(引用类型数据的变化导致无法监测到数据的变化)

6 基于axios封装公用API库

axios是一款非常流行的API请求工具,先来安装一下。

npm install axios

6.1 封装公用API库

直接上代码。

src/api/request.js:

/**
 * 网络请求配置
 */
 import axios from "axios";

 axios.defaults.timeout = 100000;
//  axios.defaults.baseURL = "http://test.mediastack.cn/";
 
 /**
  * http request 拦截器
  */
 axios.interceptors.request.use(
   (config) => {
     config.data = JSON.stringify(config.data);
     config.headers = {
       "Content-Type": "application/json",
     };
     return config;
   },
   (error) => {
     return Promise.reject(error);
   }
 );
 
 /**
  * http response 拦截器
  */
 axios.interceptors.response.use(
   (response) => {
     if (response.data.errCode === 2) {
       console.log("过期");
     }
     return response;
   },
   (error) => {
     console.log("请求出错:", error);
   }
 );
 
 /**
  * 封装get方法
  * @param url  请求url
  * @param params  请求参数
  * @returns {Promise}
  */
 export function get(url, params = {}) {
   return new Promise((resolve, reject) => {
     axios.get(url, {
         params: params,
       }).then((response) => {
         landing(url, params, response.data);
         resolve(response.data);
       })
       .catch((error) => {
         msag(error);
         reject(error);
       });
   });
 }
 
 /**
  * 封装post请求
  * @param url
  * @param data
  * @returns {Promise}
  */
 
 export function post(url, data) {
   return new Promise((resolve, reject) => {
     axios.post(url, data).then(
       (response) => {
         //关闭进度条
         resolve(response);
       },
       (error) => {
         msag(error);
         reject(error);
       }
     );
   });
 }
 
 /**
  * 封装patch请求
  * @param url
  * @param data
  * @returns {Promise}
  */
 export function patch(url, data = {}) {
   return new Promise((resolve, reject) => {
     axios.patch(url, data).then(
       (response) => {
         resolve(response);
       },
       (err) => {
         msag(err);
         reject(err);
       }
     );
   });
 }
 
 /**
  * 封装put请求
  * @param url
  * @param data
  * @returns {Promise}
  */
 
 export function put(url, data = {}) {
   return new Promise((resolve, reject) => {
     axios.put(url, data).then(
       (response) => {
         resolve(response.data);
       },
       (err) => {
         msag(err);
         reject(err);
       }
     );
   });
 }
 
 //统一接口处理,返回数据
 export default function (fecth, url, param) {
   let _data = "";
   return new Promise((resolve, reject) => {
     switch (fecth) {
       case "get":
         console.log("begin a get request,and url:", url);
         get(url, param)
           .then(function (response) {
             resolve(response);
           })
           .catch(function (error) {
             console.log("get request GET failed.", error);
             reject(error);
           });
         break;
       case "post":
         post(url, param)
           .then(function (response) {
             resolve(response);
           })
           .catch(function (error) {
             console.log("get request POST failed.", error);
             reject(error);
           });
         break;
       default:
         break;
     }
   });
 }
 
 //失败提示
 function msag(err) {
   if (err && err.response) {
     switch (err.response.status) {
       case 400:
         alert(err.response.data.error.details);
         break;
       case 401:
         alert("未授权,请登录");
         break;
 
       case 403:
         alert("拒绝访问");
         break;
 
       case 404:
         alert("请求地址出错");
         break;
 
       case 408:
         alert("请求超时");
         break;
 
       case 500:
         alert("服务器内部错误");
         break;
 
       case 501:
         alert("服务未实现");
         break;
 
       case 502:
         alert("网关错误");
         break;
 
       case 503:
         alert("服务不可用");
         break;
 
       case 504:
         alert("网关超时");
         break;
 
       case 505:
         alert("HTTP版本不受支持");
         break;
       default:
     }
   }
 }
 
 /**
  * 查看返回的数据
  * @param url
  * @param params
  * @param data
  */
 function landing(url, params, data) {
   if (data.code === -1) {
   }
 }
 
 

6.2请求隔离

所有请求都在此文件里, 这里只已登录接口做示例

src/api/index.js:

import request from "./request"
// import qs from "qs"

let base = { public: "your url", detail: null}

export const login = data => request("post",`${base.public}/login`, data);


export default {
  login
}

6.3 组件内使用

...

import { login } from "../../api"

...

    let data = {
      name: "test"
    }
    login(data).then(
      (res) => {
        console.log('zero login res.....')
      },
      (error) => {
        console.log('zero login error.....')
      }
    )
...

7.其他

有一下需求,请参考本人其他文章

1.多语言切换

2.换肤

...

搭建一个React项目,你可以按照以下步骤进行操作: 1. 首先,使用官方提供的CRA(create-react-app)脚手架新建一个React项目。在命令行中输入以下命令: [1] ``` npx create-react-app front-proj --template typescript ``` 2. 接下来,根据你的技术栈需求,安装所需的依赖项。根据你提供的引用,你可以在项目文件夹中运行以下命令: [2] ``` yarn add antd axios moment redux react-redux react-router-dom redux-saga connected-react-router redux-devtools-extension @types/react-redux @types/react-router-dom ``` 3.项目中创建一个布局组件。可以按照你提供的引用中的示例代码创建一个名为Layout的组件。该组件接受一个名为children的props,用于渲染子组件。示例代码如下: [3] ```javascript import { FC } from 'react'; interface Props { children: React.ReactNode; } const Layout: FC<Props> = ({ children }) => { return <div>Layout {children}</div>; }; export default Layout; ``` 以上是搭建一个React项目的基本步骤。你可以根据自己的需求和项目要求进行进一步的开发和配置。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [[React 实战系列] 项目搭建与配置](https://blog.csdn.net/weixin_42938619/article/details/119591695)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值