vite 搭建 react18函数组件 项目

601 阅读11分钟

node版本: v18.18.0

使用yarn作为包管理工具

技术选型选用 react 18、react-router v6、redux、ant design、vite、typescript 组合。

下载插件:

在实际开发过程中,由于这些组件和函数组件它的结构都是固定的,所以可以使用一些插件将其生成出来

image-20230206115912857

使用rcc生成类组件,使用rfc生成函数组件

1. 搭建项目

1.1 create-vite 创建

使用命令

yarn create vite  或者 npm  create vite

01

02

03

默认创建的就是 react18

04

1.2 配置别名 resolve.alias

[配置 Vite | Vite 官方中文文档]

步骤一: 在项目根目录tsconfig.json配置

compilerOptions对象下添加:

  "baseUrl": ".",
  "paths": {
      "@/*": ["./src/*"]
  },

k0

步骤二:在vue.config.ts(VUE CLI构建的项目)或vite.config.ts(VITE构建的项目)文件中添加。

安装

yarn add @types/node -D

在vite.config.ts配置

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

op

1.3 安装 css预处理环境less

只需要安装 less ,无需配置 即可在项目中使用

yarn add less

1.4 css module

要文件名 *.module.less 才行

80

2. 集成 react-router

yarn add react-router-dom

我这里用 BrowserRouter 组件

2.1 useRoutes 配置路由

react router dom v6 支持配置路由 useRoutes(hook)实现

在根目录新建views文件和router文件, 分别添加如下文件, 根据自己的情况配置,views用来存放项目中的所有路由文件,提前通过快捷键 rfc初始化函数组件

a1

router/RouterIndex.tsx路由文件配置如下:

import { useRoutes,Navigate } from "react-router-dom";
import Home from "@/views/manage/Home";
import User from "@/views/manage/User";
import Role from "@/views/manage/Role";
import Login from '@/views/Login'
import Register from "@/views/Register";
function RouterIndex() {
  let element = useRoutes([
    
    {
      path: "/home",
      element: <Home />,
      children: [
        { path: "user", element: <User />, },
        { path: "role", element: <Role /> },
      ],
    },
    { path: "/", element: <Navigate to={'/home'} /> },
    { path: "login", element: <Login /> },
    { path: "reg", element: <Register /> },
  ]);

  return element;
}

export default RouterIndex

Home组件是父路由, 只要是父路由, 必须在页面中有用来渲染子路由的容器Outlet

image-20231124114445273

然后在根组件中引入RouterIndex组件

import {BrowserRouter,NavLink} from 'react-router-dom'
import RouterIndex from './router/RouterIndex'

function App() {
  return (
    <BrowserRouter>
      <ul className='menu-list'>
        <NavLink to={'/login'}>登录</NavLink>
        <NavLink to={'/reg'}>注册</NavLink>
        <NavLink to={'/'}>系统页面</NavLink>
      </ul>
      <RouterIndex></RouterIndex>
    </BrowserRouter>
  )
}

export default App

说明: 必须用BrowserRouter包裹住所有跟路由相关的组建

react router dom v6 已经抛弃了 Switch(组件),改用 Routes(组件),写组件路由需要注意

然后就可以正常在浏览器访问到路由中配置的页面了

2.2 配置路由的懒加载

router/RouterIndex.tsx修改如下:

import {Suspense,lazy} from 'react'

import { useRoutes,Navigate } from "react-router-dom";

// 路由懒加载
const modules = import.meta.glob('../views/**/*.tsx')
export const lazyFun = (pathName: string) => {
    let comFun = modules['../views/' + pathName+'.tsx']
    const ComName = lazy(comFun as any);
    return <Suspense fallback={<div>loading</div>}><ComName></ComName></Suspense>
 }
 
function RouterIndex() {
  let element = useRoutes([
    {
      path: "/home",
      element: lazyFun('manage/Home'),
      children: [
        { path: "user", element: lazyFun('manage/User') },
        { path: "role", element: lazyFun('manage/Role') },
      ],
    },
    { path: "/", element: <Navigate to={'/home'} /> },
    { path: "login", element: lazyFun('Login') },
    { path: "reg", element: lazyFun('Register') },
  ]);

  return element;
}

