react antd confirm content list_react简单的项目架构搭建过程

本文详细介绍如何从零开始搭建一个完整的React项目架构,包括路由配置、按需加载、Redux集成及错误处理等内容。

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

e27232e6a926357e2258d43a5d02afe1.png

前言

react官方推荐的脚手架create-react-app因为想给用户的最大自由度所以并没有像vue的脚手架那样生成的项目架构那么齐全,用这个写写demo还行,但是真正用在项目上还是不太合适的。真正用在项目的react我觉得至少包含两点

  1. 有路由
  2. 有数据管理

对,有这两点就够了,剩下的就是怎么更好的把这两块融合到react项目里。

要求

我对这个react项目框架的要求就三点

  1. 足够新,因为我们还是希望使用react最新版本的一些新特性的
  2. 足够纯粹,只要满足上面两点就可以了,剩下的以后想起来拓展
  3. 足够现有的项目使用,这肯定是必须的啦

基础骨架

这块其实没多少说的,为了满足react架构足够新,那基础骨架当然就用create-react-app脚手架生成了,看下package.json

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "0.9.x"
  },
  "devDependencies": {},
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}

react版本16.13.1是最新稳定版本。具体怎么用脚手架生成请看脚手架官方文档,我这里就不多说了。

目录

一个好的项目开发架构,目录肯定也要安排的妥妥的。先看下脚手架生成的目录

04d55a51891b0fb05ff27cb4de9f9ab4.png

真的是非常简单,对照下图调整一下即可,各文件夹理论上的用处我已经标注在上面了

409b676078dad82b890774333e805852.png

test里面的index.js和index.css就是开始的app.js,app.css。

styles里面的base.css对应的就是开始的index.css。

调整好后,运行不报路径错误即可!

路由接入

路由我们就直接使用最新版的react-router-dom

直接命令安装npm install react-router-dom --save

稍微看下react-router-dom的官方文档我们得出最基础的react-router-dom使用方法。

为了后面方便这边拷贝一个views下面的test目录起名test1,记得把test1目录下的index.js导出的class改名为Test1

在router文件夹下新建个index.js代码如下:

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import test from '../views/test'
import test1 from '../views/test1'


const AppRouter = () => (
    <BrowserRouter>
        <Switch>
            <Route exact path="/test" component={test}/>
            <Route exact path="/test1" component={test1}/>
        </Switch>
    </BrowserRouter>
);

export default AppRouter

修改src/index.js文件的代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
import './styles/base.css';
import AppRouter from './router'

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

浏览器打开http://localhost:3000/test就可以看见路由已经成功了,这样就行了吗?当然还远远不够,这种路由配置很不方便,我们更希望像vue那样有个路由配置表下面这样

在router文件夹下新建个routers.js代码如下:

import test from '../views/test'
import test1 from '../views/test1'

const config = [
    {
        path: '/test',
        exact: true,
        meta: {
            title: '测试'
        },
        component:test,
    },
    {
        path: '/test1',
        exact: true,
        meta: {
            title: '测试1'
        },
        component:test1,
    }
];

export default config;

看上面配置文件我们第一反应就是循环创建Route组件,没错就是这样的

修改router/index.js代码:

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import routers from './routers';


const renderRoutes = (routers) => {
    return (
        routers.map(route=>{
            return (
                <Route
                    key={route.path}
                    exact={route.exact}
                    path={route.path}
                    component={route.component}
                />
            )
        })
    )
}

const AppRouter = () => {
    return <BrowserRouter><Switch>{renderRoutes(routers)}</Switch></BrowserRouter>
}

export default AppRouter

就这样了吗?其实还不够,我们需要做按需加载,这是单页面应用的最基本优化,react16.6之前我们做按需加载都要借助第三方来做,比如用webpack的

require.ensure([], require => cb(null, require('./test').default), 'test')

或者用

react-loadable

