vite创建react项目
搭建项目
create-vite创建
1、默认创建react18
yarn create vite [project-name] --template react-ts
2、第二种创建项目
npm init vite@latest
选择react typescript
配置resolve.alias
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve:{
alias:{
'@': path.resolve(__dirname, './src')
}
}
})
配置tsconfig.json
// tsconfig.json
{
"compilerOptions":{
"paths": {
"@/*": ["./src/*"]
},
"types":["vite/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
添加scss配置
添加sass依赖
yarn add sass -D
引入index.scss文件
// 打开vite.config.ts,添加scss的预编译选项
export default defineConfig({
css:{
preprocessorOptions:{
scss: {
additionalData: '@import "./asset/styles/index.scss;"'
}
}
}
})
useState钩子
1、useState案例
import React, {useState} from 'react';
function example(){
const [count, setCount] = useState(0)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count+1)}>click</button>
</div>
)
}
2、class案例
class example extends React.Component {
constructor(props){
super(props)
this.state = {
count:0
}
}
render(){
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => this.setState({count: this.state.count+1})}>click</button>
</div>
)
}
}
legacy.reactjs.org/docs/hooks-…
路由
安装路由
yarn add react-router-dom
HashRouter路由
// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { HashRouter as Router } from 'react-router-dom'
import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>
)
// App.tsx
import { useState } from 'react'
import {HashRouter, Route, Routes, Link, useNavigate} from 'react-router-dom'
import Login from './pages/login'
import Home from './pages/home'
import User from './pages/user'
import './App.scss'
function App(){
const [count, setCount] = useState(0)
const navigate = useNavigate()
return (
<div className="App">
{/* 指定跳转的组件, to 用来配置路由地址 */}
<Link to="/">首页</Link>
<Link to="/user">用户</Link>
<button onClick={() => navigate('/login')}>登录</button>
{/* 路由出口: 路由对应的组件会在这里渲染 */}
<Routes>
{/* 指定路由路径和组件的对应关系,path代表路径,element代表对应的组件,成对出现 */}
<Route path="/" element={<Home />}></Route>
<Route path="/user" element={<User />}></Route>
<Route path="/login" element={<Login></Login>}></Route>
</Routes>
</div>
)
}
export default App
// login.tsx
function Login(){
return (
<div>login页面</div>
)
}
export default Login;
// home.tsx
function Home(){
return (<div>home页面</div>)
}
export default Home;
// user.tsx
function User(){
return (<div>user页面</div>)
}
export default User;
BrowserRouter路由
// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import { BrowserRouter as Router } from 'react-router-dom'
import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Router>
<App></App>
</Router>
</React.StrictMode>
)
// App.tsx
import {useState} from 'react'
import {Route, Routes, Link, useNavigate} from 'react-router-dom'
import Login from './pages/login'
import Home from './pages/home'
import User from './pages/user'
import './App.scss'
function App(){
const navigate = useNavigate()
return (
<div className="App">
{/* 指定跳转的组件, to 用来配置路由地址 */}
<Link to="/">首页</Link>
<Link to="/user">用户</Link>
<button onClick={() => navigate('/login')}>登录</button>
{/* 路由出口: 路由对应的组件会在这里渲染 */}
<Routes>
{/* 指定路由路径和组件的对应关系,path代表路径,element代表对应的组件,成对出现 */}
<Route path="/" element={<Home />}></Route>
<Route path="/user" element={<User />}></Route>
<Route path="/login" element={<Login></Login>}></Route>
</Routes>
</div>
)
}
export default App
嵌套路由
// App.tsx
<Routes>
{/* 指定路由路径和组件的对应关系,path代表路径,element代表对应的组件,成对出现 */}
<Route path="/" element={<Home />}>
<Route path="user" element={<User />}></Route>
<Route path="login" element={<Login></Login>}>
</Route>
</Route>
</Routes>
// home.tsx
import {Outlet} from 'react-router-dom'
function Home(){
return (
<div>
<div>home页面</div>
<Outlet />
</div>
)
}
export default Home
默认子级路由
在嵌套路由下的子级路由,如果要设置一个默认路由,那么只要在路由上添加 index 属性
<Routes>
<Route path="/home" element={<Home></Home>}>
<Route path="user" element={<User></User>}></Route>
<Route index element={<Login></Login>}></Route>
</Route>
</Routes>
重定向路由
import {Navigate} from 'react-router-dom'
<Route path="/" element={<Navigate to="/layout"></Navigate>}></Route>
在做权限验证的时候可以使用重定向,
yarn add react-router-dom redux react-redux axios
zhuanlan.zhihu.com/p/518339176…

