携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
React项目框架搭建(三)
前置项
react:react18
打包工具:webpack5
路由:react-router-dom@6.x
UI:antd4.21.7
语言:typescript
node: node14
axios
、eslint
及prettier
代码规范配置
目前所用到的库都为当前最新版本
一起学习搭建react+webpack+ts框架啊!!!(一)
一起学习搭建react+webpack+ts框架啊!!!(二)
接着上篇继续进行项目配置
1. 基于axios的二次封装
1.1 axios是什么
Axios是一个基于
Promise
的网络请求库,作用于node.js
和浏览器中。在服务端它使用node.js
原生的http
模块,在客户端(浏览器)中使用XMLHttpRequests
。
1.2 axios特性
- 从浏览器创建中使用
XMLHttpRequests
; - 从
nodejs
创建http
请求; - 支持
Promise
api; - 拦截请求和响应;
- 转换请求和响应数据;
- 取消请求;
- 自动转换
JSON
数据; - 客户端支持防御
XSRF
攻击;
根据axios特性进行封装优化!
1.2 axios封装配置
-
依赖安装 npm i axios
-
request.tsx配置
- 创建一个axios实例
const request = axios.create({ baseURL, timeout: 1000, // 请求超时时间 // 请求头 headers: { 'Content-Type': 'multipart/form-data ;application/json; charset=utf-8', }, })
2. 取消请求的两种方式
-
AbortController
从v0.22.0
开始,Axios 支持以 fetch API 方式 ——AbortController
取消请求const controller = new AbortController(); axios.get('/foo/bar', { signal: controller.signal }).then(function(response) { //... }); // 取消请求 controller.abort()
-
CancelToken
v0.22.0
之前可以直接采用axios
中的cancel token API
,当前api
已废弃,可以采用CancelToken.source
去创建cancel token
; 当然也可以通过传递一个executor
函数到CancelToken
的构造函数来创建一个cancel token
这里我们采用第二种CancelToken
构造函数的方式处理重复请求问题
-
请求拦截
// 请求拦截 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) } }, }
-
响应拦截
// 重定向到登录页 后续提到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.tsx
及modules
文件夹
index.tsx
:all api
出口文件
modules
文件夹:用于存放各个业务模块对应的api
配置 -
index.tsx
配置
使用require.context
检索目录文件时,报如下错:
解决:require.context
是webpack
的一个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) } } }
-
使用时测试
最后将整个
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
样式正常生效
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
将我们写的公共样式覆盖掉了!!!
antd
一直存在全局样式这个问题,社区中也有很多人讨论,但目前仍没解决这个问题;
下图为antd的css打包依赖关系
解决思路:
- 两者之间的权重处理,且保证antd其他组件样式不被影响等;
- 或者想要自己写的样式权重高些,就给antd样式外包裹一层限制区域(也要注意其他组件样式不要被影响);
- 或者直接!important等样式穿透;
这里我直接选择第三种最简单的处理方式,公共样式中想要权重高些直接!important
处理;
3. react-router-dom@6配置
3.1 router
基本使用
-
安装依赖 npm i react-router-dom @types/react-router-dom
-
使用
HashRouter
引入index.tsx
将app
页面入口包裹在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>
效果图:
这样一个简单的
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.tsx
中useRoutes
中传入上面的router
即可,跟上图的效果一致; -
添加路由懒加载
React
中提供了lazy
方法
- 懒加载组件封装
/** * 懒加载组件包装器 */ 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加载即可
效果如下:
emmmmm本来还想融合antd
写个左侧导航菜单呢,嘿嘿,既然下班了,就这样吧!
不过,为了维持我周更的习惯,就直接发了~
相关链接:
一起学习搭建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
左侧导航组件功能点:
- 左侧导航数据的展示;
- 左侧菜单选中跟路由切换对应;
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> ) }
效果:
-
核心代码优化
当前采用的
antd > 2.20
,不推荐下面这种写法更改后:
<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
后续即可进入业务模块开发!!!