react16.6版本以后react提供了一个lazy和Suspense组件用来做按需加载,只需要把组件用Suspense包裹一下在用lazy处理一下就可以了,就像这样lazy(() => import('../views/test'))

所以我们修改一下路由配置文件

import { lazy } from 'react'

const config = [
    {
        path: '/test',
        exact: true,
        meta: {
            title: '测试'
        },
        component: lazy(() => import('../views/test')),
    },
    {
        path: '/test1',
        exact: true,
        meta: {
            title: '测试1'
        },
        component: lazy(() => import('../views/test1')),
    }
];

export default config;

不过当你修改后就会报编译错误,原因是我们这代码里使用的动态导入就是import('../views/test')代码,默认脚手架生成的架构是不支持这个的,但是这个我们是一定要用的,所以就来先把这个报错解决了。

其实解决这个问题本来是很简单的,只要安装babel-plugin-import这个,然后在.babelrc这里的plugin那块把babel-plugin-import插件配置进去就行了,但是用create-react-app脚手架生成的项目架构是看不见.babelrc文件的,因为它把webpack的配置都集成到react-scripts这个包里去了,我们虽然可以用npm run eject命令把它展开,但是我不推荐这么做,所以就用另外一种方式吧,就是第三方覆盖配置,这种方式我就不在这里细说了,因为这不是我这文章的重点,而且网上到处都是,我直接给出结果

首先修改package.json文件代码如下:

{
  "name": "my-app1",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-router-dom": "^5.1.2",
    "react-scripts": "^3.4.1"
  },
  "devDependencies": {
    "babel-plugin-import": "^1.13.0",
    "customize-cra": "^0.9.1",
    "react-app-rewired": "^2.1.5",
    "react-dev-utils": "^10.2.1"
  },
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired --env=jsdom",
    "eject": "react-app-rewired eject"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

npm install一下

然后在根目录增加config-overrides.js文件夹代码如下:

const {
    override,
    addDecoratorsLegacy,
    disableEsLint,
    fixBabelImports,
} = require("customize-cra");
const path = require("path");

module.exports = override(
    addDecoratorsLegacy(),
    disableEsLint(),
    fixBabelImports('import',{
        style: 'css'
    }),
);

最后重新npm run start一下,报错就会消失。好了,我们继续上面

做到这里突然发现一个问题,Suspense是个组件需要套在需要懒加载的组件外层,这个怎么处理呢?不慌,去翻一翻react-router-dom官方文档,发现Route组件都有个render属性可以传一个func,所以修改router/index.js代码如下:

import React, { Suspense } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import routers from './routers';

const fallback = () =>{
    return (
        <div>Loading...</div>
    )
}

const renderRoutes = (routers) => {
    return (
        routers.map(route=>{
            return (
                <Route
                    key={route.path}
                    exact={route.exact}
                    path={route.path}
                    render={() => {
                        return <Suspense fallback={fallback()}>
                                <route.component></route.component>
                        </Suspense>
                    }}
                />
            )
        })
    )
}

const AppRouter = () => {
    return <BrowserRouter><Switch>{renderRoutes(routers)}</Switch></BrowserRouter>
}

export default AppRouter

至此按需加载就完成了,可以自己打开/test再打开/test1路由看看效果,打开/test时候只加载了1.chunk.js,打开/test1路由时候加载了2.chunk.js。

看起来路由这块已经差不多了,但是lazy方法写在配置文件里总感觉有些不对,而且增加路由的时候还容易忘记加,所以我们考虑统一在循环创建Route组件时候加进去

再修改router/index.js代码如下:

import React, { Suspense,lazy } from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import routers from './routers';

const fallback = () =>{
    return (
        <div>Loading...</div>
    )
}

