
前言
react官方推荐的脚手架create-react-app因为想给用户的最大自由度所以并没有像vue的脚手架那样生成的项目架构那么齐全,用这个写写demo还行,但是真正用在项目上还是不太合适的。真正用在项目的react我觉得至少包含两点
- 有路由
- 有数据管理
对,有这两点就够了,剩下的就是怎么更好的把这两块融合到react项目里。
要求
我对这个react项目框架的要求就三点
- 足够新,因为我们还是希望使用react最新版本的一些新特性的
- 足够纯粹,只要满足上面两点就可以了,剩下的以后想起来拓展
- 足够现有的项目使用,这肯定是必须的啦
基础骨架
这块其实没多少说的,为了满足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是最新稳定版本。具体怎么用脚手架生成请看脚手架官方文档,我这里就不多说了。
目录
一个好的项目开发架构,目录肯定也要安排的妥妥的。先看下脚手架生成的目录

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

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个,简单解释一下:
- redux;当然就是redux了,其实redux可以用在任何框架里,包括vue,它就是一个数据流程控制方案的高度抽象
- redux-logger;这个是一个redux数据变化的日志中间件
- redux-thunk;所谓的让action支持异步中间件,其实就是让你可以把action写成一个函数,并且把真正的dispatch传给你这个函数,让你在真正dispatch(action)之前做点你想做的事情
- 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一样都是高阶组件,简单来它的作用有三个
- 就是上面帮我们绑定state和props
- 帮我们接收全局上下文context并且注入到当前调用组件的props里
- 帮我们监听绑定的store变化,然后更新当前组件
现在可以打印一下this.props看看是不是多了两个方法和一个state就像下面这样

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

到这里我们仅仅只是实现了redux的基本功能,用来做项目还是远远不够,几点
- 我们一个项目里面模块很多所以对应的reducers也很多,所以我们要有一个好的管理reducers的方式
- 我们一般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