1、搭建项目
1.1 create-vite 创建
yarn create vite react-vite --template react-ts
默认创建react18

执行yarn
1.2 配置 reslove.alias
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias:{
'@': path.resolve(__dirname, './src')
}
}
})
配置tsconfig.json
// tsconfig.json
{
"compilerOptions":{
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
1.3 Sass LessCss
yarn add sass less
1.4 css module
文件名设置 *.module.less, *.module.scss等

2、集成 react-router
yarn add react-router-dom
使用BrowserRouter组件
2.1 useRoutes 配置路由
react router dom V6 支持配置路由 useRoutes (hook) 实现
// 官方示例
import { useRoutes } from "react-router-dom";
function Router() {
let element = useRoutes([
{
path: "/",
element: <Dashboard />,
children: [
{
path: "messages",
element: <DashboardMessages />,
},
{ path: "tasks", element: <DashboardTasks /> },
],
},
{ path: "team", element: <AboutPage /> },
]);
return element;
}
2.2 Redirect 替代方案 Navigate
react router dom v6 中已经抛弃了 Redirect。但是可以使用 Navigate(组件) 或 useNavigate(hook)作为替代方案,简单的实现一个 Redirect。
import { useEffect } from 'react';
import { useNavigate, Navigate } from 'react-router-dom';
export function Redirect({ to, replace, state }: { to: string, replace: boolean, state: object }): null {
const navigate = useNavigate();
useEffect(() => {
navigate(to, { replace, state });
});
return null;
}
怎么使用,判断一下如果是重定向
<Route path="/" element={<Redirect to="/home" />} />
2.3 路由传参
react router v6 获取传参需要用两个 hook,分别是 useParams(params)和 useSearchParams(search)
(1)useParams
params 传参
import { NavLink } from 'react-router-dom';
{/* 路由定义 /article/:id */}
<NavLink to={`/article/1`}>文章1</NavLink>
接收参数
import { useParams } from 'react-router-dom'
/* params */
const params = useParams();
const { id } = params;
(2)useSearchParams
search 传参
import { NavLink } from 'react-router-dom';
<NavLink to={`/article?id=1`}>文章1</NavLink>
接收参数
import { useSearchParams } from 'react-router-dom'
/* search */
let [searchParams, setSearchParams] = useSearchParams();
const { id } = searchParams;
(3)useLocation
state 传参
import { NavLink } from 'react-router-dom';
<NavLink to="/article" state={{ id: 1 }}>文章1</NavLink>
接收参数
import { useLocation } from 'react-router-dom'
let location = useLocation();
const { id } = location.state;
2.4 编程式路由跳转 useNavigate
useHistory 已废弃,而是使用 useNavigate
import { FC, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from 'antd';
interface IndexProps{}
const Index: FC<IndexProps> = () => {
const navigate = useNavigate();
// 返回
const handleBack = () => navigate(-1);
// 前进
const handleForward = () => navigate(1);
// 刷新
const handleRefresh = () => navigate(0);
//
return <>
<Button type="primary" onClick={handleBack}>返回</Button>
<Button type="primary" onClick={handleForward}>前进</Button>
<Button type="primary" onClick={handleRefresh}>刷新</Button>
{/* 跳转路由 */}
<Button type="primary" onClick={() => navigate('/article/1', { replace: true })}>params</Button>
<Button type="primary" onClick={() => navigate('/article?id=1', { replace: true })}>search</Button>
<Button type="primary" onClick={() => navigate('/article', { replace: true, state: { id: 1 } })}>state</Button>
</>
}
export default Index;
简单测试一下

// Acticle.tsx
import { useSearchParams, useParams, useLocation } from "react-router-dom";
export default function Index(){
// params
const pId = useParams()?.id;
// search
const [search] = useSearchParams();
const sId = search?.get('id');
// state
const location = useLocation();
const stateId = location?.state?.id;
return <>
<div>params: {pId}</div>
<div>search: {sId}</div>
<div>locationState: {stateId}</div>
</>
}
编程式跳转路由
2.5 Outlet
子路由组件不再使用 props.default 传递;而是通过 Outlet 组件显示(中文意思是“插座”);其实和 props.default 的作用是一样的。
// layouts/Base.tsx
import { Outlet, useNavigate } from "react-router-dom";
import { Layout, Menu } from 'antd';
function GlobalHeader() {
const navigate = useNavigate();
return <Layout.Header style={{ position: 'fixed', zIndex: 1, width: '100%' }}>
<div className="logo" />
<Menu
mode="horizontal"
items={
[
{
key: "home",
label: "首页",
onClick: () => navigate('/home')
},
{
key: "about",
label: "关于",
onClick: () => navigate('/about')
},
]
} />
</Layout.Header>
}
function GlobalContent() {
return <Layout.Content className="site-layout" style={{ padding: '0 50px', marginTop: 64 }}>
<div className="site-layout-background" style={{ padding: 24, minHeight: 380 }}>
{/* outlet */}
<Outlet />
</div>
</Layout.Content>;
}
function GlobalFooter() {
return <Layout.Footer style={{ textAlign: 'center' }}>用 vite 创建 react18 项目 @带只拖鞋去流浪</Layout.Footer>
}
export default function Index() {
return <Layout>
<GlobalHeader />
<GlobalContent />
<GlobalFooter />
</Layout>
}
Outlet
2.6 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>;
}
2.7 路由拦截
思路:在路由组件外层套一个 BeforeEach 组件进行路由拦截。代码展示在 2.8
2.8 路由组件代码

/* router/index.tsx */
import React from 'react';
import { useRoutes, useNavigate, Navigate } from 'react-router-dom';
import routes, { routeType } from './routes';
import { Spin } from 'antd';
import _ from 'lodash';
export default function Routes() {
const element = useRoutes(renderRoutes(routes));
return element;
}
function renderRoutes(routes: Array<routeType>) {
return _.map(routes, (item: routeType) => {
interface resType extends routeType{
element?: any
}
let res: resType = { ...item };
if (!item?.path) return;
// component
if (item?.component) {
const Component = React.lazy(item.component);
res.element = (<React.Suspense fallback={<Spin size="large" />}>
<BeforeEach route={item}>
<Component />
</BeforeEach>
</React.Suspense>);
}
// children
if (item?.children) {
res.children = renderRoutes(item.children);
}
// 重定向
if (item?.redirect) {
res.element = (
<Navigate to={item.redirect} replace />
)
}
return res;
})
}
function BeforeEach(props: { route: routeType, children: any }) {
if (props?.route?.meta?.title) {
document.title = props.route.meta.title;
}
if(props?.route?.meta?.needLogin){
// 看是否登录
// const navigate = useNavigate();
// navigate('/login');
}
return <div>
{props.children}
</div>
}
/* router/routes.ts */
export interface routeType {
path: string
component?: any
children?: Array<routeType>
meta?: {
title?: string
needLogin?: boolean
}
redirect?: string
}
const routes: Array<routeType> = [
{
path: '/',
component: () => import('@/layouts/Base'),
children: [
{
path: '/',
redirect: '/home',
},
{
path: '/home',
component: () => import('@/pages/Home'),
meta: {
title: "首页",
}
},
{
path: '/about',
component: () => import('@/pages/About'),
meta: {
title: "关于",
}
},
]
}
]
export default routes;
/* main.ts */
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import Routes from '@/router/index';
import 'antd/dist/antd.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<Routes/>
</BrowserRouter>
</React.StrictMode>
);
2.9 权限管理
2.10 keep-alive
3. 集成 react-redux
Redux 中文官网 - JavaScript 应用的状态容器,提供可预测的状态管理。
react 状态、管理的库有不少, Recoil、Redux、MobX、XState。react-redux 也有很多库,例如 redux-saga、dva、Redux ToolKit(RTK)。
yarn add @reduxjs/toolkit react-redux
RTK 完全够用,压根就不需要再去选择使用其他状态管理。
3.1 redux 工作流

3.2 redux 三大原则
- 单一数据源
- state 是只读的
- 使用纯函数来执行修改
3.3 添加 store 到工程中
其实使用 Redux Toolkit 简化之后,使用起来很简单的,只是官网表述不清晰。用过 vuex 的完全可以把它当做 vuex 来使用,slice 就相当于命名空间 modules

/* store/index.ts */
import { configureStore } from "@reduxjs/toolkit";
import * as reducer from './modules';
// configureStore 创建一个 redux 数据
const store = configureStore({
// 合并多个Slice
reducer: {
...reducer,
},
});
export default store;
/* store/modules/index.ts */
export { default as about } from './about'
/* store/modules/about.ts */
import { createSlice } from '@reduxjs/toolkit';
export interface AboutState {
counter: number;
title: string
}
const initialState: AboutState = {
counter: 0,
title: "redux toolkit pre"
};
// 创建一个 Slice
export const about = createSlice({
// 命名空间
name: 'about',
// 初始化状态值
initialState,
// 定义 reducers 并生成关联的操作
reducers: {
setCounter(state, { payload }){
console.log(payload);
state.counter = payload.counter;
}
},
});
// 导出 reducers 方法
export const { setCounter } = about.actions;
// 默认导出
export default about.reducer;
main.ts
/* main.ts */
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import Routes from '@/router';
import { Provider } from 'react-redux';
import store from '@/store'
import 'antd/dist/antd.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<Routes />
</BrowserRouter>
</Provider>
</React.StrictMode>
)
3.4 组件与 rtk 数据交互
还是以 About 组件为例
/* pages/About/index.ts */
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, InputNumber } from 'antd';
import { useSelector, useDispatch } from 'react-redux';
import { setCounter } from '@/store/modules/about';
const Index: React.FC<{}> = () => {
const navigate = useNavigate();
// 返回
const handleBack = () => navigate(-1);
// 前进
const handleForward = () => navigate(1);
// 刷新
const handleRefresh = () => navigate(0);
const actions = <div style={{ marginBottom: '16px' }}>
<Button type="primary" style={{ marginRight: '8px' }} onClick={handleBack}>返回</Button>
<Button type="primary" style={{ marginRight: '8px' }} onClick={handleForward}>前进</Button>
<Button type="primary" style={{ marginRight: '8px' }} onClick={handleRefresh}>刷新</Button>
{/* 跳转路由 */}
<Button type="primary" style={{ marginRight: '8px' }} onClick={() => navigate('/article/1')}>params</Button>
<Button type="primary" style={{ marginRight: '8px' }} onClick={() => navigate('/article?id=1')}>search</Button>
<Button type="primary" style={{ marginRight: '8px' }} onClick={() => navigate('/article', { state: { id: 1 } })}>state</Button>
</div>;
// 通过useDispatch 派发事件
const dispatch = useDispatch();
// 通过useSelector直接拿到store中定义的value
const { counter } = useSelector((store: any) => store.about);
const [value, setValue] = useState(counter);
useEffect(() => {
// 监听 counter 变化
console.log(counter);
}, [counter])
return <>
{actions}
<div>
<InputNumber value={value} onChange={value => setValue(value)} />
<Button onClick={() => dispatch(setCounter({ counter: value }))}>保存</Button>
</div>
</>
}
export default Index;
组件与 rtk 数据交互
3.5 异步 actions
Using createAsyncThunk | Redux 中文官网
前面写的 actions 是同步的(也就是 reducers 里面的方法),下面用 createAsyncThunk 来创建异步的 actions
/* store/modules/article.ts */
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import _ from 'lodash';
// 异步 actions
export const getList = createAsyncThunk('article/getList',
async ({ currentPage = 1, pageSize = 5 }: { currentPage?: number, pageSize?: number, type?: string }) => {
// 模拟异步请求
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ data: { list: [1, 2, 3, 4, 5], total: 5 } });
}, 500);
});
try {
var [res] = await Promise.all([promise]);
} catch (error) {
console.error(error);
}
const payload = _.get(res, 'data', { list: [], total: 0 });
console.log(payload);
return payload;
},
);
export const article = createSlice({
// 命名空间
name: "article",
// state
initialState: {
list: [],
total: 0,
},
// 同步 actions
reducers: {},
extraReducers: (builder) => {
builder.addCase(getList.fulfilled, (state, { payload }) => {
state.list = payload.list;
state.total = payload.total;
})
}
});
// 导出 reducers 方法
export const { } = article.actions;
// 默认导出
export default article.reducer;
在 Article 页面组件中进行测试
/* pages/Article/index.tsx */
import { useDispatch, useSelector } from 'react-redux';
import { getList } from "@/store/modules/article";
import _ from 'lodash';
import { useEffect } from "react";
import type { AnyAction } from "@reduxjs/toolkit";
export default function Index() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getList({ currentPage: 2, pageSize: 10}) as unknown as AnyAction);
}, [])
const { list, total } = useSelector((store: any) => store.article);
return <>
<section>
<h3>测试异步 actions</h3>
<div>total: {total}</div>
<ul style={{ padding: 0 }}>list:
{_.map(list, (item: number, key: number) => <li key={key}>{item}</li>)}
</ul>
</section>
</>
}