const renderRoutes = (routers) => {
    return (
        routers.map(route=>{
            return (
                <Route
                    key={route.path}
                    exact={route.exact}
                    path={route.path}
                    render={() => {
                        const Wraprouter = lazy(route.component)
                        return <Suspense fallback={fallback()}>
                                <Wraprouter></Wraprouter>
                        </Suspense>
                    }}
                />
            )
        })
    )
}

const AppRouter = () => {
    return <BrowserRouter><Switch>{renderRoutes(routers)}</Switch></BrowserRouter>
}

export default AppRouter

这里改好后,配置文件夹里的lazy就可以去掉了

到这里,路由功能已经好了吗?当然没有。。。还有个非常重要的东西没有加进去就是withRouter,加了这个我们才能在组件里使用this.props.history.push等方法来跳转路由,不然的话,你只能用难看又难用的link跳转。

现在你到/test组件里console.log(this.props)看看,根本没有history等路由相关的东西就是一个空的

withRouter加起来也很简单,它就一高阶组件,最简单的用法就是在你views/test/index.js文件里导出test组件时候套一下就行就像这样:

import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import logo from '../../assets/logo.svg';
import './index.css';

class Test extends Component {
    render() {
        console.log(this.props)
        return (
            <div className="App">
                <div className="App-header">
                    <img src={logo} className="App-logo" alt="logo" />
                    <h2>Welcome to React</h2>
                </div>
                <p className="App-intro">
                    To get started, edit <code>src/App.js</code> and save to reload.
                </p>
            </div>
        );
    }
}

export default withRouter(Test);

不过我们完全没必要这样做,完全可以把这个也放到router/index.js里统一处理修改这个文件代码如下:

import React, { Suspense,lazy } from 'react';
import { BrowserRouter, Route, Switch , withRouter } from 'react-router-dom';
import routers from './routers';


const fallback = () =>{
    return (
        <div>Loading...</div>
    )
}

const composeMid = (...mid) => (...arg) => {
    return mid.reduce((a,b)=>a(b(...arg)))
}

const renderRoutes = (routers) => {
    return (
        routers.map(route=>{
            return (
                <Route
                    key={route.path}
                    exact={route.exact}
                    path={route.path}
                    render={() => {
                        const midRouter = [withRouter,lazy]
                        const Wraprouter = composeMid(...midRouter)(route.component)
                        return <Suspense fallback={fallback()}>
                                <Wraprouter></Wraprouter>
                        </Suspense>
                    }}
                />
            )
        })
    )
}

const AppRouter = () => {
    return <BrowserRouter><Switch>{renderRoutes(routers)}</Switch></BrowserRouter>
}

export default AppRouter

这次修改的代码可能看起来有些难懂,我解释下

composeMid方法可以好好理解一下,简单来说就是把你传入的参数用mid数组里的方法层层包裹。在我们这里使用起来意思就是把route.component组件先用lazy包裹,再用withRouter包裹,为什么可以这么做。。。因为我猜测lazy和withRouter一样都是个高阶组件,所以lazy处理完返回Test组件,然后再进入到withRouter高阶组件再做处理,这时再返回的Test组件按需加载和路由相关方法和属性就都有了。

到这里路由才算是真正的接入进去,达到项目可用。下节redux接入

redux接入

首先在package.json里增加4个东西,像这样:

{
  "name": "my-app1",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-router-dom": "^5.1.2",
    "react-scripts": "^3.4.1",
    "redux": "^4.0.5",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.3.0",
    "react-redux": "^7.2.0"
  },
  "devDependencies": {
    "babel-plugin-import": "^1.13.0",
    "customize-cra": "^0.9.1",
    "react-app-rewired": "^2.1.5",
    "react-dev-utils": "^10.2.1"
  },
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired --env=jsdom",
    "eject": "react-app-rewired eject"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

