构建项目

使用Vite构建

pnpm create vite@latest my-app --template react-ts

这个命令会构建一个项目名称为my-app的项目结构

构建完项目结构后执行如下的命令会安装项目所需的依赖:

pnpm install

执行该命令启动项目

pnpm run dev

添加@别名

修改ts配置

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

修改vite配置

安装依赖包

pnpm add @types/node
...
import * as path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url)); // TS 中获取 __dirname

// https://vite.dev/config/
export default defineConfig({
...
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

安装插件

Taiwind CSS

使用vite安装

安装Taiwind CSS依赖

pnpm add tailwindcss @tailwindcss/vite

vite.config.tsvite配置文件中添加taiwind的插件

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


// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})

在全局css文件中引入Taiwind css

@import "tailwindcss";

...

在使用vite构建的React项目结构中,main.tsx导入了index.css,所以可以在index.css文件中导入

App.tsx中测试Taiwin css

<h1 className='bg-red-500'>hello taiwind css</h1>

可以看到红色背景的hello taiwind css

解决默认样式覆盖问题

可以发现,<h1>标签的样式没有了,这是因为Taiwind CSS会把默认的样式覆盖。

因此可以在全局css设置中,设置h1标签的样式

// index.css
@import "tailwindcss";

...
@layer base {
...
h1 {
@apply text-3xl font-bold text-foreground tracking-tight;
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.2;
margin-bottom: 1rem;
}
}
...

shadcn/ui

安装,在tsconfig.json文件添加编译选项

{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}

编辑tsconfig.app.json文件,添加shadcn/ui组件的路径解析

{
...
"compilerOptions": {
...
/* 添加路径解析 */
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
...
}

添加依赖@types/node

pnpm add -D @types/node

编辑vite.config.ts,添加@别名设置、导入Taiwind CSS

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from "path"
import tailwindcss from "@tailwindcss/vite"

// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})

执行命令初始化shadcn/ui组件

pnpm dlx shadcn@latest init

测试添加按钮

pnpm dlx shadcn@latest add button

执行完命令后会在src文件夹下新增了一个文件夹components/ui,并且里面还多了一个button.tsx,因此我们可以直接导入这个组件

// App.tsx
import "./App.css"
import { Button } from "@/components/ui/button";

function App() {

return (
<>
<h1 className='bg-red-500'>hello taiwind css</h1>
<Button>Click me</Button>
</>
)
}

export default App

可以看到页面出现了一个黑色按钮

Material UI

安装依赖、安装字体

// 安装依赖
pnpm add @mui/material @emotion/react @emotion/styled

// 安装字体
pnpm add @fontsource/roboto

在入口ts文件中导入字体

import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';

安装图标

pnpm add @mui/icons-material

使用

import "./App.css"
import { Button } from "@/components/ui/button";
import MButton from "@mui/material/Button";
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';

function App() {

return (
<>
<h1 className='bg-red-500 text-border'>hello taiwind css</h1>
<Button>Click me</Button>
<MButton variant="contained" className="text-border"><AccessAlarmIcon /></MButton>
</>
)
}

export default App

Ant Design

安装

pnpm install antd --save

使用

import React from 'react';
import { Button } from 'antd';

const App = () => (
<div className="App">
<Button type="primary">Button</Button>
</div>
);

export default App;

react-router

安装

pnpm add react-router

在根节点中添加路由浏览组件BrowserRouter

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { BrowserRouter, Routes, Route } from "react-router";
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter >
<Routes>
<Route path="/" element={<App />} />
</Routes>
</BrowserRouter>
</StrictMode>,
)

可以看到/路由会展示App.tsx的内容

基本使用

添加路由

下面以添加关于页为例

新建about.tsx

function About() {
return (
<div className="w-full h-full">
<div className="flex flex-col">
<h1 className="text-blue-300">About</h1>
<p>This is the about page</p>
</div>
</div>
)
}

export default About;

App.tsx中添加路由

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { BrowserRouter, Routes, Route } from "react-router";
import App from './App.tsx'
import About from "./pages/about/About.tsx";

createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter >
<Routes>
<Route path="/" element={<App />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
</StrictMode>,
)

修改路由为/about的时候可以看到about页面

嵌套路由

下面以添加两个子路由为例

新建about2.tsx和about3.tsx

// about2.tsx
function About2() {
return (
<div className="w-full h-full">
<h1 className="bg-red-500">About2</h1>
<p>This is the second about page</p>
</div>
)
}

export default About2;

// about3.tsx
function About3() {
return (
<div className="w-full h-full">
<h1 className="bg-yellow-500">About3</h1>
<p>This is the third about page</p>
</div>
)
}

export default About3;

main.tsx中添加路由嵌套

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { BrowserRouter, Routes, Route } from "react-router";
import App from './App.tsx'
import About from "./pages/about/About.tsx";

// 嵌套路由
import About2 from "./pages/about/about2/About2.tsx";
import About3 from "./pages/about/about3/About3.tsx";


createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter >
<Routes>
<Route path="/" element={<App />} />
<Route path="about" element={<About />} >
<Route index element={<About2 />} />
<Route path='about3' element={<About3 />} />
</Route>
</Routes>
</BrowserRouter>
</StrictMode>,
)

其中,index表示,在访问/about路由的时候会首先在嵌套的位置展示<About2 />的内容,切换到/about/about3才会在嵌套的位置展示<About3 />的内容

路由跳转

使用useNavigate钩子函数

import "./App.css"
import { Button } from "@/components/ui/button";
// 路由跳转
import { useNavigate } from "react-router";

function App() {
let navigate = useNavigate();

const navigateToAbout = () => {
navigate("/about")
}

return (
<>
<Button onClick={navigateToAbout}>Turn to About</Button>
</>
)
}

export default App

高阶使用

路由守卫

实现类似vue的路由表

新建路由结构,Route.tsx

import { RouteObject } from "react-router";

export type RouteInterface = RouteObject & {
name?: string; // 路由名称
auth?: boolean; // 是否需要鉴权
children?: RouteInterface[];
absPath?: string; // 绝对路径
};

src中新建一个文件夹route,并且新建index.tsx,用于存储路由表

import { RouteInterface } from "@/models/Route.tsx";
import { lazy } from "react";

// 首页
const Index = lazy(() => import("../pages/Index.tsx"));
// 嵌套路由
const About = lazy(() => import("../pages/about/About.tsx"));
const About2 = lazy(() => import("../pages/about/about2/About2.tsx"));
const About3 = lazy(() => import("../pages/about/about3/About3.tsx"));
// 布局路由
const Layout = lazy(() => import("../pages/layout/Layout.tsx"));
const Header = lazy(() => import("../pages/layout/header/Header.tsx"));
const Header2 = lazy(() => import("../pages/layout/header/Header2.tsx"));
// 动态路由
const User = lazy(() => import("../pages/user/User.tsx"));
// 404
const NotFound = lazy(() => import("../pages/notFound/NotFound.tsx"));

export const routes: RouteInterface[] = [
{
path: "/",
name: "Index",
element: <Index />,
},
{
path: "about",
name: "About",
element: <About />,
children: [
{
index: true,
name: "About2",
element: <About2 />,
},
{
path: "about3",
name: "About3",
element: <About3 />,
},
],
},
{
path: "layout",
name: "Layout",
element: <Layout />,
children: [
{
index: true,
name: "Header",
element: <Header />,
},
{
path: "header2",
name: "Header2",
element: <Header2 />,
},
],
},
{
auth: true,
path: "user/:userId/:userName",
name: "User",
element: <User />,
},
{
path: "/notFound",
name: "NotFound",
element: <NotFound />,
},
];

lazy是React提供的懒加载

App.tsx中,添加路由守卫组件

import {
useLocation,
useNavigate,
useRoutes,
matchRoutes,
} from "react-router-dom";
import "./App.css";

// 导入路由配置
import { routes } from "./route/index";
import { useEffect } from "react";

function RouterBeforeEach({ children }: any) {
// 路由守卫
const location = useLocation();
const navigator = useNavigate();

useEffect(() => {
const match = matchRoutes(routes, location.pathname);
console.log(match);

// 判断是否需要鉴权
if (match) {
if (match[0].route.auth) {
console.log("需要鉴权");
// 如果需要鉴权,则跳转到首页
navigator("/");
}
} else {
console.log("未找到路由");
navigator("/notFound");
}
}, [location.pathname]);

return children;
}

function App() {
const element = useRoutes(routes);

return (
<div>
<div>
<RouterBeforeEach>{element}</RouterBeforeEach>
</div>
</div>
);
}

export default App;

useEffect有点类似vuewatch,当location.pathname发生变化的时候,才会执行里面的内容

matchRoutesreact-route提供的钩子方法,用于根据路径查找路由表中的路由,也可以识别动态路由

React知识点

组件传参

父组件向子组件传参(父 -> 子)

父子组件传参,子组件可以使用props接收父组件传入的参数

父组件内容

import Son from "./son";

function Father() {
return (
<>
<div>
<h1>I'm Father</h1>
<Son parentName="John" />
</div>
</>
)
}

export default Father;

子组件内容

function Son(props: {
parentName: string,
}) {
const { parentName } = props;
return (
<>
<div>
<h1>I'm Son</h1>
<p>My father is {parentName}</p>
</div>
</>
)
}

export default Son;

子组件向父组件传参(子 -> 父)

子组件向父组件传递数据的时候可以使用回调函数

父组件内容

import Son from "./son";
import { useState } from "react";

function Father() {
const [sonMsg, setSonMsg] = useState("")

const handleSonMsg = (data: string) => {
setSonMsg(data)
}

return (
<>
<div>
<h1>I'm Father</h1>
<p>儿子说的话:{sonMsg}</p>
<Son parentName="John" onSendMsg={handleSonMsg} />
</div>
</>
)
}

export default Father;

子组件内容

import { Button } from "@mui/material";

function Son(props: {
parentName: string,
onSendMsg?: (data: string) => void
}) {
const { parentName, onSendMsg } = props;

const speakMsgToFather = () => {
const msg = "Hello Father!"
if (onSendMsg) {
onSendMsg(msg);
}
}

return (
<>
<div className="border-2 p-2 mt-2 mb-2">
<h1>I'm Son</h1>
<p>My father is {parentName}</p>
<Button onClick={speakMsgToFather} variant="contained">给爸爸说句话</Button>
</div>
</>
)
}

export default Son;

兄弟传参

兄弟之间传参可以使用useContext钩子函数进行订阅上下文

父组件内容

import Son from "./son";
import Daughter from "./Daugther";
import { useState, createContext } from "react";

export const SharedContext = createContext<{
data: string;
setData: (value: string) => void;
}>({
data: '',
setData: () => { }
})

function Father() {
const [sharedState, setSharedState] = useState("---我是父亲,这是共享数据---")
const [sonMsg, setSonMsg] = useState("")

const handleSonMsg = (data: string) => {
setSonMsg(data)
}

return (
<>
<SharedContext.Provider value={{ data: sharedState, setData: setSharedState }}>
<div>
<h1>I'm Father</h1>
<p>儿子说的话:{sonMsg}</p>
<p>共享数据内容:{sharedState}</p>
<Son parentName="John" onSendMsg={handleSonMsg} />
<Daughter></Daughter>
</div>
</SharedContext.Provider>
</>
)
}

export default Father;

子组件内容

import { useContext } from "react";
import { Button } from "@mui/material";
import { SharedContext } from "./father";

function Son(props: {
parentName: string,
onSendMsg?: (data: string) => void
}) {
const { data, setData } = useContext(SharedContext);
const { parentName, onSendMsg } = props;

const speakMsgToFather = () => {
const msg = "Hello Father! This is my code:" + Math.random().toString(36).substring(2, 10);
if (onSendMsg) {
onSendMsg(msg);
}
}
const handleChangeShareState = () => {
const shareData = "我是子组件,正在更新共享数据"
setData(shareData);
}

return (
<>
<div className="border-2 p-2 mt-2 mb-2">
<h1>I'm Son</h1>
<p>My father is {parentName}</p>
<p>共享数据内容:{data}</p>
<div className="flex flex-row">
<Button onClick={speakMsgToFather} variant="contained">给爸爸说句话</Button>
<Button onClick={handleChangeShareState} variant="outlined">切换共享数据</Button>
</div>
</div>
</>
)
}

export default Son;

女儿组件内容

import { useContext } from "react";
import { SharedContext } from "./father";
import { Button } from "@mui/material";

function Daughter() {
const { data, setData } = useContext(SharedContext);

const handleChange = () => {
setData("---我是女儿,我改变了共享数据---")
}

return (
<>
<div className="border-2 p-2 mt-2 mb-2">
<h1>I'm Daughter</h1>
<p>共享数据内容:{data}</p>
<Button variant="text" size="large" onClick={handleChange}>改变共享数据</Button>
</div>
</>
)
}

export default Daughter;

React效率问题

路由跳转

第三方组件:react-router、Material UI

问题描述

使用mui写导航页面的时候,使用muiLink组件作为导航项,并且采用嵌套路由的方式

点击导航的时候,会发现加载速度慢,并且整个页面都在加载

解决方法

使用react-routerRouterLink替换muiLink

原因分析

做导航界面的时候不能直接使用muiLink组件,这个组件的本质是<a></a>标签,切换路由的时候会加载整个页面

填写大表单

第三方组件:React-Hook-Form、Material UI

问题描述

在开发大表单页面的时候,使用muiTextField组件作为输入框的时候,修改数据的时候(onChange)响应很慢,体验感极差。

于是考虑是否可以在输入完数据之后再进行事件响应(onBlur),结果失去焦点的事件可以执行,但是无法修改数据。

通过查看官方文档发现,TextField必须要监听onChange事件才可以修改数据。

在大表单的情况下,不可能每个字段都加上onChange,每个字段都设置State

解决方法

使用React-Hook-Form,可以有效解决大表单填写的问题