4. 全局配置文件
打包部署之后可能需要修改配置,为了避免多次重新打包,我们创建一个 config.json 文件

4.1 fetch 获取 config.json
<script>
fetch('./config.json').then(res => res.json()).then(data => {
window.globalConfig = data;
console.log(window.globalConfig);
}).catch(err => {
console.error(err);
})
</script>

执行 yarn build 打包之后,我们将 config.json 也拷贝到 dist 文件夹中。
4.2 nginx 测试部署
phpStudy 集成了 nginx,我们这里使用 phpStudy

然后在浏览器中访问
nginx 测试部署
4.3 Caddy VS Nginx
Caddy是一款功能强大,扩展性高的Web服务器。Caddy采用Go语言编写,可用于静态资源托管和反向代理。
超越 Nginx!号称下一代 Web 服务器,用起来够优雅!
Caddy具有如下主要特性:
- 对比Nginx复杂的配置,其独创的
Caddyfile配置非常简单; - 可以通过其提供的
Admin API实现动态修改配置; - 默认支持自动化HTTPS配置,能自动申请HTTPS证书并进行配置;
- 能够扩展到数以万计的站点;
- 可以在任意地方执行,没有额外的依赖;
- 采用Go语言编写,内存安全更有保证。
5. 拥抱 fetch
fetch 基于 Promise 作为 XMLHttpRequest 的替代方案,或者说是下一代 Ajax。使用 fetch 默认不存在跨域问题。
5.1 eggjs 写测试接口
侧重点不在于 eggjs,仅用于我们进行接口调试。


