react-router-dom6 +react18 + mobx6 配置
官网地址:react-router
本项目使用 react18.2.0 + antd 5.1.6 + react-router-dom 6.4.3 + mobx6.7.0配置,另外还配置了git commit自动修复eslint和模拟数据服务。
一、项目目录结构
(1).husky是git hooks文件夹。
文件夹中的pre-commit文件配置了 npm run lint-staged,commmit时会自动执行 lint-staged 进行eslit自动修复。
首先安装 lint-stage,lint-staged 是文件过滤器,它只会校验你提交或者说你修改的部分内容
npm install lint-stage --save-dev
package.json中配置
scripts里面中配置
"scripts": { "lint-staged": "lint-staged" }
再配置,lint-staged就配置完成了,到这步eslint只能手动执行命令修复,再搭配hucky可以实现提交时自动修复。
"lint-staged": { "**/*.{js,jsx}": [ "eslint --fix" ] }
hucky安装配置
# husky使用(v8.0) #### 1.npm install husky --save-dev 安装 #### 2.npx husky install 手动启用husky #### 3.npx husky add .husky/pre-commit "npm run lint-staged" 生成husky配置文件(执行完这一步,根目录会有一个 .husky目录) #### 4.为了避免手动启动在package.json的 scripts 里面添加如下配置
hucky下的pre-commit文件
#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npm run lint-staged
script里面再配置 "prepare": "husky install" ,自动修复配置完成
"scripts": { "prepare": "husky install" , "lint-staged": "lint-staged" }
(2)mock是模拟数据配置,
mock文件夹下的server文件是express node服务器的配制,此外还应用了mock.js进行模拟数据生成,使用时进入当前文件夹执行 node server 即可开启服务。
express服务器配置
//nodejs v14版以上,默认情况下不再有require,需要手动添加以下这二行代码 /*------------*/ import { createRequire } from 'module'; const require = createRequire(import.meta.url); /*-----------*/ const Mock = require('mockjs'); const express = require('express'); const app = express(); const multer = require('multer'); const upload = multer(); // for parsing multipart/form-data app.use(express.json()); // for parsing application/json app.use(express.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded // 解决跨域问题 app.all("*", function(req, res, next){ res.header("Access-Control-Allow-Origin", "*"); res.header('Access-Control-Allow-Headers', 'content-type,x-custom-header'); next(); }); /*当是Multipart/form-data的请求头时,需要给post第二个参数加upload.array() 实例 : app.post('/upload',upload.array(), function (req, res) {}) */ app.post('/upload', upload.array(), function (req, res) { //post获取参数 res.send({ user: req.body }); }); app.post('/login', function (req, res) { console.log('res', res.query); //post获取参数 req.body res.send({ user: req.body }); }); app.get('/userList', function (req, res) { //get获取参数 req.query // const params = req.query; //mock生成模拟数据 const data = Mock.mock({ "list|10": [ { "id|+1": 1, //生成id,自增1 "userName": "@cname", //生成姓名(这里生成的是中文名称) "userImg": "@Image('100*40','#c33','#ffffff','商品')", //生成随机图片(大小/背景色/字体颜色/文字信息),打印的是图片地址 "userAddress": "@county(true)", //随机生成地址 "userDate": "@date('yyyy-MM-dd')", //随机生成yyyy-MM-dd格式的日期 "userPhone": /^1(5|3|7|8)[0-9]{9}$/, //随机生成电话号码 "userStart|1-5": "★", //随机生成1至5个指定的图形(★) }, ] }); res.json({ list: data.list }); }); app.listen(3000, ()=>{ console.log("服务地址:http://localhost:3000/"); });
(3)scripts是webpack5的配置文件
webpack配置
图片配置webpack5官方有了内置模块不需要在使用url-loader和file-loader了,官网地址:https://webpack.docschina.org/guides/asset-modules/#custom-output-filename
module: { rules: [ //发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。 { test: /\.(png|jpg|jpeg|gif)/, type: 'asset/resource', generator: { filename: 'assets/images/[hash][ext][query]' } }, // 输出data URI,类似于url-loader { test: /\.(png|jpg|jpeg|gif)/, type: 'asset/inline' }, //webpack 将按照默认条件,自动地在 resource 和 inline 之间进行选择:小于 20kb 的文件,将会视为 inline 模块类型,否则会被视为 resource 模块类型。 { test: /\.(png|jpg|jpeg|gif)/, type: 'asset', // parser: { dataUrlCondition: { maxSize: 20 * 1024 // 20kb } } } ] },
打包之前清除之前的旧包,直接在output中配置clean: true就可以了。
output: { path: path.join(__dirname, '../dist'), filename: '[name].js', clean: true, // 在生成文件之前清空 output 目录 (clean-webpack-plugin插件,已经内置了,不用安装了) },
css压缩插件官方推荐使用 css-minimizer-webpack-plugin,js压缩 terser-webpack-plugin 插件官方已经内置了,不需要自定义的话可以直接引入使用,要自定义的话仍需安装。
const TerserPlugin = require("terser-webpack-plugin"); //压缩js const MiniCssExtractPlugin = require("mini-css-extract-plugin"); //css抽离 const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); //压缩 CSS optimization: { minimizer: [new TerserPlugin(), new CssMinimizerPlugin()] }
二、入口文件index.js配置,react18版本发生了一些变化,改为以下写法
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './app'; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <App /> </React.StrictMode> );
三、Routes组件替换了v5版的Switch组件
router.js里面定义的路由
import React, { lazy } from 'react' import { UserOutlined, VideoCameraOutlined } from '@ant-design/icons' const Goods = lazy(() => import(/* webpackChunkName: "Goods" */ 'pages/goods') ) const List = lazy(() => import(/* webpackChunkName: "List" */ 'pages/list') ) const router = [ { path: '/home/goods', component: Goods, name: '商品页', icon: <UserOutlined/> }, { path: '/home/list', component: List, name: '列表页', icon: <VideoCameraOutlined/> } ] export default router
二级路由引用到一级路由里面,路径要用一级路由的路径作为开头。默认显示的二级路由可以用以下写法。
<Route path="/home" element={<Home/>}> {/* 二级路由 */} <Route index element={<HomeIndex/>}/> </Route>
app.js
import React, { Suspense } from 'react'; import { HashRouter, BrowserRouter, Routes, Route } from "react-router-dom"; import { ConfigProvider } from 'antd'; import zh_CN from 'antd/lib/locale-provider/zh_CN'; import routerConfig from 'routerConfig'; import Login from 'pages/login'; import Home from 'pages/home'; import './App.less'; export default function App() { const HomeIndex = routerConfig[0]?.component; return ( <ConfigProvider locale={zh_CN}> {/* BrowserRouter 对应history模式 HashRouter对应hash 模式 */} <HashRouter> {/* react-router-dom v6 使用“Routes”代替“Switch” */} <Suspense fallback={<div>loading</div>}> <Routes> <Route path="/" element={<Login/>}/> <Route path="/home" element={<Home/>}> {/* 二级路由 */} <Route index element={<HomeIndex/>}/> { routerConfig.map(item=>{ return ( <Route path={item.path} key={item.path} element={<item.component/>} /> ); }) } </Route> <Route path="*" element={<Login/>}/> </Routes> </Suspense> </HashRouter> </ConfigProvider> ); }
四、跳转方式用useNavigate方法或者使用NavLink和Link
useNavigate方法使用,只能在函数组件中使用,参数传递可以直接路径拼接,也可以设置第二个参数,获取可以使用react-router-dom提供的 useSearchParams, useLocation 来获取。
import React from 'react'; import { useNavigate, useSearchParams, useLocation } from "react-router-dom"; const Home = (props) => { const navigate = useNavigate(); const navClick = () => { //路由跳转 navigate('/home'); //history 的replace模式 // navigate('/home',{ replace: true }) //传参 /* navigate('/home?name=tom&age=18'); //方式一 navigate('/Detail/Shop', { state: { name: 'tom', age: "20" }}); //方式二 //方式一获取参数 const [search, setsearch] = useSearchParams(); console.log(search.get('name')); console.log(search.get('age')); //方式二获取参数,使用useLocation获取search参数 const state = useLocation(); console.log(state); */ }; return ( <div onOnclick={navClick}>页面跳转</div> ); };
NavLink的使用
<NavLink to='/home'>首页</NavLink>
五、antd配置home页
react-router-dom提供了Outlet组件,它可以把二级路由的页面内容显示到这里,相当于vue的router-view
import React, { useState, Suspense, useEffect } from 'react'; import { UserOutlined, VideoCameraOutlined } from '@ant-design/icons'; import { Layout, Menu } from 'antd'; import { Routes, Route, useNavigate, Outlet, NavLink } from "react-router-dom"; import routerConfig from 'routerConfig'; import About from '../goods'; import List from '../list'; import './index.less'; const { Header, Sider, Content } = Layout; const Home = (props) => { const [collapsed, setCollapsed] = useState(false); const navigate = useNavigate(); const navClick = (e) => { //路由跳转 navigate(e.key) }; return( <Layout className='home-main'> <Sider trigger={null} collapsible collapsed={collapsed}> <div className="title"> 框架构建demo </div> <Menu className='menu' theme="dark" mode="inline" defaultSelectedKeys={['/home/goods']} onClick={navClick} items={[ { key: '/home/goods', icon: <UserOutlined />, label: "商品" }, { key: '/home/list', icon: <VideoCameraOutlined />, label: "列表" } ]} /> </Sider> <Layout className="site-layout"> <Header className="site-layout-background" style={{ padding: 0 }}></Header> <Content className="site-layout-background" style={{ margin: '24px 16px', padding: 24, minHeight: 280, }} > <Suspense fallback={<div>loading</div>}> {/* Outlet相当于vue的router-view */} <Outlet/> </Suspense> </Content> </Layout> </Layout> ) }; export default Home;
到这里基本配置就完成了。
六、mobx 6配置
mobx 6不在支持装饰器的写法需要安装 mobx 和 mobx-react-lite(此包是用来关联React与mobx的),达到数据状态管理的趋势。
npm i mobx mobx-react-lite
创建store文件,在mobx
内部引入makeAutoObservable
进行数据的响应式管理;
import { runInAction, makeAutoObservable } from 'mobx'; import * as api from '../../api/index'; export default class UserStore{ constructor() { // 对初始化数据进行响应式处理 mobx v6不支持装饰器写法了 makeAutoObservable(this); } loading = true; user = {}; userList = []; //获取用户列表 async getUserList() { const res = await api.getUserList(); if(res.data){ runInAction(()=>{ this.userList = res.data?.list; this.loading = false; }); } } //销毁组件时重置参数 reset(){ this.user = {}; this.userList = []; this.loading = true; } }
根store文件对store进行整合
import UserStore from './user'; class Store{ constructor(){ this.userStore = new UserStore(); } } export default new Store();
使用先用 mobx-react 中的 Provider 对根组件进行包裹,再把定义的store透传过去
import React, { Suspense } from 'react'; import { HashRouter, Routes, Route } from "react-router-dom"; import { Provider } from 'mobx-react'; import store from './store'; import Home from 'pages/home'; export default function App() { return ( <Provider {...store}> {/* BrowserRouter 对应history模式 HashRouter对应hash 模式 */} <HashRouter> {/* react-router-dom v6 使用“Routes”代替“Switch” */} <Suspense fallback={<div>loading</div>}> <Routes> <Route path="/" element={<Login/>}/> <Route path="/home" element={<Home/>}/> <Route path="*" element={<Login/>}/> </Routes> </Suspense> </HashRouter> </Provider> ); }
组件中,使用 mobx-react-lite 中的 observer 对组件包裹一下进行关联
import React, { useEffect } from 'react'; import { observer } from 'mobx-react-lite'; // 从mobx-react-lite内部引入observer让mobx与react进行关联 import useRootStore from "../../store"; import { Table } from "antd"; function List (props) { const { userStore } = useRootStore; useEffect(()=>{ userStore.getUserList(); return ()=>{ console.log('---组件销毁--'); userStore.reset(); }; }, []); const handClick = ()=>{ userStore.getUserList(); }; const columns = [ { title: '姓名', dataIndex: 'userName', key: 'userName', }, { title: '用户头像', dataIndex: 'userImg', key: 'userImg', }, { title: '电话', dataIndex: 'userPhone', key: 'userPhone', }, { title: '住址', dataIndex: 'userAddress', key: 'userAddress', }, ]; return ( <> <h1 onClick={handClick}>列表页</h1> <Table dataSource={userStore.userList} columns={columns} rowKey={record => record.id}/></> ); } export default observer(List);
页面展示