从0到1打造一套基于最新技术的React流水线(JS篇)

841 阅读14分钟

前言

时间犹如白驹过隙,2019-11-11这个特殊的日子,我加入了掘金的大家庭。

image.png

除了感叹时间飞逝之外,最直观的感受就是,现在好的文章实在是越来越少了,水的文章越来越多了,为什么这么说呢,可能就是我平时刷掘金时间还是蛮多的,上班摸鱼之际,下班地铁之上,家中无聊之时。也不知不觉掘金也成为了生活中的一部分,也不仅仅限于工作方面的使用了,但是作为一名勉强在刷时长方面占优的资深掘金粉,却无一篇技术文章的产出,实在是令人汗颜,最近由于个人在想拓宽技术领域(也是由于有关这个方面的文章零零散散,没有集中整合成一套体系),觉得很多人跟我一样,想去提升自己(学习React),却总是感觉差了点什么,也许你跟我一样,差的是一套完整的知识体系。

希望这篇文章能够给想学习React的同学带来那么一点点惊喜。

开始

好了,废话不多说,观众老爷们,咱们这就开始吧。

我们这篇文章讲的是纯Hook语法,相信Hook语法大家也耳熟能详,平时看的文章里面或多或少也带点Hook,它的优势也很明显,什么组件复用啊,减少代码复杂度啊,解决this的指向啊等等问题,这里就不一一赘述了,目前市面上基本都是两种方式创建React项目,一种是官方出的create-react-app脚手架,一种是利用webpack自定义,这两种方式我这里都带大家过一遍。

通过webpack手动生成项目

本文章基于最新版本的webpack5.x版本。

创建项目

// 先创建文件夹
mkdir react-app
// 初始化
yarn init -y
// 或
npm init -y

开发依赖

首先,我们需要知道我们的项目在开发环境中所需要依赖的npm包:

以下为本次项目所需要的基础依赖

//   ----webpack相关
yarn add webpack webpack-cli webpack-dev-server -D
//   ----babel相关
yarn add cross-env @babel/core @babel/preset-env @babel/preset-react @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import -D
//   ----loader相关
yarn add babel-loader css-loader style-loader postcss-loader url-loader -D
// sass
yarn add sass sass-loader -D
// 或者less
yarn add less less-loader -D

babel相关作用具体可以到这里查看文 www.babeljs.cn/docs/babel-…

目录结构

新增build文件夹,在该目录下新增三个文件

image.png

webpack.config.js      -----通用配置,开发环境和生产环境都能用到的,单独拎出来
webpack.dev.js         -----开发环境配置
webpack.prod.js        -----生产环境配置

接下来我们先完善一下目录:

image.png

通用配置

// 下载webpack插件
yarn add html-webpack-plugin mini-css-extract-plugin -D

html-webpack-plugin        -----生成html模板文件,并自动带入相关依赖,css,js等
mini-css-extract-plugin    ----文件中的css文件提取到外部文件中,通过link方式引入,不使用的话会以内联<style></style> 引入

配置webpack.config.js文件,完整代码如下:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const srcDir = path.join(__dirname, '../src')

module.exports = {
    entry: path.join(__dirname, '../src/index.js'),
    output: {
        path: path.join(__dirname, '../dist'),
        filename: 'js/[name].[chunkhash:8].js',
        chunkFilename: 'js/[name].[chunkhash:8].js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                include: [srcDir],
                use: ['babel-loader?cacheDirectory=true'],
                exclude: /node_modules/
            },
            {
                test: /\.scss$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader',
                    'postcss-loader'
                ],
                include: [srcDir]
            },
            {
                test: /\.css$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'postcss-loader'
                ]
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 10 * 1024,                                    // 不超过10kb转化为base64格式
                        name: 'images/[name].[hash:8].[ext]'                // 创建一个img的文件夹并将图片存入
                    }
                },
                include: [srcDir],
                exclude: /node_modules/
            },
            {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
                use: ['url-loader'],
                include: [srcDir],
                exclude: /node_modules/
            },
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                use: ['url-loader'],
                include: [srcDir],
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: `${srcDir}/public/index.html`,
            favicon: `${srcDir}/public/favicon.ico`
        }),
        new MiniCssExtractPlugin({
            filename: 'css/[name].[contenthash:8].css',
            chunkFilename: 'css/[name].[contenthash:8].css'
        }),
    ],
    resolve: {
        extensions: ['.js', '.jsx'],
        alias: {
            '@': srcDir,
        }
    }
}