5.2 配置 nginx 代理解决 cors
我们这里不配置 vite 的 server 选项,因为打包之后“开发服务器代理”就没有用了

# nginx.conf
http {
# 不要去代理当前跑前端项目的端口(port 3000)
# port 8888
server{
listen 8888;
location ^~ / {
proxy_pass http://127.0.0.1:3000/;
}
location ^~ /appServer/ {
proxy_pass http://127.0.0.1:7001/;
}
}
}
改配置之后需要重启 nginx


/* pages/Home/index.tsx */
const testRequest = async () => {
try {
const res = await fetch(`${window.globalConfig?.appServer}`);
const data = await res.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
useEffect(()=>{
testRequest();
},[])
通过 nginx 代理,有效避免了跨域问题
/* config.json */
{
"appServer": "http://localhost:8888/appServer"
}
5.3 Window 对象声明
我们是通过获取 config.json 来获取配置的,然后将 globalConfig 定义为全局变量。


/* declare/index.d.ts */
declare interface Window {
globalConfig: {
appServer: string,
}
}
5.4 简单封一下 fetch

6. eslint
6.1 添加远程存储库
git remote add origin 你的远程地址
6.2 添加 eslint
ESLint - 插件化的 JavaScript 代码检测工具
安装
yarn add eslint -D
初始化
npx eslint init
6.3 commit 检测
(1)husky
(2)lint-stage
(3)完整流程

7. 使用 git 的一些问题
7.1 文件名大小写不会识别
git config --get core.ignorecase
git config core.ignorecase false
7.2 commit 之后回退
回退到 commit 之前,并保留代码
git reset --soft HEAD^
8. 集成 antd
Ant Design - 一套企业级 UI 设计语言和 React 组件库
我前面以前用到了 ant design 的组件,这里提一下
yarn add antd
然后在 main.ts 中引入一下样式文件即可
/* main.ts */
import 'antd/dist/antd.css';
9. 什么是BFC,如何触发BFC?
9.1 BFC
BFC(Block formatting context)直译为“块级格式化上下文”。它是一个独立的渲染区域,只有Block-level box(块)参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。
9.2 BFC的布局规则
- 内部的Box会在垂直方向,一个接一个地放置。
- Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠(按照最大margin值设置)
- 每个元素的margin box的左边, 与包含块border box的左边相接触
- BFC的区域不会与float box重叠。
- BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。
- 计算BFC的高度时,浮动元素也参与计算
9.3 哪些元素或属性能触发BFC
- 根元素( html )
- float 属性不为 none
- position 为 absolute 或 fixed
- display 为 inline-block, table-cell, table-caption, flex, inline-flex
- overflow 不为 visible
9.4 BFC的应用
- 自适应两栏布局
- 清除内部浮动
- 防止margin上下重叠
10. react-query
React Query is often described as the missing data-fetching library for React, but in more technical terms, it makesfetching, caching, synchronizing and updating server statein your React applications a breeze.

[React Query](link.zhihu.com/?target=htt…
支持 restful 和 graphql。
11. vite3 搭建 vue3 项目
看到下面这篇文章不错,可以参照配置创建项目