dependencies下面最后4个,简单解释一下:

  1. redux;当然就是redux了,其实redux可以用在任何框架里,包括vue,它就是一个数据流程控制方案的高度抽象
  2. redux-logger;这个是一个redux数据变化的日志中间件
  3. redux-thunk;所谓的让action支持异步中间件,其实就是让你可以把action写成一个函数,并且把真正的dispatch传给你这个函数,让你在真正dispatch(action)之前做点你想做的事情
  4. react-redux;刚刚上面说了redux可以用在任何框架,这个就是让redux可以用在react里面的关键,没有它你的redux什么都不是

npm install一下开始配置,我先在redux/testRedux文件里面建一个actionTypes.js代码很简单如下:

export const ADD = 'ADD'
export const MINUS = 'MINUS'

这里其实放的就是redux四元素之一的actionType,很简单就个大写的字符串,描述你要干什么,我们单独把它放到一个文件里导出,再建一个index.js放剩下的三元素(initState,action,reducer),代码如下:

import {
    ADD,
    MINUS
} from './actionTypes'

export const INITIAL_STATE = {
    num: 0
}

export const add = () => async dispatch => {
    dispatch({
        type: ADD
    })

}

export const minus = () => {
    return {
        type: MINUS
    }
}


export default function test (state = INITIAL_STATE, action) {
    switch (action.type) {
        case ADD:
            return {
                ...state,
                num: state.num + 1
            }
        case MINUS:
            return {
                ...state,
                num: state.num - 1
            }
        default:
            return state
    }
}

这里大家可以随意继续拆分成不同的文件放,不过我个人喜欢把它们放一起

现在在store文件夹里新建一个index.js文件做store的配置代码如下:

import { createStore, applyMiddleware } from 'redux'
import test,{ INITIAL_STATE } from '../redux/testRedux'

import thunk from 'redux-thunk'

const middlewares = [thunk]

if (process.env.NODE_ENV === 'development') {
    const { logger, createLogger } = require('redux-logger')
    middlewares.push(createLogger({
        collapsed: true,
    }))
}

let store = createStore(test,INITIAL_STATE, applyMiddleware(...middlewares))

export default store

调用redux里createStore方法,传入3个参数分别是reduce,state,中间件数组;这配置网上到处都是,我这里就不做过多介绍,值得注意的是把日志中间件放在开发环境引入就好

修改src/index.js文件代码如下

import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './router'
import './styles/base.css'
import { Provider } from 'react-redux'
import store from "./store";

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

引入react-redux里面的provider组件把它包在路由组件外面,然后把store作为属性放上去。它的作用就是帮我们把store放到全局上下文context里,让其他组件都可以通过全局上下文context里拿到store。

修改views/test/index.js文件代码如下:

import React, { Component } from 'react';
import logo from '../../assets/logo.svg';
import './index.css';
import {add, minus} from '../../redux/testRedux'
import { connect } from 'react-redux'

class Test extends Component {
    render () {
        console.log(this.props)
        return (
            <div className='z'>
                <div className='but add_btn' onClick={this.props.add}>+</div>
                <div className='but dec_btn' onClick={this.props.minus}>-</div>
                <div><span>{this.props.num}</span></div>
            </div>
        );
    }
}



export default connect(
    state=>state,
    dispatch=>{
        return {
            add:()=>dispatch(add()),
            minus:()=>dispatch(minus())
        }
    }
)(Test);

引入react-redux里的connect方法,传入两个参数这两个参数都是函数,一个函数返回你需要绑定到当前组件props上state,另外一个函数是返回你要绑定到当前组件props上的方法,方法记得要用dispatch包裹一下。

connect其实和上面withRouter一样都是高阶组件,简单来它的作用有三个

  1. 就是上面帮我们绑定state和props
  2. 帮我们接收全局上下文context并且注入到当前调用组件的props里
  3. 帮我们监听绑定的store变化,然后更新当前组件

现在可以打印一下this.props看看是不是多了两个方法和一个state就像下面这样

be99d4096a3c36e9f5f072506043a805.png

可以点击一下add_btn和dec_btn试试,日志也有了,组件也会重新渲染