加载babel相关的一些插件,根目录新建.babelrc,填写如下内容:

{
    "presets": ["@babel/preset-env", "@babel/preset-react"],
    "plugins": ["@babel/plugin-syntax-dynamic-import", ["@babel/plugin-transform-runtime"]]
}

postcss-loader配置在根目录新建postcss.config.js文件然后加上以下内容

module.exports = {
    plugins: {
        autoprefixer: {},
    }
}

开发环境配置

接下来完善一下webpack.dev.js配置。

因为通用配置和开发环境配置以及生产环境配置相互分开,所以我们需要一个插件来让他们的通用配置项合并一下:

yarn add webpack-merge -D

完整webpack.dev.js如下:

const { merge } = require('webpack-merge')

const baseConfig = require('./webpack.config')

module.exports = merge(baseConfig, {
    mode: 'development',
    devtool: 'cheap-source-map',
    devServer: {
        port: 8080,
        hot: true,
        open: true,
        historyApiFallback: true,  //单页面路由都会命中同个index.html
        compress: true,   // gzip
        proxy: {
            '/api': {
                target: 'localhost:8080',
                changeOrigin: true,
                source: false,
                pathRewrite: {
                    '^/api': ''
                },
                secure: true      // https代理
            }
        }
    }
})

生产环境配置

最后完善webpack.prod.js配置。

生成配置我们用到了几个插件

yarn add clean-webpack-plugin -D      // 每次打包清掉打包目录dist
yarn add terser-webpack-plugin -D     // 压缩格式化代码
yarn add webpack-bundle-analyzer -D   // 生成报告,分析打包之后的文件大小

完整配置如下:

const webpack = require('webpack')
const { merge } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserPlugin = require("terser-webpack-plugin")

const baseConfig = require('./webpack.config')

const config = merge(baseConfig, {
    mode: 'production',
    devtool:  false,
    plugins: [
        new CleanWebpackPlugin(),
        new webpack.ids.HashedModuleIdsPlugin()
    ],
    optimization: {
        splitChunks: {
            chunks: 'all'
        },
        minimize: true,
        minimizer: [
            new TerserPlugin({
                terserOptions: {
                    format: {
                        comments: false
                    },
                    compress: {
                        drop_console: true,
                        drop_debugger: true
                    }
                },
                extractComments: false
            })
        ]
    },
    performance: {
        hints: 'warning',
        maxAssetSize: 10240000,
        maxEntrypointSize: 10240000
    }

})

