基于Vite、Typescript、React、react-router-dom、Redux、Reduxjs/toolkit、Ant Design从零搭建前端开发工程
vite项目初始化
本机环境
- node v16.14.2
- npm 8.5.0
- yarn 1.22.18
vite 项目初始化
bash
yarn create vite
按照步骤提示输入项目名,框架选择 React
,js使用 TypeScript
,就可以创建一个简单的脚手架项目。
运行项目
按照提示,使用yarn安装依赖包,之后yarn dev运行项目
项目默认包结构介绍
-
node_modules:依赖包存储位置
-
public:存储静态资源,该目录中的资源在开发时能直接通过 / 根路径访问到,并且打包时会被完整复制到目标目录的根目录下。public存放的资源一般满足下面条件:
- 不会被源码引用(例如 robots.txt)
- 压根不想引入该资源,只是想得到其 URL。
- public 中的资源不应该被 JavaScript 文件引用。
- 引入 public 中的资源永远应该使用根绝对路径 ------ 举个例子,public/icon.png 应该在源码中被引用为 /icon.png。
-
src: 存储项目源码文件和资源
-
src/assets 如果静态资源是放在src/assets目录那么会经过vite打包处理,包括压缩、重命名等
-
.eslintrc.cjs:eslint 检查规则配置
-
.gitignore:git 忽略文件配置
-
index.html:Vite 项目的入口文件。
官方解释:Vite 将 index.html 视为源码和模块图的一部分。Vite 解析 <script type="module" src="..."> ,这个标签指向你的 JavaScript 源码。甚至内联引入 JavaScript 的 <script type="module"> 和引用 CSS 的 <link href> 也能利用 Vite 特有的功能被解析。另外,index.html 中的 URL 将被自动转换,因此不再需要 %PUBLIC_URL% 占位符了。
-
package.json:项目信息、包声明
-
tsconfig.json:项目ts配置包括编译配置等
-
tsconfig.node.json: 是专门用于 vite.config.ts 的 TypeScript 配置文件。tsconfig.json 文件通过 references 字段引入 tsconfig.node.json 中的配置。
-
vite.config.ts:vite的配置文件,可以根据项目需要进行配置
createBrowserRouter 创建路由信息
在src文件夹下,建立一个 router 文件夹,存放路由配置信息 index.tsx,路由配置方式使用 react-router-dom 6.4
引入的最新的 Data API:createBrowserRouter 或者 createHashRouter
。
- createBrowserRouter:页面路由使用路径path区分
- createHashRouter:页面路由以 # hash方式区分
安装对应包,这里使用的是6.4,可以安装6.4以上版本
sh
yarn add react-router-dom@^6.4.0
配置路由之前新建几个测试页面,路由信息编写如下:
js
import About from '@/views/About'
import Home from '@/views/Home'
import Index from '@/views/Index'
import Login from '@/views/Login'
import { createBrowserRouter, Navigate } from 'react-router-dom'
// import { createHashRouter, Navigate } from 'react-router-dom'
const Router = createBrowserRouter([
{
path: '/home',
element: <Home/>,
children: [
{
path: 'index',
element: <Index/>
},
{
path: 'pa',
lazy: () => import("@/views/Pa") // 懒加载
},
{
path: 'pb',
lazy: () => import("@/views/Pb") // 懒加载
},
{
path: 'about',
element: <About/>, //非懒加载
}
]
},
{
path: '/login',
element: <Login/>
},
{
path: '/',
element: <Navigate to="/home/index"/>
},
{
path: '*',
lazy: () => import("@/views/404"),
// element: <Navigate to="/home"/>
}
])
export default Router
注意:
- children嵌套路由,需要父页面有路由出口:
<Outlet/>
可以用来占位或者定义一个路由出口 - 路由信息可以通过接口动态加载
- lazy 可以实现路由页面懒加载,减少打包体积和提高页面加载效率
- Navigate 可以实现路由重定向
最后一步将路由信息引入项目中,vite建立的项目默认入口是 main.tsx
,使用 RouterProvider
加载路由配置信息
js
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import Router from './router'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={Router} />
</React.StrictMode>,
)
使用最新官方建议使用的reduxjs/toolkit进行,redux状态管理
Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法,简称RTK。 它包含我们对于构建 Redux 应用程序必不可少的包和函数。 Redux Toolkit 的构建简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序变得更加容易。Redux Toolkit 就是目前 Redux 的最佳实践方式。
安装对应包
sh
yarn add react-redux @reduxjs/toolkit
这里使用的版本为:@reduxjs/toolkit ^1.9.7,react-redux ^8.1.3
- 第一步在src下建立一个store文件夹,新建文件 index.tsx,编写 store 配置并导出
- 第二步在src下建立一个slice文件夹,用来配置子模块状态切片(状态管理中的一块业务状态,和对应的暴露的方法)
- 将所有的 slice,引入到store\index.tsx
- 使用状态管理读取和更新状态
- index.tsx,这里引入了三个业务模块状态,作为例子,实际根据自己业务进行增删
js
import { configureStore } from '@reduxjs/toolkit' // 引入rtk的配置
import counterSlice from '@/slice/counterSlice' // 业务模块状态:计数器状态
import configSlice from '@/slice/configSlice' // 业务模块状态:配置中心状态
import opDrawerSlice from '@/slice/opDrawerSlice' // 业务模块状态:操作栏状态
const store = configureStore({
reducer: {
counter: counterSlice,
config: configSlice,
opDrawer: opDrawerSlice
},
// 配置,这里忽略了序列化失败警告
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
serializableCheck: false
})
})
export default store
// 导出两个类型方便业务模块使用时候TS类型确定
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
- 编写业务slice,这里举一个counterSlice例子
js
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
// 为 slice state 定义一个类型
interface CounterState {
value: number
}
// 使用该类型定义初始 state
const initialState: CounterState = {
value: 0
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: state => {
// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它
// 并不是真正的改变状态值,因为它使用了 Immer 库
// 可以检测到"草稿状态" 的变化并且基于这些变化生产全新的
// 不可变的状态
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})
// 导出actions方便业务组件调用方法
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// 导出reducer,被store\index.tsx引入
export default counterSlice.reducer
- 定义一个app.hooks.ts文件,引入store信息,方便业务组件使用,和TS类型联想
js
import { useSelector, useDispatch, TypedUseSelectorHook } from 'react-redux'
import { RootState, AppDispatch } from '@/store' // store\index.tsx导出的两个类型
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
- 引入store配置信息
js
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={Store}>
<RouterProvider router={Router} />
</Provider>
</React.StrictMode>,
)
- 业务组件使用
js
import { useAppDispatch, useAppSelector } from '@/hooks/app.hooks'
import { decrement, increment } from "@/slice/counterSlice"//使用切片导出的方法
import { Button, Space } from 'antd';// 这里使用了antd UI 组件
export default function Counter() {
// 获取状态
const count = useAppSelector(state => state.counter.value)
// 获取状态分发器
const dispatch = useAppDispatch()
return (
<Space size={"middle"}>
<Button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
增加
</Button>
<span>{count}</span>
<Button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
减少
</Button>
</Space>
)
}
Ant Design 使用
参考官方组件库使用方式即可,这里给出导航侧边菜单的使用
js
import React, { useState } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
import {
DesktopOutlined,
FileOutlined,
PieChartOutlined,
TeamOutlined,
UserOutlined,
BulbFilled
} from '@ant-design/icons';
import style from './index.module.css'
import type { MenuProps } from 'antd'
import type { MenuClickEventHandler } from 'rc-menu/lib/interface'
import { Layout, Menu, theme, Switch } from 'antd';
import { useAppDispatch, useAppSelector } from '@/hooks/app.hooks'
import { updateTheme } from '@/slice/configSlice';
const { Header, Content, Footer, Sider } = Layout;
type MenuItem = Required<MenuProps>['items'][number];
// function getItem(
// label: React.ReactNode,
// key: React.Key,
// icon?: React.ReactNode,
// children?: MenuItem[],
// ): MenuItem {
// return {
// key,
// icon,
// children,
// label,
// } as MenuItem;
// }
// 定义菜单
const items: MenuItem[] = [
{
label: '常用工具',
key: '/home',
icon: <PieChartOutlined />,
},
{
label: '开发工具',
key: '/about',
icon: <DesktopOutlined />
},
{
label: '格式转换',
key: '/home/pa',
icon: <TeamOutlined />
},
{
label: '生活日常',
key: '/home/pb',
icon: <FileOutlined />
},
{
label: '热站导航',
key: '/sub1',
icon: <UserOutlined />,
// children: [
// {
// label: 'Tom',
// key: '/tom',
// icon: <UserOutlined />,
// },
// {
// label: 'Bill',
// key: '/bill',
// icon: <UserOutlined />,
// }
// ]
},
];
const Home: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const { token: { colorBgContainer } } = theme.useToken();
// const headerHeight = theme.useToken().token.Layout?.headerHeight
const navigate = useNavigate();
const location = useLocation()
const menuClick: MenuClickEventHandler = (e) => {
console.log(e)
navigate(e.key)
}
const myTheme = useAppSelector(state => state.config.theme)
const dispatch = useAppDispatch()
return (
<Layout hasSider>
<Sider
theme={myTheme}
collapsedWidth={50}
width={150}
collapsed={collapsed}
onCollapse={(value) => setCollapsed(value)}
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
}}>
<div className="demo-logo-vertical" />
<Menu
theme={myTheme}
defaultSelectedKeys={[location.pathname]}
mode="inline"
items={items}
onClick={menuClick}/>
</Sider>
<Layout style={{marginLeft: '150px'}}>
<Header style={{ padding: 0, margin: '16px', background: colorBgContainer }} >
<div className={style.headerDiv}>
<Switch
checkedChildren={<BulbFilled />}
unCheckedChildren={<BulbFilled />}
defaultChecked
onChange={checked => {dispatch(updateTheme(checked ? 'light': 'dark'))}}
/>
</div>
</Header>
<Content style={{ margin: '0 16px' }}>
<div style={{ padding: 24, minHeight: 360, background: colorBgContainer }}>
<Outlet/>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>Ant Design ©2023 Created by Ant UED</Footer>
</Layout>
</Layout>
);
};
export default Home
项目源码地址
项目源码地址:gitee.com/li_zheng/re...