一、前端項目結構
在上一節的基礎上,我們分別在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官方手冊