export default RouterIndex

项目中可正常使用了

2.3 路由传参

react router v6 获取传参需要用两个 hook,分别是 useParams(params)和 useSearchParams(search)

1.search传参模式,常用

 navigate('/layout/home?name=alice&id=99')

接收search模式的参数

import {useSearchParams} from 'react-router-dom'

export default function Home() {
  const [searchArr] = useSearchParams();
  console.log(searchArr.get('name'),'name');  // 获取name这个参数
  console.log(searchArr.get('id'),'id');
  // 可以给获取到的某个参数再添加值
  searchArr.append('name','11111')
  console.log(searchArr.getAll('name'));
  
  return (
    <div>Home</div>
  )
}

注意:useSearchParams(), 解构的时候需要使用中括号, 获取某个search类型的参数,使用get

2.params传参模式

动态理由配置

路由里配置
{
    path: '/about/:id',
    element: <About></About>
}

注意:配置动态路由的时候, 使用:+key名, 与vue动态配置路由一样

跳转的时候

  const navigate = useNavigate();
  
  navigate('/about/1')

获取params参数

import {useParams} from 'react-router-dom'
...
const paramsObj = useParams();
console.log(paramsObj,'参数');
...

3.state模式传参

useLocation接收参数

 navigate('/layout/users',{
      state:{
        age:20,
        username:'alice'
      }
    })

接收参数, 通过useLocation

import React from 'react'
import {useLocation} from 'react-router-dom'
export default function User() {
  const obj = useLocation();
  console.log(obj);   //接收state模式的参数
  return (
    <div>User </div>
  )
}

3.页面系统布局

这里使用左右布局演示

3.1 根目录新建components, 添加Header.tsx和Menus.tsx组件

Menus.tsx组件如下:

import React from 'react'
import {NavLink} from 'react-router-dom'
export default function Menus() {
  return (
    <div><ul className='menu-list'>
            <NavLink to={'/home/user'}>用户</NavLink>
            <NavLink to={'/home/role'}>角色</NavLink>
    </ul></div>
  )
}

3.2 系统框架组件Home.tsx配置如下:

import { Outlet } from 'react-router-dom'
import Menus from '@/components/Menus'
import Header from '@/components/Header'
export default function Home() {
  return (
    <div className='manage-outer'>
      <div className='sider-left'>
          <Menus></Menus>
      </div>
     
      <div className='content'>
        <Header></Header>
        <Outlet></Outlet>
      </div>
     
    </div>
  )
}

布局样式文件参考:


.menu-list {
  display: flex;
  flex-direction: column;
  a { margin-right: 10px;}
}

.manage-outer {
  display: flex;
  height: 100vh;
  .sider-left {
    flex-basis: 200px;
    flex-shrink: 0;
    border-right: 1px solid blue;
  }
  .content {
    flex-grow: 1;
  }
}

目前整体布局完成

3.2 useOutletContext 子路由状态共享

// 父路由组件
function Parent() {
  const [count, setCount] = React.useState(0); 
  return <Outlet context={[count, setCount]} />;
}
// 子路由组件
import { useOutletContext } from "react-router-dom";

function Child() {
  const [count, setCount] = useOutletContext();
  const increment = () => setCount((c) => c + 1);
  return <button onClick={increment}>{count}</button>;
}

4.axios的使用

4.1 安装axios

yarn add axios

在src目录下新建utils/request.ts,

4.2 axios封装配置如下:

import axios from 'axios'
import type { AxiosResponse } from 'axios'

interface IAxiosProps {
    code: number,
    msg: string,
    data: any,
    rows:any,
}

declare module 'axios' {
    interface AxiosInstance {
        (config: AxiosRequestConfig): Promise<IAxiosProps>
    }
}

const fetchData = axios.create({
    baseURL: 'http://127.0.0.1:3000',
    timeout: 5000
})