cfd3f5ed55d16a4701f38a17c6eadf3f.png

到这里我们仅仅只是实现了redux的基本功能,用来做项目还是远远不够,几点

  1. 我们一个项目里面模块很多所以对应的reducers也很多,所以我们要有一个好的管理reducers的方式
  2. 我们一般action方法也很多,现在的这个写法看起来很傻。。。

所以我们再改造一下,在store里新建一个rootReducers.js文件代码如下:

import { combineReducers } from 'redux'

import test from '../redux/testRedux'

const reducerMap = {
    test
}

export default combineReducers(reducerMap)

修改store/index.js文件代码如下:

import { createStore, applyMiddleware } from 'redux'
import rootReducer from './rootReducers'

import thunk from 'redux-thunk'

const middlewares = [thunk]

if (process.env.NODE_ENV === 'development') {
    const { logger, createLogger } = require('redux-logger')
    middlewares.push(createLogger({
        collapsed: true,
    }))
}

let store = createStore(rootReducer, applyMiddleware(...middlewares))

export default store

修改views/test/index.js文件代码如下:

import React, { Component } from 'react';
import { bindActionCreators } from 'redux'
import './index.css';
import {add, minus} from '../../redux/testRedux'
import { connect } from 'react-redux'


class Test extends Component {
    render () {
        console.log(this.props)
        return (
            <div className='z'>
                <div className='but add_btn' onClick={this.props.add}>+</div>
                <div className='but dec_btn' onClick={this.props.minus}>-</div>
                <div><span>{this.props.num}</span></div>
            </div>
        );
    }
}


export default connect(
    state=>state.test,
    dispatch => bindActionCreators({add,minus}, dispatch)
)(Test);

这个文件我们还可以用装饰器的方法再做点改动,变成这样

import React, { Component } from 'react';
import { bindActionCreators } from 'redux'
import './index.css';
import * as actions from '../../redux/testRedux'
import { connect } from 'react-redux'

@connect(state=>state.test,dispatch => bindActionCreators(actions, dispatch))
class Test extends Component {
    render () {
        console.log(this.props)
        return (
            <div className='z'>
                <div className='but add_btn' onClick={this.props.add}>+</div>
                <div className='but dec_btn' onClick={this.props.minus}>-</div>
                <div><span>{this.props.num}</span></div>
            </div>
        );
    }
}

export default Test

到这里主要的步骤都已经完成了。

最后我们再做一步调整,有时候我们项目需要某个公共方法在一进页面时候执行,后面在整个项目生命周期不再执行。所以我们可以考虑在进入路由组件之前套一个高阶组件,在src文件夹下增加一个Hoc.js文件代码如下(顺便在这个高阶组件里做一个代码报错进入降级页面):

import React, { Component } from 'react';
export const RootHoc = (...arg) => (WrappedComponent) => class extends Component {
    state = {
        hasError: false
    }

    static getDerivedStateFromError (error) {
        return { hasError: true };
    }

    componentDidCatch (error, errorInfo) {
        //错误上报,暂时无地方
    }

    componentDidMount () {
        console.log('只执行一次')

    }

    render(){
        if (this.state.hasError) {
            return <h1>页面出错,请检查代码</h1>;
        } else {
            return <WrappedComponent {...this.props}  />
        }
    }
}

修改src/index.js代码如下:

import React,{ Component } from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './router'
import './styles/base.css'
import { Provider } from 'react-redux'
import { RootHoc } from './Hoc'
import store from "./store";

@RootHoc()
class Root extends Component {
    render () {
        return <Provider store={store}>
            <AppRouter />
        </Provider>
    }
}

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

至此,一个完整可用的react项目架构就搭建完成了。

最后

上述架构源码可以通过我自定义的脚手架创建出来,有兴趣的可以安装试试

npm install hosjoy-cli -g

hosjoy-cli create react myApp
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值