if (process.env.npm_lifecycle_event === 'build:report') {
    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    config.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = config

启动项目

在启动项目之前我们先配置一下npm命令,在package.json里面添加如下内容:

image.png

引入react相关包

yarn add react react-dom

编写index.js文件

import React from 'react';
import ReactDom from 'react-dom';
import App from './App'

ReactDom.render(<App />, document.getElementById('root'))

编写App组件

import React from 'react';

const App = () => {
    return(
        <div>
            react app 启动成功!
        </div>
    )
}

export default App
yarn dev
// 或
npm run dev

image.png

✅到这里我们通过自定义webpack配置项目已经成功啦~ 下一步,我们通过官方脚手架生成项目

通过官网脚手架自动生成项目

看官方create-react-app5.x版本已经出来,咱不管,咱以这个图为准

image.png

创建项目

看目前按照官方文档选择执行以下其中一种

image.png

Mac需要加sudo

npx create-react-app my-app

生成的文件目录如下:

image.png

启动项目

我们根据提示,启动项目

cd my-app
npm run start

image.png

好了~我们利用两种方式生成项目到这里结束啦~

你以为到这里就完了吗?

那我这文章也太水了吧~ 那可忍受不了,接下来继续带大家打造好一套后台管理基础模板供大家日常开发使用。

我们选用一套创建好的项目模板继续开发,go~

两者兼得

很多人既想要使用官方脚手架,又想要自定义的webpack配置,有没有办法呢? 答案肯定是有的,社区目前有几种方案,我们选择用@craco/craco,这也是ant-design官方推荐的一种。

因为我们一开始初始化用的是npm,以下我会统一使用npm去下载依赖

// 安装
npm i @craco/craco -D
// 接下来也把需要使用到ant-design以及react-router安装上
npm i antd react-router-dom

然后是自定义的webpack配置,我们需要做一些修改,先试更改一下项目启动方式,修改package.json文件:

"scripts": {
    - "start": "react-scripts start",
    - "build": "react-scripts build",
    - "test": "react-scripts test",
    +   "start": "craco start",
    +   "build": "craco build",
    +   "test": "craco test",
}

image.png 从官方文档里面我们可以看到有这几种方式,我这里选择的是第三种,也就是在根目录创建craco.config.js,其他的你们可以自己琢磨一下

官方文档地址:github.com/gsoft-inc/c…

接下来配置craco.config.js

// craco是适配webpack4.x的,所以我们指定一下插件版本
npm i terser-webpack-plugin@4.0.3 babel-plugin-import -D
const path = require('path')
const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
    babel: {
        plugins: [
          // 配置 babel-plugin-import,组件按需加载
            [
                'import',
                {
                    libraryName: 'antd',
                    libraryDirectory: 'es',
                    style: 'css',
                },
                'antd',
            ]
        ],
    },
    webpack: {
    // webpack的相关配置,可更改
        configure: (config, { env, paths }) => {
            paths.appBuild = 'dist'
            config.output = {
                ...config.output,
                path: path.resolve(__dirname, 'dist')
            }
            config.devtool = env === 'production'? false : config.devtool;
            // 去掉打包时的console和debugger
            config.optimization.minimizer.map(plugin => {
                if (plugin instanceof TerserPlugin) {
                    Object.assign(plugin.options.terserOptions.compress, {
                        drop_debugger: true,
                        drop_console: true
                    })
                }
                return plugin
            })
            return config
        },
        // 路径别名
        alias: {
            "@": path.resolve("src")
        },
        // 开发环境代理
        devServer: {
            proxy: {
                '/api': {
                    target: 'localhost:3000',
                    changeOrigin: true,
                    pathRewrite: {
                        '/api': ''
                    },
                    bypass: (req, res, proxyOptions) => {
                        console.log(req, res, proxyOptions)
                    }
                }
            }
        },
    },
} 

我们再启动项目看看,nice,完美~

image.png

项目布局

我们继续完善项目,首先删掉一些用不到的文件,最终如下

image.png

引入路由

新建router文件夹,建立index.js文件,新增内容

// 利用lazy和Suspense实现路由懒加载,用到该组件的时候才会加载该js文件
import React, { lazy, Suspense } from 'react'
import { Navigate, useRoutes } from 'react-router-dom'
import BasicLayout from '@/layouts'
import { Spin } from 'antd'
const Login = lazy(() => import('@/pages/login'))
const Home = lazy(() => import('@/pages/home'))

const Loading = () => {
    return (
        <Spin />
    )
}

export const routes = [
    {
        path: '/login',
        element: <Suspense fallback={<Loading />}><Login /></Suspense>
    },
    {
        path: '/',
        element: <BasicLayout />,
        children: [
            {
                index: true,
                element: <Navigate to="dashboard" replace={true} />
            },
            {
                path: 'dashboard',
                element: <Suspense fallback={<Loading />}><Home /></Suspense>
            }
        ]
    }
]

const Routes = () => {
    return useRoutes(routes)
}

export default Routes

根据路由生成相关的pages下面的文件 image.png

然后我们App.jsx文件中配置一下

import React from "react"
import { BrowserRouter as Router } from "react-router-dom"
import Routes from '@/router'

const App = () => {
    return (
        <Router>
           <Routes />
        </Router>
    )
}

export default App

我们利用ant-design编写相关的基本布局

基本布局

首先在@/layouts/index.jsx中

import React from 'react'
import { Outlet } from 'react-router-dom'
import { Layout } from 'antd'
const { Sider, Content, Header, Footer } = Layout

const BasicLayout = () => {
    return (
        <Layout>
            <Sider />
            <Layout>
                <Header />
                <Content>
                // 所有的在/下面的视图会在这里展示,可以看成router-view
                    <Outlet />
                </Content>
                <Footer></Footer>  
            </Layout>
        </Layout>
    )
}

export default BasicLayout

完善基本布局

我们要知道我们的基本布局需要包含哪些,这里我添加的是几个常用的,侧边菜单栏,菜单折叠按钮,面包屑,个人信息按钮,当然,还有其他的一些,我们也可以根据原理一一实现。

我们现在来拆分一下文件夹。

image.png

然后在layouts/index.jsx中我们来组装起来

