一、前端项目结构
在上一节的基础上,我们分别在src下创建如下文件夹:
- assets:静态文件;
- components:公共组件,比如面包屑、编辑器、svg图标、分页器等等;
- hooks:函数组件,使用 React 16.8引进的Hook 特性实现;
- layout:布局组件;
- redux:redux目录,负责状态管理;
- routes:路由,负责路由管理;
- styles:全局样式;
- utils:工具包;
- views:视图层;
二、redux目录构建
我们项目使用redux进行状态管理,在使用redux状态管理器之前,我们需要安装依赖包:
npm install redux --save npm install react-redux --save npm install redux-logger --save npm install redux-thunk --save npm install redux-devtools-extension --save
1、在redux文件夹下创建root_reducers.js文件,用于保存整个项目使用到的reducer:
/** * @author zy * @date 2020/4/5 * @Description: 合并reducer */ import {combineReducers} from \'redux\'; export default combineReducers({})
这里利用 combineReducers 函数来把多个 reducer 函数合并成一个 reducer 函数,目前还没有引入redux函数,后面我们会逐渐完善。
2、在redux文件夹下创建index.js文件:
/** * @author zy * @date 2020/4/4 * @Description: redux状态管理器配置 * 不懂原理的可以参考:https://github.com/brickspert/blog/issues/22#middleware */ import thunk from \'redux-thunk\'; //applyMiddleware用来合并多个中间件,逗号隔开 import {createStore, applyMiddleware} from \'redux\'; import rootReducers from \'./root_reducers\'; //redux的可视化工具,谷歌的应用商城工具 import {composeWithDevTools} from \'redux-devtools-extension\'; // 调用日志打印方法 collapsed是让action折叠,看着舒服点 import { createLogger } from \'redux-logger\'; //这里判断项目环境,正式的话打印的,和可视化的中间件可以去掉 const storeEnhancers = process.env.NODE_ENV === \'production\' ? applyMiddleware(thunk) : composeWithDevTools(applyMiddleware(thunk,createLogger())); /** * 创建store * @author zy * @date 2020/4/5 */ const configureStore = () => { //创建store对象 const store = createStore(rootReducers, storeEnhancers); //保存store window.store = store; //reducer热加载 if (process.env.NODE_ENV !== \'production\') { if (module.hot) { module.hot.accept(\'./root_reducers\', () => { store.replaceReducer(rootReducers) }) } } return store; } export default configureStore();
这里我们利用createStore创建了一个状态管理器,并传入了redux,此外我们还使用了thunk中间件来处理异步请求。
如果不理解这部分代码,可以先去看一下redux相关知识:
三、routes目录构建
路由构建是使用React Route路由库实现的,在使用之前,我们需要安装以下依赖:
npm install react-router-dom --save
1、在routes文件夹下创建web.js文件:
/** * @author zy * @date 2020/4/5 * @Description: web路由 * 不懂的可以参考:https://segmentfault.com/a/1190000020812860 * https://reacttraining.com/react-router/web/api/Route */ import React from \'react\'; import PageNotFound from \'@/components/404\'; function Home(props) { console.log(\'Home=>\', props); return ( <div> <h2>Home</h2> {props.children} </div> ) } function About(props) { console.log(\'About=>\', props); return <h2>About</h2>; } /** * web路由配置项 * @author zy * @date 2020/4/5 */ export default { path: \'/\', name: \'home\', component: Home, exact: false, childRoutes: [ {path: \'about\', component: About}, {path: \'*\', component: PageNotFound} ] }
2、在routes下创建index.js文件:
import React from \'react\'; import {Switch, Route} from \'react-router-dom\'; import _ from \'lodash\'; import webRouteConfig from \'./web\'; //保存所有路由配置的数组 const routeConfig = [webRouteConfig] /** * 路由配置 * @author zy * @date 2020/4/5 */ export default function () { /** * 生成路由嵌套结构 * @author: zy * @date: 2020-03-05 * @param routeConfig: 路由配置数组 * @param contextPath: 路由根路径 */ const renderRouters = (routeConfig, contextPath = \'/\') => { const routes = []; const renderRoute = (item, routeContextPath) => { //基路径 let path = item.path ? `${contextPath}/${item.path}` : contextPath; path = path.replace(/\/+/g, \'/\'); if (!item.component) { return; } //这里使用了嵌套路由 routes.push( <Route key={path} path={path} component={()=> <item.component> {item.childRoutes && renderRouters(item.childRoutes, path)} </item.component> } exact={item.childRoutes?false:true} /> ); } _.forEach(routeConfig, item => renderRoute(item, contextPath)) return <Switch>{routes}</Switch>; }; return renderRouters(routeConfig); }
这里我们使用了嵌套路由,其中/为根路由,然后他有两个子路由,分别为/about,/*,最终生成的代码等价于:
<Switch>
<Route key="/" path="/" exact={false}>
<Home>
<Switch>
<Route key="/about" path="/about" exact={true} component={About}>
<Route key="/*" path="/*" exact={true} component={PageNotFound}>
</Switch>
</Home>
</Route>
</Switch>
这里使用了Swich和exact:
- <Switch>是唯一的,因为它仅仅只会渲染一个路径,当它匹配完一个路径后,就会停止渲染了。相比之下(不使用<Switch>包裹的情况下),每一个被location匹配到的<Route>将都会被渲染;
- exact:只有页面的路由和<Route>的path属性精确比对后完全相同该<Route>才会被渲染;
当我们访问/about时,由于/不是精确匹配,因此首先匹配匹配到/,然后会继续匹配其子元素,由于子元素是精确匹配,因此匹配到/about就会停止。我们为什么采用嵌套路由呢,以江南大学为例:
我们访问不同的页面会发现,它们都有导航栏,页面之间只是存在部分差异,因此我们可以把页面的整体布局放置到路由/对应的组件中,而差异部分放置到路由精确匹配的子组件中,这样我们就不必写太多的重复代码。
需要注意的是Home组件之所以可以嵌套子组件,是因为我们的代码中指定了显示子组件:
function Home(props) { console.log(\'Home=>\', props); return ( <div> <h2>Home</h2> {props.children} </div> ) }
如果不理解这部分代码,可以先去看一下react router相关知识:
四、components目录构建
在web.js中我们使用到了PageNotFound组件,我们需要在components下创建404文件夹,并在该文件夹下创建index.jsx文件,代码如下:
/** * @author zy * @date 2020/4/5 * @Description: 找不到页面 */ import React from \'react\'; import {Result, Button} from \'antd\'; /** * 页面找不到组件 * @author zy * @date 2020/4/5 */ function PageNotFound(props) { return ( <Result status=\'404\' title=\'404\' subTitle=\'Sorry, the page you visited does not exist.\' extra={ <Button type=\'primary\' onClick={() => { props.history.push(\'/\') }}> Back Home </Button> } /> ) } export default PageNotFound
由于此处我们使用了antd组件,因此需要引入依赖:
cnpm install antd --save
关于更多antd组件的使用请查看:antd官网。
五、hooks目录构建
1、useBus
我们在hooks文件夹下创建use_bus.js文件,使用event bus可以解决非父子组件间的通信:
/** * @author zy * @date 2020/4/5 * @Description: 事件监听器 * useContext Hook 是如何工作的:https://segmentfault.com/a/1190000020111320?utm_source=tag-newest * useEffect Hook 是如何工作的:https://segmentfault.com/a/1190000020104281 * 微型库解读之200byte的EventEmitter - Mitt:https://segmentfault.com/a/1190000012997458?utm_source=tag-newest * 使用event bus进行非父子组件间的通信:https://blog.csdn.net/wengqt/article/details/80114590 我们可以通过对event的订阅和发布来进行通信,这里举一个栗子:A和B是两个互不相关的组件,A组件的功能是登录,B组件的功能是登录之后显示用户名,这里就需要A组件将用户名传递给B组件。那么我们应该怎么做呢? 1、在A组件中注册/发布一个type为login的事件; 2、在B组件中注册一个监听/订阅,监听login事件的触发; 3、然后当登录的时候login事件触发,然后B组件就可以触发这个事件的回调函数。 */ import React, {useEffect} from \'react\'; import mitt from \'mitt\'; //创建上下文 const context = React.createContext(); //外层提供数据的组件 const Provider = context.Provider; //useContext 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值 export function useBus() { return React.useContext(context); } /** * 事件监听器函数 * @author zy * @date 2020/4/5 * @param name:监听的事件名称 * @param fn:事件触发时的回调函数 */ export function busListener(name, fn) { //获取 context 的当前值 // eslint-disable-next-line react-hooks/rules-of-hooks const bus = useBus(); //组件第一次挂载执行,第二个参数发生变化时执行 // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { //事件订阅 bus.on(name, fn); //组件卸载之前执行 return () => { //取消事件订阅 bus.off(name, fn); } }, [bus, name, fn]) } //外层提供数据的组件 向后代组件跨层级传值bus,这样后代组件都可以通过useBus获取到bus的值 export function BusProvider({children}) { const [bus] = React.useState(() => mitt()); return <Provider value={bus}>{children}</Provider> }
这里使用到了React 16.8引进的Hook新特性,感兴趣可以查看以下博客:
[3]微型库解读之200byte的EventEmitter - Mitt
2、useMount
我们在hooks下创建use_mount.js文件,用于模拟类组件componentDidMount函数:
/** * @author zy * @date 2020/4/6 * @Description: 利用useEffect实现组件第一次挂载 */ import {useEffect} from \'react\' /** * useMount函数 * @author zy * @date 2020/4/6 */ export default function useMount(func) { //由于第二个参数不变,因此只会执行一次func函数 useEffect(() => { typeof func === \'function\' && func(); // eslint-disable-next-line }, []) }
六、App.js文件
我们修改App.js文件代码如下:
/** * @author zy * @date 2020/4/5 * @Description: 根组件 */ import React from \'react\'; import Routes from \'@/routes\'; import {BrowserRouter} from \'react-router-dom\'; export default function App(props) { return ( <BrowserRouter> <Routes/> </BrowserRouter> ) }
七、index.js文件
我们修改index.js文件如下:
/** * @author zy * @date 2020/4/5 * @Description: 入口文件 */ import React from \'react\'; import ReactDOM from \'react-dom\'; import App from \'./App\'; import {AppContainer} from \'react-hot-loader\'; import {BusProvider} from \'@/hooks/use_bus\'; import {Provider} from \'react-redux\'; import store from \'@/redux\'; ReactDOM.render( <AppContainer> <BusProvider> <Provider store={store}> <App/> </Provider> </BusProvider> </AppContainer>, document.getElementById(\'root\') )
这里我们引入了局部热更新,这样当我们修改部分文件时,不会造成整个页面的刷新,可以保留状态值。
npm install react-hot-loader --save
此外,我们还引入了状态管理器store,用来管理我们所有组件的状态。
在import文件的时候,我们引入了@别名,@指的的是src路径,其配置在webpack.config.js文件中:
至此,我们整个前端框架搭建完毕,我们可以运行程序,访问http://localhost:3000:
此外,我们还可以访问about页面:
我们可以看到,访问/会加载Home组件和PageNotFound组件,访问/about会加载Home和About组件。
八、源码地址
由于整个博客系统涉及到的页面较多就不一一介绍了,最终实现效果如下:
代码放在github上:前端代码:https://github.com/Zhengyang550/react-blog-zy
后端代码:https://github.com/Zhengyang550/jnu-blog-server
参考文章:
[5]antd官方手册