fetchData.interceptors.request.use(res => {
    // res.headers.Authorization = 'Bearer ' + localStorage.getItem('token')
    return res
})

fetchData.interceptors.response.use((res: AxiosResponse<any, any>) => {
    return res.data
}, err => {
    return Promise.reject(err)
})

export default fetchData

4.3 项目中使用

import fetchData from '@/utils/request';
....
fetchData({
            url:'/api/getCHCSRouterByUserId',
            method:"post",
            data:{
                userId:userid
            }
        }).then((res)=>{
            console.log(res);
        })

4.4 反向代理处理跨域

如果后端已经设置了cors跨域, 可以直接在axios实例化的时候直接使用 baseURL配置;

如果为了安全考虑,接口不支持跨域, 需要在本地开发的时候做反向代理,

使用反向代理就不需要在axios的配置中设置baseURL

配置如下:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { fileURLToPath, URL } from 'node:url'

import requireTransform from 'vite-plugin-require-transform';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),  
    requireTransform({
      fileRegex: /.ts$|.tsx$/
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server:{
    host:'0.0.0.0',
    port:8081,  //端口号
    open:true,   //是否项目启动就在浏览器打开
    proxy:{
      // '/test':{
      //   target:'https://test.iot.store',//后端真实服务器地址
      // },
      //看到:  http://localhost:8081/api/getAllChcsService
      // 实际: http://127.0.0.1:3000/api/getAllChcsService
      '/api':{
        target:'http://127.0.0.1:3000',//后端真实服务器地址
        changeOrigin:true,//允许跨域
      }
    }
  }
})

5.引入antd UI框架

5.1 安装antd

yarn add antd

页面中按需引入antd组件

import {Button} from 'antd'
...
<Button type="primary">Button</Button>
...

5.2 定制主题色

统一定制系统的主题色, 需要在根组件 app.jsx配置

import {ConfigProvider} from 'antd'

 <ConfigProvider theme={{
      token:{
        colorPrimary:'#ff6600'
      }
    }}>
    {路由配置}
 </ConfigProvider>

然后系统中所有的关于 primary类型的颜色, 统一会改变

5.3 国际化语言

antd 目前的默认文案是英文,如果需要使用其他语言,可以参考下面的方案。

ConfigProvider

antd 提供了一个 React 组件 ConfigProvider 用于全局配置国际化文案。

import zhCN from 'antd/locale/zh_CN';

return (
  <ConfigProvider locale={zhCN}>
    <App />
  </ConfigProvider>
);

注意:zh_CN 是文件名,以下表格也遵循同样的规则。

6.二次封装Menu菜单组件

为了项目的健壮性, 支持菜单的多级级联显示, 可以使用递归的思想进行封装, Menus.tsx组件整体代码如下:

import {useEffect, useState} from 'react'
import {useNavigate} from 'react-router-dom'
import {Menu} from 'antd'

const menusOld = [{
    name:'用户',
    path:'/home/user',
    children:[{
        name:'线上用户',
        path:'/home/user/online',
    },{
        name:'线下用户',
        path:'/home/user/noline',
    }]
},{
    name:'角色',
    path:'/home/role'
}]

export default function Menus() {
    const [menuList, setMenuList] = useState([])
    const navigate = useNavigate();
    function formatMenu(menus:any){
		return menus.flatMap((item:any)=>{
			return {
				key:item.path,
				// icon:item.icon,
				label:item.name,
				children: item.children && formatMenu(item.children)
				
			}
		})
	}
    const selectMenu = (key: any) => {
		navigate(key.key)
	}
    useEffect(()=>{
        const menusAll = formatMenu(menusOld)  // 格式化菜单
        setMenuList(menusAll)
    },[])
  return (
        <Menu
            mode="inline"
            items={menuList}
            onClick={selectMenu}
            defaultSelectedKeys={[location.pathname]}
        />
  )
}

说明: menusOld在项目中是需要通过接口获取到项目所有菜单的,主要通过formatMenu对菜单进行递归,得到Menu组件需要的菜单格式