import React from 'react'
import { Outlet } from 'react-router-dom'
import { Layout } from 'antd'
import MenuSide from './menu-side'
import HeaderBreadcrumb from './header-breadcrumb'
import HeaderCollapse from './header-collapse'
import HeaderUser from './header-user'
const { Header, Content, Footer } = Layout
const BasicLayout = () => {
    return (
            <Layout className="zw-layout">
                <MenuSide />
                <Layout className="zw-layout-inside zw-layout-inside-fix-with-sider">
                    <Header className="zw-layout-header zw-layout-header-fix">
                        <HeaderCollapse />  
                        <HeaderBreadcrumb />
                        <div className="zw-layout-header-right">
                            <HeaderUser />
                        </div>
                    </Header>
                    <Content className="zw-layout-inside-content zw-layout-inside-content-fix-with-header">
                        <Outlet />
                    </Content>
                    <Footer style={{ textAlign: 'center' }}>这是底部信息</Footer>
                </Layout>
            </Layout>
        )
}

export default BasicLayout

全局样式

以上我们完成了一个基本的布局,接下来我们为我们的基本布局新建一个全局的布局样式,根目录新建styles/index.scss

@import 'layout/index.scss';

然后新增styles/layout/index/scss

// submenu弹出框自定义
.custom-popper-menu {
	padding: 2px 0 !important;
	.ant-menu-sub {
            border-radius: 5px !important;
	}
}
.zw-layout {
    &-sider {
        min-height: 100vh;
		background-color: #fff;
		box-shadow: 2px 0 6px 0 rgba(29, 35, 41, 0.05);
		position: relative;
		z-index: 11;
		// 菜单自定义
		.ant-menu-root {
			.ant-menu-item {
				height: 52px !important;
				line-height: 52px !important;
				margin: 0 !important;
			}
			.ant-menu-submenu {
				&-title {
					height: 52px !important;
					line-height: 52px !important;
					margin: 0 !important;
				}
			}
		}
        &-fix {
            position: fixed;
			top: 0;
			left: 0;
            background-color: #001529 !important;
        }
        &-logo {
            width: 100%;
			height: 64px;
			line-height: 64px;
			text-align: center;
			overflow: hidden;
			img {
				vertical-align: middle;
				height: 80%;
			}
		}
    }
	&-inside {
		min-height: 100vh;
		transition: all 0.2s ease-in-out;
		overflow-x: hidden;
		flex-direction: column !important;
		&-content {
			margin: 16px !important;
			position: relative;
			&-fix-with-header {
				padding-top: 64px;
			}
		}
	}
    &-header {
        width: 100%;
		background: #fff;
		padding: 0;
		box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
		transition: all 0.2s ease-in-out;
		z-index: 9;
		display: block;
		&-fix {
			position: fixed;
			top: 0;
			right: 0;
			left: 256px;
			z-index: 11;
			&-collapse {
				left: 64px;
			}
		}
		&-trigger {
			display: inline-block;
			width: 64px;
			height: 64px;
			line-height: 64px;
			text-align: center;
			cursor: pointer;
			transition: all 0.2s ease-in-out;
			&:hover {
				background: #f8f8f9;
			}
			svg {
				font-size: 16px;
			}
			&-min {
				width: auto;
				padding: 0 12px;
				i {
					font-size: 18px;
				}
			}
		}
		&-breadcrumb {
			display: inline-block;
		}
		&-right {
			height: 64px;
			float: right;
		}
		&-user {
			line-height: 64px;
			display: block;
			&-name {
				margin-left: 12px;
				vertical-align: middle;
			}
		}
    }
}

在配置菜单的时候我们先看一下,状态管理,毕竟我们的菜单折叠要用到,等会再讲菜单是怎么配置的

状态管理

这里我用的是@reduxjs/toolkit,也有mobx版的,一开始我是基于mobx去做的,但是想到跟react的严格的单项数据流思想有点冲突,故而回过头来用redux,毕竟redux官方出的,也没有redux那么繁琐,接下来我们来使用它。

npm install @reduxjs/toolkit react-redux

然后我们来使用它,根目录新建store/index.js,这里我们来分类管理一下

import { configureStore } from '@reduxjs/toolkit'
// 布局相关的状态
import layoutReducer from './modules/layout'
// 用户相关的状态
import userReducer from './modules/user'
// 页面相关的状态
import pageReducer from './modules/page'

