环境搭建
初始化项目
执行命令
npx create-react-app react-demo-ts --template typescript
cd react-demo-ts
npm start优化项目结构,优化后的项目结构如下:
安装react-router v6
- 执行命令:npm install –save react-router-dom@6
- 在src下新建router/index.js
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Layout from '../layout';
import Category from '../views/Category';
import Home from '../views/Home';
import Login from '../views/Login';
function RouterConfig() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="/category" element={<Category />} />
</Route>
<Route path="/login" element={<Login />} />
</Routes>
)
}
export default RouterConfig - 在index.js中引入router
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import RouterConfig from './router/index'
ReactDOM.render(
<Router>
<RouterConfig />
</Router>,
document.getElementById('root')
); - 使用:在浏览器输入localhost:3000/index即可看到效果
重点:使用useRoutes管理路由
- 修改src/router/index.tsx
import { createBrowserRouter, Navigate } from "react-router-dom";
import Home from "../view/Home.tsx";
import Main from "../view/Main.tsx";
import Layout from "../layout/Layout.tsx";
import { lazy, Suspense } from "react";
const V1 = lazy(() => import("@/view/Dashboard")); // 懒加载
const router = [
{
path: "/",
element: <Layout />,
children: [
{
path: "/",
element: <Navigate to={"/dashboard"} />,
},
{
path: "/dashboard",
element: (
<Suspense>
<V1 />
</Suspense>
),
},
],
},
{
path: "/home",
element: <Home />,
},
{
path: "/*",
element: <Main />,
},
];
export default createBrowserRouter(router); - 修改app.tsx
import RouterConfig from './router/index'
import { useRoutes } from 'react-router-dom';
function App() {
return (
<div className="App">
{useRoutes(RouterConfig)}
</div>
);
}
export default App; - 修改index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
document.getElementById('root')
);
路由嵌套(v6)
规则:使用Route标签包裹,在嵌套的组件里,使用outlet标签
- 在src下新建router/index.tsx
import { lazy, ReactNode, Suspense } from 'react';
import { RouteObject } from 'react-router-dom';
import Layout from '../layout';
const Category = lazy(() => import('../views/Category'));
const Home = lazy(() => import('../views/Home'));
const Login = lazy(() => import('../views/Login'));
const Cat = lazy(() => import('../views/Cat'));
const lazyload = (children: ReactNode): ReactNode => {
return (
<Suspense fallback={<>loading</>}>
{children}
</Suspense>
)
}
const RouterConfig: RouteObject[] = [
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: lazyload(<Home />)
},
{
path: '/category',
element: lazyload(<Category />)
},
{
path: '/cat',
element: lazyload(<Cat />)
}
],
},
{
path: '/login',
element: lazyload(<Login />)
},
]
export default RouterConfig - 在src/Layout/index.js中使用outlet标签插入
import React from 'react'
import { Outlet } from 'react-router-dom'
import './index.css'
function Layout() {
return (
<div className='container'>
<div className='left'></div>
<div className='right'>
<Outlet />
</div>
</div>
)
}
export default Layout
路由跳转(v6)
使用useNavigate跳转,也可以用Link标签
import React from 'react'
import { Link, useNavigate } from 'react-router-dom'
function Home() {
const navigate = useNavigate()
const goToPage = (path) => {
navigate(path)
}
return (
<div>
<h1>home</h1>
<Link to="/category">点击跳转到category</Link>
<button onClick={() => goToPage("/login")}>点击跳转</button>
</div>
)
}
export default Home
路由懒加载
- 使用lazy和Suspense组件, 在src/router/index.js中
import React, { lazy, Suspense } from 'react';
import Layout from '../layout';
const Category = lazy(() => import('../views/Category'));
const Home = lazy(() => import('../views/Home'));
const Login = lazy(() => import('../views/Login'));
const Cat = lazy(() => import('../views/Cat'));
const lazyload = (children) => {
return (
<Suspense fallback={<>loading</>}>
{children}
</Suspense>
)
}
const RouterConfig = [
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: lazyload(<Home />)
},
{
path: '/category',
element: lazyload(<Category />)
},
{
path: '/cat',
element: lazyload(<Cat />)
}
],
},
{
path: '/login',
element: lazyload(<Login />)
},
]
export default RouterConfig
获取地址栏的参数
- 使用useSearchParam
import React from 'react'
import { useSearchParams } from 'react-router-dom'
function Login() {
const [state, setState] = useSearchParams()
const param = state.get('name')
return (
<div>
{param}
</div>
)
}
export default Login
create-react-app中使用ant-design
- 执行命令:npm install –save antd
- 在app.css中引入antd的css样式
@import '~antd/dist/antd.css'
- 使用
create-react-app中的css模块化
- 新建xx.module.css
- 在组件中引入
import styles from './xx.module.css'
- 使用
<div className={styles.button}></div>
修改ant-design的默认样式
- 使用
:golbal()
:global(.ant-collapse-header) {
background-color: red;
}
create-react-app中使用sass
- 执行命令:npm install –save sass
- 将xx.module.css改成xx.module.scss即可
create-react-app中使用redux
执行命令:npm install –save redux react-redux @types/react-redux
在src下新建store/reducers文件夹下,新建login.ts
interface IState {
username: string,
password: string
}
const initLoginState: IState = {
username: 'admin',
password: '123456'
}
const login = (state: IState = initLoginState, action: any) => {
return state
}
export default login在src下新建store/reducers文件夹下,新建home.ts
interface homeState {
dataSource: any[],
loading: boolean
}
const initHomeState: homeState = {
dataSource: [],
loading: false
}
const home = (state:homeState=initHomeState, action: any) => {
return state
}
export default home在src下新建store/reducers文件夹下,新建index.ts
import { combineReducers } from 'redux'
import login from './login'
import home from './home'
export default combineReducers({ login, home })在src下新建store/index.ts
import { createStore } from "redux";
import reducers from "./reducers";
const store = createStore(reducers);
export default store;在根路径下的index.tsx中,引入store
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>
</Provider>,
document.getElementById('root')
);在页面中使用
在src/views/Cat/index.tsx中
import { ReactElement } from 'react'
import { Dispatch } from 'redux'
import { connect } from 'react-redux'
import { rootState } from 'store'
import { ILoginActionType } from 'store/reducers/login'
interface Iprops {
username?: string,
password?: string,
changeName?: VoidFunction
}
function Cat(props: Iprops): ReactElement {
return (
<div>
cat
<hr />
{props.username}
<hr />
<button onClick={() => props.changeName && props.changeName()}>点击修改名称</button>
</div>
)
}
const mapStateToProps = (state: rootState) => {
return { ...state.login }
}
const mapDispatchToPros = (dispatch: Dispatch) => ({
changeName: () => {
dispatch({
type: ILoginActionType.CHANGE,
payload: { username: '123' }
})
}
})
export default connect(mapStateToProps, mapDispatchToPros)(Cat)在store/reducers/login.ts中
interface IState {
username: string,
password: string
}
const initLoginState: IState = {
username: 'admin',
password: '123456'
}
export enum ILoginActionType {
INIT,
CHANGE
}
const login = (state: IState = initLoginState, action: {type: ILoginActionType, payload: any}) => {
switch(action.type) {
case ILoginActionType.INIT:
return state
case ILoginActionType.CHANGE:
return { ...state, ...action.payload }
default:
return state
}
}
export default login
- 使用hooks进行改造(推荐)
- 修改src/views/Cat/index.tsx
import { useSelector, useDispatch } from "react-redux";
import { rootState } from "store";
import { ILoginActionType, ILoginState } from "store/reducers/login";
function Cat() {
const user: ILoginState = useSelector((state: rootState) => state.login)
const dispatch = useDispatch()
const changeName = () => {
dispatch({
type: ILoginActionType.CHANGE,
payload: { username: '123' }
})
}
return (
<div>
cat
<hr />
{user.username}
<hr />
<button onClick={changeName}>点击修改名称</button>
</div>
)
}
export default Cat;
- 修改src/views/Cat/index.tsx
react-hooks api学习
useState
import { useState } from "react"; |
useEffect
import { useEffect, useState } from "react"; |
useLayoutEffect
与useEffect的区别在于,useEffect会阻塞渲染,是同步的,相当于componentDidMount
import { useLayoutEffect, useState } from "react"; |
useCallback+useMemo
- 父组件
import { Input } from "antd";
import { ChangeEvent, useCallback, useState, useMemo } from "react";
import Child from "./Child/Child";
import Child2 from "./Child/Child2";
function UseCallback() {
let [username, setUsername] = useState<string>('')
let [password, setPassword] = useState<string>('')
let [age, setAge] = useState<string>('')
let newUser = useMemo(() => ({ username }), [username]);
let newPassword = useMemo(() => ({ password }), [password]);
const getUser = useCallback((v: ChangeEvent<HTMLInputElement>) => {
setUsername((username) => username = v.target.value)
}, []);
const getPassword = useCallback((v: ChangeEvent<HTMLInputElement>) => {
setPassword((password) => password = v.target.value)
}, []);
return (
<div>
测试useCallBack
<Input
placeholder="输入框"
defaultValue={age}
onChange={(v)=>{
setAge((age)=>age = v.target.value)
}}
/>
<br />
<Child
username={newUser.username}
changeValue={getUser}
/>
<hr />
<Child2 password={newPassword.password} changeValue={getPassword} />
</div>
)
}
export default UseCallback; - 子组件child1
import { Input } from "antd";
import { memo } from "react";
type Iprops = {
username: string,
changeValue: Function
}
function Child(props: Iprops) {
console.log("child")
return (
<div>
用户名:<Input placeholder="请输入用户名" defaultValue={props.username} onChange={(v)=>props.changeValue(v)} />
</div>
)
}
export default memo(Child); - 子组件2
import { Input } from "antd";
import { memo } from "react";
type Iprops = {
password: string,
changeValue: Function
}
function Child2(props: Iprops) {
console.log("child2")
return (
<div>
密码:<Input placeholder="请输入密码" defaultValue={props.password} onChange={(v)=>props.changeValue(v)} />
</div>
)
}
export default memo(Child2);
最新vite5搭建React18环境
初始化项目
- 安装命令:
yarn create vite
pnpm create vite - 安装依赖
pnpm install
配置editorConfig(不同ide相同展示)
在webstorm中会自动读取.editorcondig,vscode需要下载插件EditorConfig for VS Code
- 在根路径下创建.editorconfig文件
# https://editorconfig.org
root = true
# *表示所有的文件都生效
[*]
charset = utf-8
# 空格缩进、每次2格
indent_style = tab
indent_size = 2
# 换行
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
配置npm/yarn/pnpm镜像
- 必须要有稳定版的nodejs
- 安装cnpm、yarn或者pnpm
# 安装yarn
npm install -g yarn
# 安装pnpm
npm install -g pnpm - 查看当前镜像源
npm config get registry
- 修改npm配置
- 在项目根目录下(package.json同一目录)中新建.npmrc文件,编辑文件内容如下:
registry=https://registry.npmmirror.com
- 在项目根目录下(package.json同一目录)中新建.npmrc文件,编辑文件内容如下:
- 修改yarn配置
- 在项目根目录下(package.json同一目录)中新建.yarnrc文件,编辑文件内容如下:
registry "https://registry.npmmirror.com"
- 在项目根目录下(package.json同一目录)中新建.yarnrc文件,编辑文件内容如下:
- 命令行修改配置
npm config set registry https://registry.npmmirror.com
yarn config set registry https://registry.npmmirror.com - pnpm使用命令:
pnpm install 包名
pnpm i 包名
pnpm add 包名 -S // 默认写入dependencies
pnpm add 包名 -D // devDependencies
pnpm add 包名 -g // 全局安装
pnpm remove 包名 // 移除
pnpm up // 更新所有依赖项
pnpm upgrade 包名 // 更新包
pnpm upgrade 包名 --global // 全局更新包
pretter集成(代码格式化)
- 安装:
pnpm add prettier -D 或
yarn add prettier -D - 在项目根目录下(package.json同一目录)中新建prettierrc.cjs文件,编辑文件内容如下:
module.exports = {
// 每行最大列,超过换行
printWidth: 120,
// 使用制表符而不是空格缩进
useTabs: false,
// 缩进
tabWidth: 2,
// 结尾不用分号
semi: false,
// 使用单引号
singleQuote: true,
// 在jsx中使用单引号而不是双引号
jsxSingleQuote: true,
// 箭头函数里面,如果是一个参数的时候,去掉括号
arrowParens: 'avoid',
// 对象、数组括号与文字间添加空格
bracketSpacing: true,
// 尾随逗号
trailingComma: 'none'
} - 自动格式化
- 在vscode搜索安装prettier插件
Prettier - Code formatter
- 在项目根目录下(package.json同一目录)中新建.vscode文件夹,再新建settings.json,编辑文件内容如下:(代码保存是会自动格式化代码)
{
// 保存自动格式化代码
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
// 开启stylelint自动修复
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}
- 在vscode搜索安装prettier插件
vite配置
在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"),
},
},
server: {
host: "localhost",
port: 8000,
proxy: {
"/api": "http://api-driver.marsview.cc",
},
},
});
集成react-router6.x
- 安装:
yarn add react-router-dom 或
pnpm add react-router-dom - 在src下心间router/router.tsx,并写入:
import { createBrowserRouter, Navigate } from "react-router-dom";
import Home from "../view/Home.tsx";
import Main from "../view/Main.tsx";
import Layout from "../layout/Layout.tsx";
import Dashboard from "@/view/Dashboard";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
path: "/",
element: <Navigate to={"/dashboard"} />,
},
{
path: "/dashboard",
element: <Dashboard />,
},
],
},
{
path: "/home",
element: <Home />,
},
{
path: "/*",
element: <Main />,
},
]);
export default router; - 在main.tsx中引入router
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import router from "./router";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);
封装loading组件(使用antd的spin)
- 安装antd
yarn add antd
- 在components/loading文件夹中新建如下文件
- index.css文件
#i-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
} - index.tsx文件
import { createRoot } from "react-dom/client";
import Loading from "./loading";
let count = 0;
export const showLoading = () => {
if (count === 0) {
const loading = document.createElement("div");
loading.setAttribute("id", "i-loading");
document.getElementById("root")?.appendChild(loading);
createRoot(loading).render(<Loading />);
}
count++;
};
export const hideLoading = () => {
if (count < 0) {
return;
}
count--;
if (count === 0) {
document
.getElementById("root")
?.removeChild(document.getElementById("i-loading") as HTMLDivElement);
}
}; - Loading.tsx文件
import { Spin } from "antd";
import "./index.css";
export default function Loading({ tip = "loading" }: { tip?: string }) {
return <Spin tip={tip} size="large" className="i-loading" />;
}
- index.css文件
- 在封装的axios中使用
import { hideLoading, showLoading } from "@/components/loading";
import { message } from "antd";
import axios, { AxiosError } from "axios";
const instance = axios.create({
// baseURL: "/test",
timeout: 8000,
timeoutErrorMessage: "请求超时,请稍后再试",
withCredentials: true,
});
// 请求拦截器
instance.interceptors.request.use(
(config) => {
showLoading();
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = "Token:" + token;
}
return {
...config,
};
},
(error: AxiosError) => {
return Promise.reject(error);
},
);
// 响应拦截器
instance.interceptors.response.use(
(response) => {
const data = response.data;
hideLoading();
if (data.code === 500001) {
message.error(data.msg);
} else if (data.code != 0) {
return Promise.reject(data);
}
return data.data;
},
(error) => {
hideLoading();
message.error(error.message);
return Promise.reject(error.message);
},
);
export default {
get<T>(url: string, params?: object): Promise<T> {
return instance.get(url, { params });
},
post<T>(url: string, params?: object): Promise<T> {
return instance.post(url, params);
},
};
封装localStorage、sessionStorage、cookie
封装cookie需要安装js-cookie和@types/js-cookie
- 安装js-cookie和@types/js-cookie
yarn add js-cookie
yarn add @types/js-cookie -D
pnpm add js-cookie
pnpm add @types/js-cookie -D - 在utils/storage.ts中写入:
import Cookies from "js-cookie";
// localstorage模块封装
const getValue = (type: "local" | "session" | "cookie", key: string) => {
let value;
if (type === "local") {
value = localStorage.getItem(key);
} else if (type === "session") {
value = sessionStorage.getItem(key);
}
if (!value) return "";
try {
return JSON.parse(value);
} catch (error) {
return value;
}
};
export default {
local: {
set: (key: string, value: any) => {
localStorage.setItem(key, JSON.stringify(value));
},
get: (key: string) => {
return getValue("local", key);
},
remove: (key: string) => {
localStorage.removeItem(key);
},
clear: () => {
localStorage.clear();
},
},
session: {
set: (key: string, value: any) => {
sessionStorage.setItem(key, JSON.stringify(value));
},
get: (key: string) => {
return getValue("session", key);
},
remove: (key: string) => {
sessionStorage.removeItem(key);
},
clear: () => {
sessionStorage.clear();
},
},
cookie: Cookies,
};
vite的多环境配置(编译时环境配置)
- 在根路径下创建对应的环境文件,例如:.env.local, .env.sit, .env.production
# 环境设置
NODE_ENV=development
VITE_API_URL=https://www.fastmock.site/mock/development/f5d8d99de1a8ce59a932ad17a28ed974/temp - 打印import.meta.env能够看到所有的值(注意,环境变量必须要是VITE_开头,不然不会生效)
- 在package.json里,添加
--mode
关键词,启动对应的程序,并且要保证在跟路径下有对应的.env文件...
"scripts": {
...
"dev:development": "vite --mode development",
"dev:sit": "vite --mode sit",
...
},
vite多环境配置(运行时环境配置-更推荐)
在根路径下新建config/index.ts,写入一下代码:
type ENV = "development" | "sit" | "production";
const env = (document.documentElement.dataset.env as ENV) || "development";
const config = {
development: {
node_env: "development",
api_url:
"https://www.fastmock.site/mock/development/f5d8d99de1a8ce59a932ad17a28ed974/temp",
},
sit: {
node_env: "sit",
api_url:
"https://www.fastmock.site/mock/sit/f5d8d99de1a8ce59a932ad17a28ed974/temp",
},
production: {
node_env: "production",
api_url:
"https://www.fastmock.site/mock/production/f5d8d99de1a8ce59a932ad17a28ed974/temp",
},
};
export default {
env,
...config[env],
};在index.html的html标签添加
data-env="sit"
<!doctype html>
<html lang="en" data-env="sit">
...
</html>
配置@
- 在vite.config.ts中添加如下:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
...
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
...
}); - 在tsconfig.json中添加如下:
{
"compilerOptions": {
...
"paths": {
"@/*": ["./src/*"]
},
}
}
通过vscode提交git
- 输入:git init
- 点击vs code放大镜下面的按钮