目前效果如下:

c2

7. 项目中使用redux

Redux 中文官网

  1. 单一数据源
  2. state 是只读的
  3. 使用纯函数来执行修改

这里通过react-redux演示使用

7.1 安装redux

yarn add redux

7.2 创建store仓库

在src目录下新建store/index.js, 并创建仓库

import {legacy_createStore as createStore} from 'redux'
//使用别名 方便使用

createStore接收2个参数

createStore创建仓库

第一个参数: reducer函数, 用来管理仓库中的数据, 类似vuex中的mutaions函数

第二个参数: 是redux仓库中的初始数据

let store = createStore(reducerFun,仓库中初始数据)
functuon countReducer(state,action){
	return state;
}

let store = createStore(countReducer,{count:10});

export default store;

这里提供一个比较全的store的配置

import {legacy_createStore as createStore,combineReducers} from 'redux';
// 仓库的模块
interface IStoreModule {
	user:{
		permissions:any,
		userInfo:any,
		menuList:any
	}
}

interface IActionProps {
    type:string,
    payload:any
}
const initData:IStoreModule = {
    user:{
        permissions:[],   // 按钮级别的权限
        userInfo:{},
        menuList:[]
    }
}

function userReducer(state=initData.user,action:IActionProps){
    switch(action.type) {
        case 'user/update':
            state.permissions = action.payload.permissions;
            state.userInfo = action.payload.userInfo;
            return {...state}
        case 'user/menus':
            state.menuList =  action.payload;
            
            return {...state}
    }
    return state
}

const reducerFun = combineReducers({
    user:userReducer
})

const store = createStore(reducerFun);

export default store;

7.3 安装react-redux

为了能使用useSelector,useDispatch 获取仓库中的数据以及修改仓库中的数据, 需要安装react-redux

7.3.1安装

yarn add react-redux

7.3.2 Provider派发数据

在入口文件main.tsx中派发store, 以供其他组件可以获取数据

import {Provider} from 'react-redux'
import store from './store/index.ts';   // 7.2步导出的store

 <Provider store={store}>
        <BrowserRouter>
             <App />
         </BrowserRouter>

</Provider>

7.4 项目中实时获取仓库中的数据

import {useSelector,useDispatch} from 'react-redux'

  const menuList = useSelector((state:any)=>{
     return [...state.user.menuList]
  })
  

7.5 项目中派发数据

import {useSelector,useDispatch} from 'react-redux'

...
 const dispatch = useDispatch();
 const sendData = ()=>{
    // res.data是接口中获取的数据
 	  dispatch({
        type:'user/update',
        payload:{
          permissions:res.data.permissions,
          userInfo:res.data.user
        }
      })
 }
。。。。

7.6 数据的持久化

借助插件 redux-persist

1. 安装

yarn add redux-persist

2.配置持久化

2.1 在store/index.js文件配置

import {persistReducer} from 'redux-persist'
//  存储机制,可换成其他机制,当前使用sessionStorage机制
import storageSession from 'redux-persist/lib/storage/session'

// 持久化配置
let storeConfig = {
    key:'root',
    storage:storageSession,
    blacklist:['todo']  // reducer里不想持久化的名单, 是模块化的名字
}

persistReducer函数用来持久化所有的reducers函数, 接收2个参数, 第一个就是 持久化配置 storeConfig, 第二个是合并后的reducer函数

redux2

说明: createStore创建的仓库的时候, 第二参数初始化数据非必填, 但是不填的时候, 必须要在reducer函数中初始化的时候加上, 比如

function countReducer(state=storeData.count,action){
 switch(action.type){
     case 'count/add':
        state.num += action.payload;
        return {...state}  // react中数据不可变性, 必须通过拷贝一个新的对象,不能直接修改原对象
     default : return state
 }
}

2.2 在入口文件main.jsx中添加PersistGate 配置

import store from './store/index.js'
import {Provider} from 'react-redux'
// 持久化
import {PersistGate} from 'redux-persist/lib/integration/react'
import {persistStore} from 'redux-persist'  
const persistor = persistStore(store)  //使用persistStore持久化store

 <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
            <App />
        </PersistGate>