const store = configureStore({
    reducer: {
        layout: layoutReducer,
        user: userReducer,
        page: pageReducer,
    }
})

export default store

./modules文件夹下是相关的模块的状态管理,我们先做布局相关的状态管理,我们在./modules/layout.js

import { createSlice } from "@reduxjs/toolkit"

export const layoutSlice = createSlice({
    name: 'layout',
    initialState: {
        // 菜单折叠
        menuCollapse: false,
    },
    reducers: {
        setMenuCollapse: state => {
            state.menuCollapse = !state.menuCollapse
        },
    }
})

/*
 * Actions
 */

export const { setMenuCollapse } = layoutSlice.actions

export default layoutSlice.reducer

其他的./modules下面的模块也是类似的处理方式。

然后在根目录里面index.js我们要加上,不然的话我们的数据管理,在子组件中获取不到

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import '@/styles/index.scss'

import store from '@/store'
import { Provider } from 'react-redux'

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

菜单管理

现在我们可以继续开发/layouts/下面的其他布局模块了,在menu-side/index.jsx文件中

import React, { useState } from 'react'
import { Layout, Menu } from 'antd'
import { useNavigate, useLocation } from 'react-router-dom'
import { useSelector } from 'react-redux'
// logo
import logo from '@/assets/logo.png'
// 折叠时的logo
import logoCollapse from '@/assets/logo-collapse.png'
// 这里我把配置的菜单单独拎出来一个文件,方便管理
import menuSider from '@/menu/sider'

const { Sider } = Layout
const { SubMenu } = Menu

const MenuSide = () => {
    const menuCollapse = useSelector(state => state.layout.menuCollapse)
    const navigate = useNavigate()
    const { pathname } = useLocation()
    const [defaultSelectedKeys] = useState([pathname])
    const currentOpenKeys = menuSider.filter(item => pathname.indexOf(item.path) > -1).map(it => it.path)
    const [defaultOpenKeys] = useState(currentOpenKeys)

    const handleMenuSelect = ({ key }) => {
        navigate(key, { replace: true })
    }

    //  渲染多级菜单项
    const renderSubMenu = ({ path, title, children, icon }) => {
        return (
            <SubMenu key={path} title={title} icon={icon? icon : null} popupClassName="custom-popper-menu">
                {
                    children.map(item => {
                        return item.children? renderSubMenu(item) : renderMenuItem(item)
                    })
                }
            </SubMenu>
        )
    }

    //  渲染末级菜单项
    const renderMenuItem = ({ path, title, icon }) => {
        return (
            <Menu.Item key={path} icon={icon? icon : null}>
                {title}
            </Menu.Item>
        )
    }

    return (
        <Sider trigger={null} collapsible collapsed={menuCollapse} className="zw-layout-sider zw-layout-sider-fix" width={menuCollapse? 80 : 256}>
            <div className="zw-layout-sider-logo ">
                <a href="/">
                    <img src={menuCollapse? logoCollapse : logo} alt="" />
                </a>
            </div>
            <Menu
                mode='inline'
                theme='dark'
                defaultSelectedKeys={defaultSelectedKeys}
                defaultOpenKeys={defaultOpenKeys}
                onSelect={handleMenuSelect}>
                {
                    menuSider.map(item => {
                        return item.children ? renderSubMenu(item) : renderMenuItem(item)
                    })
                }
            </Menu>
        </Sider>
    )
}

export default MenuSide

单独拎出来的菜单如下所示

const pre = '/dashboard/'
const menuSider = [
    {
        path: '/dashboard',
        title: 'Dashboard',
        icon: <HomeOutlined />,
        // 如果需要二级菜单的话加children,三级同理
        // children: [
        //    {
        //        path: `${pre}console`,
        //        title: '主控台'
        //    },
        //    {
        //        path: `${pre}analysis`,
        //        title: '分析页'
        //    }
        // ]
    }
]
export default menuSider

接下来我们继续完善其他的布局模块

./modules/header-collapse/index.jsx

import React from 'react'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import { useSelector, useDispatch } from 'react-redux'
import { setMenuCollapse } from '@/store/modules/layout'

const HeaderCollapse = () => {
    const menuCollapse = useSelector(state => state.layout.menuCollapse)
    const dispatch = useDispatch()
    return (
        <span className="zw-layout-header-trigger" onClick={() => dispatch(setMenuCollapse())}>
            {
                menuCollapse? <MenuUnfoldOutlined /> : <MenuFoldOutlined />
            }
        </span>
    )
}

export default HeaderCollapse

./modules/header-breadcrumb/index.jsx

import React, { useEffect } from 'react'
import { useLocation } from "react-router-dom";
import { Breadcrumb } from 'antd';
import { useSelector, useDispatch } from "react-redux";
import { setBreadcrumbList } from "@/store/modules/page";
import menuSider from "@/menu/sider"

// 将每项的children与父级合并为一个数组
export function getAllMenu (menulist) {
    let allMenu = [];
    menulist.forEach(menu => {
        if (menu.children && menu.children.length) {
            const menus = getMenuChildren(menu);
            menus.forEach(item => allMenu.push(item));
        } else {
            allMenu.push(menu);
        }
    });
    return allMenu;
}
// 获取数据的子级
export function getMenuChildren (menu) {
    if (menu.children && menu.children.length) {
        return menu.children.reduce((all, item) => {
            const foundChildren = getMenuChildren(item);
            return all.concat(foundChildren);
        }, []);
    } else {
        return [menu];
    }
}
// 获取当前路由匹配的菜单
export function getMatchMenu (currentPath, menuList) {
    const currentBreadcrumbList = [];
    const allMenu = getAllMenu(menuList)
    allMenu.forEach(menu => {
        if (currentPath.indexOf(menu.path) > -1) {
            currentBreadcrumbList.push({
                path: menu.path,
                title: menu.title
            });
        }
    });
    return currentBreadcrumbList
}

const HeaderBreadcrumb = () => {
// 我们在之前的store里面的pageSlice定义的,和layout类型,你们可以自己实现一下,很简单
    const breadcrumbList = useSelector(state => state.page.breadcrumbList)
    const dispatch = useDispatch()
    const { pathname } = useLocation()
    useEffect(() => {
        const breadcrumbList = getMatchMenu(pathname, menuSider)
        dispatch(setBreadcrumbList(breadcrumbList))
    }, [pathname, dispatch])
    return (
        <Breadcrumb className="zw-layout-header-breadcrumb">
            {
                breadcrumbList.map((item, index) => {
                    return (
                        <Breadcrumb.Item key={item.path} href={index === breadcrumbList.length - 1? null : item.path}>
                            {item.title}
                        </Breadcrumb.Item>
                    )
                })
            }
        </Breadcrumb>
    )
}

export default HeaderBreadcrumb

登录路由鉴权

./layouts/header-user/index.jsx

import { useNavigate, useLocation } from "react-router-dom";
import { Menu, Dropdown, Avatar } from 'antd';
import { UserOutlined } from '@ant-design/icons';
import { useSelector } from 'react-redux'
import util from '@/utils'
const HeaderUser = () => {
    const navigate = useNavigate()
    const { pathname } = useLocation()
    // 获取用户信息,也是存在store里面
    const { userName } = useSelector(state => state.user.userInfo)
    // 退出登录移除cookie中的token
    const handleMenuClick = ({ key }) => {
        if (key === 'logout') {
            util.cookies.remove('token')
            navigate(`/login?redirect=${pathname}`, { replace: true })
        }
    }
    const userMenu = (
        <Menu onClick={handleMenuClick}>
            <Menu.Item key="message">
                消息通知
            </Menu.Item>
            <Menu.Item key="password">
                修改密码
            </Menu.Item>
            <Menu.Divider />
            <Menu.Item key="logout">退出登录</Menu.Item>
        </Menu>
    );
    return (
        <span className="zw-layout-header-trigger zw-layout-header-trigger-min">
            <Dropdown overlay={userMenu} trigger={['hover']} placement="bottomCenter">
                <span className="zw-layout-header-user">
                    <Avatar size={24} icon={<UserOutlined />} />
                    <span className="zw-layout-header-user-name">{userName}</span>
                </span>
            </Dropdown>
        </span>
    )
}

export default HeaderUser

有些时候我们的页面只需要登录才可以看到,没有登录的话是看不到的,并且跳转到登录页面,这里我是把登录获取的token存在cookie里面的,你们也可以存在storage中,新建工具类方法文件/utils/index.js


const util = {
    cookies
}
export default util

./cookies.js

npm i js-cookie
import Cookies from "js-cookie";

const cookies = {};

/**
 * @description 存储 cookie 值
 * @param {String} name cookie name
 * @param {String} value cookie value
 * @param {Object} cookieSetting cookie setting
 */
