博客管理系統開發 -- 基於React前端框架搭建


一、前端項目結構

在上一節的基礎上,我們分別在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相關知識:

[1]完全理解 redux(從零實現一個 redux)

[2]淺談對於react-thunk中間件的簡單理解

三、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相關知識:

[1]react-router-dom@5.x官方文檔翻譯

[2]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新特性,感興趣可以查看以下博客:

[1]useContext Hook 是如何工作的

[2]useEffect Hook 是如何工作的

[3]微型庫解讀之200byte的EventEmitter - Mitt

[4]React組件通信——Event Bus

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

參考文章:

[1]完全理解 redux(從零實現一個 redux)

[2]淺談對於react-thunk中間件的簡單理解

[3]react-router-dom@5.x官方文檔翻譯

[4]react-router官方手冊

[5]antd官方手冊

[6]useContext Hook 是如何工作的

[7]useEffect Hook 是如何工作的

[8]微型庫解讀之200byte的EventEmitter - Mitt

[9]React Hooks 解析(上):基礎

[10]React Hooks 解析(下):進階

[11]你不知道的 useCallback

[12]使用Redux+Hooks完成一個小實例

[13]React組件通信——Event Bus


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM