一起学习搭建react+webpack5+ts框架啊!!!(三)

221 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

React项目框架搭建(三)

8C3DF32B.gif

前置项

react:react18
打包工具:webpack5
路由:react-router-dom@6.x
UI:antd4.21.7
语言:typescript
node: node14
axioseslintprettier代码规范配置

目前所用到的库都为当前最新版本

一起学习搭建react+webpack+ts框架啊!!!(一)
一起学习搭建react+webpack+ts框架啊!!!(二)

接着上篇继续进行项目配置

1. 基于axios的二次封装

axios官方文档

1.1 axios是什么

Axios是一个基于Promise的网络请求库,作用于node.js和浏览器中。在服务端它使用node.js原生的http模块,在客户端(浏览器)中使用XMLHttpRequests

1.2 axios特性

  1. 从浏览器创建中使用XMLHttpRequests
  2. nodejs创建http请求;
  3. 支持Promiseapi;
  4. 拦截请求和响应;
  5. 转换请求和响应数据;
  6. 取消请求;
  7. 自动转换JSON数据;
  8. 客户端支持防御XSRF攻击;

根据axios特性进行封装优化!

1.2 axios封装配置

  • 依赖安装 npm i axios

  • request.tsx配置

    1. 创建一个axios实例
    const request = axios.create({
       baseURL,
       timeout: 1000, // 请求超时时间
       // 请求头
       headers: {
           'Content-Type': 'multipart/form-data ;application/json; charset=utf-8',
       },
    })
    

    2. 取消请求的两种方式

    1. AbortControllerv0.22.0 开始,Axios 支持以 fetch API 方式 —— AbortController  取消请求

        const controller = new AbortController(); 
        axios.get('/foo/bar', {
            signal: controller.signal
        }).then(function(response) { //... }); 
        // 取消请求
        controller.abort()
      
    2. CancelToken v0.22.0之前可以直接采用axios中的cancel token API,当前api已废弃,可以采用CancelToken.source去创建cancel token; 当然也可以通过传递一个executor函数到 CancelToken 的构造函数来创建一个cancel token

这里我们采用第二种CancelToken构造函数的方式处理重复请求问题

  1. 请求拦截

     // 请求拦截
     request.interceptors.request.use(
        (config: AxiosRequestConfig) => {
            // 发送请求前统一进行处理
            // 进行请求判断 -> 处理重复请求问题 通过isCancel参数
            if (config.hasOwnProperty('isCancel') && config['isCancel']) {
                // 如果有这个属性 则重复请求时 取消之前的请求 只保留最后一个请求
                config.cancelToken = new CancelToken((c:Canceler) => {
                    // 删除请求
                    apiCach.deleteTask(config)
                    // 新增请求
                    apiCach.addTask(config, c)
                })
            }
            const method: string | undefined = config.method
            // 根据实际情况处理content-type
            config.headers = Object.assign(
                method === 'get'
                    ? { 'content-type': 'application/x-www-form-urlencoded' }
                    : { 'Content-Type': 'multipart/form-data; application/json; charset=utf-8' },
                config.headers
            )
            return config
        },
        (err: Error) => {
            // 异常抛出
            return Promise.reject(err)
        }
     )
    

    删除请求、新增请求

     const apiCach = {
        // api请求任务列表
        taskList: [],
        // 新增任务
        addTask(config, cancelToken) {
            this.taskList.push({ origin: `${config.url}&${config.method}`, cancelToken })
        },
        // 删除任务
        deleteTask(config) {
            const url: string = `${config.url}&${config.method}`
            // 找着当前要取消请求的index
            const index = this.taskList.find(i => i['origin'] === url)
            if (index > -1) {
                this.taskList[index].cancelToken('cancelRequest' + config.url)
                this.taskList.splice(index, 1)
            }
        },
     }
    
  2. 响应拦截

     // 重定向到登录页 后续提到util中
     const redirectLogin = () => {}
     // 响应拦截
     request.interceptors.response.use(
        (respose: AxiosResponse) => {
            // 获取服务端响应数据
            const { code, data, message } = respose.data
            // 约定code为400|401 重新登录
            if (code === 400 || code === 401) {
                return redirectLogin
            } else if (code === 200) {
                return data
            } else if (code) {
                // 抛出错误并进行提示
                return Promise.reject(data)
            } else {
                return Promise.resolve(data)
            }
        },
        (err: Error) => {
            return Promise.reject(err)
        }
     )
    

这里关于request的配置基本完结,后面就是将它整个抛出进行引入配置

1.3 all API export配置

  • services下创建index.tsxmodules文件夹
    index.tsxall api出口文件
    modules文件夹:用于存放各个业务模块对应的api配置

  • index.tsx配置
    使用require.context检索目录文件时,报如下错:
    image.png 解决:require.contextwebpack的一个api,主要用于获取某个目录下的所有文件,需要安装对应的webpack-env

    npm i @types/webpack-env -D
    

    index.tsx大致内容,具体见github

    // 遍历每个模块
    for (const module in serviceCombine) {
        services[module] = {}
        const apiMap = serviceCombine[module]
        // 获取服务器相关配置信息 defaultServer默认服务器访问域名
        const { defaultServer, servers } = config
        // 遍历每个模块下的所有api
        for (const apiName in apiMap) {
            // 接口请求调用时就采用services.module.apiName的方式
            services[module][apiName] = (data, apiUrl) => {
                // 处理每一个api
                const apiData = apiMap[apiName]
                const url = apiUrl || apiData.url
                const serverFlag = /^http/.test(url)
                let server = ''
                if (serverFlag) {
                    // 若api指定了请求的域名server则根据所传server访问;否则就采用默认域名进行请求
                    server = apiData.server ? servers[apiData.server] : servers[defaultServer]
                }
                const requestConfig = {
                    method: apiData.type || 'get', // 默认get请求
                    url: server + url,
                    // 处理重复请求 存在不同模块就是要同时访问相同接口 这时你可以配置isCancel,这样就可以避免接口被取消掉
                    isCancel: apiData.isCancel ? apiData.isCancel : false,
                    headers: apiData.headers ? apiData.headers : {},
                }
                const dataName = requestConfig.method === 'get' ? 'params' : 'data'
                requestConfig[dataName] = data
                return request(requestConfig)
            }
        }
    }
    
  • 使用时测试

    image.png 最后将整个services挂载,可直接通过services.test.getList形式进行调用即可,后续还可以更加方便的维护api

2. antd引入

2.1 antd依赖安装

npm i antd 

2.2 antd按需引入

借助于babel-plugin-import实现按需引入

npm i babel-plugin-import -D // 安装babel-plugin-import
// `.babelrc`中配置
{
 "plugins": [
     [
         "import", // 按需加载
         {
             "libraryName": "antd",
             "libraryDirectory": "es",
             "style": "css" // 这里我们不使用less 就设置为css了
         }
     ]
   ]
}

2.3 antd组件引入测试

页面中引入antd组件

import { Button } from 'antd'
export default class App extends React.Component {
    render(): React.ReactNode {
        return (
            <div>
                <Button type='primary'>antd-button</Button>
                <div className="title"> 这里是App页面!</div>
                <div className={base.title}>模块化样式</div>
            </div>
        )
    }
}

antd样式正常生效

image.png

2.4 优化css module配置

antd与其他样式隔离开来,分别解析

// 用于antd解析
 {
    test: /\.css$/,
    include: [/node_modules/], // antd(node_modules文件)目录
    use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
// 非antd样式解析
{
    test: /\.(css|scss)$/,
    exclude: [/node_modules/], // 非antd
    // css-loader对css文件进行合并处理等
    // style-loader用于处理的css文件以style标签的形式嵌入到html页面中
    use: [
        MiniCssExtractPlugin.loader,
        {
            loader: 'css-loader',
            options: {
                // 启用css module
                modules: {
                    auto: /\.module\./, // 仅业务页面相关样式开启
                    localIdentName: '[path][name]__[local]--[hash:base64:5]',
                },
            },
        },
        'postcss-loader',
        'sass-loader',
    ],
}

注意:未开启css module的样式名避免重复导致样式污染;

2.5 antd全局样式问题

global.scss中设置body样式

body {
    background-color: black;
}

antd将我们写的公共样式覆盖掉了!!!

image.png

antd一直存在全局样式这个问题,社区中也有很多人讨论,但目前仍没解决这个问题;

下图为antd的css打包依赖关系

image.png 解决思路:

  1. 两者之间的权重处理,且保证antd其他组件样式不被影响等;
  2. 或者想要自己写的样式权重高些,就给antd样式外包裹一层限制区域(也要注意其他组件样式不要被影响);
  3. 或者直接!important等样式穿透;

这里我直接选择第三种最简单的处理方式,公共样式中想要权重高些直接!important处理;

3. react-router-dom@6配置

3.1 router基本使用

  • 安装依赖 npm i react-router-dom @types/react-router-dom

  • 使用HashRouter引入

    index.tsxapp页面入口包裹在router

    const container = document.getElementById('app')
    const root = createRoot(container as Element)
    
    root.render(
       <HashRouter>
           <App />
       </HashRouter>
    )
    
  • useRoutes钩子读取路由配置

    App.tsx读取路由配置

     import { useRoutes } from 'react-router-dom'
     const App = () => {
         const appRoutesElement = useRoutes([
             { path: '/', element: <Home /> },
             { path: '/login', element: <Login /> },
             { path: '/hello', element: <Hello /> },
         ])
         return appRoutesElement
     }
    
  • Home.tsx页面测试

    <nav>
        <li>
            <Link to="/hello">hello</Link>
        </li>
        <li>
            <Link to="/login">login</Link>
        </li>
    </nav>
    

    效果图:

    result.gif 这样一个简单的react-router-dom路由切换就完成了

3.2 router配置优化

  • 路由信息配置

    实际就是将我们的路由表信息抽取出来单独进行维护

    创建router文件夹,index.tsx中维护路由信息

    // 全局路由
    const globalRoutes: Array<Router> = [
       {
           path: '/login',
           element: <Login />,
       },
       {
           // 缺省页面
           path: '*',
           element: <NotFound />,
       },
    ]
    
    function NotFound() {
       return <div>你所访问的页面不存在!</div>
    }
    // 主路由->后续接口中动态获取
    const mainRoutes: Array<Router> = [
       {
           // 首頁
           path: '/',
           element: <Home />,
           children: [
               {
                   path: '/hello',
                   element: <Hello />,
               },
           ],
       },
    ]
    
    let router: Array<Router> = globalRoutes.concat(mainRoutes)
    // 路由配置转换处理
    function transformRoutes(routeList = router) {
       const list: Array<Router> = []
       routeList.forEach(route => {
           const obj = { ...route }
           if (route.path === undefined) return
           // 页面重定向
           if (route.redirect) {
               obj.element = <Navigate to={obj.path} replace={true} />
           }
           // 如果存在嵌套路由
           if (obj.children) {
               // 递归处理子路由
               obj.children = transformRoutes(obj.children)
           }
           list.push(obj)
       })
       return list
    }
    
    router = transformRoutes(router)
    export default router
    

    再在App.tsxuseRoutes中传入上面的router即可,跟上图的效果一致;

  • 添加路由懒加载

    1. React中提供了lazy方法

    image.png

    1. 懒加载组件封装
    /**
     * 懒加载组件包装器
     */
    const LazyWrap: FC<lazyWarpProps> = ({ path }) => {
        const LazyComponent = lazy(() => import(`../views/${path}`))
        return (
            // 渲染lazy组件
            <Suspense fallback={<div> Loading.... </div>}>
                <LazyComponent />
            </Suspense>
        )
    }
    

    3. 使用懒加载完善路由配置

    element: <LazyWrap path="Home" /> // 只需将对应的路由调用LazyWrap加载即可
    

    效果如下:

    result1.gif


emmmmm本来还想融合antd写个左侧导航菜单呢,嘿嘿,既然下班了,就这样吧!

不过,为了维持我周更的习惯,就直接发了~

971EA252.gif 971EB9C2.gif

相关链接:
一起学习搭建react+webpack+ts框架啊!!!(一)
一起学习搭建react+webpack+ts框架啊!!!(二)
github代码地址

4. 导航菜单

4.1 左侧导航菜单

  • 调整下路由配置信息 const mainRoutes: Array = [{ // 页面入口 path: '/', element: , children: [ { path: '/home', name: '首页', element: }, { path: '/hello', name: '测试', element: , }, ], }]

  • Main.tsx页面引入路由及子页面 export default class Main extends React.Component { // 左侧导航数据 get leftNav() { return mainRoutes } render(): React.ReactNode { const { Sider, Content } = Layout return ( {/* 导航栏 */}

                    {/* 页面主体内容 */}
                    <Layout className=''>
                        <Content>
                            <Outlet />
                        </Content>
                    </Layout>
                </Layout>
            )
        }
    }
    
  • 封装NavBar左侧导航组件

    功能点:

    1. 左侧导航数据的展示;
    2. 左侧菜单选中跟路由切换对应;
    export default function NavBar(props: any) {
       // 处理导航数据
       const navData: MenuItem = props.leftNav[0].children
       // 获取到当前路由信息
       const location = useLocation()
       // 每次切换路由,获取当前最新的pathname,并赋给menu组件
       useEffect(() => {
           setMenuSelect(location.pathname)
       }, [location])
       // 默认选择首页
       const [menuSelect, setMenuSelect] = useState('/home')
       return (
           <div>
               <Menu mode="inline" selectedKeys={[menuSelect]}>
                   {/* menu切换时跳转到对应路由页面 */}
                   {navData.map((route: any) => (
                       <Menu.Item key={route.path}>
                           <NavLink to={route.path}>{route.name}</NavLink>
                       </Menu.Item>
                   ))}
               </Menu>
           </div>
       )
    }
    

    效果:

    image.png

  • 核心代码优化

    当前采用的antd > 2.20,不推荐下面这种写法

    image.png

    更改后:

    <Menu
        mode="inline"
        selectedKeys={[menuSelect]}
        items={navData}
        onClick={onclick}
    />
    

    初始页面重定向,采用副钩子函数

    // 默认选择首页
    const [menuSelect, setMenuSelect] = useState('/home')
    /**
     * 副作用钩子,一般用于异步请求,接收两个参数: 第一个参数异步操作 第二个参数数组 只要数组变化,useEffect就会执行,若第二个参数为空,useEffect会在每次组件渲染时执行;
     */
    // 每次切换路由,获取当前最新的pathname,并赋给menu组件
    useEffect(() => {
        // 路由重定向
        if (location.pathname === '/') {
            navigate('/home')
        }
    }, [location.pathname])
    

    左侧菜单点击跳转

    // 点击左侧菜单 进行路由跳转
    const onclick = ({ key }) => {
        setMenuSelect(key)
        navigate(key)
    }
    

以上,就完成了左侧导航菜单功能!!!
至此,整体框架搭建基本完成!!! 代码详细见github

后续即可进入业务模块开发!!!

2FC73E4D.gif