cookies.set = function (name = 'default', value = '', cookieSetting = {}) {
    let currentCookieSetting = {
        expires: 1
    };
    Object.assign(currentCookieSetting, cookieSetting);
    Cookies.set(`admin-${name}`, value, currentCookieSetting);
};

/**
 * @description 拿到 cookie 值
 * @param {String} name cookie name
 */
cookies.get = (name = 'default') => {
    return Cookies.get(`admin-${name}`);
}

/**
 * @description 拿到 cookie 全部的值
 */
cookies.getAll = () => {
    return Cookies.get()
};

/**
 * @description 删除 cookie
 * @param {String} name cookie name
 */
cookies.remove = (name = 'default') => {
    return Cookies.remove(`admin-${name}`);
};

export default cookies

我们在/router下新建AuthRoute.jsx

import React, { lazy, Suspense} from 'react'
import { Navigate, useLocation, Routes, Route } from "react-router-dom"
import util from "@/utils"

const Login = lazy(() => import('@/pages/login'))

const Loading = () => {
    return (
        <Spin />
    )
}

const AuthRoute = ({ children }) => {
    const token = util.cookies.get('token')
    const { pathname } = useLocation()
    if (token && token !== 'undefined') {
        return children
    } else {
        return (
            <Routes>
                <Route path="*" element={<Navigate to={`/login?redirect=${pathname}`} replace={true} />} />
                <Route path="/login" element={<Suspense fallback={<Loading />}><Login /></Suspense>} />
            </Routes>
        )
    }
}

export default AuthRoute

我们在App.jsx中包裹一下

image.png 然后我们看下页面效果

image.png

image.png

我们新建一下登录页

import React from 'react'
import Particles from 'react-particles-js'
import styled from 'styled-components'
import { Form, Input, Button } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { useLocation, useNavigate } from 'react-router-dom'
import util from "@/utils"
// 使用styled-components实现样式隔离,用过的都知道好用~
const Div = styled.div`
    width: 100vw;
    height: 100vh;
    position: relative;
    .login-particles-wrapper {
        width: 100%;
        height: 100%;
        background: #001529;
    }
    .login-form-wrapper {
        width: 480px;
        padding: 30px;
        background: rgba(255, 255, 255, 0.8);
        border-radius: 5px;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        h1 {
            text-align: center;
        }
        p {
            text-align: center;
        }

        .login-form-button {
            width: 100%
        }
    }
`

const Login = () => {
    const { search } = useLocation()
    const navigate = useNavigate()
    const onFinish = () => {
        let token = 'adasdasdasdasdasdasdasdasdasd'
        util.cookies.set('token', token)
        // 模拟请求
        setTimeout(() => {
            if (search) {
                navigate(search.split("=")[1], { replace: true })
            } else {
                navigate('/', { replace: true })
            }
        }, 1000)
    }
    return (
        <Div>
            <Particles className="login-particles-wrapper" />
            <div className="login-form-wrapper">
                <h1>React Login</h1>
                <p>欢迎使用React后台管理系统</p>
                <Form
                    name="login"
                    onFinish={onFinish}
                    >
                    <Form.Item
                        name="username"
                        rules={[
                        {
                            required: true,
                            message: 'Please input your Username!',
                        },
                        ]}
                    >
                        <Input prefix={<UserOutlined />} placeholder="Username" />
                    </Form.Item>
                    <Form.Item
                        name="password"
                        rules={[
                        {
                            required: true,
                            message: 'Please input your Password!',
                        },
                        ]}
                    >
                        <Input
                            prefix={<LockOutlined />}
                            type="password"
                            placeholder="Password"
                        />
                    </Form.Item>

                    <Form.Item>
                        <Button type="primary" htmlType="submit" className="login-form-button">
                            登录
                        </Button>
                    </Form.Item>
                </Form>
            </div>
        </Div>
    )
}

export default Login

好了~。我们可以通过这样去一步一步完成我们的项目,当然这个项目还有好多地方去完善,例如路由分角色权限校验,功能点校验,但是道理都一样的,我们可以同vue一样在路由信息表带上role字段然后去判断,具体就不实现了哈,大佬们也可以多多提提意见加以改进~

这篇文章希望能够给想学习React的同学一点帮助,当然React大佬也别喷我~,最后觉得有点点帮助的话,请给我赞一下,谢谢大家~

当然后续也会出TS进阶篇~希望大家可以关注一下。