</Provider>

PersistGate:接收persistor这个api的目的是持久化store

7. 环境变量的配置

可能有多个环境, 一般情况会有开发环境, 测试环境, 线上环境, 每个环境对应的服务域名不一样, 然后配置后 可以方便不同环境的接口调用

配置步骤:

第一步: 在项目根目录新建环境文件 名字 .env+环境名字

比如这里有3个环境

.env.dev:开发环境

.env.test:测试环境

.env.pro:线上环境

每个环境配置相应的变量,以 .env.dev:开发环境

VITE_APP_URL = http://localhost:8002
VITE_IMG_SERVER = http://localhost:8002

注明: 以Vite创建的项目, 环境变量名必须是 VITE开头, VITE_自定义名字

第二步:在项目根目录的文件package.json 配置 mode模式, mode第一步中的环境名字

"scripts": {
    "dev": "vite --mode dev",
    "build": "vite build --mode pro",
    "test": "vite build --mode test",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },

第三步:服务重启后, 通过 import.meta.env访问环境变量,然后做相应配置

console.log(import.meta.env);
// 因为可能有多个服务 - 》 多个域名接口
// 一般需要实例化一个axios对象
const axiosEl = axios.create({
    baseURL:import.meta.env.VITE_APP_URL,
    timeout:5000
})

特别说明: env环境提供了2个默认mode, 开发 development, 线上production , 如果你在第一步 env.development, 可以省略第二步中 mode 中dev的配置

8.打包优化

项目开发完毕, 需要通过npm run build上线, 但是有些第三方的包跟业务没有关系, 可以单独打包做cdn缓存, 提高网页加载速度

修改配置文件:vite.config.ts

 build:{
    chunkSizeWarningLimit: 1600,   // 如果你打包的时候, 有文件超过了 1600k, 项目打包户提示错误
    rollupOptions:{
      output:{
        // echarts , react-dom , react, axios
        manualChunks:{
           echarts:['echarts','react-echarts-core'],
           react:['react','react-dom'],
           axios:['axios']
        }
      }
    }
  },

整体vite.config.ts文件的整体代码参考:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { fileURLToPath, URL } from 'node:url'

import requireTransform from 'vite-plugin-require-transform';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),  
    requireTransform({
      fileRegex: /.ts$|.tsx$/
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  build:{
    chunkSizeWarningLimit: 1600,   // 如果你打包的时候, 有文件超过了 1600k, 项目打包户提示错误
    rollupOptions:{
      output:{
        // echarts , react-dom , react, axios
        manualChunks:{
           echarts:['echarts','react-echarts-core'],
           immer:['immer','immutable','use-immer'],
           react:['react','react-dom'],
           axios:['axios']
        }
      }
    }
  },
  server:{
    host:'0.0.0.0',
    port:8081,
    open:true,
    proxy:{
      // '/test':{
      //   target:'https://test.iot.store',//后端真实服务器地址
      // },
      //看到:  http://localhost:8081/api/getAllChcsService
      // 实际: http://127.0.0.1:3000/api/getAllChcsService
      '/api':{
        target:'http://127.0.0.1:3000',//后端真实服务器地址
        changeOrigin:true,//允许跨域
      }
    }
  }
})

9.上线部署

通过npm run build打包后的文件, 可以通过nginx进行部署;

如果路由选择的是hash,不需要在nginx里面配置;

如果路由选择是history模式, 需要在nginx里做反向代理的配置;

history模式打包后想要使用正常访问的话,需要后端服务器进行配置才可以,否则可能会出现刷新后404的问题。一般情况下,服务器端使用nginx服务器进行配置。

server {
        listen      9903;
        server_name  localhost;
        location / {
            root  'G:\dist';
            index  /index.html;
            try_files $uri $uri/ /index.html;   
        }
}

如果是history模式, 上线的时候必须设置 try_files ,不然页面刷新的时候 会包404